# 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](/enterprise/telemetry) is configured. The [export format](#export-formats) 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](#exported-metrics), plus the `droid.message.*` and `droid.tool.*` event spans when [message content logging](/enterprise/telemetry/privacy#message-content-logging) is on. |
| `genai` | The [OpenTelemetry GenAI semantic conventions](#genai-semantic-convention-export): `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.

<Warning>
  **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**.
</Warning>

## 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. |

<Note>
  `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.
</Note>

## Common attributes

Every data point includes these attributes automatically:

| Attribute           | Description                                            |
| :------------------ | :---------------------------------------------------- |
| `user.id`           | Authenticated user ID. Removed under [`aggregate` granularity](/enterprise/telemetry/privacy#data-granularity). |
| `user.email`        | Authenticated user email. Removed under [`aggregate` granularity](/enterprise/telemetry/privacy#data-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](#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](/enterprise/telemetry/privacy#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:

```json
[{ "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](#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.

<Note>
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.id` linkage that the `droid.*` surface carries has no equivalent in the GenAI conventions, so a subagent's trace cannot be joined to its parent's under this format.
</Note>

## Resource attributes

To attach your own identity or lineage attributes (for example `team.id`, `repo`, `environment`), set the standard OTEL variable per machine:

```bash
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.

<Note>
  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.
</Note>

<RelatedLinks>
  <RelatedLink href="/enterprise/telemetry" title="Telemetry & Analytics">
    Configure the OTEL export that delivers this data.
  </RelatedLink>
  <RelatedLink href="/enterprise/telemetry/privacy" title="Telemetry Privacy Controls">
    Data granularity and message content logging.
  </RelatedLink>
</RelatedLinks>
