Skip to content

artifactr.telemetry

Tracing and metrics through the OpenTelemetry API. See Observability.

artifactr's tracing and metrics, through the OpenTelemetry API only (ADR-0027).

The API is the port. artifactr records spans and metrics under the artifactr scope and never configures the SDK, calls Agent.instrument_all() or creates a backend client. With no SDK configured, recording is a no-op. Applications configure the SDK themselves, or with artifactr.otel.configure_telemetry (the [otel] extra), which is an adapter.

  • artifactr.telemetry.attributes names every attribute artifactr sets.
  • artifactr.telemetry.metrics is the metric registry and its cardinality policy.
  • artifactr.telemetry.traces names the spans in artifactr's traces, and runs polling untraced.

The metric registry

Metric dataclass

Metric(
    name: str,
    instrument: Instrument,
    unit: str,
    description: str,
    attributes: frozenset[str] = frozenset(),
    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 {command}.

required
description str

What it measures.

required
attributes frozenset[str]

The attributes it may carry. Anything else is dropped when it is recorded.

frozenset()
scope str

The instrumentation scope that records it: artifactr, 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] = MappingProxyType(
    {
        metric.name: metric
        for metric in (
            COMMANDS,
            COMMIT_DURATION,
            TURNS,
            TURN_DURATION,
            RUNS,
            TOOL_CALLS,
            TOKENS,
            MESSAGES,
            ARTIFACT_CHANGES,
            PROPOSALS,
            FEEDBACK,
            STREAM_CONNECTIONS,
            STREAM_DISCONNECTS,
        )
    }
)

artifactr's own metrics, by name.

EXTERNAL_METRICS module-attribute

EXTERNAL_METRICS: Mapping[str, Metric] = MappingProxyType(
    {
        metric.name: metric
        for metric in (
            Metric(
                "gen_ai.client.token.usage",
                "histogram",
                "{token}",
                "Tokens per model request, by model and type.",
                scope="pydantic-ai",
            ),
            Metric(
                "operation.cost",
                "histogram",
                "{USD}",
                "Estimated cost per model request, by model.",
                scope="pydantic-ai",
            ),
            Metric(
                "gen_ai.client.operation.time_to_first_chunk",
                "histogram",
                "s",
                "Time to the first chunk of a streamed model response.",
                scope="pydantic-ai",
            ),
            Metric(
                "http.server.request.duration",
                "histogram",
                "s",
                "HTTP requests served, REST and MCP.",
                scope="opentelemetry.instrumentation.fastapi",
            ),
            Metric(
                "http.server.active_requests",
                "up_down_counter",
                "{request}",
                "HTTP requests in progress.",
                scope="opentelemetry.instrumentation.fastapi",
            ),
            Metric(
                "http.client.request.duration",
                "histogram",
                "s",
                "Outgoing HTTP requests, model providers included.",
                scope="opentelemetry.instrumentation.httpx",
            ),
            Metric(
                "db.client.connections.usage",
                "up_down_counter",
                "{connection}",
                "Database connections in the pool, by state.",
                scope="opentelemetry.instrumentation.sqlalchemy",
            ),
        )
    }
)

Metrics recorded by pydantic-ai and the OpenTelemetry instrumentations that artifactr's dashboards read, by name. The HTTP metrics are the stable HTTP conventions' names.

MetricsDetail module-attribute

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 = 'artifactr'

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

SCOPED module-attribute

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

The tenancy attributes, which MetricsDetail limits.

Traces

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

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 artifactr's traces, besides the model calls: artifactr'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 artifactr's traces.

A sub-scope of one of TRACE_SCOPES, such as artifactr.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. artifactr polls storage this way, in subscriptions and the feedback mirror, so an idle application sends no traces; the work a poll finds, such as a commit, is traced where it happens.

Don't commit or publish in the block: its trace ids are the unsampled parent's, so a revision or envelope 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 attribute names artifactr puts on spans and metrics, in one place.

The OpenTelemetry GenAI conventions are still in development; if their names change, they change here. attribution builds the attributes every span artifactr owns or wraps carries: the thread as the session, the person as the user, and artifactr's own ids.

SESSION_ID module-attribute

SESSION_ID: Final = 'session.id'

The session: artifactr's thread id. Langfuse groups traces into sessions by it.

USER_ID module-attribute

USER_ID: Final = 'user.id'

The person a span acts for: a user actor's id.

ERROR_TYPE module-attribute

ERROR_TYPE: Final = 'error.type'

The class of an unexpected error.

GEN_AI_CONVERSATION_ID module-attribute

GEN_AI_CONVERSATION_ID: Final = 'gen_ai.conversation.id'

The GenAI conversation: artifactr's thread id, as pydantic-ai sets it on its own spans.

GEN_AI_OPERATION_NAME module-attribute

GEN_AI_OPERATION_NAME: Final = 'gen_ai.operation.name'

The GenAI operation, such as invoke_workflow.

GEN_AI_WORKFLOW_NAME module-attribute

GEN_AI_WORKFLOW_NAME: Final = 'gen_ai.workflow.name'

The name of a GenAI workflow: turn for artifactr's turns.

GEN_AI_TOOL_NAME module-attribute

GEN_AI_TOOL_NAME: Final = 'gen_ai.tool.name'

A tool's name.

GEN_AI_TOKEN_TYPE module-attribute

GEN_AI_TOKEN_TYPE: Final = 'gen_ai.token.type'

input or output, for token counts.

LANGFUSE_OBSERVATION_TYPE module-attribute

LANGFUSE_OBSERVATION_TYPE: Final = (
    "langfuse.observation.type"
)

How Langfuse shows a span, such as chain.

RUN_ID module-attribute

RUN_ID: Final = 'artifactr.run.id'

artifactr's run id, which spans a run's pauses. pydantic-ai's own run id is per attempt.

ACTOR_KIND module-attribute

ACTOR_KIND: Final = 'artifactr.actor.kind'

The kind of actor: user, agent, external_agent or system.

COMMAND_TYPE module-attribute

COMMAND_TYPE: Final = 'artifactr.command.type'

A command's type, such as edit_artifact.

OUTCOME module-attribute

OUTCOME: Final = 'artifactr.outcome'

What a command did: applied, proposed, resolved, recorded or rejected.

REJECTION module-attribute

REJECTION: Final = 'artifactr.rejection'

Why a command was rejected: the rejection's code, such as version_conflict.

ARTIFACT_KIND module-attribute

ARTIFACT_KIND: Final = 'artifactr.artifact.kind'

An artifact's registered type name.

ARTIFACT_VERSION module-attribute

ARTIFACT_VERSION: Final = 'artifactr.artifact.version'

The version an artifact is at after a change.

PATCH_KIND module-attribute

PATCH_KIND: Final = 'artifactr.patch.kind'

json_patch or text_edits.

PATCH_SIZE module-attribute

PATCH_SIZE: Final = 'artifactr.patch.size'

How many operations or text edits a patch has.

CHANGE module-attribute

CHANGE: Final = 'artifactr.change'

What happened to an artifact: created, changed or archived.

PROPOSAL_ACTION module-attribute

PROPOSAL_ACTION: Final = 'artifactr.proposal.action'

What happened to a proposal: created, accepted or rejected.

MESSAGE_KIND module-attribute

MESSAGE_KIND: Final = 'artifactr.message.kind'

What a posted message is: a message, or a notice.

TURN_TRIGGER module-attribute

TURN_TRIGGER: Final = 'artifactr.turn.trigger'

What started a turn: message or resume.

TURN_OUTCOME module-attribute

TURN_OUTCOME: Final = 'artifactr.turn.outcome'

How a turn ended: completed, paused, stopped or failed.

RUN_STATUS module-attribute

RUN_STATUS: Final = 'artifactr.run.status'

How a run segment ended: completed, paused, stopped or failed.

RUN_REASON module-attribute

RUN_REASON: Final = 'artifactr.run.reason'

Why a run segment failed, when it has a typed reason, such as guardrail_blocked.

TOOL_STATUS module-attribute

TOOL_STATUS: Final = 'artifactr.tool.status'

How a tool call ended: ok, retry or error.

FEEDBACK_TYPE module-attribute

FEEDBACK_TYPE: Final = 'artifactr.feedback.type'

A feedback type's registered name.

FEEDBACK_TARGET module-attribute

FEEDBACK_TARGET: Final = 'artifactr.feedback.target'

What feedback is about: artifact, thread, turn or message.

CLOSE_CODE module-attribute

CLOSE_CODE: Final = 'artifactr.stream.close_code'

The WebSocket close code a thread-protocol connection ended with.

attribution

attribution(
    *,
    tenant_id: TenantId,
    workspace_id: WorkspaceId,
    thread_id: ThreadId | None = None,
    run_id: RunId | None = None,
    actor: Actor | None = None,
    user: Actor | None = None,
) -> dict[str, AttributeValue]

Return the attributes that attribute a span to its tenant, workspace, thread and actor.

Parameters:

Name Type Description Default
tenant_id TenantId

The tenant.

required
workspace_id WorkspaceId

The workspace.

required
thread_id ThreadId | None

The thread, which is also the session.

None
run_id RunId | None

artifactr's run.

None
actor Actor | None

Who acts; its kind is recorded, and its id as the user when it is a person.

None
user Actor | None

The person the span acts for, when that is not actor (a person's message starts an agent's run). Defaults to actor.

None

Recording

These are what artifactr's components record with. Applications rarely need them, except annotate and attribution to attribute spans of their own.

Telemetry

Telemetry(
    *,
    tracer_provider: TracerProvider | None = None,
    meter_provider: MeterProvider | None = None,
)

The tracer and meter one artifactr component records with.

Workspaces, Runner and the surfaces each hold one, built from the providers they are given, or the global ones. Metrics are recorded only through add and record, which keep just the attributes the metric declares.

Parameters:

Name Type Description Default
tracer_provider TracerProvider | None

Where spans go. Defaults to the global tracer provider.

None
meter_provider MeterProvider | None

Where metrics go. Defaults to the global meter provider.

None

tracer instance-attribute

tracer = tracer_provider.get_tracer(SCOPE, VERSION)

The tracer for artifactr's spans.

meter instance-attribute

meter = meter_provider.get_meter(SCOPE, VERSION)

The meter for artifactr's metrics.

add

add(
    metric: Metric, amount: int, attributes: Attributes
) -> None

Add to a counter or an up-down counter.

record

record(
    metric: Metric, value: float, attributes: Attributes
) -> None

Record a value in a histogram.

attribution

attribution(
    *,
    tenant_id: TenantId,
    workspace_id: WorkspaceId,
    thread_id: ThreadId | None = None,
    run_id: RunId | None = None,
    actor: Actor | None = None,
    user: Actor | None = None,
) -> dict[str, AttributeValue]

Return the attributes that attribute a span to its tenant, workspace, thread and actor.

Parameters:

Name Type Description Default
tenant_id TenantId

The tenant.

required
workspace_id WorkspaceId

The workspace.

required
thread_id ThreadId | None

The thread, which is also the session.

None
run_id RunId | None

artifactr's run.

None
actor Actor | None

Who acts; its kind is recorded, and its id as the user when it is a person.

None
user Actor | None

The person the span acts for, when that is not actor (a person's message starts an agent's run). Defaults to actor.

None

annotate

annotate(
    attributes: Attributes, span: Span | None = None
) -> None

Set attributes on a span (the current one by default), skipping None values.

current_trace_id

current_trace_id() -> TraceId | None

Return the id of the current trace, or None when nothing is being traced.

current_traceparent

current_traceparent() -> str | None

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

record_events

record_events(
    telemetry: Telemetry,
    events: Iterable[KnownEvent],
    *,
    actor: Actor,
    scope: Attributes,
) -> None

Count what a batch of committed events did.

Parameters:

Name Type Description Default
telemetry Telemetry

Where to record.

required
events Iterable[KnownEvent]

The events one command or fact appended.

required
actor Actor

Who committed them.

required
scope Attributes

The tenancy attributes (tenant and workspace).

required

command_attributes

command_attributes(
    command: Command,
    *,
    tenant_id: TenantId,
    workspace_id: WorkspaceId,
    actor: Actor,
) -> dict[str, AttributeValue]

Return the attributes of a command's span: its type, what it is about, and who sent it.

They carry ids, kinds and sizes, never content.

outcome_attributes

outcome_attributes(
    outcome: Outcome,
) -> dict[str, AttributeValue]

Return the attributes that record what a command did.

VERSION module-attribute

VERSION: Final = version('artifactr-ai')

The version recorded with artifactr's instrumentation scope.