Audit and reports

Three things a feature tells the people who run the site: what an account did (the audit trail), that a service it depends on failed and it carried on without it (a degraded report), and, for the probes, which readiness check failed. The framework holds the API and does nothing with it on its own; an optional feature stores the trail (splent_feature_audit) or alerts someone (splent_feature_observability). A feature calls the API unconditionally and never imports the feature behind it.

Table of contents

  1. The audit trail
    1. What the framework adds
    2. Inside a write
    3. The recorder
  2. Degraded services
  3. The request id
  4. Readiness listeners

The audit trail

from splent_framework.audit import record_event

record_event(
    "token.revoked",
    target_type="api_token",
    target_id=token.id,
    data={"scopes": token.scopes},
)
Argument Means
action area.verb in small letters: account.deleted, dataset.withdrawn. Anything else raises ValueError, with or without a recorder, so the mistake shows in the tests of a product without the trail
target_type, target_id What it was done to. The id is kept as text
actor Who did it: an account (anything with an id), an account’s id or "system". Left out, the account signed in to the request, anonymous when nobody is, and system outside a request
outcome success (the default), failure or denied
data The details, as JSON values. A key that names a credential (password, token, api_key, secret, authorization…) keeps no value, and past 4096 characters only the keys are kept
session The session of an uncommitted write the event belongs to

The call returns True when a recorder kept the event, False when none is registered or the recorder failed. A recorder that fails is logged, never raised: the action the event describes has happened, or is about to.

What the framework adds

The client’s address as the app sees it (behind SPLENT_TRUSTED_PROXIES), its user agent, the request id, the method and the endpoint. Never the URL, which may carry a token (a reset link); the endpoint says the same without it.

Inside a write

A receiver of auth’s user-deleting runs while the session flushes the deletion. Passing that session makes the recorder add the event to it, so it is written in the same flush and rolled back with it: a deletion the database refuses leaves no event behind.

@connect_signal("user-deleting", "splent_feature_tokens")
def revoke(sender, user, session, **kwargs):
    ...
    record_event("token.revoked", target_type="user", target_id=user.id, session=session)

Without session, the recorder keeps the event at once, in a transaction of its own, whatever the caller’s session does next.

The recorder

A feature that stores the trail registers one function per app, from its init_feature:

from splent_framework.audit import register_audit_recorder

register_audit_recorder(record, app)

record(event) receives an AuditEvent (action, outcome, actor, actor_id, target_type, target_id, data, ip, user_agent, request_id, method, endpoint, occurred_at in UTC, session). audit_enabled() says whether one is registered.


Degraded services

A feature that catches a failure of a service it depends on and carries on without it says so, instead of a warning in a log nobody reads:

from splent_framework.observability import report_degraded

try:
    client.index(dataset)
except ConnectionError as exc:
    report_degraded("explore", "The search index is unreachable.", exc=exc, dataset_id=dataset.id)

The framework logs a warning, with the traceback when there is an exception, and hands a Degradation (feature, message, exc, context, request_id, occurred_at) to every reporter registered with register_degradation_reporter(name, reporter, app). A reporter that fails is logged and the others still run; the call never raises.

Keep message the same every time the problem happens and free of personal data and secrets. It is what an alert says, and what two reports share to count as one problem. What varies goes in the keyword arguments, which a reporter may drop.


The request id

from splent_framework.observability import request_id

The id of the request being served: the X-Request-ID a reverse proxy set (nginx’s $request_id, with proxy_set_header X-Request-ID $request_id;) when it looks like an id (8 to 128 letters, digits, ., _, : or -), otherwise one made for the request and kept for the rest of it. Outside a request it is empty. The audit trail and the degraded reports carry it, and so can a log line or an error page.


Readiness listeners

/readyz answers yes or no, after every check registered with register_readiness_check, and keeps which one failed in the log. The web service’s healthcheck asks it (see product:build), and so does autodeploy. A feature that alerts someone registers a listener, told after every probe with the names of the checks that failed, an empty list when the app is ready:

from splent_framework.health import register_readiness_listener

register_readiness_listener("alerts", on_probe, app)
  • A check that had not answered when the probe gave up (SPLENT_READY_TIMEOUT) counts as failed.
  • The listener runs in the probe’s thread, in an app context. It returns at once and hands anything slow (a message to send) to a thread of its own.
  • A listener that raises is logged, and the answer stays what the checks said.
  • Calling run_readiness_checks(app) directly tells no listener; only a probe does.

The listener hears only the probes something asks for: in a deployment, the container’s healthcheck.


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es