Skip to content

reflexr.telemetry

Tracing and metrics through the OpenTelemetry API. See Observability.

reflexr's OpenTelemetry instrumentation: spans, the metric registry and attribute names.

reflexr depends on the OpenTelemetry API only and never configures the SDK (ADR-0018). Pass tracer and meter providers to Workspaces, or configure the global ones, to see its spans and metrics. reflexr.telemetry.traces names the spans in reflexr's traces, and runs polling untraced (ADR-0040).

The metric registry

Metric dataclass

Metric(
    name: str,
    instrument: Instrument,
    unit: str,
    description: str,
    attributes: frozenset[str] = SCOPED,
    scope: str = SCOPE,
    buckets: tuple[float, ...] | None = None,
)

One metric: what it measures, and the attributes it may carry.

Parameters:

Name Type Description Default
name str

The OpenTelemetry metric name.

required
instrument Instrument

The instrument it is recorded with.

required
unit str

The UCUM unit, or an annotation in braces such as {run}.

required
description str

What it measures.

required
attributes frozenset[str]

The attributes it may carry, tenant and workspace included.

SCOPED
scope str

The instrumentation scope that records it: reflexr, or the library whose metric a dashboard reads.

SCOPE
buckets tuple[float, ...] | None

For a histogram, the bucket boundaries it advises the SDK to use.

None

prometheus_name property

prometheus_name: str

The metric's name in Prometheus, as the OTLP translation writes it.

Dots become underscores, a unit of time or size is appended as a word (annotations in braces are not), and counters end in _total.

prometheus_series property

prometheus_series: frozenset[str]

Every series name a Prometheus query may use for this metric.

METRICS module-attribute

METRICS: Mapping[str, Metric] = {
    metric.name: metric
    for metric in (
        EVENTS_PUBLISHED,
        EVALUATION_LAG,
        EVALUATION_DURATION,
        FIRINGS,
        RULE_ERRORS,
        RUNS,
        RUN_DURATION,
        RUN_ATTEMPTS,
        DEAD_LETTERS,
        SCHEDULE_TICKS,
        FEEDBACK,
        STREAM_CONNECTIONS,
        STREAM_DISCONNECTS,
    )
}

Every metric reflexr records, by name.

EXTERNAL_METRICS module-attribute

EXTERNAL_METRICS: Mapping[str, Metric] = {
    metric.name: metric
    for metric in (
        Metric(
            "gen_ai.client.token.usage",
            "histogram",
            "{token}",
            "Tokens per model request, by model and type.",
            frozenset(),
            scope="pydantic-ai",
        ),
        Metric(
            "operation.cost",
            "histogram",
            "{USD}",
            "Estimated cost per model request, by model.",
            frozenset(),
            scope="pydantic-ai",
        ),
    )
}

Metrics recorded by others that reflexr's dashboards read, by name: pydantic-ai's, per model request of an agent action. They carry the model and token type, not reflexr's attributes.

Instrument

Instrument = Literal[
    "counter", "up_down_counter", "histogram", "gauge"
]

The kind of OpenTelemetry instrument a metric is recorded with.

MetricsDetail

MetricsDetail = Literal['workspace', 'tenant', 'none']

How much tenancy detail metrics keep: tenant and workspace, the tenant only, or neither.

kept_attributes

kept_attributes(
    metric: Metric, detail: MetricsDetail
) -> frozenset[str]

Return the attributes of metric that a deployment keeps at a level of detail.

SCOPE module-attribute

SCOPE: Final = 'reflexr'

The instrumentation scope of reflexr's own spans and metrics.

SCOPED module-attribute

SCOPED: Final = frozenset({a.TENANT_ID, a.WORKSPACE_ID})

The tenancy attributes, which MetricsDetail limits.

Traces

Which spans are reflexr's, and polling that makes no traces. See ADR-0040.

TRACE_SCOPES module-attribute

TRACE_SCOPES: Final = frozenset(
    {
        SCOPE,
        "pydantic-graph",
        "mcp-python-sdk",
        "opentelemetry.instrumentation.fastapi",
        "opentelemetry.instrumentation.asgi",
        "opentelemetry.instrumentation.sqlalchemy",
        "opentelemetry.instrumentation.asyncpg",
        "opentelemetry.instrumentation.httpx",
    }
)

The instrumentation scopes whose spans make up reflexr's traces, besides the model calls: reflexr's own, pydantic-graph's, the MCP SDK's, and the FastAPI, ASGI, SQLAlchemy, asyncpg and httpx instrumentations'.

is_trace_scope

is_trace_scope(scope: str) -> bool

Return whether spans of an instrumentation scope belong in reflexr's traces.

