Python
Send logs and uncaught exceptions from Python with dlogify.
By the end of this page your Python service sends its logs and uncaught exceptions to dlogify. The dlogify package requires Python 3.10 or later and has no dependencies.
Install
pip install dlogify
export LOGIFY_API_KEY=lgf_live_…With the SDK
Call dlogify.init() once at startup. It sends every record that passes your logging configuration, and every uncaught exception:
import logging
import dlogify
dlogify.init(service="billing")
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("billing")
def charge(order_id: str, amount: str) -> None:
log.info("charging order", extra={"order_id": order_id})
try:
cents = int(amount)
print(f"charged {cents} cents")
except ValueError:
log.exception("charge failed", extra={"order_id": order_id})
charge("ord_1", "abc")
# Before os._exit(), send what is still queued.
dlogify.flush()init()changes no logger's level and adds no handler, sologging.basicConfig()works before or after it and your console output stays the same. Python's root logger showsWARNINGand above unless you configure it.- Keys passed with
extra=become attributes.log.exception()orexc_info=Trueattaches the traceback. - Uncaught exceptions in the main thread and in other threads are sent at level
fatal. Passcapture_logging=Falseorcapture_uncaught=Falsetoinit()to turn a capture off. structlogandlogurucan write to the standardloggingmodule, which dlogify captures.
You can also log directly:
dlogify.info("order placed", attributes={"order_id": "ord_1"})
dlogify.error("charge failed", exc=err, attributes={"order_id": "ord_1"})
dlogify.log("warning", "disk almost full") # any level name, any case
dlogify.capture_exception() # inside an except blocklog() takes trace, debug, info, warn, error and fatal, and also Python's warning and critical, in any case; an unknown name logs at info. Calls before init() are dropped, and a second init() is ignored. dlogify.Client(**options) creates an independent client.
dictConfig
If you configure logging with dictConfig, you can name the handler instead of letting init() capture everything:
import logging.config
import dlogify
dlogify.init(capture_logging=False)
logging.config.dictConfig({
"version": 1,
"handlers": {"dlogify": {"class": "dlogify.LogifyHandler", "level": "INFO"}},
"root": {"handlers": ["dlogify"], "level": "INFO"},
})The handler sends through the init() client. If you leave the capture on as well, each record is still sent once.
Any process: logify run
logify run sends the output of any command:
LOGIFY_API_KEY=lgf_live_… pipx run dlogify run -- python manage.py runserverWith the package installed, the command is logify run -- <cmd>. The output still appears in your terminal exactly as before, and the exit code is the command's. Tracebacks, stack traces and Go panics stay together as single events, and JSON lines are read as structured records. A non-zero exit or a death by signal is reported as one more error, except a SIGPIPE after the reader of the output went away, as in | head. logify run is not available on Windows.
Configuration
Options passed to init() win over environment variables, which win over the defaults.
| Variable | Option | Default |
|---|---|---|
LOGIFY_API_KEY | api_key | Required; without it the client is disabled. |
LOGIFY_ENDPOINT | endpoint | https://api.dlogify.com |
LOGIFY_SERVICE | service | OTEL_SERVICE_NAME, then the [project] name of the nearest pyproject.toml from the working directory, then unknown_service |
LOGIFY_ENVIRONMENT | environment | default |
LOGIFY_RELEASE | release | empty |
LOGIFY_MIN_LEVEL | min_level | info (trace, debug, info, warn, error, fatal) |
LOGIFY_DISABLED | disabled | off (1 or true turns the client into a no-op) |
LOGIFY_DEBUG | debug | off (1 or true prints the client's diagnostics to stderr) |
Also attributes (added to every record) and max_queue_size (default 2000). disabled, debug, capture_logging and capture_uncaught take True or False; any other value is ignored with a warning.
Guarantees
- No call raises, and logging never waits for the network: a background thread sends records in compressed batches, with retries for up to 5 minutes.
- If the key is rejected, the client stops for the rest of the process with one warning. Redirects are not followed: if the endpoint redirects, one warning names the URL to put in
LOGIFY_ENDPOINT. - If the endpoint's TLS certificate cannot be verified, one warning says how to fix it: with Python from python.org on macOS, run
Install Certificates.command; behind a proxy that inspects TLS, setSSL_CERT_FILEto its CA bundle. - After an uncaught exception, the client sends what is queued, then Python reports the exception as usual. At normal exit it flushes too. An exit spends at most 2 seconds on this in total.
os._exit()skips both, so calldlogify.flush()first. - Pre-fork servers such as gunicorn work, and
multiprocessingchildren flush when their target returns.Pool.terminate()(also run when awith Pool()block ends) stops workers before they flush: callpool.close()andpool.join()instead. Other worker pools that end withos._exit()should calldlogify.flush()first. - The client's own diagnostics go to stderr, never through
logging.
What is sent
Only log records: message, level, time, logger name, the attributes you pass, the exception's type, message and traceback, and the service name, environment, release, host name and SDK version. The SDK makes no other network calls and opens no ports. See What is sent.