opentelemetry-collector/internal/telemetry/componentattribute/logger_test.go

228 lines
7.2 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package componentattribute
import (
"context"
"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/log/logtest"
"go.opentelemetry.io/otel/log/noop"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
"go.opentelemetry.io/collector/pipeline"
)
type logRecorder struct {
zapLogs *observer.ObservedLogs
otelLogs *logtest.Recorder
}
type test struct {
name string
createLogger func() (*zap.Logger, logRecorder)
check func(*testing.T, logRecorder)
}
func createZapCore() (zapcore.Core, *observer.ObservedLogs) {
core, observed := observer.New(zap.DebugLevel)
core = core.With([]zapcore.Field{zap.String("preexisting", "value")})
core = NewConsoleCoreWithAttributes(core, attribute.NewSet())
return core, observed
}
func checkZapLogs(t *testing.T, observed *observer.ObservedLogs) {
observedLogs := observed.All()
require.Len(t, observedLogs, 3)
parentContext := map[string]string{
"preexisting": "value",
SignalKey: pipeline.SignalLogs.String(),
ComponentIDKey: "filelog",
}
childContext := map[string]string{
"preexisting": "value",
ComponentIDKey: "filelog",
}
require.Equal(t, "test parent before child", observedLogs[0].Message)
require.Len(t, observedLogs[0].Context, len(parentContext))
for _, field := range observedLogs[0].Context {
require.Equal(t, parentContext[field.Key], field.String)
}
require.Equal(t, "test child", observedLogs[1].Message)
require.Len(t, observedLogs[1].Context, len(childContext))
for _, field := range observedLogs[1].Context {
require.Equal(t, childContext[field.Key], field.String)
}
require.Equal(t, "test parent after child", observedLogs[2].Message)
require.Len(t, observedLogs[2].Context, len(parentContext))
for _, field := range observedLogs[2].Context {
require.Equal(t, parentContext[field.Key], field.String)
}
}
func TestCore(t *testing.T) {
attrs := attribute.NewSet(
attribute.String(SignalKey, pipeline.SignalLogs.String()),
attribute.String(ComponentIDKey, "filelog"),
)
tests := []test{
{
name: "console",
createLogger: func() (*zap.Logger, logRecorder) {
core, observed := createZapCore()
return zap.New(core), logRecorder{zapLogs: observed}
},
check: func(t *testing.T, rec logRecorder) {
checkZapLogs(t, rec.zapLogs)
},
},
{
name: "console + otel",
createLogger: func() (*zap.Logger, logRecorder) {
core, observed := createZapCore()
recorder := logtest.NewRecorder()
core = NewOTelTeeCoreWithAttributes(core, recorder, "testinstr", attribute.NewSet())
return zap.New(core), logRecorder{zapLogs: observed, otelLogs: recorder}
},
check: func(t *testing.T, rec logRecorder) {
checkZapLogs(t, rec.zapLogs)
recorder := rec.otelLogs
logAttributes := make(map[string]attribute.Set)
for scope, records := range recorder.Result() {
require.Equal(t, "testinstr", scope.Name)
for _, record := range records {
logAttributes[record.Body.String()] = scope.Attributes
}
}
childAttrs := attribute.NewSet(
attribute.String(ComponentIDKey, "filelog"),
)
assert.Equal(t, map[string]attribute.Set{
"test parent before child": attrs,
"test child": childAttrs,
"test parent after child": attrs,
}, logAttributes)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
logger, state := test.createLogger()
parent := ZapLoggerWithAttributes(logger, attrs)
parent.Info("test parent before child")
child := ZapLoggerWithAttributes(parent, RemoveAttributes(attrs, SignalKey))
child.Info("test child")
parent.Info("test parent after child")
test.check(t, state)
})
}
}
func TestNewOTelTeeCoreWithAttributes(t *testing.T) {
t.Run("copy_accepted_logs", func(t *testing.T) {
// Only log at Info level. Debug level logs should not be copied to the LoggerProvider.
observerCore, _ := observer.New(zap.InfoLevel)
recorder := logtest.NewRecorder()
core := NewOTelTeeCoreWithAttributes(observerCore, recorder, "scope", attribute.NewSet())
timestamp := time.Now()
logger := zap.New(core, zap.WithClock(constantClock(timestamp)))
loggerWith := logger.With(zap.String("logger_key", "logger_value"))
loggerWith.Info("message", zap.String("record_key", "record_value"))
loggerWith.Debug("dropped") // should not be recorded due to observer's level
logtest.AssertEqual(t, logtest.Recording{
logtest.Scope{Name: "scope"}: []logtest.Record{{
Context: context.Background(),
Timestamp: timestamp,
Severity: log.SeverityInfo,
SeverityText: "info",
Body: log.StringValue("message"),
Attributes: []log.KeyValue{
log.String("logger_key", "logger_value"),
log.String("record_key", "record_value"),
},
}},
}, recorder.Result())
require.NoError(t, logger.Sync()) // no-op for otelzap
})
t.Run("nop_core", func(t *testing.T) {
// Using zapcore.NewNopCore should result in no logs being sent to the LoggerProvider.
recorder := logtest.NewRecorder()
core := NewOTelTeeCoreWithAttributes(zapcore.NewNopCore(), recorder, "scope", attribute.NewSet())
logger := zap.New(core)
logger.Error("message")
logtest.AssertEqual(t, logtest.Recording{logtest.Scope{Name: "scope"}: nil}, recorder.Result())
})
t.Run("noop_loggerprovider", func(t *testing.T) {
// Using a noop LoggerProvider should not impact the main zap core.
observerCore, observedLogs := observer.New(zap.InfoLevel)
noopProvider := noop.NewLoggerProvider()
core := NewOTelTeeCoreWithAttributes(observerCore, noopProvider, "scope", attribute.NewSet())
logger := zap.New(core)
logger.Info("message", zap.String("key", "value"))
logger.Debug("dropped") // should not be recorded due to observer's level
assert.Equal(t, 1, observedLogs.Len())
})
t.Run("direct_write", func(t *testing.T) {
observerCore, observedLogs := observer.New(zap.InfoLevel)
recorder := logtest.NewRecorder()
core := NewOTelTeeCoreWithAttributes(observerCore, recorder, "scope", attribute.NewSet())
// Per https://pkg.go.dev/go.uber.org/zap/zapcore#Core:
//
// If called, Write should always log the Entry and Fields;
// it should not replicate the logic of Check.
//
// Even though the observer has been configured with Info level,
// Debug level logs should therefore be written.
require.NoError(t, core.Write(zapcore.Entry{
Level: zapcore.DebugLevel,
Message: "m",
}, []zapcore.Field{{
Key: "k",
Type: zapcore.StringType,
String: "s",
}}))
logtest.AssertEqual(t, logtest.Recording{
logtest.Scope{Name: "scope"}: []logtest.Record{{
Context: context.Background(),
Severity: log.SeverityDebug,
SeverityText: "debug",
Body: log.StringValue("m"),
Attributes: []log.KeyValue{log.String("k", "s")},
}},
}, recorder.Result())
assert.Equal(t, 1, observedLogs.Len())
})
}
type constantClock time.Time
func (c constantClock) Now() time.Time { return time.Time(c) }
func (c constantClock) NewTicker(time.Duration) *time.Ticker { return &time.Ticker{} }