Summary
Add OpenTelemetry (OTEL) tracing to the api-proxy sidecar container so that every proxied LLM API request emits a span with token usage data, provider info, and request metadata. Spans should be children of the parent workflow trace (via GITHUB_AW_OTEL_TRACE_ID / GITHUB_AW_OTEL_PARENT_SPAN_ID env vars), enabling end-to-end distributed tracing from the GitHub Actions workflow through MCPG and into the api-proxy.
Motivation
The api-proxy already captures rich telemetry:
- Token usage (input/output/cache tokens, model, provider, duration) →
token-usage.jsonl
- In-memory metrics (counters, histograms) →
/metrics endpoint
- Structured logs (JSON to stdout)
However, none of this data participates in the distributed trace that gh-aw establishes for each workflow run. The MCPG already exports spans via OTLP/HTTP. Adding OTEL to the api-proxy would complete the observability picture.
How OTEL Works in the gh-aw Ecosystem Today
- Workflow frontmatter declares
observability.otlp with endpoint + headers
- gh-aw compiler generates env vars:
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_SERVICE_NAME
- gh-aw setup action generates trace context:
GITHUB_AW_OTEL_TRACE_ID, GITHUB_AW_OTEL_PARENT_SPAN_ID
- MCPG receives config via stdin JSON (
gateway.opentelemetry.{endpoint, traceId, spanId}) and exports spans via Go OTLP/HTTP exporter
- Logs/errors written to
/tmp/gh-aw/otel.jsonl and /tmp/gh-aw/otlp-export-errors.jsonl
Reference: smoke-otel-tracing.md and its compiled lock file
Proposed Architecture
┌──────────────────────────────────────────────────────────┐
│ api-proxy (Node.js) │
│ │
│ Env vars: │
│ OTEL_EXPORTER_OTLP_ENDPOINT │
│ OTEL_EXPORTER_OTLP_HEADERS │
│ OTEL_SERVICE_NAME = "awf-api-proxy" │
│ GITHUB_AW_OTEL_TRACE_ID │
│ GITHUB_AW_OTEL_PARENT_SPAN_ID │
│ │
│ New module: otel.js │
│ - SDK initialization + tracer singleton │
│ - W3C TraceContext parent propagation from env vars │
│ - OTLP/HTTP exporter (through Squid proxy) │
│ - File-based fallback when no endpoint configured │
│ │
│ Integration points: │
│ proxy-request.js → create span per request │
│ token-tracker-http.js → add token attrs to span │
│ server.js → graceful shutdown (flush spans) │
└──────────────────────────────────────────────────────────┘
│
▼ OTLP/HTTP (routed through Squid proxy)
[Sentry / Datadog / OTEL Collector]
Span Design
Each proxied request becomes a span:
| Field |
Value |
| Span name |
api_proxy.{provider}.request |
| Kind |
CLIENT |
| Parent |
Constructed from GITHUB_AW_OTEL_TRACE_ID + GITHUB_AW_OTEL_PARENT_SPAN_ID |
Span Attributes
Following GenAI Semantic Conventions:
| Attribute |
Source |
gen_ai.system |
Provider name (openai, anthropic, copilot) |
gen_ai.request.model |
Model from request body |
gen_ai.response.model |
Model from response |
gen_ai.usage.input_tokens |
From token tracker |
gen_ai.usage.output_tokens |
From token tracker |
awf.cache_read_tokens |
From token tracker |
awf.cache_write_tokens |
From token tracker |
http.request.method |
GET/POST |
http.response.status_code |
Upstream status |
url.path |
Request path |
awf.request_id |
Internal request ID |
awf.streaming |
boolean |
awf.provider |
Provider name |
Span Events
gen_ai.usage — emitted at span end with full token breakdown
rate_limit — when rate limiting is triggered
error — on upstream errors
Implementation Plan
Phase 1: Core OTEL module + span-per-request
-
Add dependencies to containers/api-proxy/package.json:
@opentelemetry/api (~50KB)
@opentelemetry/sdk-trace-node (~100KB)
@opentelemetry/exporter-trace-otlp-http (~50KB)
@opentelemetry/resources + @opentelemetry/semantic-conventions
-
Create containers/api-proxy/otel.js:
- Read env vars for endpoint/headers/service name
- Construct parent context from
GITHUB_AW_OTEL_TRACE_ID + GITHUB_AW_OTEL_PARENT_SPAN_ID
- Initialize
NodeTracerProvider with BatchSpanProcessor + OTLP exporter
- Export
getTracer(), shutdown(), isEnabled() functions
- OTLP exporter must use
HTTPS_PROXY agent (route through Squid)
-
Integrate into proxy-request.js:
- Start span at request begin (where
requestId is generated)
- Set attributes from request metadata
- End span in the response
end handler
- Set span status on error
-
Integrate into token-tracker-http.js:
- In
finalizeTracking(), add token usage attributes to active span
- Use the existing
onUsage callback hook
-
Forward env vars from src/services/api-proxy-service.ts:
- Pass
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_SERVICE_NAME
- Pass
GITHUB_AW_OTEL_TRACE_ID, GITHUB_AW_OTEL_PARENT_SPAN_ID
-
Graceful shutdown in server.js:
- Call
otel.shutdown() in the existing shutdown handler to flush pending spans
Phase 2: File fallback + tests
-
File-based fallback: When no OTLP endpoint is configured, write span data as NDJSON to /var/log/api-proxy/otel.jsonl (mirrors MCPG pattern)
-
Tests: Unit tests for otel.js (mock exporter), integration with proxy-request.js
Phase 3: Extended observability
- Additional spans: Model resolution, OIDC token refresh, rate limit decisions
- Metrics via OTEL (optional): Replace or augment in-memory metrics with OTEL metrics exporter
Design Decisions
| Decision |
Choice |
Rationale |
| SDK |
@opentelemetry/sdk-trace-node |
Standard Node.js OTEL SDK |
| Exporter |
OTLP/HTTP |
Works through Squid proxy (HTTPS) |
| Parent context |
Env vars |
Matches MCPG pattern; spans are children of workflow trace |
| Span granularity |
One per proxy request |
Natural boundary; maps to existing proxyRequest() lifecycle |
| Token data |
Span attributes |
GenAI semantic conventions; queryable in trace backends |
| Fallback |
NDJSON file |
Matches /tmp/gh-aw/otel.jsonl pattern from MCPG |
| Activation |
Opt-in via OTEL_EXPORTER_OTLP_ENDPOINT presence |
Zero overhead when not configured |
| Proxy routing |
OTLP exporter uses HTTPS_PROXY env |
Already set in api-proxy container |
Open Questions
- Should the api-proxy spans be direct children of the workflow parent span, or should they be children of the MCPG tool-call spans? (The MCPG is the one invoking the LLM via the proxy)
- Should we also export OTEL metrics (not just traces)? The api-proxy already has a Prometheus-style
/metrics endpoint.
- What is the minimum Node.js version for the OTEL SDK? (Currently requires >=14, api-proxy requires >=18, so no issue)
- Should the
token-usage.jsonl file be deprecated in favor of OTEL spans long-term, or kept as a complementary local record?
Estimated Effort
- Phase 1 (core): ~2-3 days
- Phase 2 (fallback + tests): ~1-2 days
- Phase 3 (extended): ~1-2 days
Files to Modify
containers/api-proxy/package.json — add OTEL dependencies
containers/api-proxy/otel.js — new: SDK init, tracer, shutdown
containers/api-proxy/proxy-request.js — span creation/completion
containers/api-proxy/token-tracker-http.js — add token attrs to span
containers/api-proxy/server.js — graceful shutdown integration
src/services/api-proxy-service.ts — forward OTEL env vars to container
Summary
Add OpenTelemetry (OTEL) tracing to the api-proxy sidecar container so that every proxied LLM API request emits a span with token usage data, provider info, and request metadata. Spans should be children of the parent workflow trace (via
GITHUB_AW_OTEL_TRACE_ID/GITHUB_AW_OTEL_PARENT_SPAN_IDenv vars), enabling end-to-end distributed tracing from the GitHub Actions workflow through MCPG and into the api-proxy.Motivation
The api-proxy already captures rich telemetry:
token-usage.jsonl/metricsendpointHowever, none of this data participates in the distributed trace that gh-aw establishes for each workflow run. The MCPG already exports spans via OTLP/HTTP. Adding OTEL to the api-proxy would complete the observability picture.
How OTEL Works in the gh-aw Ecosystem Today
observability.otlpwith endpoint + headersOTEL_EXPORTER_OTLP_ENDPOINT,OTEL_EXPORTER_OTLP_HEADERS,OTEL_SERVICE_NAMEGITHUB_AW_OTEL_TRACE_ID,GITHUB_AW_OTEL_PARENT_SPAN_IDgateway.opentelemetry.{endpoint, traceId, spanId}) and exports spans via Go OTLP/HTTP exporter/tmp/gh-aw/otel.jsonland/tmp/gh-aw/otlp-export-errors.jsonlReference: smoke-otel-tracing.md and its compiled lock file
Proposed Architecture
Span Design
Each proxied request becomes a span:
api_proxy.{provider}.requestCLIENTGITHUB_AW_OTEL_TRACE_ID+GITHUB_AW_OTEL_PARENT_SPAN_IDSpan Attributes
Following GenAI Semantic Conventions:
gen_ai.systemgen_ai.request.modelgen_ai.response.modelgen_ai.usage.input_tokensgen_ai.usage.output_tokensawf.cache_read_tokensawf.cache_write_tokenshttp.request.methodhttp.response.status_codeurl.pathawf.request_idawf.streamingawf.providerSpan Events
gen_ai.usage— emitted at span end with full token breakdownrate_limit— when rate limiting is triggerederror— on upstream errorsImplementation Plan
Phase 1: Core OTEL module + span-per-request
Add dependencies to
containers/api-proxy/package.json:@opentelemetry/api(~50KB)@opentelemetry/sdk-trace-node(~100KB)@opentelemetry/exporter-trace-otlp-http(~50KB)@opentelemetry/resources+@opentelemetry/semantic-conventionsCreate
containers/api-proxy/otel.js:GITHUB_AW_OTEL_TRACE_ID+GITHUB_AW_OTEL_PARENT_SPAN_IDNodeTracerProviderwithBatchSpanProcessor+ OTLP exportergetTracer(),shutdown(),isEnabled()functionsHTTPS_PROXYagent (route through Squid)Integrate into
proxy-request.js:requestIdis generated)endhandlerIntegrate into
token-tracker-http.js:finalizeTracking(), add token usage attributes to active spanonUsagecallback hookForward env vars from
src/services/api-proxy-service.ts:OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_EXPORTER_OTLP_HEADERS,OTEL_SERVICE_NAMEGITHUB_AW_OTEL_TRACE_ID,GITHUB_AW_OTEL_PARENT_SPAN_IDGraceful shutdown in
server.js:otel.shutdown()in the existing shutdown handler to flush pending spansPhase 2: File fallback + tests
File-based fallback: When no OTLP endpoint is configured, write span data as NDJSON to
/var/log/api-proxy/otel.jsonl(mirrors MCPG pattern)Tests: Unit tests for
otel.js(mock exporter), integration withproxy-request.jsPhase 3: Extended observability
Design Decisions
@opentelemetry/sdk-trace-nodeproxyRequest()lifecycle/tmp/gh-aw/otel.jsonlpattern from MCPGOTEL_EXPORTER_OTLP_ENDPOINTpresenceHTTPS_PROXYenvOpen Questions
/metricsendpoint.token-usage.jsonlfile be deprecated in favor of OTEL spans long-term, or kept as a complementary local record?Estimated Effort
Files to Modify
containers/api-proxy/package.json— add OTEL dependenciescontainers/api-proxy/otel.js— new: SDK init, tracer, shutdowncontainers/api-proxy/proxy-request.js— span creation/completioncontainers/api-proxy/token-tracker-http.js— add token attrs to spancontainers/api-proxy/server.js— graceful shutdown integrationsrc/services/api-proxy-service.ts— forward OTEL env vars to container