diff --git a/tracing/plugin/otlp.go b/tracing/plugin/otlp.go index 33b409fd6..3fed8f24e 100644 --- a/tracing/plugin/otlp.go +++ b/tracing/plugin/otlp.go @@ -23,6 +23,7 @@ import ( "net/url" "time" + "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/tracing" @@ -63,7 +64,26 @@ func init() { Requires: []plugin.Type{plugin.TracingProcessorPlugin}, Config: &TraceConfig{ServiceName: "containerd", TraceSamplingRatio: 1.0}, InitFn: func(ic *plugin.InitContext) (interface{}, error) { - return newTracer(ic) + //get TracingProcessorPlugin which is a dependency + plugins, err := ic.GetByType(plugin.TracingProcessorPlugin) + if err != nil { + return nil, fmt.Errorf("failed to get tracing processors: %w", err) + } + procs := make([]sdktrace.SpanProcessor, 0, len(plugins)) + for id, pctx := range plugins { + p, err := pctx.Instance() + if err != nil { + if plugin.IsSkipPlugin(err) { + log.G(ic.Context).WithError(err).Infof("skipping tracing processor initialization (no tracing plugin)") + } else { + log.G(ic.Context).WithError(err).Errorf("failed to initialize a tracing processor %q", id) + } + continue + } + proc := p.(sdktrace.SpanProcessor) + procs = append(procs, proc) + } + return newTracer(ic.Context, ic.Config.(*TraceConfig), procs) }, }) @@ -105,7 +125,7 @@ func newExporter(ctx context.Context, cfg *OTLPConfig) (*otlptrace.Exporter, err if cfg.Protocol == "http/protobuf" || cfg.Protocol == "" { u, err := url.Parse(cfg.Endpoint) if err != nil { - return nil, fmt.Errorf("OpenTelemetry endpoint %q is invalid: %w", cfg.Endpoint, err) + return nil, fmt.Errorf("OpenTelemetry endpoint %q %w : %v", cfg.Endpoint, errdefs.ErrInvalidArgument, err) } opts := []otlptracehttp.Option{ otlptracehttp.WithEndpoint(u.Host), @@ -124,7 +144,7 @@ func newExporter(ctx context.Context, cfg *OTLPConfig) (*otlptrace.Exporter, err return otlptracegrpc.New(ctx, opts...) } else { // Other protocols such as "http/json" are not supported. - return nil, fmt.Errorf("OpenTelemetry protocol %q is not supported", cfg.Protocol) + return nil, fmt.Errorf("OpenTelemetry protocol %q : %w", cfg.Protocol, errdefs.ErrNotImplemented) } } @@ -132,9 +152,7 @@ func newExporter(ctx context.Context, cfg *OTLPConfig) (*otlptrace.Exporter, err // its sampling ratio and returns io.Closer. // // Note that this function sets process-wide tracing configuration. -func newTracer(ic *plugin.InitContext) (io.Closer, error) { - ctx := ic.Context - config := ic.Config.(*TraceConfig) +func newTracer(ctx context.Context, config *TraceConfig, procs []sdktrace.SpanProcessor) (io.Closer, error) { res, err := resource.New(ctx, resource.WithHost(), @@ -154,25 +172,8 @@ func newTracer(ic *plugin.InitContext) (io.Closer, error) { sdktrace.WithResource(res), } - ls, err := ic.GetByType(plugin.TracingProcessorPlugin) - if err != nil { - return nil, fmt.Errorf("failed to get tracing processors: %w", err) - } - - procs := make([]sdktrace.SpanProcessor, 0, len(ls)) - for id, pctx := range ls { - p, err := pctx.Instance() - if err != nil { - if plugin.IsSkipPlugin(err) { - log.G(ctx).WithError(err).Infof("skipping tracing processor initialization (no tracing plugin)") - } else { - log.G(ctx).WithError(err).Errorf("failed to initialize a tracing processor %q", id) - } - continue - } - proc := p.(sdktrace.SpanProcessor) + for _, proc := range procs { opts = append(opts, sdktrace.WithSpanProcessor(proc)) - procs = append(procs, proc) } provider := sdktrace.NewTracerProvider(opts...) @@ -189,6 +190,7 @@ func newTracer(ic *plugin.InitContext) (io.Closer, error) { } return nil }}, nil + } // Returns a composite TestMap propagator diff --git a/tracing/plugin/otlp_test.go b/tracing/plugin/otlp_test.go new file mode 100644 index 000000000..4406583ec --- /dev/null +++ b/tracing/plugin/otlp_test.go @@ -0,0 +1,118 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package plugin + +import ( + "context" + "errors" + "testing" + + "github.com/containerd/containerd/errdefs" + "go.opentelemetry.io/otel/sdk/trace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +// TestNewExporter runs tests with different combinations of configuration for NewExporter function +func TestNewExporter(t *testing.T) { + + for _, testcase := range []struct { + name string + input OTLPConfig + output error + }{ + { + name: "Test http/protobuf protocol, expect no error", + input: OTLPConfig{Endpoint: "http://localhost:4318", + Protocol: "http/protobuf", + Insecure: false}, + output: nil, + }, + { + name: "Test invalid endpoint, expect ErrInvalidArgument error", + input: OTLPConfig{Endpoint: "http://localhost\n:4318", + Protocol: "http/protobuf", + Insecure: false}, + output: errdefs.ErrInvalidArgument, + }, + { + name: "Test default protocol, expect no error", + input: OTLPConfig{Endpoint: "http://localhost:4318", + Protocol: "", + Insecure: false}, + output: nil, + }, + { + name: "Test grpc protocol, expect no error", + input: OTLPConfig{Endpoint: "http://localhost:4317", + Protocol: "grpc", + Insecure: false}, + output: nil, + }, + { + name: "Test http/json protocol which is not supported, expect not implemented error", + input: OTLPConfig{Endpoint: "http://localhost:4318", + Protocol: "http/json", + Insecure: false}, + output: errdefs.ErrNotImplemented, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + t.Logf("input: %v", testcase.input) + + ctx := context.TODO() + exp, err := newExporter(ctx, &testcase.input) + t.Logf("output: %v", err) + + if err == nil { + if err != testcase.output { + t.Fatalf("Expect to get error: %v, however no error got\n", testcase.output) + } else if exp == nil { + t.Fatalf("Something went wrong, Exporter not created as expected\n") + } + } else { + if !errors.Is(err, testcase.output) { + t.Fatalf("Expect to get error: %v, however error %v returned\n", testcase.output, err) + } + } + + }) + } +} + +// TestNewTracer runs test for NewTracer function +func TestNewTracer(t *testing.T) { + + config := &TraceConfig{ServiceName: "containerd", TraceSamplingRatio: 1.0} + t.Logf("config: %v", config) + + procs := make([]sdktrace.SpanProcessor, 0, 1) + + //Create a dummy in memory exporter for test + exp := tracetest.NewInMemoryExporter() + proc := trace.NewBatchSpanProcessor(exp) + + procs = append(procs, proc) + + ctx := context.TODO() + tracerCloser, err := newTracer(ctx, config, procs) + if err != nil { + t.Fatalf("Something went wrong, Tracer not created as expected\n") + } + + defer tracerCloser.Close() +} diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/exporter.go b/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/exporter.go new file mode 100644 index 000000000..104489e79 --- /dev/null +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/exporter.go @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tracetest is a testing helper package for the SDK. User can +// configure no-op or in-memory exporters to verify different SDK behaviors or +// custom instrumentation. +package tracetest // import "go.opentelemetry.io/otel/sdk/trace/tracetest" + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel/sdk/trace" +) + +var _ trace.SpanExporter = (*NoopExporter)(nil) + +// NewNoopExporter returns a new no-op exporter. +func NewNoopExporter() *NoopExporter { + return new(NoopExporter) +} + +// NoopExporter is an exporter that drops all received spans and performs no +// action. +type NoopExporter struct{} + +// ExportSpans handles export of spans by dropping them. +func (nsb *NoopExporter) ExportSpans(context.Context, []trace.ReadOnlySpan) error { return nil } + +// Shutdown stops the exporter by doing nothing. +func (nsb *NoopExporter) Shutdown(context.Context) error { return nil } + +var _ trace.SpanExporter = (*InMemoryExporter)(nil) + +// NewInMemoryExporter returns a new InMemoryExporter. +func NewInMemoryExporter() *InMemoryExporter { + return new(InMemoryExporter) +} + +// InMemoryExporter is an exporter that stores all received spans in-memory. +type InMemoryExporter struct { + mu sync.Mutex + ss SpanStubs +} + +// ExportSpans handles export of spans by storing them in memory. +func (imsb *InMemoryExporter) ExportSpans(_ context.Context, spans []trace.ReadOnlySpan) error { + imsb.mu.Lock() + defer imsb.mu.Unlock() + imsb.ss = append(imsb.ss, SpanStubsFromReadOnlySpans(spans)...) + return nil +} + +// Shutdown stops the exporter by clearing spans held in memory. +func (imsb *InMemoryExporter) Shutdown(context.Context) error { + imsb.Reset() + return nil +} + +// Reset the current in-memory storage. +func (imsb *InMemoryExporter) Reset() { + imsb.mu.Lock() + defer imsb.mu.Unlock() + imsb.ss = nil +} + +// GetSpans returns the current in-memory stored spans. +func (imsb *InMemoryExporter) GetSpans() SpanStubs { + imsb.mu.Lock() + defer imsb.mu.Unlock() + ret := make(SpanStubs, len(imsb.ss)) + copy(ret, imsb.ss) + return ret +} diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/recorder.go b/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/recorder.go new file mode 100644 index 000000000..06673a1c0 --- /dev/null +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/recorder.go @@ -0,0 +1,92 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracetest // import "go.opentelemetry.io/otel/sdk/trace/tracetest" + +import ( + "context" + "sync" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +// SpanRecorder records started and ended spans. +type SpanRecorder struct { + startedMu sync.RWMutex + started []sdktrace.ReadWriteSpan + + endedMu sync.RWMutex + ended []sdktrace.ReadOnlySpan +} + +var _ sdktrace.SpanProcessor = (*SpanRecorder)(nil) + +// NewSpanRecorder returns a new initialized SpanRecorder. +func NewSpanRecorder() *SpanRecorder { + return new(SpanRecorder) +} + +// OnStart records started spans. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) OnStart(_ context.Context, s sdktrace.ReadWriteSpan) { + sr.startedMu.Lock() + defer sr.startedMu.Unlock() + sr.started = append(sr.started, s) +} + +// OnEnd records completed spans. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) OnEnd(s sdktrace.ReadOnlySpan) { + sr.endedMu.Lock() + defer sr.endedMu.Unlock() + sr.ended = append(sr.ended, s) +} + +// Shutdown does nothing. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) Shutdown(context.Context) error { + return nil +} + +// ForceFlush does nothing. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) ForceFlush(context.Context) error { + return nil +} + +// Started returns a copy of all started spans that have been recorded. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) Started() []sdktrace.ReadWriteSpan { + sr.startedMu.RLock() + defer sr.startedMu.RUnlock() + dst := make([]sdktrace.ReadWriteSpan, len(sr.started)) + copy(dst, sr.started) + return dst +} + +// Ended returns a copy of all ended spans that have been recorded. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) Ended() []sdktrace.ReadOnlySpan { + sr.endedMu.RLock() + defer sr.endedMu.RUnlock() + dst := make([]sdktrace.ReadOnlySpan, len(sr.ended)) + copy(dst, sr.ended) + return dst +} diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/span.go b/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/span.go new file mode 100644 index 000000000..bfe73de9c --- /dev/null +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/tracetest/span.go @@ -0,0 +1,167 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracetest // import "go.opentelemetry.io/otel/sdk/trace/tracetest" + +import ( + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +// SpanStubs is a slice of SpanStub use for testing an SDK. +type SpanStubs []SpanStub + +// SpanStubsFromReadOnlySpans returns SpanStubs populated from ro. +func SpanStubsFromReadOnlySpans(ro []tracesdk.ReadOnlySpan) SpanStubs { + if len(ro) == 0 { + return nil + } + + s := make(SpanStubs, 0, len(ro)) + for _, r := range ro { + s = append(s, SpanStubFromReadOnlySpan(r)) + } + + return s +} + +// Snapshots returns s as a slice of ReadOnlySpans. +func (s SpanStubs) Snapshots() []tracesdk.ReadOnlySpan { + if len(s) == 0 { + return nil + } + + ro := make([]tracesdk.ReadOnlySpan, len(s)) + for i := 0; i < len(s); i++ { + ro[i] = s[i].Snapshot() + } + return ro +} + +// SpanStub is a stand-in for a Span. +type SpanStub struct { + Name string + SpanContext trace.SpanContext + Parent trace.SpanContext + SpanKind trace.SpanKind + StartTime time.Time + EndTime time.Time + Attributes []attribute.KeyValue + Events []tracesdk.Event + Links []tracesdk.Link + Status tracesdk.Status + DroppedAttributes int + DroppedEvents int + DroppedLinks int + ChildSpanCount int + Resource *resource.Resource + InstrumentationLibrary instrumentation.Library +} + +// SpanStubFromReadOnlySpan returns a SpanStub populated from ro. +func SpanStubFromReadOnlySpan(ro tracesdk.ReadOnlySpan) SpanStub { + if ro == nil { + return SpanStub{} + } + + return SpanStub{ + Name: ro.Name(), + SpanContext: ro.SpanContext(), + Parent: ro.Parent(), + SpanKind: ro.SpanKind(), + StartTime: ro.StartTime(), + EndTime: ro.EndTime(), + Attributes: ro.Attributes(), + Events: ro.Events(), + Links: ro.Links(), + Status: ro.Status(), + DroppedAttributes: ro.DroppedAttributes(), + DroppedEvents: ro.DroppedEvents(), + DroppedLinks: ro.DroppedLinks(), + ChildSpanCount: ro.ChildSpanCount(), + Resource: ro.Resource(), + InstrumentationLibrary: ro.InstrumentationScope(), + } +} + +// Snapshot returns a read-only copy of the SpanStub. +func (s SpanStub) Snapshot() tracesdk.ReadOnlySpan { + return spanSnapshot{ + name: s.Name, + spanContext: s.SpanContext, + parent: s.Parent, + spanKind: s.SpanKind, + startTime: s.StartTime, + endTime: s.EndTime, + attributes: s.Attributes, + events: s.Events, + links: s.Links, + status: s.Status, + droppedAttributes: s.DroppedAttributes, + droppedEvents: s.DroppedEvents, + droppedLinks: s.DroppedLinks, + childSpanCount: s.ChildSpanCount, + resource: s.Resource, + instrumentationScope: s.InstrumentationLibrary, + } +} + +type spanSnapshot struct { + // Embed the interface to implement the private method. + tracesdk.ReadOnlySpan + + name string + spanContext trace.SpanContext + parent trace.SpanContext + spanKind trace.SpanKind + startTime time.Time + endTime time.Time + attributes []attribute.KeyValue + events []tracesdk.Event + links []tracesdk.Link + status tracesdk.Status + droppedAttributes int + droppedEvents int + droppedLinks int + childSpanCount int + resource *resource.Resource + instrumentationScope instrumentation.Scope +} + +func (s spanSnapshot) Name() string { return s.name } +func (s spanSnapshot) SpanContext() trace.SpanContext { return s.spanContext } +func (s spanSnapshot) Parent() trace.SpanContext { return s.parent } +func (s spanSnapshot) SpanKind() trace.SpanKind { return s.spanKind } +func (s spanSnapshot) StartTime() time.Time { return s.startTime } +func (s spanSnapshot) EndTime() time.Time { return s.endTime } +func (s spanSnapshot) Attributes() []attribute.KeyValue { return s.attributes } +func (s spanSnapshot) Links() []tracesdk.Link { return s.links } +func (s spanSnapshot) Events() []tracesdk.Event { return s.events } +func (s spanSnapshot) Status() tracesdk.Status { return s.status } +func (s spanSnapshot) DroppedAttributes() int { return s.droppedAttributes } +func (s spanSnapshot) DroppedLinks() int { return s.droppedLinks } +func (s spanSnapshot) DroppedEvents() int { return s.droppedEvents } +func (s spanSnapshot) ChildSpanCount() int { return s.childSpanCount } +func (s spanSnapshot) Resource() *resource.Resource { return s.resource } +func (s spanSnapshot) InstrumentationScope() instrumentation.Scope { + return s.instrumentationScope +} +func (s spanSnapshot) InstrumentationLibrary() instrumentation.Library { + return s.instrumentationScope +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 27fd8b51b..0362ea958 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -463,6 +463,7 @@ go.opentelemetry.io/otel/sdk/internal go.opentelemetry.io/otel/sdk/internal/env go.opentelemetry.io/otel/sdk/resource go.opentelemetry.io/otel/sdk/trace +go.opentelemetry.io/otel/sdk/trace/tracetest # go.opentelemetry.io/otel/trace v1.11.1 ## explicit; go 1.18 go.opentelemetry.io/otel/trace