dlogify docs

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:

billing.py
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, so logging.basicConfig() works before or after it and your console output stays the same. Python's root logger shows WARNING and above unless you configure it.
  • Keys passed with extra= become attributes. log.exception() or exc_info=True attaches the traceback.
  • Uncaught exceptions in the main thread and in other threads are sent at level fatal. Pass capture_logging=False or capture_uncaught=False to init() to turn a capture off.
  • structlog and loguru can write to the standard logging module, 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 block

log() 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 runserver

With 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.

VariableOptionDefault
LOGIFY_API_KEYapi_keyRequired; without it the client is disabled.
LOGIFY_ENDPOINTendpointhttps://api.dlogify.com
LOGIFY_SERVICEserviceOTEL_SERVICE_NAME, then the [project] name of the nearest pyproject.toml from the working directory, then unknown_service
LOGIFY_ENVIRONMENTenvironmentdefault
LOGIFY_RELEASEreleaseempty
LOGIFY_MIN_LEVELmin_levelinfo (trace, debug, info, warn, error, fatal)
LOGIFY_DISABLEDdisabledoff (1 or true turns the client into a no-op)
LOGIFY_DEBUGdebugoff (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, set SSL_CERT_FILE to 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 call dlogify.flush() first.
  • Pre-fork servers such as gunicorn work, and multiprocessing children flush when their target returns. Pool.terminate() (also run when a with Pool() block ends) stops workers before they flush: call pool.close() and pool.join() instead. Other worker pools that end with os._exit() should call dlogify.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.

On this page