A sub-scope of one of TRACE_SCOPES, such as reflexr.workspace, does too.

untraced

untraced() -> Generator[None]

Run a block untraced: every span started in it is a child of a span that is never sampled.

Under a parent-based sampler, the SDK's default, those spans are not recorded, so nothing started in the block is exported: the database queries of a poll, say. Metrics recorded in the block are recorded as usual, the instrumentations' own included. reflexr polls storage this way, in the reactor, subscriptions and the feedback mirror, so an idle application sends no traces; the work a poll finds, such as an evaluation or a run attempt, is traced where it happens.

Don't commit or publish in the block: its trace ids are the unsampled parent's, so an envelope or a run would record a trace that does not exist. A sampler that ignores the parent, such as always_on or traceidratio, records the block's spans again, each poll in a trace of its own.

Attributes

The names of the attributes on reflexr's spans and metrics.

They live in one module because the OpenTelemetry GenAI conventions are still in development: if a name changes, it changes here. reflexr's own names match artifactr's, with reflexr. in place of artifactr..

USER_ID module-attribute

USER_ID = 'user.id'

Set when a person published or acted.

DUPLICATE module-attribute

DUPLICATE = 'reflexr.event.duplicate'

Whether a publish found its event id already in the log and appended nothing.

RUN_REASON module-attribute

RUN_REASON = 'reflexr.run.reason'

A stable code for why a run attempt failed; bounded, since reasons are codes.

CHECKPOINT_REASON module-attribute

CHECKPOINT_REASON = 'reflexr.checkpoint.reason'

Why a run's checkpoint was not resumed: a stable code.

CHECKPOINT_ERROR module-attribute

CHECKPOINT_ERROR = 'reflexr.checkpoint.error'

What in a run's checkpoint did not validate, without the values.

EVALUATED_RULES module-attribute

EVALUATED_RULES = 'reflexr.evaluation.rules'

The rules that evaluated new envelopes in an evaluation pass.

CLOSE_CODE module-attribute

CLOSE_CODE = 'reflexr.stream.close_code'

How a WebSocket connection ended.

SESSION_ID module-attribute

SESSION_ID = 'session.id'

The causal chain's correlation id. Langfuse groups a session's traces by it.

CONVERSATION_ID module-attribute

CONVERSATION_ID = 'gen_ai.conversation.id'

The same value under the GenAI name, which pydantic-ai sets on agent spans.

Recording

These are what reflexr's components record with. Applications rarely need them, except the attribute helpers to attribute spans of their own.

Telemetry

Telemetry(
    *,
    tracer_provider: TracerProvider | None = None,
    meter_provider: MeterProvider | None = None,
    stored_namespaces: Set[str] = frozenset(),
)

reflexr's tracer, and an instrument for every metric in the registry.

Tenant and workspace are always recorded; a deployment that wants less detail says so to the SDK, with reflexr.otel.metric_views.

Parameters:

Name Type Description Default
tracer_provider TracerProvider | None

Where spans go. Defaults to the global provider.

None
meter_provider MeterProvider | None

Where metrics go. Defaults to the global provider.

None
stored_namespaces Set[str]

The rule namespaces of stored rules, whose names code does not bound. Metrics record a rule in one as the namespace, such as chat:*.

frozenset()

rule_attribute

rule_attribute(rule: RuleName) -> str

Return what metrics record as a rule's reflexr.rule.

That is its name, or for a stored rule its namespace, such as chat:*, so tenants' rules share a series per namespace.

record

record(
    metric: Metric,
    value: float,
    *,
    tenant_id: TenantId,
    workspace_id: WorkspaceId,
    attributes: Attributes | None = None,
) -> None

Record a measurement of a registered metric.

A rule is recorded as rule_attribute says.

Raises:

Type Description
KeyError

If the metric is not in the registry.

ValueError

If an attribute is not one the metric declares.

workspace_attributes

workspace_attributes(
    tenant_id: TenantId, workspace_id: WorkspaceId
) -> dict[str, str]

Return the span attributes that attribute work to a workspace.

actor_attributes

actor_attributes(actor: Actor) -> dict[str, str]

Return the span attributes that attribute work to an actor.

chain_attributes

chain_attributes(correlation_id: str) -> dict[str, str]

Return the span attributes that put a span in its causal chain's session.

current_trace_id

current_trace_id() -> str | None

Return the current span's trace id as 32 hex digits, if there is a valid span.

current_traceparent

current_traceparent() -> str | None

Return the W3C trace context of the current span, if there is one.

parse_traceparent

parse_traceparent(
    traceparent: str | None,
) -> SpanContext | None

Return the span context a W3C traceparent names, if it is valid.