Skip to main content

OpenTelemetry-native observability SDK for Python web applications with FastAPI, Django, and Flask integrations

Project description

Ledger SDK for Python

OpenTelemetry-native observability for developers who just want to ship.

Python Version PyPI Version License

Supported Frameworks: FastAPI • Django • Flask

Since v2.0.0, ledger-sdk is a thin distribution of the official opentelemetry-python SDK: real TracerProvider/LoggerProvider, OTLP/HTTP export, standard semantic-convention attributes — plus Python-specific enhancements (exception capture, endpoint monitoring, log↔trace correlation, attribute truncation) layered on top. Upgrading from 1.x? See CHANGELOG.md for the full migration guide.


Installation

pip install ledger-sdk

Setup

FastAPI

from contextlib import asynccontextmanager
from fastapi import FastAPI
from ledger import LedgerClient
from ledger.integrations.fastapi import LedgerMiddleware

ledger = LedgerClient(
    api_key="ledger_proj_1_your_api_key",
    base_url="https://ledger-server.jtuta.cloud"
)

@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    await ledger.shutdown()

app = FastAPI(lifespan=lifespan)
app.add_middleware(LedgerMiddleware, ledger_client=ledger)

Django

# settings.py
import os
from ledger import LedgerClient

LEDGER_CLIENT = LedgerClient(
    api_key=os.getenv("LEDGER_API_KEY", "ledger_proj_1_your_api_key"),
    base_url=os.getenv("LEDGER_BASE_URL", "https://ledger-server.jtuta.cloud")
)

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.common.CommonMiddleware",
    "ledger.integrations.django.LedgerMiddleware",
]

Flask

from flask import Flask
from ledger import LedgerClient
from ledger.integrations.flask import LedgerMiddleware

app = Flask(__name__)
ledger = LedgerClient(
    api_key="ledger_proj_1_your_api_key",
    base_url="https://ledger-server.jtuta.cloud"
)
app.config["LEDGER_CLIENT"] = ledger
LedgerMiddleware(app)

Manual Logging

ledger.log_info("User logged in", attributes={"user_id": 123})
ledger.log_warning("Slow query", attributes={"duration_ms": 450})
ledger.log_error("Payment failed", attributes={"error_code": "CARD_DECLINED"})

try:
    result = process_payment()
except Exception as e:
    ledger.log_exception(e, message="Payment processing failed")

Configuration

ledger = LedgerClient(
    api_key="ledger_proj_1_your_api_key",
    flush_interval=5.0,     # Seconds between flushes
    flush_size=1000,        # Logs before auto-flush
    max_buffer_size=10000,  # Max logs buffered in memory
)

Use environment variables in production:

export LEDGER_API_KEY="ledger_proj_1_your_api_key"
export LEDGER_BASE_URL="https://ledger-server.jtuta.cloud"
import os

ledger = LedgerClient(
    api_key=os.getenv("LEDGER_API_KEY"),
    base_url=os.getenv("LEDGER_BASE_URL")
)

Exclude Paths

app.add_middleware(
    LedgerMiddleware,
    ledger_client=ledger,
    exclude_paths=["/health", "/metrics"]
)

By default the SDK only logs requests that match a registered route — scanner noise, 404s, and bot traffic are dropped automatically. To log everything:

app.add_middleware(
    LedgerMiddleware,
    ledger_client=ledger,
    only_registered_routes=False
)

Distributed Tracing

Tracing is enabled automatically when you create a LedgerClient — it registers a real OpenTelemetry TracerProvider as the process-global tracer provider. Traces appear in Ledger's Trace List panel on the dashboard.

from ledger import LedgerClient, get_tracer

ledger = LedgerClient(api_key="...", base_url="...", service_name="my-service")
tracer = get_tracer(__name__)

ledger.tracer is also available directly on the client instance once constructed.

Manual spans

with tracer.start_as_current_span("process-order", attributes={"order_id": 42}) as span:
    result = process_order(42)
    span.set_attribute("status", result.status)

Cross-service propagation

Ledger uses the standard OpenTelemetry propagation API (opentelemetry.propagate), so requests/httpx calls are instrumented automatically once you call install():

import ledger.integrations.requests as ledger_requests

ledger_requests.install()  # every requests.Session.send() now propagates traceparent

To propagate manually:

import opentelemetry.propagate as propagate

with tracer.start_as_current_span("outgoing-call"):
    headers = {}
    propagate.inject(headers)
    response = httpx.get("https://downstream/api", headers=headers)

Extract context in the downstream service:

import opentelemetry.propagate as propagate

ctx = propagate.extract(request.headers)
with tracer.start_as_current_span("downstream-handler", context=ctx):
    ...

