1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-10-08 23:21:56 +02:00
Files
opentelemetry-go/sdk/log/record_test.go
Robert Pająk a99f9b56ce sdk/log: Record.Resource to return *resource.Resource (#6864)
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/6863

From https://pkg.go.dev/go.opentelemetry.io/otel/sdk/resource#Resource:

> Resource is an immutable object
> [...]
> Resources should be passed and stored as pointers
(`*resource.Resource`)

In all other places the exported API uses `*resource.Resource` and not
`resource.Resource`.
2025-06-04 07:05:17 +02:00

776 lines
18 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package log
import (
"fmt"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/log"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/trace"
)
func TestRecordEventName(t *testing.T) {
const text = "testing text"
r := new(Record)
r.SetEventName(text)
assert.Equal(t, text, r.EventName())
}
func TestRecordTimestamp(t *testing.T) {
now := time.Now()
r := new(Record)
r.SetTimestamp(now)
assert.Equal(t, now, r.Timestamp())
}
func TestRecordObservedTimestamp(t *testing.T) {
now := time.Now()
r := new(Record)
r.SetObservedTimestamp(now)
assert.Equal(t, now, r.ObservedTimestamp())
}
func TestRecordSeverity(t *testing.T) {
s := log.SeverityInfo
r := new(Record)
r.SetSeverity(s)
assert.Equal(t, s, r.Severity())
}
func TestRecordSeverityText(t *testing.T) {
text := "text"
r := new(Record)
r.SetSeverityText(text)
assert.Equal(t, text, r.SeverityText())
}
func TestRecordBody(t *testing.T) {
v := log.BoolValue(true)
r := new(Record)
r.SetBody(v)
assert.True(t, v.Equal(r.Body()))
}
func TestRecordAttributes(t *testing.T) {
attrs := []log.KeyValue{
log.Bool("0", true),
log.Int64("1", 2),
log.Float64("2", 3.0),
log.String("3", "forth"),
log.Slice("4", log.Int64Value(1)),
log.Map("5", log.Int("key", 2)),
log.Bytes("6", []byte("six")),
}
r := new(Record)
r.attributeValueLengthLimit = -1
r.SetAttributes(attrs...)
r.SetAttributes(attrs[:2]...) // Overwrite existing.
r.AddAttributes(attrs[2:]...)
assert.Equal(t, len(attrs), r.AttributesLen(), "attribute length")
for n := range attrs {
var i int
r.WalkAttributes(func(log.KeyValue) bool {
i++
return i <= n
})
assert.Equalf(t, n+1, i, "WalkAttributes did not stop at %d", n+1)
}
var i int
r.WalkAttributes(func(kv log.KeyValue) bool {
assert.Truef(t, kv.Equal(attrs[i]), "%d: %v != %v", i, kv, attrs[i])
i++
return true
})
}
func TestRecordTraceID(t *testing.T) {
id := trace.TraceID([16]byte{1})
r := new(Record)
r.SetTraceID(id)
assert.Equal(t, id, r.TraceID())
}
func TestRecordSpanID(t *testing.T) {
id := trace.SpanID([8]byte{1})
r := new(Record)
r.SetSpanID(id)
assert.Equal(t, id, r.SpanID())
}
func TestRecordTraceFlags(t *testing.T) {
flag := trace.FlagsSampled
r := new(Record)
r.SetTraceFlags(flag)
assert.Equal(t, flag, r.TraceFlags())
}
func TestRecordResource(t *testing.T) {
r := new(Record)
assert.NotPanics(t, func() { r.Resource() })
res := resource.NewSchemaless(attribute.Bool("key", true))
r.resource = res
got := r.Resource()
assert.Equal(t, res, got)
}
func TestRecordInstrumentationScope(t *testing.T) {
r := new(Record)
assert.NotPanics(t, func() { r.InstrumentationScope() })
scope := instrumentation.Scope{Name: "testing"}
r.scope = &scope
assert.Equal(t, scope, r.InstrumentationScope())
}
func TestRecordClone(t *testing.T) {
now0 := time.Now()
sev0 := log.SeverityInfo
text0 := "text"
val0 := log.BoolValue(true)
attr0 := log.Bool("0", true)
traceID0 := trace.TraceID([16]byte{1})
spanID0 := trace.SpanID([8]byte{1})
flag0 := trace.FlagsSampled
r0 := new(Record)
r0.SetTimestamp(now0)
r0.SetObservedTimestamp(now0)
r0.SetSeverity(sev0)
r0.SetSeverityText(text0)
r0.SetBody(val0)
r0.SetAttributes(attr0)
r0.SetTraceID(traceID0)
r0.SetSpanID(spanID0)
r0.SetTraceFlags(flag0)
now1 := now0.Add(time.Second)
sev1 := log.SeverityDebug
text1 := "string"
val1 := log.IntValue(1)
attr1 := log.Int64("1", 2)
traceID1 := trace.TraceID([16]byte{2})
spanID1 := trace.SpanID([8]byte{2})
flag1 := trace.TraceFlags(2)
r1 := r0.Clone()
r1.SetTimestamp(now1)
r1.SetObservedTimestamp(now1)
r1.SetSeverity(sev1)
r1.SetSeverityText(text1)
r1.SetBody(val1)
r1.SetAttributes(attr1)
r1.SetTraceID(traceID1)
r1.SetSpanID(spanID1)
r1.SetTraceFlags(flag1)
assert.Equal(t, now0, r0.Timestamp())
assert.Equal(t, now0, r0.ObservedTimestamp())
assert.Equal(t, sev0, r0.Severity())
assert.Equal(t, text0, r0.SeverityText())
assert.True(t, val0.Equal(r0.Body()))
assert.Equal(t, traceID0, r0.TraceID())
assert.Equal(t, spanID0, r0.SpanID())
assert.Equal(t, flag0, r0.TraceFlags())
r0.WalkAttributes(func(kv log.KeyValue) bool {
return assert.Truef(t, kv.Equal(attr0), "%v != %v", kv, attr0)
})
assert.Equal(t, now1, r1.Timestamp())
assert.Equal(t, now1, r1.ObservedTimestamp())
assert.Equal(t, sev1, r1.Severity())
assert.Equal(t, text1, r1.SeverityText())
assert.True(t, val1.Equal(r1.Body()))
assert.Equal(t, traceID1, r1.TraceID())
assert.Equal(t, spanID1, r1.SpanID())
assert.Equal(t, flag1, r1.TraceFlags())
r1.WalkAttributes(func(kv log.KeyValue) bool {
return assert.Truef(t, kv.Equal(attr1), "%v != %v", kv, attr1)
})
}
func TestRecordDroppedAttributes(t *testing.T) {
orig := logAttrDropped
t.Cleanup(func() { logAttrDropped = orig })
for i := 1; i < attributesInlineCount*5; i++ {
var called bool
logAttrDropped = func() { called = true }
r := new(Record)
r.attributeCountLimit = 1
attrs := make([]log.KeyValue, i)
attrs[0] = log.Bool("only key different then the rest", true)
assert.False(t, called, "non-dropped attributed logged")
r.AddAttributes(attrs...)
assert.Equalf(t, i-1, r.DroppedAttributes(), "%d: AddAttributes", i)
assert.True(t, called, "dropped attributes not logged")
r.AddAttributes(attrs...)
assert.Equalf(t, 2*i-1, r.DroppedAttributes(), "%d: second AddAttributes", i)
r.SetAttributes(attrs...)
assert.Equalf(t, i-1, r.DroppedAttributes(), "%d: SetAttributes", i)
}
}
func TestRecordAttrDeduplication(t *testing.T) {
testcases := []struct {
name string
attrs []log.KeyValue
want []log.KeyValue
}{
{
name: "EmptyKey",
attrs: make([]log.KeyValue, 10),
want: make([]log.KeyValue, 1),
},
{
name: "NonEmptyKey",
attrs: []log.KeyValue{
log.Bool("key", true),
log.Int64("key", 1),
log.Bool("key", false),
log.Float64("key", 2.),
log.String("key", "3"),
log.Slice("key", log.Int64Value(4)),
log.Map("key", log.Int("key", 5)),
log.Bytes("key", []byte("six")),
log.Bool("key", false),
},
want: []log.KeyValue{
log.Bool("key", false),
},
},
{
name: "Multiple",
attrs: []log.KeyValue{
log.Bool("a", true),
log.Int64("b", 1),
log.Bool("a", false),
log.Float64("c", 2.),
log.String("b", "3"),
log.Slice("d", log.Int64Value(4)),
log.Map("a", log.Int("key", 5)),
log.Bytes("d", []byte("six")),
log.Bool("e", true),
log.Int("f", 1),
log.Int("f", 2),
log.Int("f", 3),
log.Float64("b", 0.0),
log.Float64("b", 0.0),
log.String("g", "G"),
log.String("h", "H"),
log.String("g", "GG"),
log.Bool("a", false),
},
want: []log.KeyValue{
// Order is important here.
log.Bool("a", false),
log.Float64("b", 0.0),
log.Float64("c", 2.),
log.Bytes("d", []byte("six")),
log.Bool("e", true),
log.Int("f", 3),
log.String("g", "GG"),
log.String("h", "H"),
},
},
{
name: "NoDuplicate",
attrs: func() []log.KeyValue {
out := make([]log.KeyValue, attributesInlineCount*2)
for i := range out {
out[i] = log.Bool(strconv.Itoa(i), true)
}
return out
}(),
want: func() []log.KeyValue {
out := make([]log.KeyValue, attributesInlineCount*2)
for i := range out {
out[i] = log.Bool(strconv.Itoa(i), true)
}
return out
}(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
validate := func(t *testing.T, r *Record) {
t.Helper()
var i int
r.WalkAttributes(func(kv log.KeyValue) bool {
if assert.Lessf(t, i, len(tc.want), "additional: %v", kv) {
want := tc.want[i]
assert.Truef(t, kv.Equal(want), "%d: want %v, got %v", i, want, kv)
}
i++
return true
})
}
t.Run("SetAttributes", func(t *testing.T) {
r := new(Record)
r.attributeValueLengthLimit = -1
r.SetAttributes(tc.attrs...)
validate(t, r)
})
t.Run("AddAttributes/Empty", func(t *testing.T) {
r := new(Record)
r.attributeValueLengthLimit = -1
r.AddAttributes(tc.attrs...)
validate(t, r)
})
t.Run("AddAttributes/Duplicates", func(t *testing.T) {
r := new(Record)
r.attributeValueLengthLimit = -1
r.AddAttributes(tc.attrs...)
r.AddAttributes(tc.attrs...)
validate(t, r)
})
})
}
}
func TestApplyAttrLimitsDeduplication(t *testing.T) {
testcases := []struct {
name string
limit int
input, want log.Value
wantDroppedAttrs int
}{
{
// No de-duplication
name: "Slice",
input: log.SliceValue(
log.BoolValue(true),
log.BoolValue(true),
log.Float64Value(1.3),
log.Float64Value(1.3),
log.Int64Value(43),
log.Int64Value(43),
log.BytesValue([]byte("hello")),
log.BytesValue([]byte("hello")),
log.StringValue("foo"),
log.StringValue("foo"),
log.SliceValue(log.StringValue("baz")),
log.SliceValue(log.StringValue("baz")),
log.MapValue(log.String("a", "qux")),
log.MapValue(log.String("a", "qux")),
),
want: log.SliceValue(
log.BoolValue(true),
log.BoolValue(true),
log.Float64Value(1.3),
log.Float64Value(1.3),
log.Int64Value(43),
log.Int64Value(43),
log.BytesValue([]byte("hello")),
log.BytesValue([]byte("hello")),
log.StringValue("foo"),
log.StringValue("foo"),
log.SliceValue(log.StringValue("baz")),
log.SliceValue(log.StringValue("baz")),
log.MapValue(log.String("a", "qux")),
log.MapValue(log.String("a", "qux")),
),
},
{
name: "Map",
input: log.MapValue(
log.Bool("a", true),
log.Int64("b", 1),
log.Bool("a", false),
log.Float64("c", 2.),
log.String("b", "3"),
log.Slice("d", log.Int64Value(4)),
log.Map("a", log.Int("key", 5)),
log.Bytes("d", []byte("six")),
log.Bool("e", true),
log.Int("f", 1),
log.Int("f", 2),
log.Int("f", 3),
log.Float64("b", 0.0),
log.Float64("b", 0.0),
log.String("g", "G"),
log.String("h", "H"),
log.String("g", "GG"),
log.Bool("a", false),
),
want: log.MapValue(
// Order is important here.
log.Bool("a", false),
log.Float64("b", 0.0),
log.Float64("c", 2.),
log.Bytes("d", []byte("six")),
log.Bool("e", true),
log.Int("f", 3),
log.String("g", "GG"),
log.String("h", "H"),
),
wantDroppedAttrs: 10,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
const key = "key"
kv := log.KeyValue{Key: key, Value: tc.input}
r := Record{attributeValueLengthLimit: -1}
t.Run("AddAttributes", func(t *testing.T) {
r.AddAttributes(kv)
assertKV(t, r, log.KeyValue{Key: key, Value: tc.want})
assert.Equal(t, tc.wantDroppedAttrs, r.DroppedAttributes())
})
t.Run("SetAttributes", func(t *testing.T) {
r.SetAttributes(kv)
assertKV(t, r, log.KeyValue{Key: key, Value: tc.want})
assert.Equal(t, tc.wantDroppedAttrs, r.DroppedAttributes())
})
})
}
}
func TestApplyAttrLimitsTruncation(t *testing.T) {
testcases := []struct {
name string
limit int
input, want log.Value
}{
{
name: "Empty",
limit: 0,
input: log.Value{},
want: log.Value{},
},
{
name: "Bool",
limit: 0,
input: log.BoolValue(true),
want: log.BoolValue(true),
},
{
name: "Float64",
limit: 0,
input: log.Float64Value(1.3),
want: log.Float64Value(1.3),
},
{
name: "Int64",
limit: 0,
input: log.Int64Value(43),
want: log.Int64Value(43),
},
{
name: "Bytes",
limit: 0,
input: log.BytesValue([]byte("foo")),
want: log.BytesValue([]byte("foo")),
},
{
name: "String",
limit: 0,
input: log.StringValue("foo"),
want: log.StringValue(""),
},
{
name: "Slice",
limit: 0,
input: log.SliceValue(
log.BoolValue(true),
log.Float64Value(1.3),
log.Int64Value(43),
log.BytesValue([]byte("hello")),
log.StringValue("foo"),
log.StringValue("bar"),
log.SliceValue(log.StringValue("baz")),
log.MapValue(log.String("a", "qux")),
),
want: log.SliceValue(
log.BoolValue(true),
log.Float64Value(1.3),
log.Int64Value(43),
log.BytesValue([]byte("hello")),
log.StringValue(""),
log.StringValue(""),
log.SliceValue(log.StringValue("")),
log.MapValue(log.String("a", "")),
),
},
{
name: "Map",
limit: 0,
input: log.MapValue(
log.Bool("0", true),
log.Float64("1", 1.3),
log.Int64("2", 43),
log.Bytes("3", []byte("hello")),
log.String("4", "foo"),
log.String("5", "bar"),
log.Slice("6", log.StringValue("baz")),
log.Map("7", log.String("a", "qux")),
),
want: log.MapValue(
log.Bool("0", true),
log.Float64("1", 1.3),
log.Int64("2", 43),
log.Bytes("3", []byte("hello")),
log.String("4", ""),
log.String("5", ""),
log.Slice("6", log.StringValue("")),
log.Map("7", log.String("a", "")),
),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
const key = "key"
kv := log.KeyValue{Key: key, Value: tc.input}
r := Record{attributeValueLengthLimit: tc.limit}
t.Run("AddAttributes", func(t *testing.T) {
r.AddAttributes(kv)
assertKV(t, r, log.KeyValue{Key: key, Value: tc.want})
})
t.Run("SetAttributes", func(t *testing.T) {
r.SetAttributes(kv)
assertKV(t, r, log.KeyValue{Key: key, Value: tc.want})
})
})
}
}
func assertKV(t *testing.T, r Record, kv log.KeyValue) {
t.Helper()
var kvs []log.KeyValue
r.WalkAttributes(func(kv log.KeyValue) bool {
kvs = append(kvs, kv)
return true
})
require.Len(t, kvs, 1)
assert.Truef(t, kv.Equal(kvs[0]), "%s != %s", kv, kvs[0])
}
func TestTruncate(t *testing.T) {
type group struct {
limit int
input string
expected string
}
tests := []struct {
name string
groups []group
}{
// Edge case: limit is negative, no truncation should occur
{
name: "NoTruncation",
groups: []group{
{-1, "No truncation!", "No truncation!"},
},
},
// Edge case: string is already shorter than the limit, no truncation
// should occur
{
name: "ShortText",
groups: []group{
{10, "Short text", "Short text"},
{15, "Short text", "Short text"},
{100, "Short text", "Short text"},
},
},
// Edge case: truncation happens with ASCII characters only
{
name: "ASCIIOnly",
groups: []group{
{1, "Hello World!", "H"},
{5, "Hello World!", "Hello"},
{12, "Hello World!", "Hello World!"},
},
},
// Truncation including multi-byte characters (UTF-8)
{
name: "ValidUTF-8",
groups: []group{
{7, "Hello, 世界", "Hello, "},
{8, "Hello, 世界", "Hello, 世"},
{2, "こんにちは", "こん"},
{3, "こんにちは", "こんに"},
{5, "こんにちは", "こんにちは"},
{12, "こんにちは", "こんにちは"},
},
},
// Truncation with invalid UTF-8 characters
{
name: "InvalidUTF-8",
groups: []group{
{11, "Invalid\x80text", "Invalidtext"},
// Do not modify invalid text if equal to limit.
{11, "Valid text\x80", "Valid text\x80"},
// Do not modify invalid text if under limit.
{15, "Valid text\x80", "Valid text\x80"},
{5, "Hello\x80World", "Hello"},
{11, "Hello\x80World\x80!", "HelloWorld!"},
{15, "Hello\x80World\x80Test", "HelloWorldTest"},
{15, "Hello\x80\x80\x80World\x80Test", "HelloWorldTest"},
{15, "\x80\x80\x80Hello\x80\x80\x80World\x80Test\x80\x80", "HelloWorldTest"},
},
},
// Truncation with mixed validn and invalid UTF-8 characters
{
name: "MixedUTF-8",
groups: []group{
{6, "€"[0:2] + "hello€€", "hello€"},
{6, "€" + "€"[0:2] + "hello", "€hello"},
{11, "Valid text\x80📜", "Valid text📜"},
{11, "Valid text📜\x80", "Valid text📜"},
{14, "😊 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"},
{14, "😊\x80 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"},
{14, "😊\x80 Hello\x80World🌍\x80🚀", "😊 HelloWorld🌍🚀"},
{14, "😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"},
{14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"},
},
},
// Edge case: empty string, should return empty string
{
name: "Empty",
groups: []group{
{5, "", ""},
},
},
// Edge case: limit is 0, should return an empty string
{
name: "Zero",
groups: []group{
{0, "Some text", ""},
{0, "", ""},
},
},
}
for _, tt := range tests {
for _, g := range tt.groups {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := truncate(g.limit, g.input)
assert.Equalf(
t, g.expected, got,
"input: %q([]rune%v))\ngot: %q([]rune%v)\nwant %q([]rune%v)",
g.input, []rune(g.input),
got, []rune(got),
g.expected, []rune(g.expected),
)
})
}
}
}
func BenchmarkTruncate(b *testing.B) {
run := func(limit int, input string) func(b *testing.B) {
return func(b *testing.B) {
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
var out string
for pb.Next() {
out = truncate(limit, input)
}
_ = out
})
}
}
b.Run("Unlimited", run(-1, "hello 😊 world 🌍🚀"))
b.Run("Zero", run(0, "Some text"))
b.Run("Short", run(10, "Short Text"))
b.Run("ASCII", run(5, "Hello, World!"))
b.Run("ValidUTF-8", run(10, "hello 😊 world 🌍🚀"))
b.Run("InvalidUTF-8", run(6, "€"[0:2]+"hello€€"))
b.Run("MixedUTF-8", run(14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80"))
}
func BenchmarkWalkAttributes(b *testing.B) {
for _, tt := range []struct {
attrCount int
}{
{attrCount: 1},
{attrCount: 10},
{attrCount: 100},
{attrCount: 1000},
} {
b.Run(fmt.Sprintf("%d attributes", tt.attrCount), func(b *testing.B) {
record := &Record{}
for i := 0; i < tt.attrCount; i++ {
record.SetAttributes(
log.String(fmt.Sprintf("key-%d", tt.attrCount), "value"),
)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
record.WalkAttributes(func(log.KeyValue) bool {
return true
})
}
})
}
}
func BenchmarkSetAddAttributes(b *testing.B) {
kv := log.String("key", "value")
b.Run("SetAttributes", func(b *testing.B) {
records := make([]Record, b.N)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
records[i].SetAttributes(kv)
}
})
b.Run("AddAttributes", func(b *testing.B) {
records := make([]Record, b.N)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
records[i].AddAttributes(kv)
}
})
}