Skip to content

Commit 861ffa2

Browse files
authored
Address review feedback: clarify genai_attrs comment, fix span description, add span error tests
1 parent e7b372b commit 861ffa2

3 files changed

Lines changed: 128 additions & 3 deletions

File tree

internal/proxy/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func (h *proxyHandler) handleWithDIFC(w http.ResponseWriter, r *http.Request, pa
186186
errMsg := "returning 503: proxy enforcement not configured (no --policy flag provided)"
187187
logHandler.Print(errMsg)
188188
logger.LogError("proxy", "%s", errMsg)
189-
tracing.RecordSpanError(difcSpan, errors.New("proxy enforcement not configured"), "service unavailable")
189+
tracing.RecordSpanError(difcSpan, errors.New("proxy enforcement not configured"), "proxy enforcement not configured")
190190
httputil.WriteErrorResponse(w, http.StatusServiceUnavailable, "service_unavailable", "proxy enforcement not configured")
191191
return
192192
}

internal/proxy/handler_difc_test.go

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

1212
"github.com/github/gh-aw-mcpg/internal/difc"
1313
"github.com/github/gh-aw-mcpg/internal/guard"
14+
"github.com/github/gh-aw-mcpg/internal/tracing"
1415
"github.com/stretchr/testify/assert"
1516
"github.com/stretchr/testify/require"
17+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
18+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
1619
)
1720

