Skip to main content

observe-kit

PyPI Python CI Coverage License PRs welcome

One decorator, @observed, that times a call, decides how it ended, writes a structlog line and emits an event to a sink you choose. It works on plain and async def functions and methods. Its only dependency is structlog.

pip install observe-kit
from observe_kit import observed


class Billing:
    def __init__(self, log, sink, notifier):
        self.log, self.sink, self.notifier = log, sink, notifier

    @observed("billing.charge", expected=(CardDeclined,), fields=("customer_id",))
    def charge(self, customer_id: str, cents: int) -> Receipt: ...

Each call to charge then produces one log line and one event:

billing.charge.finished   customer_id=c_42 duration_ms=183
billing.charge.expected   customer_id=c_42 duration_ms=95  error="CardDeclined: insufficient funds"

Four outcomes

Every call ends in exactly one of them. You decide which exceptions are which, so there is no bare except Exception: pass anywhere in your code.

outcome when logged at then
finished the call returned info (level="debug" for chatty calls) the value is returned
expected raised one of expected= warning re-raised for the caller
swallowed raised one of swallow= debug default= is returned
raised raised any other Exception error, with traceback the notifier is told, then re-raised

A type listed in both expected and swallow counts as expected. BaseExceptions that are not Exceptions (KeyboardInterrupt, SystemExit, asyncio.CancelledError) pass through untouched unless you list them. The re-raise is a bare raise, so tracebacks gain no frame from the decorator.

Arguments

@observed(
    "orders.ship",              # event root; the outcome is appended: orders.ship.finished
    expected=(OutOfStock,),     # normal operation, re-raised
    swallow=(KeyError,),        # "it wasn't there", replaced by default
    default=None,
    fields=("order_id",),       # argument names added to the log line and the event
    detail="carrier",           # one argument kept as a string on the event, not in the log context
    level="info",               # log level of `finished`
    notify_policy=POLICY,       # what a `raised` notification may carry (below)
)

fields and detail are resolved against the signature, so positional and keyword arguments both work. duration_ms is measured with perf_counter and, for async def, covers the whole await. Generators are refused at decoration time: the call returns before any work happens, so the timing would mean nothing. Decorate the function that consumes the generator instead.

Where the logger, sink and notifier come from

observed looks at the first argument (self for a method) for these attributes, and falls back to a default when one is missing or has the wrong shape:

attribute expected shape fallback
log a structlog logger (has .bind) structlog.get_logger(module)
sink an EventSink: emit(event) NullSink, which drops events
notifier a Notifier: error(title, text) none; nobody is told
observe_context a mapping, e.g. {"run_id": 7} {}

observe_context is bound onto every log line and event from that instance. Plain functions get the fallbacks, so @observed on a module-level function just logs.

Events and sinks

Each outcome becomes an ObservedEvent:

ObservedEvent(
    name="billing.charge",
    outcome=CallOutcome.EXPECTED,
    duration_ms=95,
    error="CardDeclined: insufficient funds",  # None when finished
    context={"customer_id": "c_42"},
    detail=None,
)
event.event  # "billing.charge.expected"

A sink is anything with emit(event). Write one that increments a Prometheus counter, inserts a row into a table, or pushes to a queue:

class CountingSink:
    def __init__(self, counter):
        self.counter = counter

    def emit(self, event):
        self.counter.labels(event.name, event.outcome).inc()

Testing with MemorySink

MemorySink keeps every event in a list, so tests assert on what happened instead of parsing logs:

from observe_kit import CallOutcome, MemorySink


def test_declined_card_is_expected():
    sink = MemorySink()
    billing = Billing(log=structlog.get_logger(), sink=sink, notifier=None)

    with pytest.raises(CardDeclined):
        billing.charge("c_42", 500)

    [event] = sink.named("billing.charge")
    assert event.outcome is CallOutcome.EXPECTED

Notifications and NotifyPolicy

On raised, the instance's notifier gets a title ("billing.charge raised TimeoutError") and a short text. Alerts end up in chat apps, phones and mailboxes, so the text is deliberately thin:

  • only context fields you allow travel; the rest are counted, never shown;
  • only the first line of the error travels; the lines after it are counted;
  • URLs in that line are replaced by <url withheld>.

The default policy allows no fields. Set your own once and pass it where you decorate:

from functools import partial
from observe_kit import NotifyPolicy, observed as _observed

POLICY = NotifyPolicy(
    fields=frozenset({"run_id", "count", "duration_ms"}),
    withheld_hint="see logs/app.jsonl",
)
observed = partial(_observed, notify_policy=POLICY)
billing.charge raised TimeoutError

app.billing.Billing.charge
TimeoutError: Page.goto: Timeout 30000ms exceeded. (+2 line(s) withheld)
run_id=7 (1 field(s) withheld — see logs/app.jsonl)

The full error, traceback and context are still in the log line and the event; only the notification is trimmed.

Lineage

observe-kit is a rewrite of atlassian-labs/observe, which I wrote at Atlassian in 2020. It keeps the idea and the Apache-2.0 license; the code is new. See NOTICE.

License

Apache-2.0

Release files for observe-kit 0.1.1

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

Source distribution (sdist)

Source distribution for observe-kit 0.1.1
File Size Uploaded
observe_kit-0.1.1.tar.gz 69.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for observe-kit 0.1.1
File Interpreter ABI Platform
observe_kit-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 86.8 kB

Release files / observe_kit-0.1.1.tar.gz

Download URL observe_kit-0.1.1.tar.gz
Size 69.4 kB
Tags Source
SHA-256 checksum
How to use checksums
8af67c32419e7074bebfb9501727ebf4497f17eecd1145b8c02e35aeb3e42e71
BLAKE2b-256 checksum
How to use checksums
967a6f86f8469c523c9e2e27a456d5e5bd88cfe5c15ad456fbeb59b300a3bfda
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 Sep 25, 2026.

Transparency log

Release files / observe_kit-0.1.1-py3-none-any.whl

Download URL observe_kit-0.1.1-py3-none-any.whl
Size 17.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
06345556176d948805ccde3355dbae5ebc2d4b454283ad8ab15bcf9dfb551f8d
BLAKE2b-256 checksum
How to use checksums
c7e67319065381770ee5b4d5a16bb6d510006ca809cd3e494542e4c6daf7e1ed
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 Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.1 This release

2 release files

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