Skip to content

feat: group observability spans by run - #195

Merged
delete-cloud merged 1 commit into
mainfrom
feat/observability-trace-grouping
May 16, 2026
Merged

delete-cloud merged 1 commit into
mainfrom
feat/observability-trace-grouping

Conversation

@delete-cloud

@delete-cloud delete-cloud commented May 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • derive stable OTLP trace ids from session_id/run_id so one turn groups together
  • export service/session/run metadata as OTLP resource attributes
  • cover same-run grouping and different-run separation

Verification

  • uv run pytest tests/coding_agent/test_observability.py tests/coding_agent/test_bootstrap.py tests/agentkit/tools/test_toolset.py tests/agentkit/runtime tests/agentkit/observability -q
  • uv run basedpyright --level error src/coding_agent/observability.py tests/coding_agent/test_observability.py src/coding_agent/app.py tests/coding_agent/test_bootstrap.py src/agentkit/observability
  • uv run ruff check src/coding_agent/observability.py tests/coding_agent/test_observability.py src/coding_agent/app.py tests/coding_agent/test_bootstrap.py
  • uv run ruff format --check src/coding_agent/observability.py tests/coding_agent/test_observability.py src/coding_agent/app.py tests/coding_agent/test_bootstrap.py
  • git diff --check

Summary by CodeRabbit

  • New Features
    • Enhanced OTLP/HTTP trace exports now include richer resource metadata containing service name, session ID, and run ID.
    • Improved trace correlation with deterministic trace identifiers derived from session and run data, replacing random ID generation.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 16, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR enhances OTLP/HTTP span exports with stable trace identifiers and richer resource metadata. Trace IDs are now deterministically derived from session and run IDs via SHA-256 hashing (truncated to 32 hex characters), or fall back to random tokens when unavailable. Resource attributes now include service.name, session.id, and run.id derived from span attributes, enabling better trace grouping and observability across sessions.

Changes

Stable OTLP Trace Identifiers and Resource Metadata

Layer / File(s) Summary
Resource attributes and trace ID generation
coding-agent/src/coding_agent/observability.py
hashlib is imported. Helper functions _resource_attributes() and _trace_id() are introduced to build OTLP resource attributes with service.name, optional session.id, and run.id, and to generate trace IDs deterministically from session/run ID pairs via SHA-256 hash (truncated to 32 hex chars) or a random 16-byte token when both IDs are absent.
OTLP payload export integration
coding-agent/src/coding_agent/observability.py
The OTLP/HTTP export payload is updated to populate resource.attributes using the new helper instead of leaving the resource empty, and to set traceId using deterministic generation instead of always generating a random value.
Test coverage for trace grouping and resource attributes
coding-agent/tests/coding_agent/test_observability.py
json import is added. Test helpers _parse_request_payload(), _get_first_span(), and _resource_attributes() parse the OTLP payload and extract spans and resource metadata. Two new test cases verify that spans with the same session_id and run_id produce identical traceId values, and that session.id and run.id are exported as OTLP resource attributes with stringValue type.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Traces now dance in stable lines,
Session and run by hash align,
Resource riches fill each span,
OTLP exports a richer plan! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the pull request: implementing grouping of observability spans by run through stable trace ID derivation and resource metadata export.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/observability-trace-grouping

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
coding-agent/tests/coding_agent/test_observability.py (2)

91-124: ⚡ Quick win

Consider adding test coverage for the random trace ID fallback.

The current test verifies deterministic grouping when both session_id and run_id are present. Consider adding a test case that verifies the fallback to random trace IDs when either identifier is missing or invalid, to document this behavior.

📝 Example test case
def test_otlp_sink_uses_random_trace_id_without_session_or_run() -> None:
    transport = RecordingTransport()
    sink = OtlpHttpObservationSink(
        endpoint="https://otel.example.test/v1/traces",
        client=httpx.Client(transport=httpx.MockTransport(transport.handler)),
    )

    # Span without session_id or run_id should get random trace ID
    sink.record_span(
        SpanRecord(
            name="tool.call",
            status="ok",
            attributes={"other_attr": "value"},
        )
    )
    sink.record_span(
        SpanRecord(
            name="llm.generation",
            status="ok",
            attributes={"other_attr": "value"},
        )
    )

    first_trace_id = _first_span(_payload(transport.requests[0]))["traceId"]
    second_trace_id = _first_span(_payload(transport.requests[1]))["traceId"]
    # Random trace IDs should be different
    assert first_trace_id != second_trace_id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coding-agent/tests/coding_agent/test_observability.py` around lines 91 - 124,
Add a new test that verifies OtlpHttpObservationSink falls back to random trace
IDs when session_id/run_id are missing: create a RecordingTransport and
OtlpHttpObservationSink, record two SpanRecord instances that do NOT include
session_id or run_id in their attributes, then extract trace IDs with
_first_span(_payload(transport.requests[i])) and assert the two trace IDs are
different; this complements the existing
test_otlp_sink_groups_spans_by_session_and_run and uses the same helpers
(RecordingTransport, OtlpHttpObservationSink, SpanRecord, _first_span, _payload)
for consistency.

127-144: 💤 Low value

Consider verifying the service.name resource attribute.

The _resource_attributes function always includes service.name in the resource attributes. Consider adding an assertion to verify its presence and value for completeness.

📝 Suggested addition
     resource_attributes = _resource_attributes(_payload(transport.requests[0]))
+    assert resource_attributes["service.name"] == {"stringValue": "coding-agent"}
     assert resource_attributes["session.id"] == {"stringValue": "session-1"}
     assert resource_attributes["run.id"] == {"stringValue": "run-1"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coding-agent/tests/coding_agent/test_observability.py` around lines 127 -