1821
// stubGuard is a configurable test double for guard.Guard.
@@ -536,3 +539,123 @@ func tagsAsStrings(tags []difc.Tag) []string {
536539
}
537540
return result
538541
}
542+
543+
// newRecordingProxyHandler creates a proxyHandler with an in-memory recording tracer
544+
// injected. The returned function flushes and returns all recorded spans.
545+
func newRecordingProxyHandler(t *testing.T, s *Server) (*proxyHandler, func() []tracetest.SpanStub) {
546+
t.Helper()
547+
exporter := tracetest.NewInMemoryExporter()
548+
sp := sdktrace.NewSimpleSpanProcessor(exporter)
549+
tp := sdktrace.NewTracerProvider(
550+
sdktrace.WithSpanProcessor(sp),
551+
sdktrace.WithSampler(sdktrace.AlwaysSample()),
552+
)
553+
t.Cleanup(func() { _ = tp.Shutdown(t.Context()) })
554+
h := &proxyHandler{
555+
server: s,
556+
CachedTracer: tracing.CachedTracer{Tracer: tp.Tracer("test")},
557+
}
558+
return h, func() []tracetest.SpanStub { return exporter.GetSpans() }
559+
}
560+
561+
// spanByName returns the first recorded span with the given name, or nil.
562+
func spanByName(spans []tracetest.SpanStub, name string) *tracetest.SpanStub {
563+
for i := range spans {
564+
if spans[i].Name == name {
565+
return &spans[i]
566+
}
567+
}
568+
return nil
569+
}
570+
571+
// ─── Span error recording: guard not initialized → 503 ───────────────────────
572+
573+
func TestHandleWithDIFC_GuardNotInitialized_SetsSpanError(t *testing.T) {
574+
s := &Server{
575+
guard: guard.NewNoopGuard(),
576+
DIFCComponents: difc.DIFCComponents{
577+
Mode: difc.EnforcementFilter,
578+
Evaluator: difc.NewEvaluatorWithMode(difc.EnforcementFilter),
579+
AgentRegistry: difc.NewAgentRegistryWithDefaults(nil, nil),
580+
Capabilities: difc.NewCapabilities(),
581+
},
582+
githubAPIURL: "http://unused",
583+
httpClient: &http.Client{},
584+
guardInitialized: false,
585+
}
586+
h, getSpans := newRecordingProxyHandler(t, s)
587+
588+
req := httptest.NewRequest(http.MethodGet, "/repos/org/repo/issues", nil)
589+
w := httptest.NewRecorder()
590+
h.handleWithDIFC(w, req, "/repos/org/repo/issues", "list_issues",
591+
map[string]interface{}{}, nil)
592+
593+
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
594+
595+
spans := getSpans()
596+
difcSpan := spanByName(spans, "proxy.difc_pipeline")
597+
require.NotNil(t, difcSpan, "proxy.difc_pipeline span must be recorded")
598+
assert.Equal(t, "Error", difcSpan.Status.Code.String(), "difc span status must be Error")
599+
assert.Equal(t, "proxy enforcement not configured", difcSpan.Status.Description)
600+
require.NotEmpty(t, difcSpan.Events, "difc span must have exception event")
601+
assert.Equal(t, "exception", difcSpan.Events[0].Name)
602+
}
603+
604+
// ─── Span error recording: RunPipelinePrePhases failure → 502 ────────────────
605+
606+
func TestHandleWithDIFC_LabelResourceError_SetsSpanError(t *testing.T) {
607+
upstream := mockUpstream(t, http.StatusOK, []interface{}{map[string]interface{}{"id": 1}})
608+
defer upstream.Close()
609+
610+
g := &stubGuard{
611+
labelResourceErr: errors.New("guard unavailable"),
612+
}
613+
s := newTestServerWithStub(t, upstream.URL, g, difc.EnforcementFilter)
614+
h, getSpans := newRecordingProxyHandler(t, s)
615+
616+
req := httptest.NewRequest(http.MethodGet, "/repos/org/repo/issues", nil)
617+
w := httptest.NewRecorder()
618+
h.handleWithDIFC(w, req, "/repos/org/repo/issues", "list_issues",
619+
map[string]interface{}{"owner": "org", "repo": "repo"}, nil)
620+
621+
assert.Equal(t, http.StatusBadGateway, w.Code)
622+
623+
spans := getSpans()
624+
difcSpan := spanByName(spans, "proxy.difc_pipeline")
625+
require.NotNil(t, difcSpan, "proxy.difc_pipeline span must be recorded")
626+
assert.Equal(t, "Error", difcSpan.Status.Code.String(), "difc span status must be Error")
627+
assert.Equal(t, "resource labeling failed", difcSpan.Status.Description)
628+
require.NotEmpty(t, difcSpan.Events, "difc span must have exception event")
629+
assert.Equal(t, "exception", difcSpan.Events[0].Name)
630+
}
631+
632+
// ─── Span error recording: upstream forwarding failure → 502 ─────────────────
633+
634+
func TestHandleWithDIFC_UpstreamFailure_SetsSpanError(t *testing.T) {
635+
// Point at a port that refuses connections so forwardAndReadBody returns nil.
636+
g := &stubGuard{
637+
labelResourceResult: publicResource(),
638+
labelResourceOp: difc.OperationRead,
639+
}
640+
s := newTestServerWithStub(t, "http://127.0.0.1:1", g, difc.EnforcementFilter)
641+
h, getSpans := newRecordingProxyHandler(t, s)
642+
643+
req := httptest.NewRequest(http.MethodGet, "/repos/org/repo/issues", nil)
644+
w := httptest.NewRecorder()
645+
h.handleWithDIFC(w, req, "/repos/org/repo/issues", "list_issues",
646+
map[string]interface{}{"owner": "org", "repo": "repo"}, nil)
647+
648+
assert.Equal(t, http.StatusBadGateway, w.Code)
649+
650+
spans := getSpans()
651+
652+
difcSpan := spanByName(spans, "proxy.difc_pipeline")
653+
require.NotNil(t, difcSpan, "proxy.difc_pipeline span must be recorded")
654+
assert.Equal(t, "Error", difcSpan.Status.Code.String(), "difc span status must be Error")
655+
assert.Equal(t, "upstream request failed", difcSpan.Status.Description)
656+
657+
fwdSpan := spanByName(spans, "proxy.backend.forward")
658+
require.NotNil(t, fwdSpan, "proxy.backend.forward span must be recorded")
659+
assert.Equal(t, "Error", fwdSpan.Status.Code.String(), "fwd span status must be Error")
660+
assert.Equal(t, "upstream request failed", fwdSpan.Status.Description)
661+
}

internal/tracing/genai_attrs.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
)
1010

1111
// GenAI semantic convention attribute keys.
12-
// These are aliases for the official OpenTelemetry gen_ai semconv constants
13-
// (semconv/v1.41.0), re-exported here for convenience.
12+
// Most are aliases for the official OpenTelemetry gen_ai semconv constants
13+
// (semconv/v1.41.0), re-exported here for convenience. GenAISystem is the
14+
// exception: it was removed in semconv/v1.41.0 and is defined as a raw
15+
// attribute.Key to preserve wire compatibility.
1416
const (
1517
// GenAISystem identifies the GenAI system family for MCP spans.
1618
// gen_ai.system was removed from semconv/v1.41.0; the key string is preserved for compatibility.

0 commit comments

Comments
 (0)