Skip to main content

Sereno for Python

Sends logs and errors from Python, Django, FastAPI and any ASGI app to a Sereno project channel. It plugs into the standard logging module and posts events to the Sereno ingest endpoint (POST {SERENO_URL}/api/ingest/events) with a source API key. It also captures uncaught exceptions in the process and in threads, and ships a LOGGING helper for Django and a middleware for FastAPI and other ASGI apps. No runtime dependencies (it uses urllib).

Requirements

  • Python 3.10 or newer
  • Optional, only for the integration you use: django >= 4.2, fastapi >= 0.100 (the extras only declare the dependency; the middleware works with any ASGI app)

Installation

Create a project in Sereno, open its Integrations page and choose "Create log source". The dialog shows the source key (wsk_...) only once; if you lose it, rotate the key. Then, in your application:

pip install sereno-sdk
pip install "sereno-sdk[django]"    # or [fastapi]

The package is installed as sereno-sdk and imported as sereno.

Set SERENO_URL (the base URL of the Sereno API, without a path) and SERENO_KEY in the environment, as described in Configuration, and check them with python -m sereno test (see Testing the connection).

Plain Python and logging

Call sereno.init() once, as early as possible in your entry point:

import logging
import sereno

sereno.init()  # reads SERENO_URL and SERENO_KEY from the environment

logger = logging.getLogger(__name__)
logger.warning("Payment is slow", extra={"order_id": 42})

try:
    charge_card()
except Exception as error:
    sereno.capture_exception(error, order_id=42)

You can also pass the settings: sereno.init(key="wsk_your_key", url="https://your-sereno-api.example.com", level="warning"). Arguments passed in code win over the environment. init returns the client, and calling it again replaces the previous setup (the buffered events of the old one are sent first).

Arguments (all optional): key, url, level (minimum level sent, default debug), timeout (seconds per request, see Configuration) and handlers (False to not install the logging handler and the exception hooks described below). Any other keyword argument goes to sereno.Client (retry=False makes a single attempt per batch; notifier, sender, headers and flush_delay are also accepted).

Without a URL or a key, init still returns a client, but it is inert: nothing is buffered, nothing is installed and no request is made. The functions below do nothing before init.

Functions:

  • capture_exception(exc=None, **context): buffers an exception at level error. Without exc it takes the exception being handled (inside an except block); outside one it does nothing. Keyword arguments go to the context. An exception is sent with its class, message, location and traceback; any other value is sent as str(value).
  • flush(timeout=None): sends what is buffered and returns True, or False if timeout seconds pass first. It never raises. Call it before a serverless handler returns.
  • close(timeout=None): removes the handler and hooks, sends what is buffered and leaves the SDK uninitialized.
  • get_client(): the active Client, or None before init.

Standard logging