Any log emitted inside an active span automatically includes trace_id and span_id, linking logs to traces in the dashboard.

FastAPI auto-instrumentation

With LedgerMiddleware, every request automatically becomes a root span. Spans created inside request handlers are nested under it:

@app.get("/orders/{id}")
async def get_order(id: int):
    tracer = get_tracer(__name__)
    with tracer.start_as_current_span("db-fetch", attributes={"order_id": id}):
        return await db.get_order(id)

Standard library logging bridge

Forward every logging.getLogger(...) call — yours or a third-party library's — to Ledger alongside SDK-native logs:

ledger = LedgerClient(api_key="...")
ledger.instrument_logging()

import logging
logging.getLogger(__name__).warning("this reaches Ledger too")

Global exception capture

Catch uncaught exceptions across threads and asyncio tasks with one call:

ledger = LedgerClient(api_key="...")
ledger.capture_uncaught()

Chains to any previously-installed sys.excepthook/threading.excepthook/asyncio exception handler rather than replacing them — safe to call alongside a debugger or another monitoring tool. Idempotent (calling it twice doesn't double-log).


before_send hook & PII scrubbing

Mutate or drop log records right before export:

def redact(record: dict) -> dict | None:
    if record["attributes"].get("internal"):
        return None  # drop internal-only logs entirely
    record["attributes"].pop("session_cookie", None)
    return record

ledger = LedgerClient(api_key="...", before_send=redact)

Or turn on the built-in scrubbers (redacts common PII: emails, credit-card-like digit sequences, Authorization/X-API-Key-shaped attributes, and any attribute key containing password/secret/token/api_key):

ledger = LedgerClient(api_key="...", scrub_pii=True)

Both together: built-in scrubbers run first, then your before_send hook.


loguru / structlog sinks

pip install "ledger-sdk[loguru]"   # or [structlog], or [all]
# loguru
from ledger.integrations.loguru import add_loguru_sink
from loguru import logger as loguru_logger

add_loguru_sink(ledger)
loguru_logger.info("this reaches Ledger too")
# structlog
import structlog
from ledger.integrations.structlog import ledger_structlog_processor

structlog.configure(processors=[ledger_structlog_processor(ledger), structlog.processors.JSONRenderer()])

Custom metrics

ledger.metric_increment("orders_processed", tags={"region": "eu"})
ledger.metric_gauge("queue_depth", 42)
ledger.metric_histogram("request_duration_ms", 123.4, tags={"route": "/api"})

# Or the standard OTel Meter API for full control
meter = ledger.get_meter("my-service")
meter.create_counter("requests").add(1, {"route": "/health"})

See the API reference for querying what you send.


Uptime monitors (heartbeat)

Ping a dead-man's-switch monitor at the end of a cron job or scheduled task:

ledger.heartbeat("YOUR_MONITOR_TOKEN")

Plain synchronous HTTP call, no OTel pipeline involved — safe to call from short-lived scripts that exit right after. Raises RuntimeError on failure so a failed ping doesn't silently vanish.


OpenTelemetry environment variables

The underlying OTel exporters honor the standard OTEL_EXPORTER_OTLP_* environment variables, so you can override the endpoint or add extra headers without touching application code:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://ledger-server.jtuta.cloud"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ledger_proj_1_your_api_key"

SDK Health

if ledger.is_healthy():
    print("SDK is healthy")

status = ledger.get_health_status()
metrics = ledger.get_metrics()

Expose as an endpoint:

@app.get("/sdk/health")
async def sdk_health():
    return ledger.get_health_status()

Links

License

MIT License — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ledger_sdk-2.0.0.tar.gz (56.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ledger_sdk-2.0.0-py3-none-any.whl (36.2 kB view details)

Uploaded Python 3

File details

Details for the file ledger_sdk-2.0.0.tar.gz.

File metadata

  • Download URL: ledger_sdk-2.0.0.tar.gz
  • Upload date:
  • Size: 56.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for ledger_sdk-2.0.0.tar.gz
Algorithm Hash digest
SHA256 ccb82d81e2d9a5c855f8df027ab3452d646ccb02c4029f7fc9038f8f0f2ea0ee
MD5 1d8426aae55bb730293e17c9e2ce142a
BLAKE2b-256 0ffadda5e9052c0727c40dae1fad36742a4873f8f9aad9829e81cadac2d1b69f

See more details on using hashes here.

File details

Details for the file ledger_sdk-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: ledger_sdk-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 36.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for ledger_sdk-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 655efd4bf90fcd32aea4c81dee95f462edd697399ddbf94615097db9e6c2d238
MD5 a12f0965b788dd78d3e7f67809d50545
BLAKE2b-256 098a9ff041aecbbede24353d984d21ef0422984f52c411481cf52cb39546334a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page