Skip to content

Commit bacd262

Browse files
committed
address comments from shaneutt
Signed-off-by: Guangya Liu <gyliu513@gmail.com>
1 parent ea46ec9 commit bacd262

4 files changed

Lines changed: 163 additions & 11 deletions

File tree

pkg/framework/interface/modelselector/plugins.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,8 @@ type Scorer interface {
4242
// Picker picks the final model(s) to send the request to.
4343
type Picker interface {
4444
plugin.Plugin
45+
// Pick selects a target model from the scored candidates. Implementations
46+
// must return a non-nil *PipelineRunResult with a non-nil TargetModel;
47+
// callers rely on this guarantee and do not nil-check the result.
4548
Pick(ctx context.Context, cycleState *plugin.CycleState, scoredModels []*ScoredModel) *PipelineRunResult
4649
}

pkg/handlers/request_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ package handlers
1919
import (
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

590595
var _ 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.

pkg/handlers/server.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import (
2323
"time"
2424

2525
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
26-
"go.opentelemetry.io/otel"
27-
"go.opentelemetry.io/otel/attribute"
2826
"go.opentelemetry.io/otel/trace"
2927
"google.golang.org/grpc/codes"
3028
"google.golang.org/grpc/status"
@@ -33,11 +31,11 @@ import (
3331
envoy "github.com/llm-d/llm-d-inference-payload-processor/pkg/common/envoy"
3432
errcommon "github.com/llm-d/llm-d-inference-payload-processor/pkg/common/error"
3533
logutil "github.com/llm-d/llm-d-inference-payload-processor/pkg/common/observability/logging"
34+
"github.com/llm-d/llm-d-inference-payload-processor/pkg/common/observability/tracing"
3635
datasource "github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/datalayer/datasource"
3736
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/plugin"
3837
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/requesthandling"
3938
"github.com/llm-d/llm-d-inference-payload-processor/pkg/metrics"
40-
"github.com/llm-d/llm-d-inference-payload-processor/version"
4139
)
4240

4341
const (
@@ -89,14 +87,7 @@ func (s *Server) Process(srv extProcPb.ExternalProcessor_ProcessServer) error {
8987
ctx := srv.Context()
9088

9189
// Start tracing span for the request
92-
tracer := otel.Tracer(
93-
"llm-d-inference-payload-processor/pkg/handlers",
94-
trace.WithInstrumentationVersion(version.BuildRef),
95-
trace.WithInstrumentationAttributes(
96-
attribute.String("commit-sha", version.CommitSHA),
97-
),
98-
)
99-
ctx, span := tracer.Start(ctx, "gateway.request", trace.WithSpanKind(trace.SpanKindServer))
90+
ctx, span := tracing.Tracer(handlersTracerScope).Start(ctx, "gateway.request", trace.WithSpanKind(trace.SpanKindServer))
10091
defer span.End()
10192

10293
logger := log.FromContext(ctx)

pkg/modelselector/model_selector_pipeline_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import (
2020
"context"
2121
"testing"
2222

23+
"go.opentelemetry.io/otel"
24+
"go.opentelemetry.io/otel/attribute"
25+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
26+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
27+
2328
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/datalayer"
2429
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/modelselector"
2530
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/plugin"
@@ -80,6 +85,78 @@ func (p *testPicker) Pick(_ context.Context, _ *plugin.CycleState, scoredModels
8085
return &modelselector.PipelineRunResult{TargetModel: best.Model}
8186
}
8287

88+
// TestRunPickerPlugin_Span verifies that runPickerPlugin emits a
89+
// "plugin.<type>" span carrying the plugin and picker attributes, including the
90+
// selected model.
91+
func TestRunPickerPlugin_Span(t *testing.T) {
92+
recorder := tracetest.NewSpanRecorder()
93+
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
94+
origTP := otel.GetTracerProvider()
95+
otel.SetTracerProvider(tp)
96+
t.Cleanup(func() { otel.SetTracerProvider(origTP) })
97+
98+
picker := &testPicker{typedName: plugin.TypedName{Type: "test-picker", Name: "max-score"}}
99+
pipeline := NewModelSelectorPipeline().WithPicker(picker)
100+
101+
scoredModels := map[string]*modelselector.ScoredModel{
102+
"model-a": {Model: datalayer.NewModel("model-a"), Score: 0.2},
103+
"model-b": {Model: datalayer.NewModel("model-b"), Score: 0.9},
104+
}
105+
106+
result := pipeline.runPickerPlugin(context.Background(), plugin.NewCycleState(), scoredModels)
107+
if result == nil || result.TargetModel == nil {
108+
t.Fatalf("runPickerPlugin returned no target model")
109+
}
110+
if got := result.TargetModel.GetName(); got != "model-b" {
111+
t.Fatalf("selected model = %q, want %q", got, "model-b")
112+
}
113+
114+
span := findSpanByName(t, recorder.Ended(), "plugin.test-picker")
115+
attrs := attrMap(span.Attributes())
116+
117+
wantStrings := map[attribute.Key]string{
118+
"llm_d.plugin.extension_point": pickerExtensionPoint,
119+
"llm_d.plugin.type": "test-picker",
120+
"llm_d.plugin.name": "max-score",
121+
"llm_d.picker.selected_model": "model-b",
122+
}
123+
for k, want := range wantStrings {
124+
if got := attrs[k].AsString(); got != want {
125+
t.Errorf("attribute %q = %q, want %q", k, got, want)
126+
}
127+
}
128+
if got := attrs["llm_d.picker.candidate_count"].AsInt64(); got != 2 {
129+
t.Errorf("candidate_count = %d, want 2", got)
130+
}
131+
}
132+
133+
// findSpanByName returns the single ended span with the given name, failing the
134+
// test if there is not exactly one.
135+
func findSpanByName(t *testing.T, spans []sdktrace.ReadOnlySpan, name string) sdktrace.ReadOnlySpan {
136+
t.Helper()
137+
var match sdktrace.ReadOnlySpan
138+
count := 0
139+
for _, s := range spans {
140+
if s.Name() == name {
141+
match = s
142+
count++
143+
}
144+
}
145+
if count != 1 {
146+
t.Fatalf("expected exactly 1 span named %q, got %d", name, count)
147+
}
148+
return match
149+
}
150+
151+
// attrMap indexes a span's attributes by key for easy lookup.
152+
func attrMap(kvs []attribute.KeyValue) map[attribute.Key]attribute.Value {
153+
m := make(map[attribute.Key]attribute.Value, len(kvs))
154+
for _, kv := range kvs {
155+
m[kv.Key] = kv.Value
156+
}
157+
return m
158+
}
159+
83160
func TestPipelineRun(t *testing.T) {
84161
modelA := datalayer.NewModel("model-a")
85162
modelB := datalayer.NewModel("model-b")

0 commit comments

Comments
 (0)