1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-04-09 07:03:54 +02:00

277 lines
7.4 KiB
Go
Raw Normal View History

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package trace
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/internal/env"
ottest "go.opentelemetry.io/otel/sdk/internal/internaltest"
"go.opentelemetry.io/otel/trace"
)
func TestSettingSpanLimits(t *testing.T) {
envLimits := func(val string) map[string]string {
return map[string]string{
env.SpanAttributeValueLengthKey: val,
env.SpanEventCountKey: val,
env.SpanAttributeCountKey: val,
env.SpanLinkCountKey: val,
env.SpanEventAttributeCountKey: val,
env.SpanLinkAttributeCountKey: val,
}
}
limits := func(n int) *SpanLimits {
lims := NewSpanLimits()
lims.AttributeValueLengthLimit = n
lims.AttributeCountLimit = n
lims.EventCountLimit = n
lims.LinkCountLimit = n
lims.AttributePerEventCountLimit = n
lims.AttributePerLinkCountLimit = n
return &lims
}
tests := []struct {
name string
env map[string]string
opt *SpanLimits
rawOpt *SpanLimits
want SpanLimits
}{
{
name: "defaults",
want: NewSpanLimits(),
},
{
name: "env",
env: envLimits("42"),
want: *(limits(42)),
},
{
name: "opt",
opt: limits(42),
want: *(limits(42)),
},
{
name: "raw-opt",
rawOpt: limits(42),
want: *(limits(42)),
},
{
name: "opt-override",
env: envLimits("-2"),
// Option take priority.
opt: limits(43),
want: *(limits(43)),
},
{
name: "raw-opt-override",
env: envLimits("-2"),
// Option take priority.
rawOpt: limits(43),
want: *(limits(43)),
},
{
name: "last-opt-wins",
opt: limits(-2),
rawOpt: limits(-3),
want: *(limits(-3)),
},
{
name: "env(unlimited)",
// OTel spec says negative SpanLinkAttributeCountKey is invalid,
// but since we will revert to the default (unlimited) which uses
// negative values to signal this than this value is expected to
// pass through.
env: envLimits("-1"),
want: *(limits(-1)),
},
{
name: "opt(unlimited)",
// Corrects to defaults.
opt: limits(-1),
want: NewSpanLimits(),
},
{
name: "raw-opt(unlimited)",
rawOpt: limits(-1),
want: *(limits(-1)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.env != nil {
es := ottest.NewEnvStore()
t.Cleanup(func() { require.NoError(t, es.Restore()) })
for k, v := range test.env {
es.Record(k)
require.NoError(t, os.Setenv(k, v))
}
}
var opts []TracerProviderOption
if test.opt != nil {
opts = append(opts, WithSpanLimits(*test.opt))
}
if test.rawOpt != nil {
opts = append(opts, WithRawSpanLimits(*test.rawOpt))
}
assert.Equal(t, test.want, NewTracerProvider(opts...).spanLimits)
})
}
}
type recorder []ReadOnlySpan
func (r *recorder) OnStart(context.Context, ReadWriteSpan) {}
func (r *recorder) OnEnd(s ReadOnlySpan) { *r = append(*r, s) }
func (r *recorder) ForceFlush(context.Context) error { return nil }
func (r *recorder) Shutdown(context.Context) error { return nil }
func testSpanLimits(t *testing.T, limits SpanLimits) ReadOnlySpan {
rec := new(recorder)
tp := NewTracerProvider(WithRawSpanLimits(limits), WithSpanProcessor(rec))
tracer := tp.Tracer("testSpanLimits")
ctx := context.Background()
a := []attribute.KeyValue{attribute.Bool("one", true), attribute.Bool("two", true)}
l := trace.Link{
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
TraceID: [16]byte{0x01},
SpanID: [8]byte{0x01},
}),
Attributes: a,
}
_, span := tracer.Start(ctx, "span-name", trace.WithLinks(l, l))
span.SetAttributes(
attribute.String("string", "abc"),
attribute.StringSlice("stringSlice", []string{"abc", "def"}),
attribute.String("euro", "€"), // this is a 3-byte rune
)
span.AddEvent("event 1", trace.WithAttributes(a...))
span.AddEvent("event 2", trace.WithAttributes(a...))
span.End()
require.NoError(t, tp.Shutdown(ctx))
require.Len(t, *rec, 1, "exported spans")
return (*rec)[0]
}
func TestSpanLimits(t *testing.T) {
t.Run("AttributeValueLengthLimit", func(t *testing.T) {
limits := NewSpanLimits()
// Unlimited.
limits.AttributeValueLengthLimit = -1
attrs := testSpanLimits(t, limits).Attributes()
assert.Contains(t, attrs, attribute.String("string", "abc"))
assert.Contains(t, attrs, attribute.StringSlice("stringSlice", []string{"abc", "def"}))
assert.Contains(t, attrs, attribute.String("euro", "€"))
limits.AttributeValueLengthLimit = 2
attrs = testSpanLimits(t, limits).Attributes()
// Ensure string and string slice attributes are truncated.
assert.Contains(t, attrs, attribute.String("string", "ab"))
assert.Contains(t, attrs, attribute.StringSlice("stringSlice", []string{"ab", "de"}))
Fix attribute value truncation (#5997) Fix #5996 ### Correctness From the [OTel specification](https://github.com/open-telemetry/opentelemetry-specification/blob/88bffeac48aa5eb37ac8044e931af63a0b55fe00/specification/common/README.md#attribute-limits): > - set an attribute value length limit such that for each attribute value: > - if it is a string, if it exceeds that limit (counting any character in it as 1), SDKs MUST truncate that value, so that its length is at most equal to the limit... Our current implementation truncates on number of bytes not characters. Unit tests are added/updated to validate this fix and prevent regressions. ### Performance ``` goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/sdk/trace cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz │ commit-b6264913(old).txt │ commit-54c61ac2(new).txt │ │ sec/op │ sec/op vs base │ Truncate/Unlimited-8 1.2300n ± 7% 0.8757n ± 3% -28.80% (p=0.000 n=10) Truncate/Zero-8 2.341n ± 2% 1.550n ± 9% -33.77% (p=0.000 n=10) Truncate/Short-8 31.6800n ± 3% 0.9960n ± 4% -96.86% (p=0.000 n=10) Truncate/ASCII-8 8.821n ± 1% 3.567n ± 3% -59.57% (p=0.000 n=10) Truncate/ValidUTF-8-8 11.960n ± 1% 7.163n ± 1% -40.10% (p=0.000 n=10) Truncate/InvalidUTF-8-8 56.35n ± 0% 37.34n ± 18% -33.74% (p=0.000 n=10) Truncate/MixedUTF-8-8 81.83n ± 1% 50.00n ± 1% -38.90% (p=0.000 n=10) geomean 12.37n 4.865n -60.68% │ commit-b6264913(old).txt │ commit-54c61ac2(new).txt │ │ B/op │ B/op vs base │ Truncate/Unlimited-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Zero-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Short-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ASCII-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ValidUTF-8-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/InvalidUTF-8-8 16.00 ± 0% 16.00 ± 0% ~ (p=1.000 n=10) ¹ Truncate/MixedUTF-8-8 32.00 ± 0% 32.00 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean │ commit-b6264913(old).txt │ commit-54c61ac2(new).txt │ │ allocs/op │ allocs/op vs base │ Truncate/Unlimited-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Zero-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Short-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ASCII-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ValidUTF-8-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/InvalidUTF-8-8 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/MixedUTF-8-8 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean ``` #### Values shorter than limit This is the default code path. Most attribute values will be shorter than the default 128 limit that users will not modify. The current code, `safeTruncate` requires a full iteration of the value to determine it is valid and under the limit. The replacement, `truncate`, first checks if the number of bytes in the value are less than or equal to the limit (which guarantees the number of characters are less than or equal to the limit) and returns immediately. This will mean that invalid encoding less than the limit is not changed, which meets the specification requirements. #### Values longer than the limit For values who's number of bytes exceeds the limit, they are iterated only once with the replacement, `truncate`. In comparison, the current code, `safeTruncate`, can iterate the string up to three separate times when the string contains invalid characters.
2024-11-26 00:41:55 -08:00
assert.Contains(t, attrs, attribute.String("euro", "€"))
limits.AttributeValueLengthLimit = 0
attrs = testSpanLimits(t, limits).Attributes()
assert.Contains(t, attrs, attribute.String("string", ""))
assert.Contains(t, attrs, attribute.StringSlice("stringSlice", []string{"", ""}))
assert.Contains(t, attrs, attribute.String("euro", ""))
})
t.Run("AttributeCountLimit", func(t *testing.T) {
limits := NewSpanLimits()
// Unlimited.
limits.AttributeCountLimit = -1
assert.Len(t, testSpanLimits(t, limits).Attributes(), 3)
limits.AttributeCountLimit = 1
assert.Len(t, testSpanLimits(t, limits).Attributes(), 1)
// Ensure this can be disabled.
limits.AttributeCountLimit = 0
assert.Empty(t, testSpanLimits(t, limits).Attributes())
})
t.Run("EventCountLimit", func(t *testing.T) {
limits := NewSpanLimits()
// Unlimited.
limits.EventCountLimit = -1
assert.Len(t, testSpanLimits(t, limits).Events(), 2)
limits.EventCountLimit = 1
assert.Len(t, testSpanLimits(t, limits).Events(), 1)
// Ensure this can be disabled.
limits.EventCountLimit = 0
assert.Empty(t, testSpanLimits(t, limits).Events())
})
t.Run("AttributePerEventCountLimit", func(t *testing.T) {
limits := NewSpanLimits()
// Unlimited.
limits.AttributePerEventCountLimit = -1
for _, e := range testSpanLimits(t, limits).Events() {
assert.Len(t, e.Attributes, 2)
}
limits.AttributePerEventCountLimit = 1
for _, e := range testSpanLimits(t, limits).Events() {
assert.Len(t, e.Attributes, 1)
}
// Ensure this can be disabled.
limits.AttributePerEventCountLimit = 0
for _, e := range testSpanLimits(t, limits).Events() {
assert.Empty(t, e.Attributes)
}
})
t.Run("LinkCountLimit", func(t *testing.T) {
limits := NewSpanLimits()
// Unlimited.
limits.LinkCountLimit = -1
assert.Len(t, testSpanLimits(t, limits).Links(), 2)
limits.LinkCountLimit = 1
assert.Len(t, testSpanLimits(t, limits).Links(), 1)
// Ensure this can be disabled.
limits.LinkCountLimit = 0
assert.Empty(t, testSpanLimits(t, limits).Links())
})
t.Run("AttributePerLinkCountLimit", func(t *testing.T) {
limits := NewSpanLimits()
// Unlimited.
limits.AttributePerLinkCountLimit = -1
for _, l := range testSpanLimits(t, limits).Links() {
assert.Len(t, l.Attributes, 2)
}
limits.AttributePerLinkCountLimit = 1
for _, l := range testSpanLimits(t, limits).Links() {
assert.Len(t, l.Attributes, 1)
}
// Ensure this can be disabled.
limits.AttributePerLinkCountLimit = 0
for _, l := range testSpanLimits(t, limits).Links() {
assert.Empty(t, l.Attributes)
}
})
}