Runtime Wiring
In normal application code, telemetry is usually driven by runtime events rather than manualopenRoot() calls.
The pattern is:
- create one shared event dispatcher
- create one shared
Telemetryinstance - create the projectors for the runtimes you use
- attach them through
RuntimeEventBridge - build your runtime objects with the same event dispatcher
Minimal Wiring Example
$events into the runtime objects that should emit telemetry.
Which Projectors To Add
Add only the projectors for the packages you actually use:- agents:
AgentsTelemetryProjector - agent control:
AgentCtrlTelemetryProjector - instructor:
InstructorTelemetryProjector - polyglot:
PolyglotTelemetryProjector - http client:
HttpClientTelemetryProjector
Metric Catalog
Each projector also emits metrics throughTelemetry::metric(), using the
canonical types from Cognesy\Metrics\Data\* (Counter, Gauge, Histogram,
Timer). This is what the two runtime projectors emit today.
PolyglotTelemetryProjector
PolyglotTelemetryProjector emits no Gauge. There is no point-in-time resource
level on the inference path — per-call sizes are distributions, not levels — and
forcing one would misrepresent the data. Gauges belong where a loop actually
carries state, which is the agents projector below.
AgentsTelemetryProjector
Type Policy
- event and failure totals ->
Counter - durations ->
Timer - point-in-time state (current context size, current subagent nesting depth) ->
Gauge - distributions (token counts, steps per run) ->
Histogram
Timer::create() throws on a negative duration, so every duration Timer above
is guarded: a projector must not let an observability path take down the call
it is observing just because a clock went backwards. Each Timer is skipped, not
clamped, when the duration is missing or negative.
Tag Discipline
Metric tags are aggregation dimensions, not span attributes, so every tag value must be low-cardinality: tool names, subagent names, statuses, outcomes, booleans. Per-run identifiers (agent.id, execution ids) belong on spans, not
tags — one tag value per run means one time series per run in the metrics
backend.
inference.client.token.usage.* is the single deliberate exception, and it is
exempt from the rule as a whole rather than for one tag: it is a
correlation-carrying metric, not an aggregation-friendly one. It carries
inference.execution.id because LangfusePayloadMapper::matchesMetric()
correlates a metric back to its span by that id, which already makes it one
series per run — so the agent.id the agents projector also puts on it costs
nothing further and stays useful for grouping token spend by agent. Every other
metric above carries no id tag at all, so it never matches an observation and
never reaches attributesForMetric() — which returns [] for unknown names
anyway.
Cognesy\Telemetry\Domain\Metric\MetricNames holds the token-usage metric names
and the inference.execution.id / inference.usage.final tag keys shared by
PolyglotTelemetryProjector, AgentsTelemetryProjector and
LangfusePayloadMapper, so the three cannot drift apart. Metric names used by a
single producer (agent.*, inference.embeddings.*) stay private to that
producer and are not in MetricNames.
Practical Examples
The examples directory has working end-to-end setups:examples/D05_AgentTroubleshooting/TelemetryLangfuse/run.phpexamples/D05_AgentTroubleshooting/TelemetryLogfire/run.phpexamples/D05_AgentTroubleshooting/SubagentTelemetryLangfuse/run.phpexamples/D05_AgentTroubleshooting/SubagentTelemetryLogfire/run.php