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:
parent
fb029273a7
commit
7092c1f71d
@ -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.
|
||||
|
72
exporters/stdout/stdoutlog/exporter.go
Normal file
72
exporters/stdout/stdoutlog/exporter.go
Normal 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
|
||||
}
|
321
exporters/stdout/stdoutlog/exporter_test.go
Normal file
321
exporters/stdout/stdoutlog/exporter_test.go
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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=
|
||||
|
62
exporters/stdout/stdoutlog/record.go
Normal file
62
exporters/stdout/stdoutlog/record.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user