diff --git a/pkg/imageverifier/bindir/bindir.go b/pkg/imageverifier/bindir/bindir.go index 198f7643a452a..cc053c7f0557e 100644 --- a/pkg/imageverifier/bindir/bindir.go +++ b/pkg/imageverifier/bindir/bindir.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/containerd/v2/internal/tomlext" "github.com/containerd/containerd/v2/pkg/imageverifier" + "github.com/containerd/containerd/v2/pkg/tracing" "github.com/containerd/log" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -123,6 +124,14 @@ func (v *ImageVerifier) runVerifier(ctx context.Context, bin string, imageName s cmd := exec.CommandContext(ctx, binPath, args...) + // Attach OTEL propagators trace context env var to the child process + if traceContext, err := tracing.GetPropagatorsTraceContext(ctx); err != nil { + log.G(ctx).Warn("could not marshall propagators trace context", err) + } else { + traceContextEnv := fmt.Sprintf("OTEL_PROPAGATORS_TRACE_CONTEXT=%s", traceContext) + cmd.Env = append(os.Environ(), traceContextEnv) + } + // We construct our own pipes instead of using the default StdinPipe, // StoutPipe, and StderrPipe in order to set timeouts on reads and writes. stdinRead, stdinWrite, err := os.Pipe() diff --git a/pkg/tracing/plugin/otlp.go b/pkg/tracing/plugin/otlp.go index 5da9ad80bb57a..dfa7e792de88e 100644 --- a/pkg/tracing/plugin/otlp.go +++ b/pkg/tracing/plugin/otlp.go @@ -22,6 +22,7 @@ import ( "io" "os" "strconv" + "strings" "time" "github.com/containerd/containerd/v2/pkg/deprecation" @@ -52,8 +53,10 @@ const ( otlpProtocolEnv = "OTEL_EXPORTER_OTLP_PROTOCOL" otlpTracesProtocolEnv = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" - otelTracesExporterEnv = "OTEL_TRACES_EXPORTER" - otelServiceNameEnv = "OTEL_SERVICE_NAME" + otelTracesExporterEnv = "OTEL_TRACES_EXPORTER" + otelServiceNameEnv = "OTEL_SERVICE_NAME" + otelTracesSamplerEnv = "OTEL_TRACES_SAMPLER" + otelTracesSamplerArgEnv = "OTEL_TRACES_SAMPLER_ARG" ) func init() { @@ -193,6 +196,15 @@ func newTracer(ctx context.Context, procs []trace.SpanProcessor) (io.Closer, err for _, proc := range procs { opts = append(opts, trace.WithSpanProcessor(proc)) } + + // Configure custom NameBased sampler if specified in the env + if nameSampler := nameSamplerFromEnv(); nameSampler != nil { + opts = append(opts, trace.WithSampler(nameSampler)) + // Unset the env vars so that otel sdk does not attempt to configure the sampler automatically + os.Unsetenv(otelTracesSamplerEnv) + os.Unsetenv(otelTracesSamplerArgEnv) + } + provider := trace.NewTracerProvider(opts...) otel.SetTracerProvider(provider) @@ -202,6 +214,25 @@ func newTracer(ctx context.Context, procs []trace.SpanProcessor) (io.Closer, err } +func nameSamplerFromEnv() trace.Sampler { + sampler, ok := os.LookupEnv(otelTracesSamplerEnv) + if !ok { + return nil + } + + sampler = strings.ToLower(strings.TrimSpace(sampler)) + allowedNames := strings.Split(strings.TrimSpace(os.Getenv(otelTracesSamplerArgEnv)), ",") + + switch sampler { + case samplerNameBased: + return NameBased(allowedNames) + case samplerParentBasedName: + return trace.ParentBased(NameBased(allowedNames)) + default: + return nil + } +} + func warnTraceConfig(ic *plugin.InitContext) error { if ic.Config == nil { return nil diff --git a/pkg/tracing/plugin/sampler.go b/pkg/tracing/plugin/sampler.go new file mode 100644 index 0000000000000..6f87a0d1ddb2e --- /dev/null +++ b/pkg/tracing/plugin/sampler.go @@ -0,0 +1,51 @@ +package plugin + +import ( + "fmt" + + sdkTrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +const ( + samplerNameBased = "namebased" + samplerParentBasedName = "parentbased_name" +) + +type NameSampler struct { + // allow is a set of names that should be sampled. + // Uses a map of empty structs for O(1) lookups and no memory overhead. + allow map[string]struct{} +} + +// NameBased returns a Sampler that samples every span having a certain name. +// It should be used in conjunction with the ParentBased sampler so that the child spans are also sampled. +func NameBased(allowedNames []string) NameSampler { + allowedNamesMap := make(map[string]struct{}, len(allowedNames)) + for _, name := range allowedNames { + allowedNamesMap[name] = struct{}{} + } + return NameSampler{ + allow: allowedNamesMap, + } +} + +func (ns NameSampler) ShouldSample(parameters sdkTrace.SamplingParameters) sdkTrace.SamplingResult { + psc := trace.SpanContextFromContext(parameters.ParentContext) + + if _, ok := ns.allow[parameters.Name]; ok { + return sdkTrace.SamplingResult{ + Decision: sdkTrace.RecordAndSample, + Tracestate: psc.TraceState(), + } + } + + return sdkTrace.SamplingResult{ + Decision: sdkTrace.Drop, + Tracestate: psc.TraceState(), + } +} + +func (ns NameSampler) Description() string { + return fmt.Sprintf("NameBased:{%v}", ns.allow) +} diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index 48d760feb8f75..310272e43ad16 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -18,6 +18,7 @@ package tracing import ( "context" + "encoding/json" "net/http" "strings" @@ -25,6 +26,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" ) @@ -130,3 +132,11 @@ func Attribute(k string, v any) attribute.KeyValue { func HTTPStatusCodeAttributes(code int) []attribute.KeyValue { return []attribute.KeyValue{semconv.HTTPStatusCodeKey.Int(code)} } + +// GetPropagatorsTraceContext returns the current propagators trace context as a JSON string +func GetPropagatorsTraceContext(ctx context.Context) ([]byte, error) { + propagator := propagation.TraceContext{} + carrier := propagation.MapCarrier{} + propagator.Inject(ctx, carrier) + return json.Marshal(carrier) +}