Skip to content

feat(sensor): add opt-in GenAI semconv attributes to OTel session logs - #161

Open
Zhuoxi2000 wants to merge 2 commits into
uber:mainfrom
Zhuoxi2000:fix-otel-genai-semconv-attrs
Open

Zhuoxi2000 wants to merge 2 commits into
uber:mainfrom
Zhuoxi2000:fix-otel-genai-semconv-attrs

Conversation

@Zhuoxi2000

Copy link
Copy Markdown

What type of PR is this? (check all applicable)

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Related issue: Closes #153

What changed?

  • exporters/config.py: new optional key gen_ai_attributes. It is a strict boolean and defaults to false.

  • exporters/opentelemetry.py: when the key is on, adr.agent.session records also carry:

    • gen_ai.agent.name: the source, the same value as adr.source.
    • gen_ai.conversation.id: the harness's own session ID.

    The body, all adr.* attributes (including adr.session.id), system-configuration records and health records are unchanged.

  • How the ID is mapped: every parser builds session_id as <source prefix><native id>. A table keyed by entry.source (_CONVERSATION_ID_BY_SOURCE) undoes that prefix. Mapping in the exporter means AgentEvent gets no new field, so payloads and per-session checkpoint hashes stay the same. The attribute is left out when:

    • the source is not in the table;
    • session_id doesn't have the expected prefix;
    • the native ID would be empty;
    • the native ID is unavailable (see the Claude Desktop row below).

    It never falls back to adr.session.id.

  • exporters/delivery_checkpoint.py: the new config field is left out of the destination fingerprint while it is false, so existing .adr-otel-delivery.<hash>.json checkpoints stay valid after upgrade. Turning it on changes the destination and resends once.

  • README.md: documents the option, a per-source mapping table and the omission rule.

source ADR session_id gen_ai.conversation.id
antigravity antigravity_<id> <id>: conversation directory name
claude claude_<id> / claude_<id>_agent_<agent> <id>: JSONL sessionId. Subagents get their parent's sessionId, matching Claude Code's own telemetry
claude_desktop claude_desktop_[dispatch_]<uuid> metadata cliSessionId, omitted when absent
cline cline_<id> <id>: task directory name
codex codex_<id> <id>: session_meta.payload.id
copilot copilot_<id> <id>: session.start/session.resume sessionId, falling back to the session-state directory name, as the parser does
cursor cursor_<id> <id>: composer ID
dsh dsh_<id> <id>: session header id
gemini gemini_<id> <id>: sessionId. Subagents keep their own
opencode opencode_<id> ses_<id>
warp warp_<id> <id>: agent_conversations.conversation_id

