@@ -19,13 +19,18 @@ package handlers
1919import (
2020 "context"
2121 "encoding/json"
22+ "errors"
2223 "strconv"
2324 "strings"
2425 "testing"
2526
2627 basepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
2728 extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
2829 "github.com/google/go-cmp/cmp"
30+ "go.opentelemetry.io/otel"
31+ otelcodes "go.opentelemetry.io/otel/codes"
32+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
33+ "go.opentelemetry.io/otel/sdk/trace/tracetest"
2934 "google.golang.org/protobuf/testing/protocmp"
3035 metricsutils "k8s.io/component-base/metrics/testutil"
3136 crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
@@ -589,6 +594,82 @@ func (p *fakeRequestPlugin) ProcessRequest(ctx context.Context, _ *plugin.CycleS
589594
590595var _ requesthandling.RequestProcessor = & fakeRequestPlugin {}
591596
597+ // TestRunRequestPlugins_Spans verifies that runRequestPlugins emits a
598+ // "request_plugins" stage span with a child "plugin.*" span per plugin, and
599+ // that a failing plugin's span is marked with an error status.
600+ func TestRunRequestPlugins_Spans (t * testing.T ) {
601+ recorder := tracetest .NewSpanRecorder ()
602+ tp := sdktrace .NewTracerProvider (sdktrace .WithSpanProcessor (recorder ))
603+ origTP := otel .GetTracerProvider ()
604+ otel .SetTracerProvider (tp )
605+ t .Cleanup (func () { otel .SetTracerProvider (origTP ) })
606+
607+ wantErr := errors .New ("boom" )
608+ plugins := []requesthandling.RequestProcessor {
609+ & fakeRequestPlugin {name : "ok" , mutateFn : func (context.Context , * requesthandling.InferenceRequest ) error { return nil }},
610+ & fakeRequestPlugin {name : "boom" , mutateFn : func (context.Context , * requesthandling.InferenceRequest ) error { return wantErr }},
611+ }
612+
613+ s := & Server {}
614+ err := s .runRequestPlugins (context .Background (), plugin .NewCycleState (), requesthandling .NewInferenceRequest (), plugins )
615+ if ! errors .Is (err , wantErr ) {
616+ t .Fatalf ("runRequestPlugins error = %v, want %v" , err , wantErr )
617+ }
618+
619+ ended := recorder .Ended ()
620+
621+ // Stage span grouping the per-plugin spans.
622+ stage := findSpan (t , ended , "request_plugins" )
623+
624+ // Both plugins ran (the first succeeded, the second failed and aborted the
625+ // loop), so two plugin spans should exist, each parented to the stage span.
626+ pluginSpans := findSpans (ended , "plugin.fake" )
627+ if len (pluginSpans ) != 2 {
628+ t .Fatalf ("expected 2 plugin.fake spans, got %d" , len (pluginSpans ))
629+ }
630+ for _ , ps := range pluginSpans {
631+ if ps .Parent ().SpanID () != stage .SpanContext ().SpanID () {
632+ t .Errorf ("plugin span parent = %v, want stage span %v" , ps .Parent ().SpanID (), stage .SpanContext ().SpanID ())
633+ }
634+ }
635+
636+ // Exactly one plugin span (the failing one) carries an error status.
637+ var errored int
638+ for _ , ps := range pluginSpans {
639+ if ps .Status ().Code == otelcodes .Error {
640+ errored ++
641+ if ps .Status ().Description != wantErr .Error () {
642+ t .Errorf ("error span description = %q, want %q" , ps .Status ().Description , wantErr .Error ())
643+ }
644+ }
645+ }
646+ if errored != 1 {
647+ t .Errorf ("expected exactly 1 plugin span with error status, got %d" , errored )
648+ }
649+ }
650+
651+ // findSpan returns the single ended span with the given name, failing the test
652+ // if there is not exactly one.
653+ func findSpan (t * testing.T , spans []sdktrace.ReadOnlySpan , name string ) sdktrace.ReadOnlySpan {
654+ t .Helper ()
655+ matches := findSpans (spans , name )
656+ if len (matches ) != 1 {
657+ t .Fatalf ("expected exactly 1 span named %q, got %d" , name , len (matches ))
658+ }
659+ return matches [0 ]
660+ }
661+
662+ // findSpans returns all ended spans with the given name.
663+ func findSpans (spans []sdktrace.ReadOnlySpan , name string ) []sdktrace.ReadOnlySpan {
664+ var matches []sdktrace.ReadOnlySpan
665+ for _ , s := range spans {
666+ if s .Name () == name {
667+ matches = append (matches , s )
668+ }
669+ }
670+ return matches
671+ }
672+
592673// TestHandleRequestBody_MultiPluginHeaderMutations tests the end-to-end behavior of
593674// HandleRequestBody when multiple request plugins set and/or remove headers.
594675// Each sub-test verifies the HeaderMutation in the resulting ProcessingResponse.
0 commit comments