1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-30 04:40:41 +02:00

stdoutlog: Add exporter (#5172)

This commit is contained in:
Sam Xie 2024-04-11 00:18:42 -07:00 committed by GitHub
parent fb029273a7
commit 7092c1f71d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 491 additions and 4 deletions

View File

@ -9,9 +9,9 @@ import (
)
var (
defaultWriter = os.Stdout
defaultPrettyPrint = false
defaultTimestamps = true
defaultWriter io.Writer = os.Stdout
defaultPrettyPrint = false
defaultTimestamps = true
)
// config contains options for the STDOUT exporter.

View File

@ -0,0 +1,72 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
import (
"context"
"encoding/json"
"sync/atomic"
"go.opentelemetry.io/otel/sdk/log"
)
var _ log.Exporter = &Exporter{}
// Exporter writes JSON-encoded log records to an [io.Writer] ([os.Stdout] by default).
// Exporter must be created with [New].
type Exporter struct {
encoder atomic.Pointer[json.Encoder]
timestamps bool
}
// New creates an [Exporter].
func New(options ...Option) (*Exporter, error) {
cfg := newConfig(options)
enc := json.NewEncoder(cfg.Writer)
if cfg.PrettyPrint {
enc.SetIndent("", "\t")
}
e := Exporter{
timestamps: cfg.Timestamps,
}
e.encoder.Store(enc)
return &e, nil
}
// Export exports log records to writer.
func (e *Exporter) Export(ctx context.Context, records []log.Record) error {
enc := e.encoder.Load()
if enc == nil {
return nil
}
for _, record := range records {
// Honor context cancellation.
if err := ctx.Err(); err != nil {
return err
}
// Encode record, one by one.
recordJSON := e.newRecordJSON(record)
if err := enc.Encode(recordJSON); err != nil {
return err
}
}
return nil
}
// Shutdown shuts down the Exporter.
// Calls to Export will perform no operation after this is called.
func (e *Exporter) Shutdown(context.Context) error {
e.encoder.Store(nil)
return nil
}
// ForceFlush performs no action.
func (e *Exporter) ForceFlush(context.Context) error {
return nil
}

View File

@ -0,0 +1,321 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdoutout"
import (
"bytes"
"context"
"encoding/json"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/log"
sdklog "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/trace"
)
func TestExporter(t *testing.T) {
var buf bytes.Buffer
now := time.Now()
testCases := []struct {
name string
exporter *Exporter
want string
}{
{
name: "zero value",
exporter: &Exporter{},
want: "",
},
{
name: "new",
exporter: func() *Exporter {
defaultWriterSwap := defaultWriter
defer func() {
defaultWriter = defaultWriterSwap
}()
defaultWriter = &buf
exporter, err := New()
require.NoError(t, err)
require.NotNil(t, exporter)
return exporter
}(),
want: getJSON(now),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Write to buffer for testing
defaultWriterSwap := defaultWriter
defer func() {
defaultWriter = defaultWriterSwap
}()
defaultWriter = &buf
buf.Reset()
var err error
exporter := tc.exporter
record := getRecord(now)
// Export a record
err = exporter.Export(context.Background(), []sdklog.Record{record})
assert.NoError(t, err)
// Check the writer
assert.Equal(t, tc.want, buf.String())
// Flush the exporter
err = exporter.ForceFlush(context.Background())
assert.NoError(t, err)
// Shutdown the exporter
err = exporter.Shutdown(context.Background())
assert.NoError(t, err)
// Export a record after shutdown, this should not be written
err = exporter.Export(context.Background(), []sdklog.Record{record})
assert.NoError(t, err)
// Check the writer
assert.Equal(t, tc.want, buf.String())
})
}
}
func TestExporterExport(t *testing.T) {
now := time.Now()
record := getRecord(now)
records := []sdklog.Record{record, record}
testCases := []struct {
name string
options []Option
ctx context.Context
records []sdklog.Record
wantResult string
wantError error
}{
{
name: "default",
options: []Option{},
ctx: context.Background(),
records: records,
wantResult: getJSONs(now),
},
{
name: "NoRecords",
options: []Option{},
ctx: context.Background(),
records: nil,
wantResult: "",
},
{
name: "WithPrettyPrint",
options: []Option{WithPrettyPrint()},
ctx: context.Background(),
records: records,
wantResult: getPrettyJSONs(now),
},
{
name: "WithoutTimestamps",
options: []Option{WithoutTimestamps()},
ctx: context.Background(),
records: records,
wantResult: getJSONs(time.Time{}),
},
{
name: "WithoutTimestamps and WithPrettyPrint",
options: []Option{WithoutTimestamps(), WithPrettyPrint()},
ctx: context.Background(),
records: records,
wantResult: getPrettyJSONs(time.Time{}),
},
{
name: "WithCanceledContext",
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}(),
records: records,
wantResult: "",
wantError: context.Canceled,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Write to buffer for testing
var buf bytes.Buffer
exporter, err := New(append(tc.options, WithWriter(&buf))...)
assert.NoError(t, err)
err = exporter.Export(tc.ctx, tc.records)
assert.Equal(t, tc.wantError, err)
assert.Equal(t, tc.wantResult, buf.String())
})
}
}
func getJSON(now time.Time) string {
serializedNow, _ := json.Marshal(now)
return "{\"Timestamp\":" + string(serializedNow) + ",\"ObservedTimestamp\":" + string(serializedNow) + ",\"Severity\":9,\"SeverityText\":\"INFO\",\"Body\":{},\"Attributes\":[{\"Key\":\"key\",\"Value\":{}},{\"Key\":\"key2\",\"Value\":{}},{\"Key\":\"key3\",\"Value\":{}},{\"Key\":\"key4\",\"Value\":{}},{\"Key\":\"key5\",\"Value\":{}},{\"Key\":\"bool\",\"Value\":{}}],\"TraceID\":\"0102030405060708090a0b0c0d0e0f10\",\"SpanID\":\"0102030405060708\",\"TraceFlags\":\"01\",\"Resource\":{},\"Scope\":{\"Name\":\"\",\"Version\":\"\",\"SchemaURL\":\"\"},\"AttributeValueLengthLimit\":0,\"AttributeCountLimit\":0}\n"
}
func getJSONs(now time.Time) string {
return getJSON(now) + getJSON(now)
}
func getPrettyJSON(now time.Time) string {
serializedNow, _ := json.Marshal(now)
return `{
"Timestamp": ` + string(serializedNow) + `,
"ObservedTimestamp": ` + string(serializedNow) + `,
"Severity": 9,
"SeverityText": "INFO",
"Body": {},
"Attributes": [
{
"Key": "key",
"Value": {}
},
{
"Key": "key2",
"Value": {}
},
{
"Key": "key3",
"Value": {}
},
{
"Key": "key4",
"Value": {}
},
{
"Key": "key5",
"Value": {}
},
{
"Key": "bool",
"Value": {}
}
],
"TraceID": "0102030405060708090a0b0c0d0e0f10",
"SpanID": "0102030405060708",
"TraceFlags": "01",
"Resource": {},
"Scope": {
"Name": "",
"Version": "",
"SchemaURL": ""
},
"AttributeValueLengthLimit": 0,
"AttributeCountLimit": 0
}
`
}
func getPrettyJSONs(now time.Time) string {
return getPrettyJSON(now) + getPrettyJSON(now)
}
func TestExporterShutdown(t *testing.T) {
exporter, err := New()
assert.NoError(t, err)
assert.NoError(t, exporter.Shutdown(context.Background()))
}
func TestExporterForceFlush(t *testing.T) {
exporter, err := New()
assert.NoError(t, err)
assert.NoError(t, exporter.ForceFlush(context.Background()))
}
func getRecord(now time.Time) sdklog.Record {
traceID, _ := trace.TraceIDFromHex("0102030405060708090a0b0c0d0e0f10")
spanID, _ := trace.SpanIDFromHex("0102030405060708")
// Setup records
record := sdklog.Record{}
record.SetTimestamp(now)
record.SetObservedTimestamp(now)
record.SetSeverity(log.SeverityInfo1)
record.SetSeverityText("INFO")
record.SetBody(log.StringValue("test"))
record.SetAttributes([]log.KeyValue{
// More than 5 attributes to test back slice
log.String("key", "value"),
log.String("key2", "value"),
log.String("key3", "value"),
log.String("key4", "value"),
log.String("key5", "value"),
log.Bool("bool", true),
}...)
record.SetTraceID(traceID)
record.SetSpanID(spanID)
record.SetTraceFlags(trace.FlagsSampled)
return record
}
func TestExporterConcurrentSafe(t *testing.T) {
testCases := []struct {
name string
exporter *Exporter
}{
{
name: "zero value",
exporter: &Exporter{},
},
{
name: "new",
exporter: func() *Exporter {
exporter, err := New()
require.NoError(t, err)
require.NotNil(t, exporter)
return exporter
}(),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
exporter := tc.exporter
const goroutines = 10
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
err := exporter.Export(context.Background(), []sdklog.Record{{}})
assert.NoError(t, err)
err = exporter.ForceFlush(context.Background())
assert.NoError(t, err)
err = exporter.Shutdown(context.Background())
assert.NoError(t, err)
}()
}
wg.Wait()
})
}
}

