diff --git a/internal/proxy/handler.go b/internal/proxy/handler.go index 75dbf5bf..0b38f8bf 100644 --- a/internal/proxy/handler.go +++ b/internal/proxy/handler.go @@ -12,7 +12,6 @@ import ( "time" "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" oteltrace "go.opentelemetry.io/otel/trace" "github.com/github/gh-aw-mcpg/internal/difc" @@ -199,9 +198,11 @@ func (h *proxyHandler) handleWithDIFC(w http.ResponseWriter, r *http.Request, pa if denied, _ := guard.HandlePrePhaseError(err); denied != nil { logHandler.Printf("[DIFC] Phase 2: BLOCKED %s %s — %s", r.Method, path, denied.EvalResult.Reason) deniedErr := fmt.Errorf("DIFC policy violation: %s", denied.EvalResult.Reason) - difcSpan.AddEvent("difc.access_denied", oteltrace.WithAttributes( - attribute.String("reason", denied.EvalResult.Reason), - )) + if difcSpan.IsRecording() { + difcSpan.AddEvent("difc.access_denied", oteltrace.WithAttributes( + attribute.String("reason", denied.EvalResult.Reason), + )) + } tracing.RecordSpanError(difcSpan, deniedErr, "access denied: "+denied.EvalResult.Reason) writeDIFCForbidden(w, deniedErr.Error()) return @@ -211,7 +212,9 @@ func (h *proxyHandler) handleWithDIFC(w http.ResponseWriter, r *http.Request, pa httputil.WriteErrorResponse(w, http.StatusBadGateway, "bad_gateway", "resource labeling failed") return } - difcSpan.AddEvent("difc.pre_phases_complete") + if difcSpan.IsRecording() { + difcSpan.AddEvent("difc.pre_phases_complete") + } // **Phase 3: Forward to upstream GitHub API** clientAuth := r.Header.Get("Authorization") @@ -226,14 +229,16 @@ func (h *proxyHandler) handleWithDIFC(w http.ResponseWriter, r *http.Request, pa resp, respBody = h.forwardAndReadBody(w, fwdCtx, r.Method, path, nil, "", clientAuth) } if resp != nil { - fwdSpan.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(resp.StatusCode)) + fwdSpan.SetAttributes(tracing.HTTPResponseStatusCodeKey.Int(resp.StatusCode)) if rateLimited, resetHeader := rateLimitSignal(resp); rateLimited { fwdSpan.SetAttributes(tracing.RateLimitHit.Bool(true)) - eventAttrs := []attribute.KeyValue{} - if resetAt := githubhttp.ParseRateLimitResetHeader(resetHeader); !resetAt.IsZero() { - eventAttrs = append(eventAttrs, attribute.String("reset_at", resetAt.UTC().Format(time.RFC3339))) + if difcSpan.IsRecording() { + eventAttrs := []attribute.KeyValue{} + if resetAt := githubhttp.ParseRateLimitResetHeader(resetHeader); !resetAt.IsZero() { + eventAttrs = append(eventAttrs, attribute.String("reset_at", resetAt.UTC().Format(time.RFC3339))) + } + difcSpan.AddEvent("rate_limit.detected", oteltrace.WithAttributes(eventAttrs...)) } - difcSpan.AddEvent("rate_limit.detected", oteltrace.WithAttributes(eventAttrs...)) } } if resp == nil { diff --git a/internal/server/unified.go b/internal/server/unified.go index 1b191259..f6bfa42d 100644 --- a/internal/server/unified.go +++ b/internal/server/unified.go @@ -438,9 +438,11 @@ func (us *UnifiedServer) callBackendTool(ctx context.Context, serverID, toolName if denied, detailedErr := guard.HandlePrePhaseError(err); denied != nil { logger.LogWarn("difc", "Access DENIED for agent %s to %s: %s", agentID, denied.Resource.Description, denied.EvalResult.Reason) - toolSpan.AddEvent("difc.access_denied", oteltrace.WithAttributes( - attribute.String("reason", denied.EvalResult.Reason), - )) + if toolSpan.IsRecording() { + toolSpan.AddEvent("difc.access_denied", oteltrace.WithAttributes( + attribute.String("reason", denied.EvalResult.Reason), + )) + } tracing.RecordSpanError(toolSpan, detailedErr, "access denied: "+denied.EvalResult.Reason) httpStatusCode = 403 return mcp.NewErrorCallToolResult(detailedErr) @@ -449,8 +451,9 @@ func (us *UnifiedServer) callBackendTool(ctx context.Context, serverID, toolName httpStatusCode = 500 return mcp.NewErrorCallToolResult(fmt.Errorf("guard labeling failed: %w", err)) } - toolSpan.AddEvent("difc.pre_phases_complete") - + if toolSpan.IsRecording() { + toolSpan.AddEvent("difc.pre_phases_complete") + } // Add agent tags snapshot to context for enriched MCP backend logging (Phase 3). ctx = context.WithValue(ctx, mcp.AgentTagsSnapshotContextKey, &mcp.AgentTagsSnapshot{ Secrecy: difc.TagsToStrings(pre.AgentLabels.GetSecrecyTags()), @@ -474,9 +477,10 @@ func (us *UnifiedServer) callBackendTool(ctx context.Context, serverID, toolName // Transport errors (connection failure, JSON parse error, etc.) are not rate-limit // events and must not affect the consecutive rate-limit counter. Leave the circuit // breaker state unchanged so genuine rate-limit history is preserved. - // Use a generic error message for trace recording to avoid leaking internal details - // to trace backends; the full error is returned to the caller and logged separately. - tracing.RecordSpanError(execSpan, fmt.Errorf("tool execution failed"), "tool execution failed") + // Use RecordSpanErrorSafe to avoid leaking internal transport/parse error + // details to trace backends; the full error is returned to the caller and + // logged separately. + tracing.RecordSpanErrorSafe(execSpan, err, "tool execution failed") httpStatusCode = 500 return mcp.NewErrorCallToolResult(err) } @@ -486,11 +490,13 @@ func (us *UnifiedServer) callBackendTool(ctx context.Context, serverID, toolName cb.RecordRateLimit(resetAt) execSpan.SetAttributes(tracing.RateLimitHit.Bool(true)) toolSpan.SetAttributes(tracing.RateLimitHit.Bool(true)) - eventAttrs := []attribute.KeyValue{} - if !resetAt.IsZero() { - eventAttrs = append(eventAttrs, attribute.String("reset_at", resetAt.UTC().Format(time.RFC3339))) + if toolSpan.IsRecording() { + eventAttrs := []attribute.KeyValue{} + if !resetAt.IsZero() { + eventAttrs = append(eventAttrs, attribute.String("reset_at", resetAt.UTC().Format(time.RFC3339))) + } + toolSpan.AddEvent("rate_limit.detected", oteltrace.WithAttributes(eventAttrs...)) } - toolSpan.AddEvent("rate_limit.detected", oteltrace.WithAttributes(eventAttrs...)) tracing.RecordSpanErrorOnAll(errRateLimitExceeded, rateLimitExceededStatus, execSpan, toolSpan) httpStatusCode = 429 // Preserve the original backend error text so the agent sees the actual upstream diff --git a/internal/tracing/fanout.go b/internal/tracing/fanout.go index 60880bf6..906820a3 100644 --- a/internal/tracing/fanout.go +++ b/internal/tracing/fanout.go @@ -4,6 +4,7 @@ import ( "context" "errors" "sync" + "time" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) @@ -66,12 +67,32 @@ func (f *fanoutExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadO }) } -// Shutdown shuts down each underlying exporter concurrently, collecting any -// errors. All errors are joined and returned. +// Shutdown shuts down each underlying exporter concurrently, giving each +// exporter its own context derived from the parent's remaining deadline. +// This prevents a slow or unresponsive exporter from consuming the entire +// deadline and leaving insufficient time for other exporters to complete. func (f *fanoutExporter) Shutdown(ctx context.Context) error { logTracing.Printf("fanoutExporter.Shutdown: shutting down %d backends", len(f.exporters)) + + // Capture the remaining time budget from the parent context once so that + // every goroutine starts with the same deadline, even if Go's scheduler + // delays some goroutines after others begin executing. + var remaining time.Duration + if deadline, ok := ctx.Deadline(); ok { + remaining = time.Until(deadline) + } + err := f.forEachExporter("Shutdown", func(e sdktrace.SpanExporter) error { - return e.Shutdown(ctx) + // When there is no deadline, use the parent context as-is so that + // parent cancellation still propagates. + if remaining <= 0 { + return e.Shutdown(ctx) + } + // Give each exporter a fresh, independent context with the remaining + // budget so one slow exporter cannot starve the others. + exporterCtx, cancel := context.WithTimeout(ctx, remaining) + defer cancel() + return e.Shutdown(exporterCtx) }) logTracing.Printf("fanoutExporter.Shutdown: completed") return err diff --git a/internal/tracing/genai_attrs.go b/internal/tracing/genai_attrs.go index 9a2d42fe..0b95e627 100644 --- a/internal/tracing/genai_attrs.go +++ b/internal/tracing/genai_attrs.go @@ -1,38 +1,16 @@ // Package tracing provides OpenTelemetry OTLP trace export for the MCP Gateway. -// This file defines gen_ai semantic convention attribute keys per -// https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ +// This file defines custom and non-semconv attribute keys for gen_ai and MCP spans. +// The semconv-derived GenAI attribute keys (GenAIToolName, GenAIOperationName, etc.) +// are defined in semconv.go, which is the single file that imports +// go.opentelemetry.io/otel/semconv/v1.41.0. package tracing -import ( - "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" -) - -// GenAI semantic convention attribute keys. -// Most are aliases for the official OpenTelemetry gen_ai semconv constants -// (semconv/v1.41.0), re-exported here for convenience. GenAISystem is the -// exception: it was removed in semconv/v1.41.0 and is defined as a raw -// attribute.Key to preserve wire compatibility. -const ( - // GenAISystem identifies the GenAI system family for MCP spans. - // gen_ai.system was removed from semconv/v1.41.0; the key string is preserved for compatibility. - GenAISystem = attribute.Key("gen_ai.system") - - // GenAIToolName is the name of the tool utilized by the agent. - GenAIToolName = semconv.GenAIToolNameKey +import "go.opentelemetry.io/otel/attribute" - // GenAIOperationName is the name of the operation being performed. - GenAIOperationName = semconv.GenAIOperationNameKey - - // GenAIConversationID is the unique identifier for a conversation (session). - GenAIConversationID = semconv.GenAIConversationIDKey - - // GenAIAgentName is the human-readable name of the GenAI agent. - GenAIAgentName = semconv.GenAIAgentNameKey - - // GenAIAgentID is the unique identifier of the GenAI agent (server ID). - GenAIAgentID = semconv.GenAIAgentIDKey -) +// GenAISystem identifies the GenAI system family for MCP spans. +// gen_ai.system was removed from semconv/v1.41.0; the key string is preserved for +// wire compatibility with observability backends that still expect it. +const GenAISystem = attribute.Key("gen_ai.system") // MCP-specific attribute keys (no gen_ai equivalent in the spec). const ( diff --git a/internal/tracing/http.go b/internal/tracing/http.go index 5c77cace..6f2afc21 100644 --- a/internal/tracing/http.go +++ b/internal/tracing/http.go @@ -11,7 +11,6 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" oteltrace "go.opentelemetry.io/otel/trace" "github.com/github/gh-aw-mcpg/internal/httputil" @@ -58,11 +57,11 @@ func WrapHTTPHandler(next http.Handler, spanName string, extraAttrs ...attribute } attrs := append([]attribute.KeyValue{ - semconv.HTTPRequestMethodKey.String(r.Method), - semconv.URLPathKey.String(r.URL.Path), + HTTPRequestMethodKey.String(r.Method), + URLPathKey.String(r.URL.Path), }, extraAttrs...) if route != "" { - attrs = append(attrs, semconv.HTTPRouteKey.String(route)) + attrs = append(attrs, HTTPRouteKey.String(route)) } ctx, span := t.Start(ctx, spanName, @@ -75,7 +74,7 @@ func WrapHTTPHandler(next http.Handler, spanName string, extraAttrs ...attribute srw := &statusResponseWriter{BaseResponseWriter: httputil.BaseResponseWriter{ResponseWriter: w, StatusCode: http.StatusOK}} defer func() { - span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(srw.StatusCode)) + span.SetAttributes(HTTPResponseStatusCodeKey.Int(srw.StatusCode)) if srw.StatusCode >= 500 { msg := http.StatusText(srw.StatusCode) if msg == "" { diff --git a/internal/tracing/provider.go b/internal/tracing/provider.go index 60c4799a..df285e2a 100644 --- a/internal/tracing/provider.go +++ b/internal/tracing/provider.go @@ -29,7 +29,6 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" @@ -192,8 +191,8 @@ func InitProvider(ctx context.Context, cfg *config.TracingConfig) (*Provider, er resource.WithTelemetrySDK(), resource.WithContainer(), resource.WithAttributes( - semconv.ServiceName(serviceName), - semconv.ServiceVersion(version.Get()), + ServiceName(serviceName), + ServiceVersion(version.Get()), ), resource.WithProcessPID(), resource.WithHost(), @@ -201,9 +200,9 @@ func InitProvider(ctx context.Context, cfg *config.TracingConfig) (*Provider, er if err != nil { logTracing.Printf("Warning: failed to create OTEL resource: %v", err) serviceResource := resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName(serviceName), - semconv.ServiceVersion(version.Get()), + SchemaURL, + ServiceName(serviceName), + ServiceVersion(version.Get()), ) // resource.New can return a best-effort resource alongside an error. // Preserve detected attributes and only ensure service identity exists. @@ -231,7 +230,13 @@ func InitProvider(ctx context.Context, cfg *config.TracingConfig) (*Provider, er } sdkTP := sdktrace.NewTracerProvider( - sdktrace.WithBatcher(exporter), + sdktrace.WithBatcher(exporter, + // Tune batch processor for gateway throughput: halve the default batch + // size (512→256) and reduce the flush interval (5s→2s) to lower tail + // latency for trace delivery without sacrificing export efficiency. + sdktrace.WithMaxExportBatchSize(256), + sdktrace.WithBatchTimeout(2*time.Second), + ), sdktrace.WithResource(res), sdktrace.WithSampler(sampler), ) diff --git a/internal/tracing/semconv.go b/internal/tracing/semconv.go new file mode 100644 index 00000000..04c4b991 --- /dev/null +++ b/internal/tracing/semconv.go @@ -0,0 +1,65 @@ +// Package tracing provides OpenTelemetry OTLP trace export for the MCP Gateway. +// This file is the single source of truth for go.opentelemetry.io/otel/semconv/v1.41.0 +// imports in the tracing package. All callers inside and outside this package should +// reference these re-exports so that upgrading semconv only requires editing this file. +package tracing + +import ( + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" +) + +// SchemaURL is the semconv schema URL for this semantic conventions version. +const SchemaURL = semconv.SchemaURL + +// HTTP and URL semantic convention attribute keys. +const ( + // HTTPRequestMethodKey is the HTTP request method. + HTTPRequestMethodKey = semconv.HTTPRequestMethodKey + // HTTPRouteKey is the matched HTTP route template. + HTTPRouteKey = semconv.HTTPRouteKey + // HTTPResponseStatusCodeKey is the HTTP response status code. + HTTPResponseStatusCodeKey = semconv.HTTPResponseStatusCodeKey + // URLPathKey is the full URL path. + URLPathKey = semconv.URLPathKey + // ServerAddressKey is the server domain name or IP address. + ServerAddressKey = semconv.ServerAddressKey +) + +// Error semantic convention keys. +const ( + // ErrorTypeKey is the attribute key for error.type. + ErrorTypeKey = semconv.ErrorTypeKey +) + +// GenAI semantic convention attribute keys (semconv/v1.41.0). +// gen_ai.system was removed from the spec; see GenAISystem in genai_attrs.go. +const ( + // GenAIToolName is the name of the tool utilized by the agent. + GenAIToolName = semconv.GenAIToolNameKey + // GenAIOperationName is the name of the operation being performed. + GenAIOperationName = semconv.GenAIOperationNameKey + // GenAIConversationID is the unique identifier for a conversation (session). + GenAIConversationID = semconv.GenAIConversationIDKey + // GenAIAgentName is the human-readable name of the GenAI agent. + GenAIAgentName = semconv.GenAIAgentNameKey + // GenAIAgentID is the unique identifier of the GenAI agent (server ID). + GenAIAgentID = semconv.GenAIAgentIDKey +) + +// ErrorType returns the error.type attribute KeyValue for err. +// It wraps semconv.ErrorType so callers outside this package can use it +// without importing the semconv package directly. +func ErrorType(err error) attribute.KeyValue { + return semconv.ErrorType(err) +} + +// ServiceName returns the service.name attribute KeyValue for val. +func ServiceName(val string) attribute.KeyValue { + return semconv.ServiceName(val) +} + +// ServiceVersion returns the service.version attribute KeyValue for val. +func ServiceVersion(val string) attribute.KeyValue { + return semconv.ServiceVersion(val) +} diff --git a/internal/tracing/span_helpers.go b/internal/tracing/span_helpers.go index b9e6dede..3dbcefba 100644 --- a/internal/tracing/span_helpers.go +++ b/internal/tracing/span_helpers.go @@ -4,10 +4,10 @@ package tracing import ( "context" + "errors" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -18,11 +18,27 @@ func RecordSpanError(span oteltrace.Span, err error, msg string) { logTracing.Printf("Recording span error: msg=%s, err=%v", msg, err) span.RecordError(err, oteltrace.WithStackTrace(true)) if err != nil { - span.SetAttributes(semconv.ErrorType(err)) + span.SetAttributes(ErrorType(err)) } span.SetStatus(codes.Error, msg) } +// RecordSpanErrorSafe records a scrubbed error on span for security-sensitive paths. +// internalErr is the actual error (logged and returned to the caller via normal channels); +// publicMsg is the message recorded on the span and set as the status description. +// This prevents internal error details from leaking to trace backends, which may be +// operated by third parties. +func RecordSpanErrorSafe(span oteltrace.Span, internalErr error, publicMsg string) { + if !span.IsRecording() { + return + } + logTracing.Printf("Recording span error (safe): msg=%s, internal=%v", publicMsg, internalErr) + publicErr := errors.New(publicMsg) + span.RecordError(publicErr, oteltrace.WithStackTrace(true)) + span.SetAttributes(ErrorType(publicErr)) + span.SetStatus(codes.Error, publicMsg) +} + // RecordSpanErrorOnAll records err on all provided spans with a stack trace and sets their // status to Error. Useful when both a parent and child span must reflect the same failure. func RecordSpanErrorOnAll(err error, msg string, spans ...oteltrace.Span) { @@ -71,7 +87,7 @@ func StartDIFCPipelineSpan(ctx context.Context, tracer oteltrace.Tracer, toolNam logTracing.Printf("Starting DIFC pipeline span: toolName=%s, urlPath=%s", toolName, urlPath) return startSpan(ctx, tracer, "proxy.difc_pipeline", oteltrace.SpanKindInternal, GenAIToolName.String(toolName), - semconv.URLPathKey.String(urlPath), + URLPathKey.String(urlPath), ) } @@ -80,8 +96,8 @@ func StartDIFCPipelineSpan(ctx context.Context, tracer oteltrace.Tracer, toolNam func StartProxyForwardSpan(ctx context.Context, tracer oteltrace.Tracer, toolName, urlPath, serverAddress string) (context.Context, oteltrace.Span) { logTracing.Printf("Starting proxy forward span: toolName=%s, urlPath=%s", toolName, urlPath) return startSpan(ctx, tracer, "proxy.backend.forward", oteltrace.SpanKindClient, - semconv.URLPathKey.String(urlPath), - semconv.ServerAddressKey.String(serverAddress), + URLPathKey.String(urlPath), + ServerAddressKey.String(serverAddress), GenAIToolName.String(toolName), ) } diff --git a/internal/tracing/span_helpers_test.go b/internal/tracing/span_helpers_test.go index d26d03f0..1ed67688 100644 --- a/internal/tracing/span_helpers_test.go +++ b/internal/tracing/span_helpers_test.go @@ -10,7 +10,6 @@ import ( "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" - semconv "go.opentelemetry.io/otel/semconv/v1.41.0" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -50,7 +49,7 @@ func TestRecordSpanError_SetsStatusAndRecordsEvent(t *testing.T) { assert.Equal(t, "Error", recorded.Status.Code.String(), "span status should be Error") assert.Equal(t, "test failure", recorded.Status.Description) - assert.True(t, hasAttr(recorded.Attributes, semconv.ErrorTypeKey, "*errors.errorString")) + assert.True(t, hasAttr(recorded.Attributes, ErrorTypeKey, "*errors.errorString")) var foundStackTrace bool for _, event := range recorded.Events { @@ -104,6 +103,50 @@ func TestRecordSpanErrorOnAll_SingleSpan_BehavesLikeRecordSpanError(t *testing.T assert.Equal(t, "single span msg", spans[0].Status.Description) } +func TestRecordSpanErrorSafe_RecordsPublicMsgNotInternalError(t *testing.T) { + span, getSpans := newRecordingSpan(t, "op") + // Use a distinctive string in the internal error to verify it is never surfaced. + internalErr := errors.New("transport error: secret-token-12345 expired") + publicMsg := "tool execution failed" + + RecordSpanErrorSafe(span, internalErr, publicMsg) + + spans := getSpans() + require.Len(t, spans, 1) + recorded := spans[0] + + assert.Equal(t, "Error", recorded.Status.Code.String(), "span status should be Error") + assert.Equal(t, publicMsg, recorded.Status.Description) + + // Verify the public message — not the internal error — is what's recorded on the span. + require.NotEmpty(t, recorded.Events) + exceptionEvent := recorded.Events[0] + assert.Equal(t, "exception", exceptionEvent.Name) + foundMsg := false + for _, attr := range exceptionEvent.Attributes { + if attr.Key == "exception.message" { + foundMsg = true + assert.Equal(t, publicMsg, attr.Value.AsString(), + "exception.message should be the public message, not the internal error") + assert.NotContains(t, attr.Value.AsString(), "secret-token-12345", + "internal error details must not leak to the trace backend") + } + } + require.True(t, foundMsg, "exception.message attribute should be present on exception event") +} + +func TestRecordSpanErrorSafe_SetsErrorTypeFromPublicErr(t *testing.T) { + span, getSpans := newRecordingSpan(t, "op") + + RecordSpanErrorSafe(span, errors.New("sensitive internal details"), "tool execution failed") + + spans := getSpans() + require.Len(t, spans, 1) + // error.type attribute should reflect *errors.errorString (the public error type), + // not expose anything from the internal error. + assert.True(t, hasAttr(spans[0].Attributes, ErrorTypeKey, "*errors.errorString")) +} + // newRecordingTracer creates an in-memory tracer provider and returns the tracer // together with a function that flushes and returns all recorded spans. func newRecordingTracer(t *testing.T) (oteltrace.Tracer, func() []tracetest.SpanStub) { @@ -174,7 +217,7 @@ func TestStartDIFCPipelineSpan(t *testing.T) { assert.Equal(t, "proxy.difc_pipeline", s.Name) assert.Equal(t, oteltrace.SpanKindInternal, s.SpanKind) assert.True(t, hasAttr(s.Attributes, GenAIToolName, "my_tool")) - assert.True(t, hasAttr(s.Attributes, semconv.URLPathKey, "/api/v3/repos")) + assert.True(t, hasAttr(s.Attributes, URLPathKey, "/api/v3/repos")) } func TestStartProxyForwardSpan(t *testing.T) { @@ -188,7 +231,7 @@ func TestStartProxyForwardSpan(t *testing.T) { s := spans[0] assert.Equal(t, "proxy.backend.forward", s.Name) assert.Equal(t, oteltrace.SpanKindClient, s.SpanKind) - assert.True(t, hasAttr(s.Attributes, semconv.URLPathKey, "/api/v3/repos")) - assert.True(t, hasAttr(s.Attributes, semconv.ServerAddressKey, "api.github.com")) + assert.True(t, hasAttr(s.Attributes, URLPathKey, "/api/v3/repos")) + assert.True(t, hasAttr(s.Attributes, ServerAddressKey, "api.github.com")) assert.True(t, hasAttr(s.Attributes, GenAIToolName, "my_tool")) }