Skip to content

Commit 6864802

Browse files
committed
feat: instrument controller with distributed tracing and A2A trace propagation
Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
1 parent 4abeb99 commit 6864802

16 files changed

Lines changed: 405 additions & 37 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ open-dev-container:
440440
otel-local:
441441
docker rm -f jaeger-desktop || true
442442
docker run -d --name jaeger-desktop --restart=always -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/jaeger:2.7.0
443-
open http://localhost:16686/
443+
@echo "Jaeger UI available at http://localhost:16686/"
444444

445445
.PHONY: kind-debug
446446
kind-debug:

go/core/go.mod

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/fatih/color v1.18.0
1313
github.com/glebarez/sqlite v1.11.0
1414
github.com/go-logr/logr v1.4.3
15+
github.com/google/uuid v1.6.0
1516
github.com/gorilla/mux v1.8.1
1617
github.com/hashicorp/go-multierror v1.1.1
1718
github.com/jedib0t/go-pretty/v6 v6.7.8
@@ -28,6 +29,10 @@ require (
2829
github.com/spf13/viper v1.21.0
2930
github.com/stoewer/go-strcase v1.3.1
3031
github.com/stretchr/testify v1.11.1
32+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
33+
go.opentelemetry.io/otel v1.40.0
34+
go.opentelemetry.io/otel/sdk v1.40.0
35+
go.opentelemetry.io/otel/trace v1.40.0
3136
go.uber.org/automaxprocs v1.6.0
3237
golang.org/x/text v0.34.0
3338
google.golang.org/protobuf v1.36.11
@@ -45,7 +50,7 @@ require (
4550
)
4651

4752
require (
48-
cel.dev/expr v0.24.0 // indirect
53+
cel.dev/expr v0.25.1 // indirect
4954
github.com/abiosoft/ishell v2.0.0+incompatible // indirect
5055
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
5156
github.com/anthropics/anthropic-sdk-go v1.22.1 // indirect
@@ -57,15 +62,11 @@ require (
5762
github.com/blang/semver/v4 v4.0.0 // indirect
5863
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
5964
github.com/cespare/xxhash/v2 v2.3.0 // indirect
60-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250617194119-3f1d09f7d826 // indirect
6165
github.com/charmbracelet/colorprofile v0.3.2 // indirect
6266
github.com/charmbracelet/x/ansi v0.10.1 // indirect
6367
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 // indirect
6468
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f // indirect
65-
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260305213658-fe36e8c10185 // indirect
66-
github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
6769
github.com/charmbracelet/x/term v0.2.1 // indirect
68-
github.com/charmbracelet/x/windows v0.2.1 // indirect
6970
github.com/chzyer/test v1.0.0 // indirect
7071
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
7172
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
@@ -103,7 +104,6 @@ require (
103104
github.com/google/gnostic-models v0.7.1 // indirect
104105
github.com/google/go-cmp v0.7.0 // indirect
105106
github.com/google/jsonschema-go v0.4.2 // indirect
106-
github.com/google/uuid v1.6.0 // indirect
107107
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 // indirect
108108
github.com/hashicorp/errwrap v1.1.0 // indirect
109109
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -155,13 +155,9 @@ require (
155155
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
156156
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
157157
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
158-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
159-
go.opentelemetry.io/otel v1.40.0 // indirect
160158
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
161159
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
162160
go.opentelemetry.io/otel/metric v1.40.0 // indirect
163-
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
164-
go.opentelemetry.io/otel/trace v1.40.0 // indirect
165161
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
166162
go.uber.org/multierr v1.11.0 // indirect
167163
go.uber.org/zap v1.27.1 // indirect

go/core/go.sum

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
2-
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
1+
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
2+
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
33
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
44
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
55
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
@@ -38,30 +38,18 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
3838
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
3939
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
4040
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
41-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250617194119-3f1d09f7d826 h1:pQxCWMojVjHePqGzWsANhplourZYsD6AuR6eR2Hi5yc=
42-
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250617194119-3f1d09f7d826/go.mod h1:bfzSaUDPMKrWDjD6wo/ato9lfDdEX83rZgwcXHYWJ98=
4341
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
4442
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
4543
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
4644
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
4745
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
4846
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
49-
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
50-
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
5147
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 h1:MTSs/nsZNfZPbYk/r9hluK2BtwoqvEYruAujNVwgDv0=
5248
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
53-
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
54-
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
5549
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f h1:UytXHv0UxnsDFmL/7Z9Q5SBYPwSuRLXHbwx+6LycZ2w=
5650
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
57-
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260305213658-fe36e8c10185 h1:JgaLfHQ4IuluEWG2yq541AN1696JYiF9lPX3/29G75c=
58-
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260305213658-fe36e8c10185/go.mod h1:zYyXKZx1gN3GWYa+S45Tc7Pp8IHiH07VmsoxXSbxqw4=
59-
github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
60-
github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
6151
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
6252
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
63-
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
64-
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
6553
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
6654
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
6755
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=

go/core/internal/a2a/a2a_handler_mux.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type A2AHandlerMux interface {
1919
agentRef string,
2020
client *client.A2AClient,
2121
card server.AgentCard,
22+
tracing server.Middleware,
2223
) error
2324
RemoveAgentHandler(
2425
agentRef string,
@@ -47,8 +48,13 @@ func (a *handlerMux) SetAgentHandler(
4748
agentRef string,
4849
client *client.A2AClient,
4950
card server.AgentCard,
51+
tracing server.Middleware,
5052
) error {
51-
srv, err := server.NewA2AServer(card, NewPassthroughManager(client), server.WithMiddleWare(authimpl.NewA2AAuthenticator(a.authenticator)))
53+
middlewares := []server.Middleware{authimpl.NewA2AAuthenticator(a.authenticator)}
54+
if tracing != nil {
55+
middlewares = append(middlewares, tracing)
56+
}
57+
srv, err := server.NewA2AServer(card, NewPassthroughManager(client), server.WithMiddleWare(middlewares...))
5258
if err != nil {
5359
return fmt.Errorf("failed to create A2A server: %w", err)
5460
}

go/core/internal/a2a/a2a_registrar.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,19 @@ func (a *A2ARegistrar) upsertAgentHandler(ctx context.Context, agent *v1alpha2.A
124124
agentRef := types.NamespacedName{Namespace: agent.GetNamespace(), Name: agent.GetName()}
125125
card := agent_translator.GetA2AAgentCard(agent)
126126

127+
provider := resolveProviderName(ctx, a.cache, agent)
128+
127129
client, err := a2aclient.NewA2AClient(
128130
card.URL,
129131
append(
130132
a.a2aBaseOptions,
131133
a2aclient.WithHTTPReqHandler(
132-
authimpl.A2ARequestHandler(
133-
a.authenticator,
134-
agentRef,
135-
),
134+
&traceInjectHandler{
135+
next: authimpl.A2ARequestHandler(
136+
a.authenticator,
137+
agentRef,
138+
),
139+
},
136140
),
137141
)...,
138142
)
@@ -143,7 +147,7 @@ func (a *A2ARegistrar) upsertAgentHandler(ctx context.Context, agent *v1alpha2.A
143147
cardCopy := *card
144148
cardCopy.URL = fmt.Sprintf("%s/%s/", a.a2aBaseUrl, agentRef)
145149

146-
if err := a.handlerMux.SetAgentHandler(agentRef.String(), client, cardCopy); err != nil {
150+
if err := a.handlerMux.SetAgentHandler(agentRef.String(), client, cardCopy, newA2ATracingMiddleware(agentRef, provider)); err != nil {
147151
return fmt.Errorf("set handler for %s: %w", agentRef, err)
148152
}
149153

go/core/internal/a2a/trace.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package a2a
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"go.opentelemetry.io/otel"
8+
"go.opentelemetry.io/otel/attribute"
9+
"go.opentelemetry.io/otel/propagation"
10+
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
11+
"go.opentelemetry.io/otel/trace"
12+
"k8s.io/apimachinery/pkg/types"
13+
crcache "sigs.k8s.io/controller-runtime/pkg/cache"
14+
a2aclient "trpc.group/trpc-go/trpc-a2a-go/client"
15+
16+
"github.com/kagent-dev/kagent/go/api/v1alpha2"
17+
)
18+
19+
// a2aTracingMiddleware is an A2A server middleware that creates an invoke_agent
20+
// span for each inbound A2A request, annotated with GenAI semantic convention
21+
// attributes. The span becomes the parent of any outbound proxy calls made by
22+
// traceInjectHandler, giving a clean agent-invocation span hierarchy in Jaeger.
23+
type a2aTracingMiddleware struct {
24+
agentRef types.NamespacedName
25+
provider attribute.KeyValue
26+
}
27+
28+
func newA2ATracingMiddleware(agentRef types.NamespacedName, provider attribute.KeyValue) *a2aTracingMiddleware {
29+
return &a2aTracingMiddleware{agentRef: agentRef, provider: provider}
30+
}
31+
32+
func (m *a2aTracingMiddleware) Wrap(next http.Handler) http.Handler {
33+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34+
ctx, span := otel.Tracer("kagent").Start(r.Context(), "invoke_agent",
35+
trace.WithSpanKind(trace.SpanKindServer),
36+
trace.WithAttributes(
37+
semconv.GenAIOperationNameInvokeAgent,
38+
m.provider,
39+
semconv.GenAIAgentName(m.agentRef.Name),
40+
semconv.GenAIAgentID(m.agentRef.String()),
41+
),
42+
)
43+
defer span.End()
44+
next.ServeHTTP(w, r.WithContext(ctx))
45+
})
46+
}
47+
48+
// traceInjectHandler wraps an HTTPReqHandler and injects W3C TraceContext
49+
// headers (traceparent, tracestate) from the Go context into every outgoing
50+
// proxy request, so the downstream agent receives the active span as its parent.
51+
type traceInjectHandler struct {
52+
next a2aclient.HTTPReqHandler
53+
}
54+
55+
func (h *traceInjectHandler) Handle(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
56+
propagation.TraceContext{}.Inject(ctx, propagation.HeaderCarrier(req.Header))
57+
return h.next.Handle(ctx, client, req)
58+
}
59+
60+
// resolveProviderName looks up the ModelConfig for a declarative agent and
61+
// returns the corresponding gen_ai.provider.name attribute. Falls back to "kagent"
62+
// for BYO agents or if the ModelConfig cannot be fetched.
63+
func resolveProviderName(ctx context.Context, cache crcache.Cache, agent *v1alpha2.Agent) attribute.KeyValue {
64+
if agent.Spec.Declarative == nil {
65+
return semconv.GenAIProviderNameKey.String("kagent")
66+
}
67+
mcName := agent.Spec.Declarative.ModelConfig
68+
if mcName == "" {
69+
mcName = "default-model-config"
70+
}
71+
mc := &v1alpha2.ModelConfig{}
72+
if err := cache.Get(ctx, types.NamespacedName{Namespace: agent.GetNamespace(), Name: mcName}, mc); err != nil {
73+
return semconv.GenAIProviderNameKey.String("kagent")
74+
}
75+
return genAIProviderName(mc.Spec.Provider)
76+
}
77+
78+
// genAIProviderName maps kagent's ModelProvider values to the standard
79+
// gen_ai.provider.name attributes defined by the OpenTelemetry GenAI semantic
80+
// conventions. Custom values are used for providers not in the standard list.
81+
func genAIProviderName(p v1alpha2.ModelProvider) attribute.KeyValue {
82+
switch p {
83+
case v1alpha2.ModelProviderOpenAI:
84+
return semconv.GenAIProviderNameOpenAI
85+
case v1alpha2.ModelProviderAzureOpenAI:
86+
return semconv.GenAIProviderNameAzureAIOpenAI
87+
case v1alpha2.ModelProviderAnthropic:
88+
return semconv.GenAIProviderNameAnthropic
89+
case v1alpha2.ModelProviderGemini:
90+
return semconv.GenAIProviderNameGCPGemini
91+
case v1alpha2.ModelProviderGeminiVertexAI:
92+
return semconv.GenAIProviderNameGCPVertexAI
93+
case v1alpha2.ModelProviderAnthropicVertexAI:
94+
return semconv.GenAIProviderNameKey.String("anthropic.vertex_ai")
95+
case v1alpha2.ModelProviderBedrock:
96+
return semconv.GenAIProviderNameAWSBedrock
97+
case v1alpha2.ModelProviderOllama:
98+
return semconv.GenAIProviderNameKey.String("ollama")
99+
default:
100+
return semconv.GenAIProviderNameKey.String("kagent")
101+
}
102+
}

go/core/internal/a2a/trace_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package a2a
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"go.opentelemetry.io/otel"
10+
"go.opentelemetry.io/otel/propagation"
11+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
12+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
13+
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
14+
"go.opentelemetry.io/otel/trace"
15+
"k8s.io/apimachinery/pkg/types"
16+
)
17+
18+
// mockHTTPReqHandler captures the request passed to Handle for inspection.
19+
type mockHTTPReqHandler struct {
20+
capturedReq *http.Request
21+
}
22+
23+
func (m *mockHTTPReqHandler) Handle(_ context.Context, _ *http.Client, req *http.Request) (*http.Response, error) {
24+
m.capturedReq = req
25+
return &http.Response{StatusCode: http.StatusOK}, nil
26+
}
27+
28+
func TestTraceInjectHandler_InjectsHeader(t *testing.T) {
29+
const rawTraceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
30+
31+
ctx := propagation.TraceContext{}.Extract(
32+
context.Background(),
33+
propagation.MapCarrier{"traceparent": rawTraceparent},
34+
)
35+
36+
mock := &mockHTTPReqHandler{}
37+
h := &traceInjectHandler{next: mock}
38+
39+
req := httptest.NewRequest(http.MethodPost, "/", nil)
40+
if _, err := h.Handle(ctx, nil, req); err != nil {
41+
t.Fatalf("unexpected error: %v", err)
42+
}
43+
44+
got := mock.capturedReq.Header.Get("traceparent")
45+
if got == "" {
46+
t.Fatal("expected traceparent header on outgoing request, got none")
47+
}
48+
49+
// The injected header must carry the same trace ID as the incoming context.
50+
outCtx := propagation.TraceContext{}.Extract(context.Background(), propagation.HeaderCarrier(mock.capturedReq.Header))
51+
wantTraceID := trace.SpanContextFromContext(ctx).TraceID()
52+
gotTraceID := trace.SpanContextFromContext(outCtx).TraceID()
53+
if wantTraceID != gotTraceID {
54+
t.Errorf("trace ID: want %s, got %s", wantTraceID, gotTraceID)
55+
}
56+
}
57+
58+
func TestTraceInjectHandler_NoHeaderWhenNoTrace(t *testing.T) {
59+
mock := &mockHTTPReqHandler{}
60+
h := &traceInjectHandler{next: mock}
61+
62+
req := httptest.NewRequest(http.MethodPost, "/", nil)
63+
if _, err := h.Handle(context.Background(), nil, req); err != nil {
64+
t.Fatalf("unexpected error: %v", err)
65+
}
66+
67+
if got := mock.capturedReq.Header.Get("traceparent"); got != "" {
68+
t.Errorf("expected no traceparent header, got %q", got)
69+
}
70+
}
71+
72+
func TestA2ATracingMiddleware_SetsGenAIAttributes(t *testing.T) {
73+
exporter := tracetest.NewInMemoryExporter()
74+
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
75+
prev := otel.GetTracerProvider()
76+
otel.SetTracerProvider(tp)
77+
t.Cleanup(func() {
78+
otel.SetTracerProvider(prev)
79+
_ = tp.Shutdown(context.Background())
80+
})
81+
82+
agentRef := types.NamespacedName{Namespace: "default", Name: "my-agent"}
83+
mw := newA2ATracingMiddleware(agentRef, semconv.GenAIProviderNameOpenAI)
84+
85+
called := false
86+
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
87+
called = true
88+
w.WriteHeader(http.StatusOK)
89+
})
90+
91+
req := httptest.NewRequest(http.MethodPost, "/", nil)
92+
rr := httptest.NewRecorder()
93+
mw.Wrap(inner).ServeHTTP(rr, req)
94+
95+
if !called {
96+
t.Fatal("inner handler was not called")
97+
}
98+
99+
spans := exporter.GetSpans()
100+
if len(spans) != 1 {
101+
t.Fatalf("expected 1 span, got %d", len(spans))
102+
}
103+
104+
wantAttrs := map[string]string{
105+
"gen_ai.operation.name": "invoke_agent",
106+
"gen_ai.provider.name": "openai",
107+
"gen_ai.agent.name": "my-agent",
108+
"gen_ai.agent.id": "default/my-agent",
109+
}
110+
gotAttrs := make(map[string]string)
111+
for _, a := range spans[0].Attributes {
112+
gotAttrs[string(a.Key)] = a.Value.AsString()
113+
}
114+
for k, want := range wantAttrs {
115+
if got := gotAttrs[k]; got != want {
116+
t.Errorf("attribute %s: want %q, got %q", k, want, got)
117+
}
118+
}
119+
120+
if spans[0].Name != "invoke_agent" {
121+
t.Errorf("span name: want %q, got %q", "invoke_agent", spans[0].Name)
122+
}
123+
}

0 commit comments

Comments
 (0)