Skip to content

Commit bdd8048

Browse files
authored
feat: add OpenTelemetry tracing for agent execution (#89)
* feat: add OpenTelemetry tracing for agent execution Add distributed tracing across the full agent request lifecycle: LLM API calls, agentic tool loop iterations, tool execution, HTTP request handling, and A2A delegation. Conversation content (messages, tool args/results) is captured as span events with 10KB truncation. Token usage is recorded as span attributes. Tracing is zero-overhead when disabled (OTel no-op provider). Three export modes: OTLP gRPC (Jaeger/Tempo), stdout JSON (kubectl logs), or both simultaneously. New flags: --otel-enabled, --otel-collector-endpoint, --otel-stdout Closes #34 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> * fix: rename ZT-AGENT to DOCSCLAW and auto-detect log format Rename the logger component from the legacy "ZT-AGENT" (Zero Trust demo) to "DOCSCLAW". Auto-detect output format based on TTY: color text for local dev terminals, structured JSON for K8s pods and pipes. Override with LOG_FORMAT=json|text. SPIFFE_DEMO_LOG_FORMAT kept as legacy alias. NO_COLOR still respected in text mode. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> * fix: address code review findings for OTel tracing - Fix statusWriter to implement http.Flusher (and Unwrap) so SSE streaming continues to work through the OTel HTTP middleware - Remove codes.Error from denied tool calls — policy denial is expected behavior, not an operational error - Set error status on loopSpan for max iterations and context cancellation so these are distinguishable from success in traces - Add response content and token usage as span events/attributes on both providers' Complete methods (closes observability gap) - Extract W3C baggage header alongside traceparent in A2A executor Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> * fix: address CodeRabbit review findings - Fix truncate to account for suffix length in byte budget so result never exceeds maxBytes - Default OTLP gRPC to TLS; only use insecure when explicitly set to OTEL_EXPORTER_OTLP_INSECURE=true - Save/restore global TracerProvider in test instead of always setting noop - Set loopSpan error status when CompleteWithTools fails so parent span reflects child failure - Add span error status to all error paths in both providers' Complete methods - Give OTel shutdown its own timeout context separate from HTTP server shutdown Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> --------- Signed-off-by: Pavel Anni <panni@redhat.com>
1 parent 4dbefe3 commit bdd8048

16 files changed

Lines changed: 859 additions & 35 deletions

File tree

go.mod

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@ require (
99
github.com/prometheus/client_golang v1.23.2
1010
github.com/spf13/cobra v1.10.2
1111
github.com/spf13/viper v1.21.0
12+
go.opentelemetry.io/otel v1.44.0
13+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
14+
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0
15+
go.opentelemetry.io/otel/sdk v1.44.0
16+
go.opentelemetry.io/otel/trace v1.44.0
1217
golang.org/x/sync v0.20.0
1318
gopkg.in/yaml.v3 v3.0.1
1419
modernc.org/sqlite v1.50.0
1520
)
1621

1722
require (
1823
github.com/beorn7/perks v1.0.1 // indirect
24+
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
1925
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2026
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2127
github.com/dustin/go-humanize v1.0.1 // indirect
2228
github.com/fsnotify/fsnotify v1.9.0 // indirect
29+
github.com/go-logr/logr v1.4.3 // indirect
30+
github.com/go-logr/stdr v1.2.2 // indirect
2331
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2432
github.com/google/jsonschema-go v0.4.3 // indirect
2533
github.com/google/uuid v1.6.0 // indirect
34+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
2635
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2736
github.com/mattn/go-isatty v0.0.20 // indirect
2837
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -46,13 +55,21 @@ require (
4655
github.com/tidwall/pretty v1.2.1 // indirect
4756
github.com/tidwall/sjson v1.2.5 // indirect
4857
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
58+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
59+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
60+
go.opentelemetry.io/otel/metric v1.44.0 // indirect
61+
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
4962
go.yaml.in/yaml/v2 v2.4.2 // indirect
5063
go.yaml.in/yaml/v3 v3.0.4 // indirect
51-
golang.org/x/mod v0.33.0 // indirect
52-
golang.org/x/oauth2 v0.35.0 // indirect
53-
golang.org/x/sys v0.42.0 // indirect
54-
golang.org/x/text v0.32.0 // indirect
55-
google.golang.org/protobuf v1.36.10 // indirect
64+
golang.org/x/mod v0.35.0 // indirect
65+
golang.org/x/net v0.55.0 // indirect
66+
golang.org/x/oauth2 v0.36.0 // indirect
67+
golang.org/x/sys v0.45.0 // indirect
68+
golang.org/x/text v0.37.0 // indirect
69+
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
70+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
71+
google.golang.org/grpc v1.81.1 // indirect
72+
google.golang.org/protobuf v1.36.11 // indirect
5673
modernc.org/libc v1.72.0 // indirect
5774
modernc.org/mathutil v1.7.1 // indirect
5875
modernc.org/memory v1.11.0 // indirect

go.sum

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/anthropics/anthropic-sdk-go v1.33.0 h1:YlRqiI+PjULBA8NoeeJmkXtFzTmjZZ
44
github.com/anthropics/anthropic-sdk-go v1.33.0/go.mod h1:dSIO7kSrOI7MA4fE6RRVaw8tyWP7HNQU5/H/KS4cax8=
55
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
66
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
8+
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
79
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
810
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
911
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -17,10 +19,17 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
1719
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1820
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
1921
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
22+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
23+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
24+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
25+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
26+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
2027
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
2128
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
2229
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
2330
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
31+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
32+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
2433
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2534
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2635
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
@@ -29,6 +38,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
2938
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
3039
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3140
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
41+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
42+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
3243
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
3344
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
3445
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -63,8 +74,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
6374
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
6475
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
6576
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
66-
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
67-
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
77+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
78+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
6879
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
6980
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
7081
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
@@ -101,27 +112,57 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
101112
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
102113
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
103114
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
115+
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
116+
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
117+
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
118+
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
119+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
120+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
121+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
122+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
123+
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs=
124+
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
125+
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
126+
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
127+
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
128+
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
129+
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
130+
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
131+
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
132+
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
133+
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
134+
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
104135
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
105136
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
106137
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
107138
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
108139
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
109140
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
110-
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
111-
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
112-
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
113-
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
141+
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
142+
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
143+
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
144+
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
145+
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
146+
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
114147
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
115148
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
116149
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
117-
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
118-
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
119-
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
120-
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
121-
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
122-
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
123-
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
124-
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
150+
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
151+
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
152+
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
153+
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
154+
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
155+
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
156+
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
157+
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
158+
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
159+
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
160+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
161+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
162+
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
163+
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
164+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
165+
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
125166
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
126167
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
127168
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

internal/anthropic/anthropic.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import (
88

99
"github.com/anthropics/anthropic-sdk-go"
1010
"github.com/anthropics/anthropic-sdk-go/option"
11+
"go.opentelemetry.io/otel"
12+
"go.opentelemetry.io/otel/codes"
13+
"go.opentelemetry.io/otel/trace"
1114

15+
"github.com/redhat-et/docsclaw/internal/telemetry"
1216
"github.com/redhat-et/docsclaw/pkg/llm"
1317
)
1418

@@ -56,6 +60,13 @@ func NewAnthropicProvider(cfg llm.Config) (*AnthropicProvider, error) {
5660

5761
// Complete sends a message to the Claude API and returns the response
5862
func (p *AnthropicProvider) Complete(ctx context.Context, systemPrompt, userPrompt string) (string, error) {
63+
ctx, span := otel.Tracer(telemetry.TracerName).Start(ctx, "llm.api.call",
64+
trace.WithAttributes(
65+
telemetry.AttrLLMProvider.String(p.ProviderName()),
66+
telemetry.AttrLLMModel.String(p.model),
67+
))
68+
defer span.End()
69+
5970
ctx, cancel := context.WithTimeout(ctx, p.timeout)
6071
defer cancel()
6172

@@ -70,11 +81,13 @@ func (p *AnthropicProvider) Complete(ctx context.Context, systemPrompt, userProm
7081
},
7182
})
7283
if err != nil {
84+
span.SetStatus(codes.Error, err.Error())
7385
return "", fmt.Errorf("API request failed: %w", err)
7486
}
7587

7688
// Extract text content from response
7789
if len(message.Content) == 0 {
90+
span.SetStatus(codes.Error, "empty response")
7891
return "", fmt.Errorf("empty response from API")
7992
}
8093

@@ -85,6 +98,13 @@ func (p *AnthropicProvider) Complete(ctx context.Context, systemPrompt, userProm
8598
}
8699
}
87100

101+
span.SetAttributes(
102+
telemetry.AttrLLMInputTokens.Int(int(message.Usage.InputTokens)),
103+
telemetry.AttrLLMOutputTokens.Int(int(message.Usage.OutputTokens)),
104+
telemetry.AttrLLMTotalTokens.Int(int(message.Usage.InputTokens+message.Usage.OutputTokens)),
105+
)
106+
telemetry.AddMessageEvent(span, "llm.response", "assistant", result)
107+
88108
return result, nil
89109
}
90110

@@ -102,6 +122,13 @@ func (p *AnthropicProvider) StreamWithTools(ctx context.Context,
102122
messages []llm.Message, tools []llm.ToolDefinition,
103123
onEvent func(llm.StreamEvent)) (*llm.Response, error) {
104124

125+
ctx, span := otel.Tracer(telemetry.TracerName).Start(ctx, "llm.api.call",
126+
trace.WithAttributes(
127+
telemetry.AttrLLMProvider.String(p.ProviderName()),
128+
telemetry.AttrLLMModel.String(p.model),
129+
))
130+
defer span.End()
131+
105132
ctx, cancel := context.WithTimeout(ctx, p.timeout)
106133
defer cancel()
107134

@@ -216,6 +243,7 @@ func (p *AnthropicProvider) StreamWithTools(ctx context.Context,
216243
Content: fmt.Sprintf("stream error: %v", err),
217244
})
218245
}
246+
span.SetStatus(codes.Error, err.Error())
219247
return nil, fmt.Errorf("API streaming failed: %w", err)
220248
}
221249