Three rows involve a judgment call, and I'm happy to change any of them:

  1. claude_desktop: the parser strips the metadata sessionId prefix (local_ / local_ditto_). This PR uses cliSessionId instead. It is kept verbatim in session_context.cli_session_id, and it is the ID that matches Claude Code CLI telemetry. Sessions without it get no gen_ai.conversation.id. The alternative is to rebuild local_[ditto_]<uuid> from the ADR ID.
  2. opencode: the parser strips ses_ only when it is present. Its docstring says opencode session IDs always start with ses_, so this PR adds it back. If you'd rather not rely on that, the alternative is to omit the attribute for opencode.
  3. antigravity: this harness landed in feat: support Google Antigravity CLI and agent harness (#133) #152 after the issue was filed. It maps to the conversation directory name, which the parser also keeps verbatim as session_context.conversation_id.

Why?

As discussed in #153, GenAI and agent-observability backends correlate on the OpenTelemetry GenAI registry attributes, and a Collector shouldn't be required just for these. gen_ai.conversation.id carries the harness-native ID, so ADR sessions can join the session IDs that harnesses report in their own telemetry. Model, provider and token usage are left out for the reasons in the issue.

How did you test it?

cd Sensor
uv sync --extra dev --locked --python 3.12
.venv/bin/python -m pytest tests/ -q
# 608 passed (also 608 passed with --python 3.9)
.venv/bin/python -m pytest tests/test_gen_ai_mapping.py tests/test_opentelemetry_exporter.py tests/test_exporter_config.py tests/test_delivery_checkpoint.py -q
# 83 passed
.venv/bin/ruff check adr_sensor/exporters tests/test_gen_ai_mapping.py tests/test_opentelemetry_exporter.py tests/test_exporter_config.py tests/test_delivery_checkpoint.py
# All checks passed!
.venv/bin/ruff format --check adr_sensor/exporters tests/test_gen_ai_mapping.py tests/test_opentelemetry_exporter.py tests/test_exporter_config.py tests/test_delivery_checkpoint.py
# 8 files already formatted

New tests/test_gen_ai_mapping.py:

  • One test per harness. Each runs the real parser on a small fixture modeled on that parser's existing tests and exports the result with gen_ai_attributes on. It then asserts that gen_ai.conversation.id equals the raw native ID in the fixture, that the body equals get_non_null_fields(), and that adr.session.id is unchanged. If a parser later changes its prefix, these tests fail.
    • Special cases covered: Claude subagents (direct and nested-workflow), Claude Desktop with cliSessionId, the Copilot directory fallback, Gemini subagents, and opencode ses_.
  • test_unavailable_conversation_id_is_omitted: unknown source, wrong prefix, empty native ID, and a Claude subagent without a parent session. Each yields no gen_ai.conversation.id, while gen_ai.agent.name and the exact adr.* set remain. Claude Desktop without cliSessionId is covered separately, through the parser.
  • test_every_parser_has_a_conversation_id_mapping: every parser exported from adr_sensor.parsers needs an entry in the mapping table, so adding a harness forces a decision.

Other new tests:

  • test_export_adds_gen_ai_attributes_when_enabled[gpt-test|None]: the exact attribute set when the option is on.
  • test_load_opentelemetry_config_defaults / _all_fields (extended) and two new non-boolean cases in test_load_opentelemetry_config_rejects_invalid_values.
  • test_enabling_gen_ai_attributes_resends_sessions.
  • test_default_gen_ai_attributes_keep_existing_checkpoint: pins the checkpoint filename that main produces for the default config.

test_export_preserves_complete_agent_event_payload is unchanged and still pins the default attribute set and body.

With these tests on top of main's source, test_gen_ai_mapping.py fails at import, and all the other new or extended tests except the checkpoint-name one fail (7 failures, for example unexpected keyword argument 'gen_ai_attributes' and unknown OpenTelemetry config field(s): gen_ai_attributes). The remaining 57 tests in test_opentelemetry_exporter.py, test_exporter_config.py and test_delivery_checkpoint.py pass. They include test_default_gen_ai_attributes_keep_existing_checkpoint, which shows that the pinned checkpoint name is the one main produces.

Against the first commit in this PR, where gen_ai.conversation.id was still the ADR session ID, test_gen_ai_mapping.py also fails at import, because _CONVERSATION_ID_BY_SOURCE doesn't exist yet. With that import stubbed so the module loads, 21 of the 42 tests in test_gen_ai_mapping.py and test_opentelemetry_exporter.py fail, for example assert 'codex_sess1' == 'sess1'.

Potential risks

None while the option is off: attributes, body and checkpoint paths are identical to main. When it is on:

  • The attribute names follow GenAI semantic conventions that are still in Development status and may change upstream.
  • The first run after enabling it resends sessions in the lookback window.
  • The mapping depends on each parser's session_id prefix. The per-harness tests catch a prefix change.

Add a gen_ai_attributes OpenTelemetry config option. When enabled,
adr.agent.session log records also carry gen_ai.conversation.id and
gen_ai.agent.name so GenAI observability backends can correlate ADR
sessions without a Collector transform. The body and adr.* attributes
are unchanged, and the option is off by default.

The default value is left out of the delivery checkpoint fingerprint so
existing checkpoints stay valid after upgrade.
gen_ai.conversation.id now carries each harness's own session ID instead
of the prefixed ADR session ID, so ADR sessions join with the telemetry
the harness reports itself. adr.session.id and the body are unchanged.

The exporter maps each source back from the prefix its parser adds.
Claude subagents get their parent's sessionId, Claude Desktop uses
cliSessionId, and opencode gets its "ses_" prefix back. The attribute is
omitted rather than guessed when the source is unknown, the prefix does
not match, or the native ID is unavailable.

The README documents the mapping per source. Tests run each parser on a
fixture and check the exported ID, and fail if a new parser has no
mapping entry.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@barisozbas barisozbas left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!
@Zhuoxi2000 Please sign the CLA so that it can be merge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sensor OTel export: opt-in gen_ai.* semantic-convention attributes on adr.agent.session logs

3 participants