144, Update the test
test_otlp_sink_exports_session_and_run_as_resource_attributes to also assert
that the resource attributes include the service.name attribute; after calling
resource_attributes = _resource_attributes(_payload(transport.requests[0])) add
an assertion that resource_attributes contains "service.name" and either equals
the expected service name value used by OtlpHttpObservationSink (as
{"stringValue": "<expected>"}), or at minimum that its stringValue is present
and non-empty to ensure the service name is exported; locate this change around
the existing assertions in that test which call _resource_attributes, _payload,
and use the OtlpHttpObservationSink/test transport.
coding-agent/src/coding_agent/observability.py (1)

75-85: ⚡ Quick win

Consider using a collision-resistant separator strategy.

The current implementation concatenates session_id and run_id with a colon separator before hashing. If either identifier can contain colons, different (session_id, run_id) pairs could produce identical hash inputs, leading to trace ID collisions and incorrect span grouping.

For example:

  • session_id="a:b", run_id="c" → hash input "a:b:c"
  • session_id="a", run_id="b:c" → hash input "a:b:c" (collision)

While unlikely if IDs are UUIDs or structured identifiers without colons, consider one of these approaches:

♻️ Option 1: Length-prefixed encoding (most robust)
-        return hashlib.sha256(f"{session_id}:{run_id}".encode("utf-8")).hexdigest()[:32]
+        combined = f"{len(session_id)}:{session_id}:{run_id}"
+        return hashlib.sha256(combined.encode("utf-8")).hexdigest()[:32]
♻️ Option 2: Document the constraint

Add a docstring comment:

def _trace_id(attributes: Mapping[str, Any]) -> str:
    """Generate deterministic trace ID from session and run IDs.
    
    Requires session_id and run_id to not contain colon characters.
    """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coding-agent/src/coding_agent/observability.py` around lines 75 - 85,
_trace_id currently concatenates session_id and run_id with ":" which can
collide if IDs contain colons; change the input to the hash to use a
collision-resistant encoding (e.g., length-prefixed encoding or another
unambiguous separator) before calling hashlib.sha256 so distinct (session_id,
run_id) pairs cannot produce the same preimage; update the logic in _trace_id
(and keep the existing secrets.token_hex fallback) so the deterministic branch
hashes the new encoded string built from session_id and run_id rather than plain
f"{session_id}:{run_id}".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@coding-agent/src/coding_agent/observability.py`:
- Around line 75-85: _trace_id currently concatenates session_id and run_id with
":" which can collide if IDs contain colons; change the input to the hash to use
a collision-resistant encoding (e.g., length-prefixed encoding or another
unambiguous separator) before calling hashlib.sha256 so distinct (session_id,
run_id) pairs cannot produce the same preimage; update the logic in _trace_id
(and keep the existing secrets.token_hex fallback) so the deterministic branch
hashes the new encoded string built from session_id and run_id rather than plain
f"{session_id}:{run_id}".

In `@coding-agent/tests/coding_agent/test_observability.py`:
- Around line 91-124: Add a new test that verifies OtlpHttpObservationSink falls
back to random trace IDs when session_id/run_id are missing: create a
RecordingTransport and OtlpHttpObservationSink, record two SpanRecord instances
that do NOT include session_id or run_id in their attributes, then extract trace
IDs with _first_span(_payload(transport.requests[i])) and assert the two trace
IDs are different; this complements the existing
test_otlp_sink_groups_spans_by_session_and_run and uses the same helpers
(RecordingTransport, OtlpHttpObservationSink, SpanRecord, _first_span, _payload)
for consistency.
- Around line 127-144: Update the test
test_otlp_sink_exports_session_and_run_as_resource_attributes to also assert
that the resource attributes include the service.name attribute; after calling
resource_attributes = _resource_attributes(_payload(transport.requests[0])) add
an assertion that resource_attributes contains "service.name" and either equals
the expected service name value used by OtlpHttpObservationSink (as
{"stringValue": "<expected>"}), or at minimum that its stringValue is present
and non-empty to ensure the service name is exported; locate this change around
the existing assertions in that test which call _resource_attributes, _payload,
and use the OtlpHttpObservationSink/test transport.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f73002c1-993b-462d-8666-d8363e970833

📥 Commits

Reviewing files that changed from the base of the PR and between b0f4370 and b680962.

📒 Files selected for processing (2)
  • coding-agent/src/coding_agent/observability.py
  • coding-agent/tests/coding_agent/test_observability.py

@delete-cloud
delete-cloud merged commit 598641d into main May 16, 2026
1 check passed
@delete-cloud
delete-cloud deleted the feat/observability-trace-grouping branch May 16, 2026 12:35
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.

1 participant