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 levelerror. Withoutexcit takes the exception being handled (inside anexceptblock); 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 asstr(value).flush(timeout=None): sends what is buffered and returnsTrue, orFalseiftimeoutseconds 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 activeClient, orNonebeforeinit.
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 (
DEBUGtodebug,INFOtoinfo,WARNINGtowarning,ERRORtoerror,CRITICALtocritical; a custom level such as 25 isinfo). Sereno also hasnotice,alertandemergency, whichloggingcannot produce. - The level of the root logger is not changed. Python's default is
WARNING, sologger.info(...)never reaches the handler until you configure it, for example withlogging.basicConfig(level=logging.INFO). Thelevelofinit(orSERENO_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 aslogger. - 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
serenologger (the SDK's own) are ignored, so a delivery failure can never loop back. - A logger with
propagate = Falsedoes not reach the root; attachsereno.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 levelerror. OnlyExceptionsubclasses are captured;KeyboardInterrupt,SystemExitand otherBaseExceptionpass untouched.
Independently of handlers, importing sereno registers an atexit function that flushes the buffer on a normal exit,
waiting up to 10 seconds.
SIGTERM:initinstalls a handler that flushes the buffer for up to 5 seconds and then lets the signal act as it would without Sereno. If you already had aSIGTERMhandler it is chained (called after the flush); otherwise the default action is restored and the signal is sent again, so the exit status stays the same. It is installed only wheninitruns in the main thread, never on Windows, and not ifSIGTERMis ignored (SIG_IGN) or its handler was installed from C (Python cannot read or chain it, so it is left alone). A handler you set afterinitreplaces it: callsereno.flush()in it. Passsigterm=Falsetoinitto skip it.
Things to know:
os._exit()andSIGKILLend the process without runningatexit, so what is still buffered is lost. Callsereno.flush()first where you can. The same goes for an out-of-memory kill and forSIGTERMwhen the handler is not installed.- 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 withexc_infoand 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.requestat levelerror. Responses with a status below 500 (404, 403, 400...) are not sent:DjangoHandlerdrops those records, also for the root handler ofinit(). - From the request, the context carries only
methodandroute(the URL pattern, or the concrete path without the query string when nothing matched). It also carriesloggerand the other fields of the log record (ondjango.request,status_code). Headers, cookies, body and query are never sent. init()andLOGGINGcan be in any order; the handler looks up the client ofinit()on each record.- A
LOGGINGwith arootkey replaces the root handlers, including the oneinit()added. Addsereno.SerenoHandlerto that root, or callinit()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 ininit()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 astatus_codebelow 500 are not sent. - The context carries only
methodandroute(the route pattern such as/users/{id}, with the prefix of aMount), or the path without the query string when nothing matched; never headers, body or query. It applies tohttpandwebsocketrequests. - 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), or its class and message (Class|message) when it has no
traceback. 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:
Enumby itsvalue;Decimalas a string (NaNand infinities asNAN/INFstrings);bytesdecoded as UTF-8 (invalid bytes become?).set,frozenset,tupleanddequeas lists.dateas midnight UTC (T00:00:00+00:00);datetimekeeps 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,
flushandclosesend 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,422or429) 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
urllibtimeout 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 (
inititself raisesTypeErrorfor an unknown keyword argument). If a batch is not delivered, or an event cannot be formatted, a message with the reason is written tostderrand 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.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sereno_sdk-0.2.0.tar.gz | 44.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sereno_sdk-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 72.2 kB
Release files / sereno_sdk-0.2.0.tar.gz
| Download URL | sereno_sdk-0.2.0.tar.gz |
|---|---|
| Size | 44.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e1a9a9bec6bb603d74996b0eef0cd0df166e84a1c03b882ef046de6dbd9cedc5
|
|
BLAKE2b-256 checksum How to use checksums |
cfcafc281ecf0b78fa1f23e6ddc0a24dde76b7f6b86d2dc59e6756614174ab9b
|
| 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 18, 2026.
Transparency logRelease files / sereno_sdk-0.2.0-py3-none-any.whl
| Download URL | sereno_sdk-0.2.0-py3-none-any.whl |
|---|---|
| Size | 28.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9433a5c748e2a9a402cbf3655e1f7c366a95efdaf08ada27a832b09b84213a65
|
|
BLAKE2b-256 checksum How to use checksums |
64fd8dea63e4e763925d5c2c94200f8fe817c9639856d39e87e7058868ef2ad4
|
| 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 18, 2026.
Transparency logSecurity for sereno-sdk 0.2.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.
Attestation for sereno_sdk-0.2.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 18, 2026.
Transparency logA 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.