@@ -236,6 +264,13 @@ func (p *AnthropicProvider) StreamWithTools(ctx context.Context,
236264
resp.StopReason = llm.StopReasonEndTurn
237265
}
238266

267+
span.SetAttributes(
268+
telemetry.AttrLLMInputTokens.Int(resp.Usage.InputTokens),
269+
telemetry.AttrLLMOutputTokens.Int(resp.Usage.OutputTokens),
270+
telemetry.AttrLLMTotalTokens.Int(resp.Usage.TotalTokens),
271+
telemetry.AttrLLMStopReason.String(string(resp.StopReason)),
272+
)
273+
239274
for _, block := range accumulated.Content {
240275
switch block.Type {
241276
case "text":

internal/bridge/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
"github.com/a2aproject/a2a-go/v2/a2a"
1010
"github.com/a2aproject/a2a-go/v2/a2aclient"
11+
"go.opentelemetry.io/otel"
12+
"go.opentelemetry.io/otel/propagation"
1113
)
1214

1315
// A2AClient wraps the a2aclient to send requests to A2A agents.
@@ -84,6 +86,14 @@ func (c *A2AClient) Invoke(ctx context.Context, req *InvokeRequest) (*InvokeResu
8486
if req.AgentSPIFFEID != "" {
8587
sp.Append("x-delegation-agent", req.AgentSPIFFEID)
8688
}
89+
90+
// Inject W3C trace context so downstream agents continue the trace.
91+
carrier := propagation.MapCarrier{}
92+
otel.GetTextMapPropagator().Inject(ctx, carrier)
93+
for k, v := range carrier {
94+
sp.Append(k, v)
95+
}
96+
8797
if len(sp) > 0 {
8898
opts = append(opts, a2aclient.WithCallInterceptors(&serviceParamsInjector{params: sp}))
8999
}

internal/bridge/executor.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import (
77

88
"github.com/a2aproject/a2a-go/v2/a2a"
99
"github.com/a2aproject/a2a-go/v2/a2asrv"
10+
"go.opentelemetry.io/otel"
11+
"go.opentelemetry.io/otel/attribute"
12+
"go.opentelemetry.io/otel/propagation"
13+
"go.opentelemetry.io/otel/trace"
1014

1115
"github.com/redhat-et/docsclaw/internal/logger"
16+
"github.com/redhat-et/docsclaw/internal/telemetry"
1217
"github.com/redhat-et/docsclaw/internal/session"
1318
"github.com/redhat-et/docsclaw/pkg/llm"
1419
)
@@ -71,8 +76,26 @@ func (e *AgentExecutor) Execute(ctx context.Context, execCtx *a2asrv.ExecutorCon
7176
if vals, found := sp.Get("x-session-id"); found && len(vals) > 0 {
7277
sessionID = vals[0]
7378
}
79+
80+
// Extract W3C trace context from incoming A2A request
81+
// so this agent's spans join the caller's distributed trace.
82+
carrier := propagation.MapCarrier{}
83+
for _, key := range []string{"traceparent", "tracestate", "baggage"} {
84+
if vals, found := sp.Get(key); found && len(vals) > 0 {
85+
carrier.Set(key, vals[0])
86+
}
87+
}
88+
ctx = otel.GetTextMapPropagator().Extract(ctx, carrier)
7489
}
7590

91+
// Create a span for the A2A execution, parented to the extracted trace.
92+
ctx, execSpan := otel.Tracer(telemetry.TracerName).Start(ctx, "a2a.execute",
93+
trace.WithAttributes(
94+
attribute.String("a2a.task_id", string(execCtx.TaskID)),
95+
telemetry.AttrSessionID.String(sessionID),
96+
))
97+
defer execSpan.End()
98+
7699
// Store delegation context so DelegationTransport can inject headers
77100
// on outbound HTTP requests (e.g., to document-service).
78101
if userSPIFFEID != "" || agentSPIFFEID != "" {

0 commit comments

Comments
 (0)