Logging¶
almapy uses stdlib logging and follows library best practice: a NullHandler
is registered on the almapy root logger at import time, so no output is
produced unless you configure handlers. The library never writes to your
console uninvited.
The four loggers¶
| Logger | Level | Event | Extra fields |
|---|---|---|---|
almapy.http |
DEBUG | Request sent | method, url |
almapy.http |
DEBUG | Response received | method, url, status_code, elapsed_ms |
almapy.retry |
WARNING | Retry attempt | attempt, max_attempts, exc_type |
almapy.throttle |
DEBUG | TokenBucket wait | wait_secs, tokens_available |
almapy.throttle |
WARNING | Rate cut (AIMD backoff) | old_rate, new_rate |
almapy.throttle |
INFO | Rate recovery | old_rate, new_rate |
almapy.error |
WARNING | Alma error before raising | status_code, alma_code (where applicable) |
They are semantic rather than per-module, so you can turn on exactly the category you care about – retries without the full HTTP firehose, say.
Correlation IDs¶
Every record carries a req_id field: a uuid4().hex set at the start of each
execute() call and reset in finally. It lives in a contextvars.ContextVar,
so it survives across await boundaries and stays correct under concurrency.
Use it to reassemble the story of a single request – its retries, its throttle waits, and the error it finally raised – out of interleaved output from 150 concurrent calls.
Turning it on¶
import logging
# Everything
logging.getLogger("almapy").setLevel(logging.DEBUG)
logging.getLogger("almapy").addHandler(logging.StreamHandler())
# Or just one area – retry warnings only
logging.getLogger("almapy.retry").setLevel(logging.WARNING)
logging.getLogger("almapy.retry").addHandler(logging.StreamHandler())
To include the correlation ID in your output:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(levelname)s %(name)s [%(req_id)s] %(message)s"))
logging.getLogger("almapy").addHandler(handler)
%(req_id)s needs every record to carry it
A formatter referencing %(req_id)s raises KeyError on any record from
another library. Either scope the handler to the almapy logger as above,
or use a filter that defaults the field.
What is never logged¶
API keys and full response bodies are never written to logs. Error records carry Alma's status code and numeric error code, and unparseable bodies are truncated to a 500-character excerpt.
structlog¶
All extra fields flow into structlog automatically via ExtraAdder():
import logging
import structlog
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
)
formatter = structlog.stdlib.ProcessorFormatter(
foreign_pre_chain=[
structlog.stdlib.ExtraAdder(), # pulls req_id, status_code, elapsed_ms
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
],
processor=structlog.processors.JSONRenderer(),
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.getLogger("almapy").addHandler(handler)
logging.getLogger("almapy").setLevel(logging.DEBUG)
Each record then renders as JSON with req_id, status_code and elapsed_ms
as first-class keys, ready for your log aggregator.