View File

@ -2,10 +2,33 @@ module go.opentelemetry.io/otel/exporters/stdout/stdoutlog
go 1.21
require github.com/stretchr/testify v1.9.0
require (
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel/log v0.1.0-alpha
go.opentelemetry.io/otel/sdk v1.25.0
go.opentelemetry.io/otel/sdk/log v0.0.0
go.opentelemetry.io/otel/trace v1.25.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel v1.25.0 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
golang.org/x/sys v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace go.opentelemetry.io/otel/sdk/log => ../../../sdk/log
replace go.opentelemetry.io/otel/log => ../../../log
replace go.opentelemetry.io/otel => ../../..
replace go.opentelemetry.io/otel/trace => ../../../trace
replace go.opentelemetry.io/otel/sdk => ../../../sdk
replace go.opentelemetry.io/otel/metric => ../../../metric

View File

@ -1,9 +1,18 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -0,0 +1,62 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
import (
"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"
)
// recordJSON is a JSON-serializable representation of a Record.
type recordJSON 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
Scope instrumentation.Scope
AttributeValueLengthLimit int
AttributeCountLimit int
}
func (e *Exporter) newRecordJSON(r sdklog.Record) recordJSON {
newRecord := recordJSON{
Severity: r.Severity(),
SeverityText: r.SeverityText(),
Body: r.Body(),
TraceID: r.TraceID(),
SpanID: r.SpanID(),
TraceFlags: r.TraceFlags(),
Attributes: make([]log.KeyValue, 0, r.AttributesLen()),
Resource: r.Resource(),
Scope: r.InstrumentationScope(),
AttributeValueLengthLimit: r.AttributeValueLengthLimit(),
AttributeCountLimit: r.AttributeCountLimit(),
}
r.WalkAttributes(func(kv log.KeyValue) bool {
newRecord.Attributes = append(newRecord.Attributes, kv)
return true
})
if e.timestamps {
newRecord.Timestamp = r.Timestamp()
newRecord.ObservedTimestamp = r.ObservedTimestamp()
}
return newRecord
}