From b88895cd80fef804ac8509689f67cbc51356a094 Mon Sep 17 00:00:00 2001 From: thomasgouveia Date: Mon, 6 Nov 2023 16:04:01 +0100 Subject: [PATCH] feat: add `HttpFilterOut` sampler to filter traces based on request path (#454) Add a new built-in sampler to filter out traces with attribute `http.target` matching a given filter. The filter could be provided either using the `WithPathFilter` method or using the environment variable `OTEL_GO_AUTO_HTTP_INSTRUMENTATION_FILTER_PATH`. Signed-off-by: thomasgouveia --- CHANGELOG.md | 1 + instrumentation.go | 41 ++++++++++++- internal/pkg/builtin/sampling.go | 83 +++++++++++++++++++++++++++ internal/pkg/builtin/sampling_test.go | 62 ++++++++++++++++++++ version.go | 2 +- 5 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 internal/pkg/builtin/sampling.go create mode 100644 internal/pkg/builtin/sampling_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 833288abe..92cac52b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http - Add the `WithTraceExporter` `InstrumentationOption` to configure the trace `SpanExporter` used by an `Instrumentation`. ([#426](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/426)) - Add HTTP status code attribute to `net/http` server instrumentation. ([#428](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/428)) - The instrumentation scope now includes the version of the auto-instrumentation project. ([#442](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/442)) +- Add a new built-in sampler to filter out traces with attribute `http.target` matching a given filter. ([#454](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/468)). The filter could be provided either using the `WithPathFilter` method or using the environment variable `OTEL_GO_AUTO_HTTP_INSTRUMENTATION_FILTER_PATH`. ### Changed diff --git a/instrumentation.go b/instrumentation.go index 212b89292..3e41ac072 100644 --- a/instrumentation.go +++ b/instrumentation.go @@ -21,9 +21,12 @@ import ( "log" "os" "path/filepath" + "regexp" "runtime" "strings" + "go.opentelemetry.io/auto/internal/pkg/builtin" + "github.com/go-logr/logr" "github.com/go-logr/stdr" "github.com/go-logr/zapr" @@ -51,6 +54,9 @@ const ( // envTracesExportersKey is the key for the environment variable value // containing what OpenTelemetry trace exporter to use. envTracesExportersKey = "OTEL_TRACES_EXPORTER" + // envGoAutoHttpInstrumentationFilterPath is the key for the environment variable value + // containing a regex allowing to filter out some paths. + envGoAutoHttpInstrumentationFilterPath = "OTEL_GO_AUTO_HTTP_INSTRUMENTATION_FILTER_PATH" ) // Instrumentation manages and controls all OpenTelemetry Go @@ -166,6 +172,7 @@ type instConfig struct { traceExp trace.SpanExporter target process.TargetArgs serviceName string + pathFilter *regexp.Regexp } func newInstConfig(ctx context.Context, opts []InstrumentationOption) (instConfig, error) { @@ -216,13 +223,20 @@ func (c instConfig) validate() error { func (c instConfig) tracerProvider() *trace.TracerProvider { return trace.NewTracerProvider( - trace.WithSampler(trace.AlwaysSample()), + trace.WithSampler(c.sampler()), trace.WithResource(c.res()), trace.WithBatcher(c.traceExp), trace.WithIDGenerator(opentelemetry.NewEBPFSourceIDGenerator()), ) } +func (c instConfig) sampler() trace.Sampler { + if c.pathFilter != nil { + return builtin.HttpFilterOut(c.pathFilter) + } + return trace.AlwaysSample() +} + func (c instConfig) res() *resource.Resource { runVer := strings.TrimPrefix(runtime.Version(), "go") runName := runtime.Compiler @@ -382,3 +396,28 @@ func WithTraceExporter(exp trace.SpanExporter) InstrumentationOption { return c, nil }) } + +// WithPathFilter returns an [InstrumentationOption] that will configure +// an [Instrumentation] to filter out the HTTP request matching the given filter. +// +// If OTEL_GO_AUTO_HTTP_INSTRUMENTATION_FILTER_PATH is defined, it will take precedence over any value passed here. +// the pathFilter parameter value. +func WithPathFilter(pathFilter string) InstrumentationOption { + return fnOpt(func(_ context.Context, c instConfig) (instConfig, error) { + filter := pathFilter + + // Check if the OTEL_GO_AUTO_HTTP_INSTRUMENTATION_FILTER_PATH is defined + // and update the value of the filter in that case + if v, ok := lookupEnv(envGoAutoHttpInstrumentationFilterPath); ok && v != "" { + filter = v + } + + regex, err := regexp.Compile(filter) + if err != nil { + return c, fmt.Errorf("pathFilter could not be compiled as a regular expression: %w", err) + } + + c.pathFilter = regex + return c, nil + }) +} diff --git a/internal/pkg/builtin/sampling.go b/internal/pkg/builtin/sampling.go new file mode 100644 index 000000000..5a8f0f375 --- /dev/null +++ b/internal/pkg/builtin/sampling.go @@ -0,0 +1,83 @@ +// 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 builtin + +import ( + "fmt" + "regexp" + + "go.opentelemetry.io/otel/attribute" + otelsdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +// httpFilterOutPathSampler is a custom implementation of OpenTelemetry sampler +// used to filter out traces based on their HTTP request path. +type httpFilterOutPathSampler struct { + filter *regexp.Regexp +} + +// Ensure that the implementation satisfies the interface. +var _ otelsdk.Sampler = (*httpFilterOutPathSampler)(nil) + +// HttpFilterOut returns a Sampler that filter out samples with HTTP path +// matching the given filter. Useful for filter HTTP requests with +// path like /health, /metrics or whatever. +func HttpFilterOut(filter *regexp.Regexp) otelsdk.Sampler { + return &httpFilterOutPathSampler{filter} +} + +// ShouldSample implements the interface [otelsdk.Sampler]. +func (f httpFilterOutPathSampler) ShouldSample(p otelsdk.SamplingParameters) otelsdk.SamplingResult { + if f.shouldDrop(p) { + return otelsdk.SamplingResult{ + Decision: otelsdk.Drop, + Tracestate: trace.SpanContextFromContext(p.ParentContext).TraceState(), + } + } + + return otelsdk.SamplingResult{ + Decision: otelsdk.RecordAndSample, + Tracestate: trace.SpanContextFromContext(p.ParentContext).TraceState(), + } +} + +// Description implements the interface [otelsdk.Sampler]. +func (f httpFilterOutPathSampler) Description() string { + return "HttpFilterOut" +} + +// shouldDrop determines if the given sample should be dropped or not. +func (f httpFilterOutPathSampler) shouldDrop(p otelsdk.SamplingParameters) bool { + attr, err := getAttribute(p, "http.target") + if err != nil { + // If we have an error here, it is because the attribute is not + // found on the given parameters. We can safely return false. + return false + } + httpPath := attr.Value.AsString() + + return f.filter.MatchString(httpPath) +} + +// getAttribute returns the attribute at the given key in the given sampling parameters or an error if the key has no value. +func getAttribute(p otelsdk.SamplingParameters, key string) (attribute.KeyValue, error) { + for _, attr := range p.Attributes { + if attr.Key == attribute.Key(key) { + return attr, nil + } + } + return attribute.KeyValue{}, fmt.Errorf("no attribute with key %s", key) +} diff --git a/internal/pkg/builtin/sampling_test.go b/internal/pkg/builtin/sampling_test.go new file mode 100644 index 000000000..7ca3e67e2 --- /dev/null +++ b/internal/pkg/builtin/sampling_test.go @@ -0,0 +1,62 @@ +// 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 builtin + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + otelsdk "go.opentelemetry.io/otel/sdk/trace" +) + +func TestHttpFilterOut(t *testing.T) { + testCases := []struct { + name string + httpPath string + filter *regexp.Regexp + expectedDecision otelsdk.SamplingDecision + }{ + { + "traceNotDropped", + "/api/v1/something", + regexp.MustCompile("/health.+"), + otelsdk.RecordAndSample, + }, + { + "traceDropped", + "/health/liveness", + regexp.MustCompile("/health.+"), + otelsdk.Drop, + }, + { + "traceNotDroppedIfPatternIsUsedAnywhereInPath", + "/v1/deployments/health", + regexp.MustCompile("/health.+"), + otelsdk.RecordAndSample, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params := otelsdk.SamplingParameters{ + Attributes: []attribute.KeyValue{attribute.String("http.target", tc.httpPath)}, + } + sampler := HttpFilterOut(tc.filter) + assert.Equal(t, tc.expectedDecision, sampler.ShouldSample(params).Decision) + }) + } +} diff --git a/version.go b/version.go index c79bb14e5..7ea86ea5b 100644 --- a/version.go +++ b/version.go @@ -16,5 +16,5 @@ package auto // Version is the current release version of OpenTelemetry Go auto-instrumentation in use. func Version() string { - return "v0.7.0-alpha" + return "v0.8.0-alpha" }