You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-07-13 01:00:22 +02:00
sdk/log/logtest: Add RecordFactory (#5258)
This commit is contained in:
@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `RecordFactory` in `go.opentelemetry.io/otel/sdk/log/logtest` to facilitate testing the exporter and processor implementations. (#5258)
|
||||||
|
|
||||||
## [1.26.0/0.48.0/0.2.0-alpha] 2024-04-24
|
## [1.26.0/0.48.0/0.2.0-alpha] 2024-04-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
3
sdk/log/logtest/README.md
Normal file
3
sdk/log/logtest/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Log Test SDK
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/log/logtest)
|
64
sdk/log/logtest/example_test.go
Normal file
64
sdk/log/logtest/example_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package logtest is a testing helper package.
|
||||||
|
package logtest_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
logapi "go.opentelemetry.io/otel/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log/logtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleRecordFactory() {
|
||||||
|
exp := exporter{os.Stdout}
|
||||||
|
rf := logtest.RecordFactory{
|
||||||
|
InstrumentationScope: instrumentation.Scope{Name: "myapp"},
|
||||||
|
}
|
||||||
|
|
||||||
|
rf.Body = logapi.StringValue("foo")
|
||||||
|
r1 := rf.NewRecord()
|
||||||
|
|
||||||
|
rf.Body = logapi.StringValue("bar")
|
||||||
|
r2 := rf.NewRecord()
|
||||||
|
|
||||||
|
_ = exp.Export(context.Background(), []log.Record{r1, r2})
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// scope=myapp msg=foo
|
||||||
|
// scope=myapp msg=bar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile time check exporter implements log.Exporter.
|
||||||
|
var _ log.Exporter = exporter{}
|
||||||
|
|
||||||
|
type exporter struct{ io.Writer }
|
||||||
|
|
||||||
|
func (e exporter) Export(ctx context.Context, records []log.Record) error {
|
||||||
|
for i, r := range records {
|
||||||
|
if i != 0 {
|
||||||
|
if _, err := e.Write([]byte("\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(e, "scope=%s msg=%s", r.InstrumentationScope().Name, r.Body().String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e exporter) Shutdown(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// appropriate error should be returned in these situations.
|
||||||
|
func (e exporter) ForceFlush(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
102
sdk/log/logtest/factory.go
Normal file
102
sdk/log/logtest/factory.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package logtest is a testing helper package.
|
||||||
|
package logtest // import "go.opentelemetry.io/otel/sdk/log/logtest"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecordFactory is used to facilitate unit testing implementations of
|
||||||
|
// [go.opentelemetry.io/otel/sdk/log.Exporter]
|
||||||
|
// and [go.opentelemetry.io/otel/sdk/log.Processor].
|
||||||
|
//
|
||||||
|
// Do not use RecordFactory to create records in production code.
|
||||||
|
type RecordFactory struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
ObservedTimestamp time.Time
|
||||||
|
Severity log.Severity
|
||||||
|
SeverityText string
|
||||||
|
Body log.Value
|
||||||
|
Attributes []log.KeyValue
|
||||||
|
TraceID trace.TraceID
|
||||||
|
SpanID trace.SpanID
|
||||||
|
TraceFlags trace.TraceFlags
|
||||||
|
|
||||||
|
Resource *resource.Resource
|
||||||
|
InstrumentationScope instrumentation.Scope
|
||||||
|
|
||||||
|
DroppedAttributes int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRecord returns a log record.
|
||||||
|
func (b RecordFactory) NewRecord() sdklog.Record {
|
||||||
|
var record sdklog.Record
|
||||||
|
p := processor(func(r sdklog.Record) {
|
||||||
|
r.SetTimestamp(b.Timestamp)
|
||||||
|
r.SetObservedTimestamp(b.ObservedTimestamp)
|
||||||
|
r.SetSeverity(b.Severity)
|
||||||
|
r.SetSeverityText(b.SeverityText)
|
||||||
|
r.SetBody(b.Body)
|
||||||
|
r.SetAttributes(slices.Clone(b.Attributes)...)
|
||||||
|
|
||||||
|
// Generate dropped attributes.
|
||||||
|
for i := 0; i < b.DroppedAttributes; i++ {
|
||||||
|
r.AddAttributes(log.KeyValue{})
|
||||||
|
}
|
||||||
|
|
||||||
|
r.SetTraceID(b.TraceID)
|
||||||
|
r.SetSpanID(b.SpanID)
|
||||||
|
r.SetTraceFlags(b.TraceFlags)
|
||||||
|
|
||||||
|
record = r
|
||||||
|
})
|
||||||
|
|
||||||
|
attributeCountLimit := -1
|
||||||
|
if b.DroppedAttributes > 0 {
|
||||||
|
// Make sure that we can generate dropped attributes.
|
||||||
|
attributeCountLimit = len(b.Attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := sdklog.NewLoggerProvider(
|
||||||
|
sdklog.WithResource(b.Resource),
|
||||||
|
sdklog.WithAttributeCountLimit(attributeCountLimit),
|
||||||
|
sdklog.WithAttributeValueLengthLimit(-1),
|
||||||
|
sdklog.WithProcessor(p),
|
||||||
|
)
|
||||||
|
|
||||||
|
l := provider.Logger(b.InstrumentationScope.Name,
|
||||||
|
log.WithInstrumentationVersion(b.InstrumentationScope.Version),
|
||||||
|
log.WithSchemaURL(b.InstrumentationScope.SchemaURL),
|
||||||
|
)
|
||||||
|
l.Emit(context.Background(), log.Record{}) // This executes the processor function.
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
type processor func(r sdklog.Record)
|
||||||
|
|
||||||
|
func (p processor) OnEmit(ctx context.Context, r sdklog.Record) error {
|
||||||
|
p(r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (processor) Enabled(context.Context, sdklog.Record) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (processor) Shutdown(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (processor) ForceFlush(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
124
sdk/log/logtest/factory_test.go
Normal file
124
sdk/log/logtest/factory_test.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package logtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecordFactory(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
observed := now.Add(time.Second)
|
||||||
|
severity := log.SeverityDebug
|
||||||
|
severityText := "DBG"
|
||||||
|
body := log.StringValue("Message")
|
||||||
|
attrs := []log.KeyValue{
|
||||||
|
log.Int("int", 1),
|
||||||
|
log.String("str", "foo"),
|
||||||
|
log.Float64("flt", 3.14),
|
||||||
|
}
|
||||||
|
traceID := trace.TraceID([16]byte{1})
|
||||||
|
spanID := trace.SpanID([8]byte{2})
|
||||||
|
traceFlags := trace.FlagsSampled
|
||||||
|
dropped := 3
|
||||||
|
scope := instrumentation.Scope{
|
||||||
|
Name: t.Name(),
|
||||||
|
}
|
||||||
|
r := resource.NewSchemaless(attribute.Bool("works", true))
|
||||||
|
|
||||||
|
got := RecordFactory{
|
||||||
|
Timestamp: now,
|
||||||
|
ObservedTimestamp: observed,
|
||||||
|
Severity: severity,
|
||||||
|
SeverityText: severityText,
|
||||||
|
Body: body,
|
||||||
|
Attributes: attrs,
|
||||||
|
TraceID: traceID,
|
||||||
|
SpanID: spanID,
|
||||||
|
TraceFlags: traceFlags,
|
||||||
|
DroppedAttributes: dropped,
|
||||||
|
InstrumentationScope: scope,
|
||||||
|
Resource: r,
|
||||||
|
}.NewRecord()
|
||||||
|
|
||||||
|
assert.Equal(t, now, got.Timestamp())
|
||||||
|
assert.Equal(t, observed, got.ObservedTimestamp())
|
||||||
|
assert.Equal(t, severity, got.Severity())
|
||||||
|
assert.Equal(t, severityText, got.SeverityText())
|
||||||
|
assertBody(t, body, got)
|
||||||
|
assertAttributes(t, attrs, got)
|
||||||
|
assert.Equal(t, dropped, got.DroppedAttributes())
|
||||||
|
assert.Equal(t, traceID, got.TraceID())
|
||||||
|
assert.Equal(t, spanID, got.SpanID())
|
||||||
|
assert.Equal(t, traceFlags, got.TraceFlags())
|
||||||
|
assert.Equal(t, scope, got.InstrumentationScope())
|
||||||
|
assert.Equal(t, *r, got.Resource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordFactoryMultiple(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
attrs := []log.KeyValue{
|
||||||
|
log.Int("int", 1),
|
||||||
|
log.String("str", "foo"),
|
||||||
|
log.Float64("flt", 3.14),
|
||||||
|
}
|
||||||
|
scope := instrumentation.Scope{
|
||||||
|
Name: t.Name(),
|
||||||
|
}
|
||||||
|
|
||||||
|
f := RecordFactory{
|
||||||
|
Timestamp: now,
|
||||||
|
Attributes: attrs,
|
||||||
|
DroppedAttributes: 1,
|
||||||
|
InstrumentationScope: scope,
|
||||||
|
}
|
||||||
|
|
||||||
|
record1 := f.NewRecord()
|
||||||
|
|
||||||
|
f.Attributes = append(f.Attributes, log.Bool("added", true))
|
||||||
|
f.DroppedAttributes = 2
|
||||||
|
record2 := f.NewRecord()
|
||||||
|
|
||||||
|
assert.Equal(t, now, record2.Timestamp())
|
||||||
|
assertAttributes(t, append(attrs, log.Bool("added", true)), record2)
|
||||||
|
assert.Equal(t, 2, record2.DroppedAttributes())
|
||||||
|
assert.Equal(t, scope, record2.InstrumentationScope())
|
||||||
|
|
||||||
|
// Previously returned record is unharmed by the builder changes.
|
||||||
|
assert.Equal(t, now, record1.Timestamp())
|
||||||
|
assertAttributes(t, attrs, record1)
|
||||||
|
assert.Equal(t, 1, record1.DroppedAttributes())
|
||||||
|
assert.Equal(t, scope, record1.InstrumentationScope())
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertBody(t *testing.T, want log.Value, r sdklog.Record) {
|
||||||
|
t.Helper()
|
||||||
|
got := r.Body()
|
||||||
|
if !got.Equal(want) {
|
||||||
|
t.Errorf("Body value is not equal:\nwant: %v\ngot: %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAttributes(t *testing.T, want []log.KeyValue, r sdklog.Record) {
|
||||||
|
t.Helper()
|
||||||
|
var got []log.KeyValue
|
||||||
|
r.WalkAttributes(func(kv log.KeyValue) bool {
|
||||||
|
got = append(got, kv)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !slices.EqualFunc(want, got, log.KeyValue.Equal) {
|
||||||
|
t.Errorf("Attributes are not equal:\nwant: %v\ngot: %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user