diff --git a/output/adapter.go b/output/adapter.go index 43d9913..ae26e1c 100644 --- a/output/adapter.go +++ b/output/adapter.go @@ -67,3 +67,35 @@ func (a *writerAsMetricConsumer) ConsumeMetrics(ctx context.Context, points []em } return nil } + +// WriterAsTraceConsumer wraps a TraceWriter so it can be used in +// contexts that expect an embed.TraceConsumer. The adapter pushes each +// span in the batch through TraceWriter.WriteTrace in order, returning +// the first error it encounters. +// +// Standalone CLI trace-generator wiring uses this adapter to bridge +// modules that talk to embed.TraceConsumer with existing outputs that +// implement TraceWriter. +// +// Panics on nil writer — a nil writer is a programming bug, not a +// runtime condition, and catching it at construction surfaces the +// failure at the boundary rather than deep in ConsumeTraces. +func WriterAsTraceConsumer(w TraceWriter) embed.TraceConsumer { + if w == nil { + panic("output.WriterAsTraceConsumer: writer cannot be nil") + } + return &writerAsTraceConsumer{w: w} +} + +type writerAsTraceConsumer struct { + w TraceWriter +} + +func (a *writerAsTraceConsumer) ConsumeTraces(ctx context.Context, spans []embed.Span) error { + for i := range spans { + if err := a.w.WriteTrace(ctx, spans[i]); err != nil { + return err + } + } + return nil +} diff --git a/output/adapter_test.go b/output/adapter_test.go index 7119307..61de34a 100644 --- a/output/adapter_test.go +++ b/output/adapter_test.go @@ -114,6 +114,67 @@ func TestWriterAsMetricConsumerStopsOnFirstError(t *testing.T) { } } +type recordingTraceWriter struct { + mu sync.Mutex + spans []output.TraceRecord + err error +} + +func (w *recordingTraceWriter) WriteTrace(_ context.Context, rec output.TraceRecord) error { + w.mu.Lock() + defer w.mu.Unlock() + if w.err != nil { + return w.err + } + w.spans = append(w.spans, rec) + return nil +} + +func TestWriterAsTraceConsumerPushesEachSpan(t *testing.T) { + w := &recordingTraceWriter{} + c := output.WriterAsTraceConsumer(w) + + batch := []embed.Span{ + {Name: "one"}, + {Name: "two"}, + {Name: "three"}, + } + if err := c.ConsumeTraces(context.Background(), batch); err != nil { + t.Fatalf("ConsumeTraces: %v", err) + } + if got, want := len(w.spans), 3; got != want { + t.Fatalf("recorded %d, want %d", got, want) + } + for i, want := range []string{"one", "two", "three"} { + if w.spans[i].Name != want { + t.Errorf("span %d: %q, want %q", i, w.spans[i].Name, want) + } + } +} + +func TestWriterAsTraceConsumerStopsOnFirstError(t *testing.T) { + wantErr := errors.New("trace boom") + w := &recordingTraceWriter{err: wantErr} + c := output.WriterAsTraceConsumer(w) + + err := c.ConsumeTraces(context.Background(), []embed.Span{ + {Name: "one"}, + {Name: "two"}, + }) + if !errors.Is(err, wantErr) { + t.Fatalf("expected wantErr, got %v", err) + } +} + +func TestWriterAsTraceConsumerPanicsOnNilWriter(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic on nil writer, got none") + } + }() + _ = output.WriterAsTraceConsumer(nil) +} + func TestWriterAsLogConsumerPanicsOnNilWriter(t *testing.T) { defer func() { if r := recover(); r == nil {