init adds a SerenoHandler to the root logger, so every record that reaches the root is sent:

  • The level of the record is mapped to the closest Sereno level at or below it (DEBUG to debug, INFO to info, WARNING to warning, ERROR to error, CRITICAL to critical; a custom level such as 25 is info). Sereno also has notice, alert and emergency, which logging cannot produce.
  • The level of the root logger is not changed. Python's default is WARNING, so logger.info(...) never reaches the handler until you configure it, for example with logging.basicConfig(level=logging.INFO). The level of init (or SERENO_LEVEL) filters further; it does not lower the root's.
  • The fields you pass in extra={...} go to the context, and the logger name goes to the context as logger.
  • A record with exc_info (logger.exception(...), logger.error(..., exc_info=True)) is sent with the exception and grouped by its class, file and line.
  • Records of the sereno logger (the SDK's own) are ignored, so a delivery failure can never loop back.
  • A logger with propagate = False does not reach the root; attach sereno.SerenoHandler(sereno.get_client()) to it if you need that.

To group repeated events pass extra={"fingerprint": "..."}.

What it captures in the process

Unless you pass handlers=False, init chains these hooks and always calls the previous ones, so the traceback and the exit code do not change:

  • Uncaught exceptions (sys.excepthook) and exceptions that end a thread (threading.excepthook): sent at level error. Only Exception subclasses are captured; KeyboardInterrupt, SystemExit and other BaseException pass untouched.

Independently of handlers, importing sereno registers an atexit function that flushes the buffer on a normal exit, waiting up to 10 seconds.

Things to know:

  • SIGTERM, os._exit() and SIGKILL end the process without running atexit, so what is still buffered is lost. Call sereno.flush() first, for example in your own SIGTERM handler. The same goes for an out-of-memory kill.
  • After os.fork() the child starts with an empty buffer and its own background thread; events buffered by the parent are sent by the parent.
  • An exception is sent once per client: if you capture it with capture_exception, log it with exc_info and let it reach the excepthook, Sereno receives one event. The mark lives on the exception object, so an instance you reuse (a constant or a cached exception) is sent only the first time.

Django

# settings.py
import sereno
from sereno.django import logging_config

sereno.init()  # SERENO_URL and SERENO_KEY

LOGGING = logging_config()

To add Sereno to an existing configuration, pass it: LOGGING = logging_config(LOGGING). It returns a copy with a sereno handler (sereno.django.DjangoHandler) on the django.request and django.security loggers, keeping the handlers you already list for them. Django's defaults (console and mail_admins) keep working.

Notes:

  • Django logs every unhandled view exception on django.request at level error. Responses with a status below 500 (404, 403, 400...) are not sent: DjangoHandler drops those records, also for the root handler of init().
  • From the request, the context carries only method and route (the URL pattern, or the concrete path without the query string when nothing matched). It also carries logger and the other fields of the log record (on django.request, status_code). Headers, cookies, body and query are never sent.
  • init() and LOGGING can be in any order; the handler looks up the client of init() on each record.
  • A LOGGING with a root key replaces the root handlers, including the one init() added. Add sereno.SerenoHandler to that root, or call init() after Django configures logging.
  • A record is sent once per client even if it passes through the Django handler and the root handler. A DjangoHandler(client=other_client) different from the one in init() does send twice.

FastAPI and ASGI

import sereno
from fastapi import FastAPI
from sereno.asgi import SerenoMiddleware

sereno.init()

app = FastAPI()
app.add_middleware(SerenoMiddleware)


@app.get("/boom")
def boom():
    raise RuntimeError("boom")

Run it with uvicorn main:app. The middleware is plain ASGI, so it also works with Starlette or any ASGI app (app = SerenoMiddleware(app)). Call init() before the first request.

  • It sends the exception that ends in a 500 and raises it again unchanged, so the response and the server's own log are the same as without Sereno. HTTPException, 404s and validation errors never reach it, and exceptions with a status_code below 500 are not sent.
  • The context carries only method and route (the route pattern such as /users/{id}, with the prefix of a Mount), or the path without the query string when nothing matched; never headers, body or query. It applies to http and websocket requests.
  • Uvicorn logs the same traceback on uvicorn.error. With uvicorn's default logging configuration that logger does not propagate to the root, so it is not sent from there; if you make it propagate, the exception is still sent once, because the middleware already marked it.

Configuration

Variable Default Description
SERENO_URL none Base URL of the Sereno API, without a path (for example https://api.example.com).
SERENO_KEY none The source API key, sent as a Bearer token.
SERENO_TIMEOUT 2 Seconds per request (a batch makes at most two, see Delivery behavior). Capped at 10; a missing, non numeric or non positive value falls back to 2.
SERENO_LEVEL debug Minimum level sent. An invalid value falls back to debug.

init reads the four variables and the arguments passed in code win over them. The test command reads SERENO_URL and SERENO_KEY. Python does not read .env files by itself: export the variables or load them with your own tool.

Testing the connection

python -m sereno test
sereno test --key=wsk_your_key --url=https://your-sereno-api.example.com

Both forms are equivalent (sereno is the script that pip installs). The test subcommand is required. It sends one test event (level info, message "Sereno connection test") to {url}/api/ingest/events with a 5 second timeout and no retry. Without --key and --url (also accepted as --key value) it uses SERENO_KEY and SERENO_URL. The outcome is one of:

  • Connected: Sereno answered 202. Exit code 0.
  • Unauthorized: 401, the key is invalid or has been revoked.
  • Missing URL or key: nothing is sent.
  • Failed: network failure, timeout or any other HTTP status (a redirect hints at a wrong URL scheme).

Every outcome other than "Connected" exits with code 1. The key never appears in the output.

Events

Each record becomes an event with id, level, message, occurred_at (UTC), optional context and optional fingerprint. The fingerprint groups repeated events: pass fingerprint in the context, or log an exception, which uses its class, file and line (Class|file:line). Fingerprints over 255 characters are replaced by their SHA-1. Limits keep the payload valid for the API: messages are cut at 8192 characters, context at 32 KB, 8 levels of depth and 50 items per list or dict, strings in the context at 2048 characters and tracebacks at 2000. When anything is cut, the context carries _truncated: true. An empty message is sent as -.

The id is a UUID generated once when the record is formatted, so it stays the same when a batch is retried. In the API the id is optional (1 to 64 characters, unique per log source): an event whose id was already received from the same source is accepted but discarded, so resending a batch never duplicates events or inflates the group counters.

Python values in the context are converted so they always serialize:

  • Enum by its value; Decimal as a string (NaN and infinities as NAN/INF strings); bytes decoded as UTF-8 (invalid bytes become ?).
  • set, frozenset, tuple and deque as lists.
  • date as midnight UTC (T00:00:00+00:00); datetime keeps its own offset (a naive one is written as +00:00).
  • Integers outside the signed 64 bit range as strings.
  • Functions as [function] and any other object as [object module.Class]; the SDK never calls your __str__ or __repr__ on them.

Privacy: the SDK sends only what you put in the context (including extra), plus the exception itself. The integrations add only the method and the route pattern, never the query string, headers, cookies or body. Do not put secrets in messages or contexts.

Delivery behavior

  • Background thread, buffered and batched. Logging a record never blocks on the network: it is formatted and buffered in memory, and a daemon thread sends batches of at most 100 events per request. Reaching 100 buffered events sends immediately, and otherwise the buffer is sent 1 second after the first record. If the thread cannot be created, flush and close send synchronously.
  • One retry, only on transient failures. If a request fails with a network error, a timeout or a 5xx response, the same batch, with the same event ids, is sent once more; the server discards the events it already stored. Any 4xx response (for example 401, 413, 422 or 429) is not retried. Both attempts share a budget of 10 seconds: the retry uses the configured timeout or what is left of the budget, whichever is lower, and is skipped when less than half a second is left. Redirects are not followed.
  • Timeout. It is the urllib timeout of each socket operation (connect, send, read), not a total deadline: it does not cover DNS resolution, and a server that sends bytes slowly can take longer.
  • Buffer cap. At most 1000 events are buffered between sends. Beyond that, new records are dropped and one notice with the number of dropped events is written to stderr (through the notifier) when the next batch is sent.
  • Never raises at runtime. Once initialized, no function of the SDK raises into your code (init itself raises TypeError for an unknown keyword argument). If a batch is not delivered, or an event cannot be formatted, a message with the reason is written to stderr and the events of that batch are dropped. The API key and the credentials in the URL are removed from those messages. While Sereno is sending or reporting a failure, records logged from inside the SDK are ignored, so a failure can never loop.
  • Flush points. A full buffer, the 1 second delay, flush(), close() and a normal exit. Events still buffered when the process is killed are lost.

License

MIT. See LICENSE.

Metadata

Release files for sereno-sdk 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sereno-sdk 0.1.0
File Size Uploaded Provenance
sereno_sdk-0.1.0.tar.gz 40.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sereno-sdk 0.1.0
File Interpreter ABI Platform
sereno_sdk-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 67.2 kB

Release files / sereno_sdk-0.1.0.tar.gz

Download URL sereno_sdk-0.1.0.tar.gz
Size 40.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e28f1062bf50045ee9d936cb4ece22b9ceb16efb57b92ab8078370b62fe8d76c
BLAKE2b-256 checksum
How to use checksums
23aa55f46989169247980b072ed60906d5a824881e40d45b4a87b6526b8f4ebe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / sereno_sdk-0.1.0-py3-none-any.whl

Download URL sereno_sdk-0.1.0-py3-none-any.whl
Size 27.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cb7c659461f26b63d2768a0f3eff28f55441cd36be48f1085d46090b4c711278
BLAKE2b-256 checksum
How to use checksums
2b93326c30012f9f6474d1f631c77840ba49ce4d3e073f7da9b87a6e0c2a5130
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Security for sereno-sdk 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for sereno_sdk-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page