Telemetry Data Reference
Reference for every metric, span, and attribute Droid exports over OpenTelemetry, in both the droid.* and GenAI semantic convention formats.
This page is the reference for what arrives at your collector once OTEL export is configured. The export format decides which of the two surfaces you receive.
Export formats
Your collector can receive one of two conventions. Factory's own collector receives the droid.* metric catalog and is unaffected by this choice.
| Format | Your collector receives |
|---|---|
legacy (default) | The droid.* metric catalog, plus the droid.message.* and droid.tool.* event spans when message content logging is on. |
genai | The OpenTelemetry GenAI semantic conventions: gen_ai.* spans and gen_ai.* metrics, consumable by off-the-shelf GenAI dashboards. |
Set it centrally with telemetry.format in org-managed settings, or per machine with OTEL_TELEMETRY_FORMAT. The organization's value wins: where telemetry.format is set to anything at all, the environment variable is not read, and an unrecognized value falls back to legacy rather than to the machine. This keeps a developer's shell from switching an organization's collector onto a different convention.
The two formats replace each other; they do not stack. Under genai, your collector receives no droid.* metric and no droid.message.* / droid.tool.* span at all. Rebuild dashboards before you switch an organization over, and expect the switch to be visible as a gap in any droid.* query.
Durations also change unit. Every gen_ai.* duration is in seconds, per the semantic conventions. Every droid.* duration is in milliseconds.
Exported metrics
Under the default legacy format, all metrics use the droid.* namespace. Every instrument declares a unit.
| Metric | Instrument | Unit | Meaning |
|---|---|---|---|
droid.code.files_modified | Counter | {file} | Files created or updated by an edit tool. |
droid.code.files_read | Counter | {file} | Files read. |
droid.code.lines_modified | Counter | {line} | Lines added and removed, split by an operation attribute. |
droid.git.commits | Counter | {commit} | Commits created. |
droid.git.pull_requests | Counter | {pull_request} | Pull requests created. |
droid.tool.invocations | Counter | {invocation} | Tool executions. |
droid.tool.execution_time | Histogram | ms | Tool execution duration. |
droid.command.blocked | Counter | {command} | Commands blocked by policy. Carries the tool name only, never the command text. |
droid.mcp.tool_invocations | Counter | {invocation} | MCP tool calls. |
droid.skill.invocations | Counter | {invocation} | Skill runs. |
droid.skill.installed | Counter | {skill} | Inventory of installed non-builtin skills, emitted once per session. |
droid.hook.invocations | Counter | {invocation} | Hook commands run. |
droid.slash_command.invocations | Counter | {invocation} | Slash commands run. |
droid.auth.login_success | Counter | {login} | Successful logins. |
droid.session.count | Counter | {session} | Sessions started. Compaction successors and forks are not counted. |
droid.repo.metadata | Counter | {repository} | One per workspace, carrying repository metadata as attributes. |
droid.skill.installed was previously emitted once per model call, which multiplied the count by the number of calls in a session. It is now emitted once per session, so values recorded before Droid 0.199.0 are inflated and are not comparable with later ones.
Common attributes
Every data point includes these attributes automatically:
| Attribute | Description |
|---|---|
user.id | Authenticated user ID. Removed under aggregate granularity. |
user.email | Authenticated user email. Removed under aggregate granularity. |
organization.id | Organization ID |
session.id | Current Droid session ID |
model.id | Active model ID (when available) |
parent_session.id | Parent session, when DROID_PARENT_SESSION_ID is set |
Tool-specific attributes (tool.name, mcp.server, skill.name, etc.) are included where applicable. Resource attributes include service.name (cli), service.version, os.type, os.version, host.arch, and terminal.type, plus telemetry.granularity when the organization runs in aggregate mode.
GenAI semantic convention export
Set telemetry.format to genai and your collector receives the OpenTelemetry GenAI semantic conventions instead of the droid.* surface. This is what off-the-shelf GenAI dashboards expect, so you can point one at Droid without writing custom queries.
Spans
Each user turn produces one flat trace: a root span with the model calls and tool runs as direct children. Tool spans are siblings of model calls, never nested inside them.
| Span | Kind | Parent |
|---|---|---|
invoke_agent droid | Internal | None. This is the trace root. |
chat <model> | Client | invoke_agent droid |
execute_tool <tool> | Internal | invoke_agent droid |
Attributes, by span:
| Attribute | Span | Notes |
|---|---|---|
gen_ai.operation.name | all | invoke_agent, chat, or execute_tool. |
gen_ai.agent.name | root | Always droid. |
gen_ai.conversation.id | all | The Droid session ID. |
session.id | all | The same value, kept under its general-convention name so you can join against metrics. |
error.type | all | Present on failure. On a tool span, the literal rejected means the call was denied by policy rather than failed. |
gen_ai.request.model | chat | Omitted when no model ID is known. |
gen_ai.provider.name | chat | See Provider names. |
gen_ai.response.finish_reasons | chat | An array of one string. |
server.address, server.port | chat | The model API host. The port is omitted when the scheme default applies. |
gen_ai.tool.name, gen_ai.tool.call.id | execute_tool | A model-authored name or ID that fails a shape check is replaced with invalid_tool_name or invalid_tool_use_id. |
gen_ai.tool.type | execute_tool | Always function. |
gen_ai.input.messages | root | Message content only. See Message content logging. |
gen_ai.output.messages | chat | Message content only. |
gen_ai.tool.call.arguments, gen_ai.tool.call.result | execute_tool | Message content only. |
gen_ai.input.messages and gen_ai.output.messages are JSON strings holding the conventions' message array:
[{ "role": "user", "parts": [{ "type": "text", "content": "..." }] }]Metrics
Every GenAI instrument is a histogram, including the two call counts. To total tool calls, sum the histogram rather than reading a counter.
| Metric | Unit | Meaning |
|---|---|---|
gen_ai.client.operation.duration | s | Model call duration. |
gen_ai.client.operation.time_to_first_chunk | s | Model call time to first streamed chunk. |
gen_ai.execute_tool.duration | s | Tool execution duration. |
gen_ai.invoke_agent.duration | s | Turn duration. |
gen_ai.invoke_agent.inference_calls | {inference_call} | Model calls per turn. |
gen_ai.invoke_agent.tool_calls | {tool_call} | Tool calls per turn. |
Datapoints carry gen_ai.operation.name, plus gen_ai.request.model and gen_ai.provider.name on the model instruments, gen_ai.tool.name on the tool instrument, gen_ai.agent.name on the turn instruments, and error.type on failures. The common attributes are stamped on top of those.
Provider names
gen_ai.provider.name is the model vendor, derived from the model ID prefix, not Factory's routing provider. It resolves to one of anthropic, openai, gcp.gemini, x_ai, deepseek, or mistral_ai. A model whose vendor has no value in the conventions gets no provider attribute rather than a guessed one, so do not build a dashboard that requires the attribute to be present.
Two behaviors will otherwise look like gaps in your data:
- A denied tool call produces a span but no duration datapoint. The call never ran, and a zero-second measurement would distort the distribution of the calls that did. Denied calls still count toward
gen_ai.invoke_agent.tool_calls. - Subagent turns are separate trace roots. The
parent_session.idlinkage that thedroid.*surface carries has no equivalent in the GenAI conventions, so a subagent's trace cannot be joined to its parent's under this format.
Resource attributes
To attach your own identity or lineage attributes (for example team.id, repo, environment), set the standard OTEL variable per machine:
export OTEL_RESOURCE_ATTRIBUTES="team.id=platform,environment=ci"Your pairs reach your collector only. They are never sent to Factory's, including a pair that spoofs an identity key.
They are applied in three places:
- On the resource of exported metrics.
- On the resource of exported spans.
- Spread onto individual metric datapoint attributes, which is legacy behavior kept because existing dashboards query it. There is no span-attribute equivalent.
Two collisions resolve against you. Droid's own resource values win, so a pair setting service.name, service.version, os.type, os.version, host.arch, or terminal.type is ignored. On datapoints, the identity keys user.id, user.email, organization.id, session.id, and parent_session.id are dropped outright rather than merged, so an operator-set pair cannot impersonate a user.
There is no org-managed (Raw Configuration) field for resource attributes, and headers cannot carry them. To attribute headless or CI runs, set OTEL_RESOURCE_ATTRIBUTES, and DROID_PARENT_SESSION_ID for lineage, in each runner's environment.