Skip to content

Commit

Permalink
sdk/log/logtest: Add RecordFactory (#5258)
Browse files Browse the repository at this point in the history
  • Loading branch information
pellared committed Apr 25, 2024
1 parent 9656d0a commit df455db
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [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

### Added
Expand Down
3 changes: 3 additions & 0 deletions sdk/log/logtest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Log Test SDK

[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/sdk/log/logtest)](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/log/logtest)
64 changes: 64 additions & 0 deletions sdk/log/logtest/example_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 102 additions & 0 deletions sdk/log/logtest/factory.go
Original file line number Diff line number Diff line change
@@ -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 changes: 124 additions & 0 deletions sdk/log/logtest/factory_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit df455db

Please sign in to comment.