---
title: "Python Monitoring SDK"
description: "The Silly Development Python monitoring SDK: init() options, counters, gauges, timers, events, capturing exceptions, heartbeats and shutting down cleanly."
url: "https://sillydev.co.uk/docs/monitoring/python"
updated: "2026-10-14"
---

Monitoring

# Python SDK

Part of the \`sillydev\` package on PyPI. \`import sillydev\` does not load it: import \`monitoring\` from it when you want it.

[← Monitoring docs](https://sillydev.co.uk/docs/monitoring) · Updated 14 October 2026

**Browse the docs**

## init()

Call it once, as early as you can. Every argument is optional. Without a key (as an argument or in `SILLYDEV_MONITORING_KEY`) it does nothing. Calling it again replaces the previous setup.

`runtime_metrics` sends CPU, memory, garbage collection and thread counts, and the heartbeat. `capture_unhandled` records exceptions nothing caught, in the main thread and in other threads, without changing what Python does with them. `sample_rate` is the share of errors and events sent, from 0 to 1. `before_send` is called with the kind (`"error"`, `"event"` or `"slow"`) and the item, and returns the item, changed or not, or `None` to drop it. `debug` logs every payload to the `sillydev.monitoring` logger.

Python

```
from sillydev import monitoring

def scrub(kind, item):
    if kind == "error" and "card" in item.get("message", ""):
        return None  # never send this one
    return item

monitoring.init(
    key=None,                 # default: SILLYDEV_MONITORING_KEY
    service="orders-bot",     # default: SILLYDEV_MONITORING_SERVICE, then "default"
    environment="production", # default: SILLYDEV_MONITORING_ENVIRONMENT, then "production"
    release="1.4.2",          # default: SILLYDEV_MONITORING_RELEASE
    runtime_metrics=True,
    capture_unhandled=True,
    sample_rate=1.0,
    before_send=scrub,
    debug=False,
)
```

## Metrics

`increment(name, value=1, tags=None)` adds to a counter, `gauge(name, value, tags=None)` records the latest value of something, and `timing(name, ms, tags=None)` records a duration in milliseconds. `with monitoring.timer(name):` times a block and `@monitoring.timed(name)` times every call of a function or coroutine; a timed call slower than a second is also kept as a slow operation.

Names are lowercase letters, digits, dots and underscores, starting with a letter. Tags are up to 8 string pairs. Each distinct name and set of tags is one series, and a project has a limit on series, so tag by things with few values (a command, a plan) and never by a user or message id.

Python

```
monitoring.increment("orders.created", tags={"plan": "free"})
monitoring.gauge("queue.length", len(queue))
monitoring.timing("payment.duration", 182.5, tags={"provider": "stripe"})

with monitoring.timer("reports.build"):
    build_report()

@monitoring.timed("orders.sync")
async def sync_orders():
    ...
```

## Errors and events

`capture_exception(exc=None, tags=None, context=None)` reports an exception; with no argument, the one being handled. `capture_message(message)` reports something that is not an exception as an error. Context is any JSON-friendly dictionary shown on the issue page; values whose names look like passwords, tokens or cookies are replaced with `[redacted]`.

`event(name, attributes=None)` records something that happened, such as a deploy or a sign-up. Events are listed on the Events tab, and a `deploy` event is marked on the Overview charts.

Python

```
try:
    charge(order)
except PaymentError:
    monitoring.capture_exception(tags={"provider": "stripe"}, context={"order": order.id})

monitoring.capture_message("Stock fell below ten")
monitoring.event("deploy", {"version": "1.4.2"})
```

## Heartbeats, flushing and shutting down

With runtime metrics on, every send is a heartbeat. With them off, call `heartbeat()` from your main loop so the project knows the app is alive; an alert can tell you when it stops.

The SDK sends from a background thread every ten seconds, and at exit. `flush(timeout=2.0)` sends everything now and returns whether it all went; `shutdown()` flushes and stops. A short script that exits straight after its work should call `flush()` first.

For asyncio apps that do not use one of the integrations, `monitoring.instrument_loop()` measures the running loop's lag and records exceptions in tasks nothing awaited.

Read next

- [Integrations One line each for the common Python and JavaScript frameworks and Discord libraries.](https://sillydev.co.uk/docs/monitoring/integrations)
- [Metrics and events Counting, timing and recording what matters to your project.](https://sillydev.co.uk/docs/monitoring/metrics-and-events)
- [JavaScript SDK Every function in \`sillydev/monitoring\`, for Node.js and Bun.](https://sillydev.co.uk/docs/monitoring/javascript)
