Skip to content

Observability: OpenTelemetry GenAI spans for turns + tool calls (closes #41) - #47

Merged
lezama merged 1 commit into
mainfrom
ship/issue-41-otel-tracing
May 18, 2026
Merged

Observability: OpenTelemetry GenAI spans for turns + tool calls (closes #41)#47
lezama merged 1 commit into
mainfrom
ship/issue-41-otel-tracing

Conversation

@lezama

@lezama lezama commented May 17, 2026

Copy link
Copy Markdown
Owner

Closes #41

Summary

  • New OpenclaWP_Tracer buffers per-request spans in memory and batch-POSTs them as an OTLP/HTTP JSON ExportTraceServiceRequest payload via wp_remote_post() on shutdown. No PHP OTel SDK dependency — payload is hand-rolled per the OTLP spec.
  • Hooks: openclawp_chat_turn_completed emits the root chat span per turn; agents_api_loop_event (tool_call + tool_result) emits child tool spans parented to the active turn. One trace per PHP request, so a turn and all its tool calls show up as one trace tree in the backend.
  • Default off — the tracer registers itself only when OPENCLAWP_OTEL_ENDPOINT (env), openclawp_otel_endpoint (WP option), or the openclawp_otel_endpoint filter resolves to a non-empty URL. Optional auth header via OPENCLAWP_OTEL_AUTH_HEADER / option / filter.
  • Runtime context (openclawp.session.id, openclawp.agent.slug, openclawp.user.id, openclawp.channel) is stamped onto every span by the runner before the loop runs.

Attribute conventions (verified by tests)

GenAI semconv attributes are emitted verbatim:

  • gen_ai.system — provider id (e.g. anthropic, ollama)
  • gen_ai.operation.namechat for root spans, execute_tool for tool children
  • gen_ai.request.model — pinned model id
  • gen_ai.usage.input_tokens / gen_ai.usage.output_tokens — int
  • gen_ai.response.finish_reasons — string array (["stop"] / ["error"])
  • gen_ai.tool.name — on child spans

Site-specific identifiers are namespaced under openclawp.*.

Overhead measurement (5ms budget)

bin/bench-tracer.php drives one synthetic turn + one tool call through the tracer 1000 times:

samples=1000  avg=2.0us  p50=2us  p95=3us  p99=3us  max=17us

~2µs per turn — three orders of magnitude under the 5ms budget. A PHPUnit test (test_overhead_under_5ms_per_turn) enforces the budget on every CI run.

Test plan

  • vendor/bin/phpunit --testsuite unit — 41 tests, 101 assertions, 10 new in TracerTest
  • php tests/smoke.php — exits 0 (no regressions in the WP-light path)
  • npm run lint — clean
  • PHPUnit test captures spans against a fake wp_remote_post backend and asserts OTLP envelope shape + every GenAI semconv attribute is present with the right type
  • Bench: php bin/bench-tracer.php

Deferred (follow-ups, not blocking #41 close)

The issue lists a few "nice to have" items beyond the core tracer surface — moved to separate issues:

  • Sampling (head-based, always-sample-on-error) — easy add via the existing register() gate
  • Settings page UI + "Send a test span" button — runner already records context; UI is admin-only
  • Deep-link "View trace" button in the admin conversation list — depends on the backend URL template
  • Provider HTTP child spans separate from the turn root — would need a hook inside wp_ai_client_prompt we don't own yet

@lezama
lezama marked this pull request as ready for review May 18, 2026 10:39
@lezama
lezama force-pushed the ship/issue-41-otel-tracing branch from 89b8234 to b11cd24 Compare May 18, 2026 10:41
@lezama
lezama merged commit bfa596a into main May 18, 2026
6 checks passed
@lezama
lezama deleted the ship/issue-41-otel-tracing branch May 18, 2026 10:42
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.

Emit OpenTelemetry GenAI spans for every turn + tool call

1 participant