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 docsUpdated

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