Skip to content

Observability

Opt-in, dependency-free logging + tracing. openvc emits structured events on logging.getLogger("openvc") at the resolve / fetch / status / verify boundaries and wraps each in an optional span you can wire to OpenTelemetry. Both are off by default and never carry secrets.

openvc.observability

openvc.observability — optional, dependency-free logging + tracing hooks.

On a production failure an operator otherwise gets only an exception and no trace of which hop or check produced it. openvc emits structured events on the stdlib logger logging.getLogger("openvc") at the resolve / fetch / status / verify boundaries, and wraps each in an optional tracing :func:span an integrator can wire to OpenTelemetry via :func:set_span_hook.

Both are off by default and dependency-free: the logger is silent until the application attaches a handler and sets a level (standard stdlib logging — openvc never configures handlers or calls basicConfig), and the span hook is a no-op until installed. No tracing dependency enters core.

Never logs secrets. Events and span attributes carry public identifiers only — the credential format, an issuer / subject DID, the DID or URL host being resolved, and a check's outcome. Private-key material, token bytes, proofValue, SD-JWT disclosures and claim contents are never logged.

Enable logs::

import logging; logging.getLogger("openvc").setLevel(logging.DEBUG)

Wire tracing to OpenTelemetry::

from opentelemetry import trace
from openvc.observability import set_span_hook
tracer = trace.get_tracer("openvc")
set_span_hook(lambda name, attrs: tracer.start_as_current_span(name, attributes=attrs))

set_span_hook(hook)

Install a span factory called around each resolve / fetch / status / verify boundary — e.g. one that opens an OpenTelemetry span. Pass None to reset to the no-op default. The hook receives (name, attributes) where attributes are public identifiers only (never keys/tokens); the returned context manager is entered for the duration of the operation (so it also observes any exception on exit).

Source code in src/openvc/observability.py
60
61
62
63
64
65
66
67
def set_span_hook(hook: SpanHook | None) -> None:
    """Install a *span factory* called around each resolve / fetch / status / verify
    boundary — e.g. one that opens an OpenTelemetry span. Pass ``None`` to reset to the
    no-op default. The hook receives ``(name, attributes)`` where *attributes* are public
    identifiers only (never keys/tokens); the returned context manager is entered for the
    duration of the operation (so it also observes any exception on exit)."""
    global _span_hook
    _span_hook = hook if hook is not None else _noop_span

span(name, **attributes)

A tracing span around an operation — a no-op unless a hook is installed via :func:set_span_hook. Pass only public identifiers as attributes (never secrets).

Observability never changes a verification outcome. A hook that errors on enter or exit is logged and ignored, and a hook can not suppress the wrapped operation's exception — it always propagates regardless of the hook's __exit__ return value — so a buggy tracing hook can never turn a fail-closed check (e.g. an unreachable status list) into a fail-open one.

Source code in src/openvc/observability.py
70
71
72
73
74
75
76
77
78
79
80
81
82
def span(name: str, **attributes: Any) -> ContextManager[Any]:
    """A tracing span around an operation — a no-op unless a hook is installed via
    :func:`set_span_hook`. Pass only public identifiers as *attributes* (never secrets).

    **Observability never changes a verification outcome.** A hook that errors on enter or
    exit is logged and ignored, and a hook can *not* suppress the wrapped operation's
    exception — it always propagates regardless of the hook's ``__exit__`` return value —
    so a buggy tracing hook can never turn a fail-closed check (e.g. an unreachable status
    list) into a fail-open one."""
    hook = _span_hook
    if hook is _noop_span:
        return _NULLCTX
    return _guarded_span(hook, name, attributes)