diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index 75b95b0276..d33f6544d3 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -947,6 +947,18 @@ type Tracing struct { // // +optional SamplingFraction *gwapiv1.Fraction `json:"samplingFraction,omitempty"` + // ClientSamplingFraction represents the fraction of requests that should be + // selected for tracing when requested by the client. + // If unspecified, client-forced tracing is disabled by default and users must + // set this field to opt in. + // + // +optional + ClientSamplingFraction *gwapiv1.Fraction `json:"clientSamplingFraction,omitempty"` + // OverallSamplingFraction represents the fraction of requests that should be + // selected for tracing after all other sampling checks have been applied. + // + // +optional + OverallSamplingFraction *gwapiv1.Fraction `json:"overallSamplingFraction,omitempty"` // CustomTags defines the custom tags to add to each span. // If provider is kubernetes, pod name and namespace are added by default. // diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2195616a06..4551b1a7c3 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -8473,6 +8473,16 @@ func (in *Tracing) DeepCopyInto(out *Tracing) { *out = new(v1.Fraction) (*in).DeepCopyInto(*out) } + if in.ClientSamplingFraction != nil { + in, out := &in.ClientSamplingFraction, &out.ClientSamplingFraction + *out = new(v1.Fraction) + (*in).DeepCopyInto(*out) + } + if in.OverallSamplingFraction != nil { + in, out := &in.OverallSamplingFraction, &out.OverallSamplingFraction + *out = new(v1.Fraction) + (*in).DeepCopyInto(*out) + } if in.CustomTags != nil { in, out := &in.CustomTags, &out.CustomTags *out = make(map[string]CustomTag, len(*in)) diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 72e4b69d56..575c1136c5 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -3152,6 +3152,28 @@ spec: This takes precedence over EnvoyProxy tracing when set. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -3216,6 +3238,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator samplingFraction: description: |- SamplingFraction represents the fraction of requests that should be diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index f09493a2f5..d8a048a90b 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -16329,6 +16329,28 @@ spec: Tracing defines tracing configuration for managed proxies. If unspecified, will not send tracing data. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -16393,6 +16415,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator provider: description: Provider defines the tracing provider. properties: diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index a79ec46e60..7572080368 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -3151,6 +3151,28 @@ spec: This takes precedence over EnvoyProxy tracing when set. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -3215,6 +3237,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator samplingFraction: description: |- SamplingFraction represents the fraction of requests that should be diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 9211cd86da..5736db43cd 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -16328,6 +16328,28 @@ spec: Tracing defines tracing configuration for managed proxies. If unspecified, will not send tracing data. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -16392,6 +16414,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator provider: description: Provider defines the tracing provider. properties: diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index a520dbc212..8ee139d7e9 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1191,10 +1191,12 @@ func buildBackendTracing(tracing *egv1a1.Tracing) *ir.BackendTracing { return nil } return &ir.BackendTracing{ - SamplingFraction: tracing.SamplingFraction, - CustomTags: ir.CustomTagMapToSlice(tracing.CustomTags), - Tags: ir.MapToSlice(tracing.Tags), - SpanName: tracing.SpanName, + SamplingFraction: tracing.SamplingFraction, + ClientSamplingFraction: tracing.ClientSamplingFraction, + OverallSamplingFraction: tracing.OverallSamplingFraction, + CustomTags: ir.CustomTagMapToSlice(tracing.CustomTags), + Tags: ir.MapToSlice(tracing.Tags), + SpanName: tracing.SpanName, } } diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index a9dd8270f3..de318a8034 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -1015,12 +1015,14 @@ func (t *Translator) processTracing(gwCtx *GatewayContext, envoyproxy *egv1a1.En } return &ir.Tracing{ - Authority: authority, - ServiceName: serviceName, - SamplingRate: proxySamplingRate(tracing), - CustomTags: ir.CustomTagMapToSlice(tracing.CustomTags), - Tags: ir.MapToSlice(tracing.Tags), - ResourceAttributes: ir.MapToSlice(getOpenTelemetryTracingResourceAttributes(&tracing.Provider)), + Authority: authority, + ServiceName: serviceName, + SamplingRate: proxySamplingRate(tracing.SamplingRate, tracing.SamplingFraction), + ClientSamplingRate: proxySamplingFractionPtr(tracing.ClientSamplingFraction), + OverallSamplingRate: proxySamplingFractionPtr(tracing.OverallSamplingFraction), + CustomTags: ir.CustomTagMapToSlice(tracing.CustomTags), + Tags: ir.MapToSlice(tracing.Tags), + ResourceAttributes: ir.MapToSlice(getOpenTelemetryTracingResourceAttributes(&tracing.Provider)), Destination: ir.RouteDestination{ Name: destName, Settings: ds, @@ -1033,20 +1035,29 @@ func (t *Translator) processTracing(gwCtx *GatewayContext, envoyproxy *egv1a1.En }, nil } -func proxySamplingRate(tracing *egv1a1.ProxyTracing) float64 { - rate := 100.0 - if tracing.SamplingRate != nil { - rate = float64(*tracing.SamplingRate) - } else if tracing.SamplingFraction != nil { - numerator := float64(tracing.SamplingFraction.Numerator) - denominator := ptr.Deref(tracing.SamplingFraction.Denominator, 100) - - rate = numerator * 100 / float64(denominator) - // Identifies a percentage, in the range [0.0, 100.0] - rate = math.Max(0, rate) - rate = math.Min(100, rate) +func proxySamplingFractionPtr(fraction *gwapiv1.Fraction) *float64 { + if fraction == nil { + return nil + } + return new(proxySamplingRate(nil, fraction)) +} + +func proxySamplingRate(rate *uint32, fraction *gwapiv1.Fraction) float64 { + if rate != nil { + return float64(*rate) } - return rate + if fraction == nil { + return 100.0 + } + + numerator := float64(fraction.Numerator) + denominator := ptr.Deref(fraction.Denominator, 100) + + value := numerator * 100 / float64(denominator) + // Identifies a percentage, in the range [0.0, 100.0] + value = math.Max(0, value) + value = math.Min(100, value) + return value } // getAuthorityFromDestination extracts the gRPC authority from a destination setting. diff --git a/internal/gatewayapi/listener_test.go b/internal/gatewayapi/listener_test.go index 2edeef9f71..3905930964 100644 --- a/internal/gatewayapi/listener_test.go +++ b/internal/gatewayapi/listener_test.go @@ -34,23 +34,47 @@ type expectedListenerStatus struct { message string } -func TestProxySamplingRate(t *testing.T) { +func TestProxySamplingRates(t *testing.T) { cases := []struct { - name string - tracing *egv1a1.ProxyTracing - expected float64 + name string + tracing *egv1a1.ProxyTracing + expectedRandom float64 + expectedClient float64 + expectedOverall float64 }{ { - name: "default", - tracing: &egv1a1.ProxyTracing{}, - expected: 100.0, + name: "default", + tracing: &egv1a1.ProxyTracing{}, + expectedRandom: 100.0, + expectedClient: 100.0, + expectedOverall: 100.0, }, { name: "rate", tracing: &egv1a1.ProxyTracing{ SamplingRate: new(uint32(10)), }, - expected: 10.0, + expectedRandom: 10.0, + expectedClient: 100.0, + expectedOverall: 100.0, + }, + { + name: "client and overall fraction", + tracing: &egv1a1.ProxyTracing{ + Tracing: egv1a1.Tracing{ + ClientSamplingFraction: &gwapiv1.Fraction{ + Numerator: 1, + Denominator: new(int32(4)), + }, + OverallSamplingFraction: &gwapiv1.Fraction{ + Numerator: 3, + Denominator: new(int32(4)), + }, + }, + }, + expectedRandom: 100.0, + expectedClient: 25.0, + expectedOverall: 75.0, }, { name: "fraction numerator only", @@ -61,7 +85,9 @@ func TestProxySamplingRate(t *testing.T) { }, }, }, - expected: 100, + expectedRandom: 100, + expectedClient: 100, + expectedOverall: 100, }, { name: "fraction", @@ -73,7 +99,9 @@ func TestProxySamplingRate(t *testing.T) { }, }, }, - expected: 10, + expectedRandom: 10, + expectedClient: 100, + expectedOverall: 100, }, { name: "less than zero", @@ -85,7 +113,9 @@ func TestProxySamplingRate(t *testing.T) { }, }, }, - expected: 0, + expectedRandom: 0, + expectedClient: 100, + expectedOverall: 100, }, { name: "greater than 100", @@ -97,7 +127,9 @@ func TestProxySamplingRate(t *testing.T) { }, }, }, - expected: 100, + expectedRandom: 100, + expectedClient: 100, + expectedOverall: 100, }, { name: "less than 1", @@ -109,16 +141,20 @@ func TestProxySamplingRate(t *testing.T) { }, }, }, - expected: 0.1, + expectedRandom: 0.1, + expectedClient: 100, + expectedOverall: 100, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - actual := proxySamplingRate(tc.tracing) - if actual != tc.expected { - t.Errorf("expected %v, got %v", tc.expected, actual) - } + random := proxySamplingRate(tc.tracing.SamplingRate, tc.tracing.SamplingFraction) + client := proxySamplingRate(nil, tc.tracing.ClientSamplingFraction) + overall := proxySamplingRate(nil, tc.tracing.OverallSamplingFraction) + assert.Equal(t, tc.expectedRandom, random) + assert.Equal(t, tc.expectedClient, client) + assert.Equal(t, tc.expectedOverall, overall) }) } } diff --git a/internal/gatewayapi/testdata/tracing-sampling-fraction.in.yaml b/internal/gatewayapi/testdata/tracing-sampling-fraction.in.yaml index b5ce63a9f9..836a9ee812 100644 --- a/internal/gatewayapi/testdata/tracing-sampling-fraction.in.yaml +++ b/internal/gatewayapi/testdata/tracing-sampling-fraction.in.yaml @@ -11,6 +11,12 @@ envoyProxyForGatewayClass: samplingFraction: numerator: 1 denominator: 100 + clientSamplingFraction: + numerator: 20 + denominator: 100 + overallSamplingFraction: + numerator: 30 + denominator: 100 provider: host: otel-collector.monitoring.svc.cluster.local port: 4317 diff --git a/internal/gatewayapi/testdata/tracing-sampling-fraction.out.yaml b/internal/gatewayapi/testdata/tracing-sampling-fraction.out.yaml index 1a3b551dd2..8e4ac2b447 100644 --- a/internal/gatewayapi/testdata/tracing-sampling-fraction.out.yaml +++ b/internal/gatewayapi/testdata/tracing-sampling-fraction.out.yaml @@ -194,6 +194,12 @@ infraIR: mergeGateways: true telemetry: tracing: + clientSamplingFraction: + denominator: 100 + numerator: 20 + overallSamplingFraction: + denominator: 100 + numerator: 30 provider: host: otel-collector.monitoring.svc.cluster.local port: 4317 @@ -361,6 +367,7 @@ xdsIR: port: 19003 tracing: authority: otel-collector.monitoring.svc.cluster.local + clientSamplingRate: 20 destination: metadata: kind: EnvoyProxy @@ -375,6 +382,7 @@ xdsIR: name: tracing/backend/-1 protocol: GRPC weight: 1 + overallSamplingRate: 30 provider: host: otel-collector.monitoring.svc.cluster.local port: 4317 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index d8ff851451..ddd94ea293 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1086,10 +1086,12 @@ type BackendTelemetry struct { // BackendTracing defines the tracing configuration for the backend. // +k8s:deepcopy-gen=true type BackendTracing struct { - SamplingFraction *gwapiv1.Fraction `json:"samplingFraction,omitempty" yaml:"samplingFraction,omitempty"` - CustomTags []CustomTagMapEntry `json:"customTags,omitempty" yaml:"customTags,omitempty"` - Tags []MapEntry `json:"tags,omitempty" yaml:"tags,omitempty"` - SpanName *egv1a1.TracingSpanName `json:"spanName,omitempty" yaml:"spanName,omitempty"` + SamplingFraction *gwapiv1.Fraction `json:"samplingFraction,omitempty" yaml:"samplingFraction,omitempty"` + ClientSamplingFraction *gwapiv1.Fraction `json:"clientSamplingFraction,omitempty" yaml:"clientSamplingFraction,omitempty"` + OverallSamplingFraction *gwapiv1.Fraction `json:"overallSamplingFraction,omitempty" yaml:"overallSamplingFraction,omitempty"` + CustomTags []CustomTagMapEntry `json:"customTags,omitempty" yaml:"customTags,omitempty"` + Tags []MapEntry `json:"tags,omitempty" yaml:"tags,omitempty"` + SpanName *egv1a1.TracingSpanName `json:"spanName,omitempty" yaml:"spanName,omitempty"` } // BackendMetrics defines the metrics configuration for the backend. @@ -2995,17 +2997,19 @@ func (o *JSONPatchOperation) Validate() error { // Tracing defines the configuration for tracing a Envoy xDS Resource // +k8s:deepcopy-gen=true type Tracing struct { - ServiceName string `json:"serviceName"` - Authority string `json:"authority,omitempty"` - SamplingRate float64 `json:"samplingRate,omitempty"` - CustomTags []CustomTagMapEntry `json:"customTags,omitempty"` - Tags []MapEntry `json:"tags,omitempty"` - ResourceAttributes []MapEntry `json:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty"` - Destination RouteDestination `json:"destination,omitempty"` - Traffic *TrafficFeatures `json:"traffic,omitempty"` - Provider egv1a1.TracingProvider `json:"provider"` - Headers []gwapiv1.HTTPHeader `json:"headers,omitempty" yaml:"headers,omitempty"` - SpanName *egv1a1.TracingSpanName `json:"spanName,omitempty"` + ServiceName string `json:"serviceName"` + Authority string `json:"authority,omitempty"` + SamplingRate float64 `json:"samplingRate,omitempty"` + ClientSamplingRate *float64 `json:"clientSamplingRate,omitempty"` + OverallSamplingRate *float64 `json:"overallSamplingRate,omitempty"` + CustomTags []CustomTagMapEntry `json:"customTags,omitempty"` + Tags []MapEntry `json:"tags,omitempty"` + ResourceAttributes []MapEntry `json:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty"` + Destination RouteDestination `json:"destination,omitempty"` + Traffic *TrafficFeatures `json:"traffic,omitempty"` + Provider egv1a1.TracingProvider `json:"provider"` + Headers []gwapiv1.HTTPHeader `json:"headers,omitempty" yaml:"headers,omitempty"` + SpanName *egv1a1.TracingSpanName `json:"spanName,omitempty"` } // Metrics defines the configuration for metrics generated by Envoy diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index f4e3a81b02..a4f4df9bee 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -524,6 +524,16 @@ func (in *BackendTracing) DeepCopyInto(out *BackendTracing) { *out = new(v1.Fraction) (*in).DeepCopyInto(*out) } + if in.ClientSamplingFraction != nil { + in, out := &in.ClientSamplingFraction, &out.ClientSamplingFraction + *out = new(v1.Fraction) + (*in).DeepCopyInto(*out) + } + if in.OverallSamplingFraction != nil { + in, out := &in.OverallSamplingFraction, &out.OverallSamplingFraction + *out = new(v1.Fraction) + (*in).DeepCopyInto(*out) + } if in.CustomTags != nil { in, out := &in.CustomTags, &out.CustomTags *out = make([]CustomTagMapEntry, len(*in)) @@ -5025,6 +5035,16 @@ func (in *Timeout) DeepCopy() *Timeout { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Tracing) DeepCopyInto(out *Tracing) { *out = *in + if in.ClientSamplingRate != nil { + in, out := &in.ClientSamplingRate, &out.ClientSamplingRate + *out = new(float64) + **out = **in + } + if in.OverallSamplingRate != nil { + in, out := &in.OverallSamplingRate, &out.OverallSamplingRate + *out = new(float64) + **out = **in + } if in.CustomTags != nil { in, out := &in.CustomTags, &out.CustomTags *out = make([]CustomTagMapEntry, len(*in)) diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 4426dc2efe..21eb69951c 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -877,7 +877,9 @@ func buildRouteTracing(httpRoute *ir.HTTPRoute) (*routev3.Tracing, error) { op, upstreamOp := buildTracingOperation(tracing.SpanName) return &routev3.Tracing{ + ClientSampling: fractionalpercent.FromFractionOrZero(tracing.ClientSamplingFraction), RandomSampling: fractionalpercent.FromFraction(tracing.SamplingFraction), + OverallSampling: fractionalpercent.FromFraction(tracing.OverallSamplingFraction), CustomTags: tags, Operation: op, UpstreamOperation: upstreamOp, diff --git a/internal/xds/translator/route_test.go b/internal/xds/translator/route_test.go index ab3a0bdcc3..af72994b03 100644 --- a/internal/xds/translator/route_test.go +++ b/internal/xds/translator/route_test.go @@ -10,11 +10,66 @@ import ( "testing" routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/stretchr/testify/require" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" ) +func TestBuildRouteTracingSampling(t *testing.T) { + tests := []struct { + name string + clientSampling *gwapiv1.Fraction + wantClientSampling *xdstype.FractionalPercent + }{ + { + name: "client sampling configured", + clientSampling: &gwapiv1.Fraction{ + Numerator: 2, + Denominator: new(int32(100)), + }, + wantClientSampling: &xdstype.FractionalPercent{Numerator: 2}, + }, + { + name: "client sampling defaults to zero", + wantClientSampling: &xdstype.FractionalPercent{ + Numerator: 0, + Denominator: xdstype.FractionalPercent_HUNDRED, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + httpRoute := &ir.HTTPRoute{ + Traffic: &ir.TrafficFeatures{ + Telemetry: &ir.BackendTelemetry{ + Tracing: &ir.BackendTracing{ + SamplingFraction: &gwapiv1.Fraction{ + Numerator: 1, + Denominator: new(int32(100)), + }, + ClientSamplingFraction: tc.clientSampling, + OverallSamplingFraction: &gwapiv1.Fraction{ + Numerator: 3, + Denominator: new(int32(100)), + }, + }, + }, + }, + } + + got, err := buildRouteTracing(httpRoute) + require.NoError(t, err) + require.Equal(t, &xdstype.FractionalPercent{Numerator: 1}, got.RandomSampling) + require.Equal(t, tc.wantClientSampling, got.ClientSampling) + require.Equal(t, &xdstype.FractionalPercent{Numerator: 3}, got.OverallSampling) + }) + } +} + func TestBuildHashPolicy(t *testing.T) { tests := []struct { name string diff --git a/internal/xds/translator/testdata/in/xds-ir/btp-telemetry.yaml b/internal/xds/translator/testdata/in/xds-ir/btp-telemetry.yaml index f979b7cd15..f86f90e153 100644 --- a/internal/xds/translator/testdata/in/xds-ir/btp-telemetry.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/btp-telemetry.yaml @@ -58,3 +58,9 @@ http: samplingFraction: denominator: 100 numerator: 1 + clientSamplingFraction: + denominator: 100 + numerator: 2 + overallSamplingFraction: + denominator: 100 + numerator: 3 diff --git a/internal/xds/translator/testdata/in/xds-ir/tracing.yaml b/internal/xds/translator/testdata/in/xds-ir/tracing.yaml index eae619a45d..8912dca2aa 100644 --- a/internal/xds/translator/testdata/in/xds-ir/tracing.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/tracing.yaml @@ -1,6 +1,8 @@ tracing: serviceName: "fake-name.fake-ns" samplingRate: 90 + clientSamplingRate: 20 + overallSamplingRate: 30 customTags: - key: "env1" value: diff --git a/internal/xds/translator/testdata/out/xds-ir/btp-telemetry.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/btp-telemetry.routes.yaml index 9f0ed45ef9..b9fa4a98a2 100644 --- a/internal/xds/translator/testdata/out/xds-ir/btp-telemetry.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/btp-telemetry.routes.yaml @@ -28,6 +28,8 @@ upgradeConfigs: - upgradeType: websocket tracing: + clientSampling: + numerator: 2 customTags: - environment: defaultValue: '-' @@ -40,5 +42,7 @@ defaultValue: '-' name: X-Request-Id tag: req1 + overallSampling: + numerator: 3 randomSampling: numerator: 1 diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-datadog-uds.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-datadog-uds.listeners.yaml index f5d891adb1..820c707840 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-datadog-uds.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-datadog-uds.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} customTags: - environment: defaultValue: '-' diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-datadog.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-datadog.listeners.yaml index f5d891adb1..820c707840 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-datadog.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-datadog.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} customTags: - environment: defaultValue: '-' diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.listeners.yaml index c685e2ac55..ce39b69418 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} customTags: - environment: defaultValue: '-' diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-otel-headers.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-otel-headers.listeners.yaml index 593a345e55..928ac8e2d7 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-otel-headers.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-otel-headers.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} overallSampling: value: 100 provider: diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler-always-on.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler-always-on.listeners.yaml index bb942f6128..8c3c66fc03 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler-always-on.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler-always-on.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} overallSampling: value: 100 provider: diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler.listeners.yaml index 5486bf5860..d4712ec0e0 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-otel-sampler.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} overallSampling: value: 100 provider: diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.listeners.yaml index 5c972f24ee..879726f4e5 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} operation: '%REQ(X-ENVOY-ORIGINAL-HOST)%%PATH(NQ:ORIG)%' overallSampling: value: 100 @@ -83,8 +82,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-8888 tracing: - clientSampling: - value: 100 + clientSampling: {} operation: '%REQ(X-ENVOY-ORIGINAL-HOST)%%PATH(NQ:ORIG)%' overallSampling: value: 100 diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.routes.yaml index 21a2ca5eea..37f0593833 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-span-name.routes.yaml @@ -28,6 +28,7 @@ upgradeConfigs: - upgradeType: websocket tracing: + clientSampling: {} operation: custom-client-span-%REQ(X-CUSTOM-HEADER)% upstreamOperation: custom-server-span-%UPSTREAM_HOST% - ignorePortInHostMatching: true diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.listeners.yaml index 362f098ff2..ecbf2155ae 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.listeners.yaml @@ -29,8 +29,7 @@ serverHeaderTransformation: PASS_THROUGH statPrefix: http-10080 tracing: - clientSampling: - value: 100 + clientSampling: {} customTags: - environment: defaultValue: '-' diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml index b7497f7924..0f9da01cbe 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml @@ -30,7 +30,7 @@ statPrefix: http-10080 tracing: clientSampling: - value: 100 + value: 20 customTags: - environment: defaultValue: '-' @@ -44,7 +44,7 @@ name: X-Request-Id tag: req1 overallSampling: - value: 100 + value: 30 provider: name: envoy.tracers.opentelemetry typedConfig: diff --git a/internal/xds/translator/tracing.go b/internal/xds/translator/tracing.go index 9dc9c0c732..2473729a1c 100644 --- a/internal/xds/translator/tracing.go +++ b/internal/xds/translator/tracing.go @@ -116,10 +116,12 @@ func buildHCMTracing(tracing *ir.Tracing) (*hcm.HttpConnectionManager_Tracing, e return &hcm.HttpConnectionManager_Tracing{ ClientSampling: &xdstype.Percent{ - Value: 100.0, + // Default to 0 so Envoy Gateway does not honor client-forced tracing + // unless users explicitly opt in with clientSamplingFraction. + Value: ptr.Deref(tracing.ClientSamplingRate, 0), }, OverallSampling: &xdstype.Percent{ - Value: 100.0, + Value: ptr.Deref(tracing.OverallSamplingRate, 100), }, RandomSampling: &xdstype.Percent{ Value: randomSamplingValue(tracing), diff --git a/internal/xds/translator/tracing_test.go b/internal/xds/translator/tracing_test.go index 13c6c3b1d9..932a8d0263 100644 --- a/internal/xds/translator/tracing_test.go +++ b/internal/xds/translator/tracing_test.go @@ -15,6 +15,61 @@ import ( "github.com/envoyproxy/gateway/internal/ir" ) +func TestBuildHCMTracingSampling(t *testing.T) { + testCases := []struct { + name string + tracing *ir.Tracing + expectedRandomSampling float64 + expectedClientSampling float64 + expectedOverallSampling float64 + }{ + { + name: "explicit sampling values", + tracing: &ir.Tracing{ + ServiceName: "test-service", + SamplingRate: 10, + ClientSamplingRate: new(20.0), + OverallSamplingRate: new(30.0), + Destination: ir.RouteDestination{ + Name: "tracing", + }, + Provider: egv1a1.TracingProvider{ + Type: egv1a1.TracingProviderTypeOpenTelemetry, + }, + }, + expectedRandomSampling: 10.0, + expectedClientSampling: 20.0, + expectedOverallSampling: 30.0, + }, + { + name: "unset client sampling defaults to zero", + tracing: &ir.Tracing{ + ServiceName: "test-service", + SamplingRate: 10, + Destination: ir.RouteDestination{ + Name: "tracing", + }, + Provider: egv1a1.TracingProvider{ + Type: egv1a1.TracingProviderTypeOpenTelemetry, + }, + }, + expectedRandomSampling: 10.0, + expectedClientSampling: 0.0, + expectedOverallSampling: 100.0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := buildHCMTracing(tc.tracing) + require.NoError(t, err) + require.Equal(t, tc.expectedRandomSampling, got.RandomSampling.Value) + require.Equal(t, tc.expectedClientSampling, got.ClientSampling.Value) + require.Equal(t, tc.expectedOverallSampling, got.OverallSampling.Value) + }) + } +} + func TestBuildSampler(t *testing.T) { testCases := []struct { name string diff --git a/internal/xds/utils/fractionalpercent/fractional_percent.go b/internal/xds/utils/fractionalpercent/fractional_percent.go index 3d18ba0a3f..717bfbb80d 100644 --- a/internal/xds/utils/fractionalpercent/fractional_percent.go +++ b/internal/xds/utils/fractionalpercent/fractional_percent.go @@ -89,6 +89,20 @@ func FromFraction(fraction *gwapiv1.Fraction) *xdstype.FractionalPercent { } } +// FromFractionOrZero translates a gwapiv1.Fraction instance to envoy.type.FractionalPercent. +// If fraction is nil, it returns an explicit zero percent value. +func FromFractionOrZero(fraction *gwapiv1.Fraction) *xdstype.FractionalPercent { + fp := FromFraction(fraction) + if fp != nil { + return fp + } + + return &xdstype.FractionalPercent{ + Numerator: 0, + Denominator: xdstype.FractionalPercent_HUNDRED, + } +} + func ToPercent(fp *gwapiv1.Fraction) float64 { if fp == nil { return 0 diff --git a/internal/xds/utils/fractionalpercent/fractional_percent_test.go b/internal/xds/utils/fractionalpercent/fractional_percent_test.go index 4206d6bca6..a8f2ccfbc4 100644 --- a/internal/xds/utils/fractionalpercent/fractional_percent_test.go +++ b/internal/xds/utils/fractionalpercent/fractional_percent_test.go @@ -188,3 +188,40 @@ func TestFromFraction(t *testing.T) { }) } } + +func TestFromFractionOrZero(t *testing.T) { + cases := []struct { + name string + input *gwapiv1.Fraction + expected *xdstype.FractionalPercent + }{ + { + name: "configured fraction", + input: &gwapiv1.Fraction{ + Numerator: 2, + Denominator: new(int32(100)), + }, + expected: &xdstype.FractionalPercent{ + Numerator: 2, + Denominator: xdstype.FractionalPercent_HUNDRED, + }, + }, + { + name: "nil fraction defaults to zero", + input: nil, + expected: &xdstype.FractionalPercent{ + Numerator: 0, + Denominator: xdstype.FractionalPercent_HUNDRED, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := FromFractionOrZero(tc.input) + if result.Numerator != tc.expected.Numerator || result.Denominator != tc.expected.Denominator { + t.Errorf("expected %v, got %v", tc.expected, result) + } + }) + } +} diff --git a/release-notes/current/breaking_changes/9372-tracing-client-sampling-default.md b/release-notes/current/breaking_changes/9372-tracing-client-sampling-default.md new file mode 100644 index 0000000000..efca4ac4cf --- /dev/null +++ b/release-notes/current/breaking_changes/9372-tracing-client-sampling-default.md @@ -0,0 +1 @@ +Tracing client sampling now defaults to 0% instead of 100%, so Envoy Gateway no longer honors client-forced tracing unless users explicitly opt in by setting `clientSamplingFraction`. diff --git a/release-notes/current/new_features/9372-tracing-client-overall-sampling.md b/release-notes/current/new_features/9372-tracing-client-overall-sampling.md new file mode 100644 index 0000000000..c6a9e56b58 --- /dev/null +++ b/release-notes/current/new_features/9372-tracing-client-overall-sampling.md @@ -0,0 +1 @@ +Added support for configuring Envoy tracing client and overall sampling fractions on EnvoyProxy and BackendTrafficPolicy tracing settings. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 7d78e634ab..047cfd9ea8 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -5019,6 +5019,8 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `samplingFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | SamplingFraction represents the fraction of requests that should be
selected for tracing if no prior sampling decision has been made. | +| `clientSamplingFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | ClientSamplingFraction represents the fraction of requests that should be
selected for tracing when requested by the client.
If unspecified, client-forced tracing is disabled by default and users must
set this field to opt in. | +| `overallSamplingFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | OverallSamplingFraction represents the fraction of requests that should be
selected for tracing after all other sampling checks have been applied. | | `customTags` | _object (keys:string, values:[CustomTag](#customtag))_ | false | | CustomTags defines the custom tags to add to each span.
If provider is kubernetes, pod name and namespace are added by default.
Deprecated: Use Tags instead. | | `tags` | _object (keys:string, values:string)_ | false | | Tags defines the custom tags to add to each span.
Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) may be used in the value.
The [format string documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-strings) provides more information.
If provider is kubernetes, pod name and namespace are added by default.
Same keys take precedence over CustomTags. | | `spanName` | _[TracingSpanName](#tracingspanname)_ | false | | SpanName defines the name of the span which will be used for tracing.
Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) may be used in the value.
The [format string documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-strings) provides more information.
If not set, the span name is provider specific.
e.g. Datadog use `ingress` as the default client span name,
and `router egress` as the server span name. | @@ -6315,6 +6317,8 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `samplingFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | SamplingFraction represents the fraction of requests that should be
selected for tracing if no prior sampling decision has been made. | +| `clientSamplingFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | ClientSamplingFraction represents the fraction of requests that should be
selected for tracing when requested by the client.
If unspecified, client-forced tracing is disabled by default and users must
set this field to opt in. | +| `overallSamplingFraction` | _[Fraction](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#fraction)_ | false | | OverallSamplingFraction represents the fraction of requests that should be
selected for tracing after all other sampling checks have been applied. | | `customTags` | _object (keys:string, values:[CustomTag](#customtag))_ | false | | CustomTags defines the custom tags to add to each span.
If provider is kubernetes, pod name and namespace are added by default.
Deprecated: Use Tags instead. | | `tags` | _object (keys:string, values:string)_ | false | | Tags defines the custom tags to add to each span.
Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) may be used in the value.
The [format string documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-strings) provides more information.
If provider is kubernetes, pod name and namespace are added by default.
Same keys take precedence over CustomTags. | | `spanName` | _[TracingSpanName](#tracingspanname)_ | false | | SpanName defines the name of the span which will be used for tracing.
Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) may be used in the value.
The [format string documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-strings) provides more information.
If not set, the span name is provider specific.
e.g. Datadog use `ingress` as the default client span name,
and `router egress` as the server span name. | diff --git a/test/e2e/testdata/tracing-otel-sampling-fractions.yaml b/test/e2e/testdata/tracing-otel-sampling-fractions.yaml new file mode 100644 index 0000000000..e79303b902 --- /dev/null +++ b/test/e2e/testdata/tracing-otel-sampling-fractions.yaml @@ -0,0 +1,131 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: tracing-otel-overall-sampling-zero + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: tracing-otel-overall-sampling-zero +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: tracing-otel-overall-sampling-zero + namespace: gateway-conformance-infra +spec: + ipFamily: IPv4 + telemetry: + tracing: + samplingFraction: + numerator: 100 + clientSamplingFraction: + numerator: 100 + overallSamplingFraction: + numerator: 0 + provider: + backendRefs: + - name: otel-collector + namespace: monitoring + port: 4317 + tags: + "provider": "otel-overall-sampling-zero" + "k8s.cluster.name": "envoy-gateway" + "k8s.pod.name": "%ENVIRONMENT(ENVOY_POD_NAME)%" + "k8s.namespace.name": "%ENVIRONMENT(ENVOY_POD_NAMESPACE)%" + shutdown: + drainTimeout: 5s + minDrainDuration: 1s +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tracing-otel-overall-sampling-zero + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: tracing-otel-overall-sampling-zero + rules: + - matches: + - path: + type: PathPrefix + value: /otel-overall-sampling-zero + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: tracing-otel-client-sampling-zero + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: tracing-otel-client-sampling-zero +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: tracing-otel-client-sampling-zero + namespace: gateway-conformance-infra +spec: + ipFamily: IPv4 + telemetry: + tracing: + samplingFraction: + numerator: 0 + clientSamplingFraction: + numerator: 0 + overallSamplingFraction: + numerator: 100 + provider: + backendRefs: + - name: otel-collector + namespace: monitoring + port: 4317 + tags: + "provider": "otel-client-sampling-zero" + "k8s.cluster.name": "envoy-gateway" + "k8s.pod.name": "%ENVIRONMENT(ENVOY_POD_NAME)%" + "k8s.namespace.name": "%ENVIRONMENT(ENVOY_POD_NAMESPACE)%" + shutdown: + drainTimeout: 5s + minDrainDuration: 1s +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tracing-otel-client-sampling-zero + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: tracing-otel-client-sampling-zero + rules: + - matches: + - path: + type: PathPrefix + value: /otel-client-sampling-zero + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/tests/tracing.go b/test/e2e/tests/tracing.go index 706ef16dd7..3b555e7e84 100644 --- a/test/e2e/tests/tracing.go +++ b/test/e2e/tests/tracing.go @@ -33,7 +33,11 @@ func init() { var OpenTelemetryTracingTest = suite.ConformanceTest{ ShortName: "OpenTelemetryTracing", Description: "Make sure OpenTelemetry tracing is working (default and custom service name)", - Manifests: []string{"testdata/tracing-otel.yaml", "testdata/tracing-otel-custom-service-name.yaml"}, + Manifests: []string{ + "testdata/tracing-otel.yaml", + "testdata/tracing-otel-custom-service-name.yaml", + "testdata/tracing-otel-sampling-fractions.yaml", + }, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { cases := []struct { name string @@ -82,6 +86,55 @@ var OpenTelemetryTracingTest = suite.ConformanceTest{ }) } + samplingCases := []struct { + name string + routeName string + path string + headers map[string]string + provider string + }{ + { + name: "OverallSamplingFractionZero", + routeName: "tracing-otel-overall-sampling-zero", + path: "/otel-overall-sampling-zero", + provider: "otel-overall-sampling-zero", + }, + { + name: "ClientSamplingFractionZero", + routeName: "tracing-otel-client-sampling-zero", + path: "/otel-client-sampling-zero", + headers: map[string]string{ + "X-Client-Trace-Id": "force-client-trace", + }, + provider: "otel-client-sampling-zero", + }, + } + for _, tc := range samplingCases { + t.Run(tc.name, func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: tc.routeName, Namespace: ns} + gwNN := types.NamespacedName{Name: tc.routeName, Namespace: ns} + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN) + expectedResponse := httputils.ExpectedResponse{ + Request: httputils.Request{ + Path: tc.path, + Headers: tc.headers, + }, + Response: httputils.Response{ + StatusCodes: []int{200}, + }, + Namespace: ns, + } + httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + tags := map[string]string{ + "component": "proxy", + "provider": tc.provider, + "service.name": naming.ServiceName(gwNN), + } + expectNoTraceCountIncrease(t, suite, gwAddr, &expectedResponse, tags) + }) + } + t.Run("ControlPlane", func(t *testing.T) { tags := map[string]string{ "service.name": "envoy-gateway", @@ -103,6 +156,40 @@ var OpenTelemetryTracingTest = suite.ConformanceTest{ }, } +func expectNoTraceCountIncrease(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expectedResponse *httputils.ExpectedResponse, tags map[string]string) { + t.Helper() + + var preCount int + err := wait.PollUntilContextTimeout(t.Context(), time.Second, 15*time.Second, true, func(_ context.Context) (done bool, err error) { + count, err := tracing.QueryTraceFromTempo(t, suite.Client, tags) + if err != nil { + tlog.Logf(t, "failed to get trace count from tempo: %v", err) + return false, nil + } + preCount = count + return true, nil + }) + require.NoError(t, err, "failed to get initial trace count from tempo") + + httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, *expectedResponse) + + observedCurrentCount := false + err = wait.PollUntilContextTimeout(t.Context(), time.Second, 15*time.Second, true, func(_ context.Context) (bool, error) { + curCount, err := tracing.QueryTraceFromTempo(t, suite.Client, tags) + if err != nil { + tlog.Logf(t, "failed to get current trace count from tempo: %v", err) + return false, nil + } + observedCurrentCount = true + if curCount > preCount { + return false, fmt.Errorf("trace count increased from %d to %d", preCount, curCount) + } + return false, nil + }) + require.ErrorIs(t, err, context.DeadlineExceeded) + require.True(t, observedCurrentCount, "failed to get current trace count from tempo") +} + var ZipkinTracingTest = suite.ConformanceTest{ ShortName: "ZipkinTracing", Description: "Make sure Zipkin tracing is working", diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 37a82f491c..8f8da667ef 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -27769,6 +27769,28 @@ spec: This takes precedence over EnvoyProxy tracing when set. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -27833,6 +27855,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator samplingFraction: description: |- SamplingFraction represents the fraction of requests that should be @@ -50014,6 +50056,28 @@ spec: Tracing defines tracing configuration for managed proxies. If unspecified, will not send tracing data. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -50078,6 +50142,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator provider: description: Provider defines the tracing provider. properties: diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 7adb011c27..389a62580b 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -3707,6 +3707,28 @@ spec: This takes precedence over EnvoyProxy tracing when set. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -3771,6 +3793,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator samplingFraction: description: |- SamplingFraction represents the fraction of requests that should be @@ -25952,6 +25994,28 @@ spec: Tracing defines tracing configuration for managed proxies. If unspecified, will not send tracing data. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -26016,6 +26080,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator provider: description: Provider defines the tracing provider. properties: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index f926f93ec0..dd8f56bd32 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -3707,6 +3707,28 @@ spec: This takes precedence over EnvoyProxy tracing when set. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -3771,6 +3793,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator samplingFraction: description: |- SamplingFraction represents the fraction of requests that should be @@ -25952,6 +25994,28 @@ spec: Tracing defines tracing configuration for managed proxies. If unspecified, will not send tracing data. properties: + clientSamplingFraction: + description: |- + ClientSamplingFraction represents the fraction of requests that should be + selected for tracing when requested by the client. + If unspecified, client-forced tracing is disabled by default and users must + set this field to opt in. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator customTags: additionalProperties: properties: @@ -26016,6 +26080,26 @@ spec: Deprecated: Use Tags instead. type: object + overallSamplingFraction: + description: |- + OverallSamplingFraction represents the fraction of requests that should be + selected for tracing after all other sampling checks have been applied. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator provider: description: Provider defines the tracing provider. properties: