From acc89abd9d2ff4f12ceaabad1c01821f03516aee Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Wed, 18 Jan 2023 21:46:22 -0500 Subject: [PATCH 1/6] Add Log API module --- log/config.go | 145 ++++++++++++++++++++++++++++++++++++++++++++ log/config_test.go | 87 ++++++++++++++++++++++++++ log/doc.go | 66 ++++++++++++++++++++ log/go.mod | 79 ++++++++++++++++++++++++ log/go.sum | 19 ++++++ log/log.go | 108 +++++++++++++++++++++++++++++++++ log/log_test.go | 15 +++++ log/nonrecording.go | 22 +++++++ log/noop.go | 61 +++++++++++++++++++ log/noop_test.go | 34 +++++++++++ 10 files changed, 636 insertions(+) create mode 100644 log/config.go create mode 100644 log/config_test.go create mode 100644 log/doc.go create mode 100644 log/go.mod create mode 100644 log/go.sum create mode 100644 log/log.go create mode 100644 log/log_test.go create mode 100644 log/nonrecording.go create mode 100644 log/noop.go create mode 100644 log/noop_test.go diff --git a/log/config.go b/log/config.go new file mode 100644 index 00000000000..388b7017bbb --- /dev/null +++ b/log/config.go @@ -0,0 +1,145 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "time" + + "go.opentelemetry.io/otel/attribute" +) + +// LoggerConfig is a group of options for a Logger. +type LoggerConfig struct { + instrumentationVersion string + // Schema URL of the telemetry emitted by the Logger. + schemaURL string +} + +// InstrumentationVersion returns the version of the library providing instrumentation. +func (t *LoggerConfig) InstrumentationVersion() string { + return t.instrumentationVersion +} + +// SchemaURL returns the Schema URL of the telemetry emitted by the Logger. +func (t *LoggerConfig) SchemaURL() string { + return t.schemaURL +} + +// NewLoggerConfig applies all the options to a returned LoggerConfig. +func NewLoggerConfig(options ...LoggerOption) LoggerConfig { + var config LoggerConfig + for _, option := range options { + config = option.apply(config) + } + return config +} + +// LoggerOption applies an option to a LoggerConfig. +type LoggerOption interface { + apply(LoggerConfig) LoggerConfig +} + +type loggerOptionFunc func(LoggerConfig) LoggerConfig + +func (fn loggerOptionFunc) apply(cfg LoggerConfig) LoggerConfig { + return fn(cfg) +} + +// LogRecordConfig is a group of options for a LogRecord. +type LogRecordConfig struct { + attributes []attribute.KeyValue + timestamp time.Time +} + +// Attributes describe the associated qualities of a LogRecord. +func (cfg *LogRecordConfig) Attributes() []attribute.KeyValue { + return cfg.attributes +} + +// Timestamp is a time in a LogRecord life-cycle. +func (cfg *LogRecordConfig) Timestamp() time.Time { + return cfg.timestamp +} + +// NewLogRecordConfig applies all the options to a returned LogRecordConfig. +// No validation is performed on the returned LogRecordConfig (e.g. no uniqueness +// checking or bounding of data), it is left to the SDK to perform this +// action. +func NewLogRecordConfig(options ...LogRecordOption) LogRecordConfig { + var c LogRecordConfig + for _, option := range options { + c = option.applyLogRecord(c) + } + return c +} + +// LogRecordOption applies an option to a LogRecordConfig. These options are applicable +// only when the span is created. +type LogRecordOption interface { + applyLogRecord(LogRecordConfig) LogRecordConfig +} + +type logRecordOptionFunc func(LogRecordConfig) LogRecordConfig + +func (fn logRecordOptionFunc) applyLogRecord(cfg LogRecordConfig) LogRecordConfig { + return fn(cfg) +} + +type attributeOption []attribute.KeyValue + +func (o attributeOption) applyLogRecord(c LogRecordConfig) LogRecordConfig { + c.attributes = append(c.attributes, []attribute.KeyValue(o)...) + return c +} + +// WithAttributes adds the attributes related to a span life-cycle event. +// These attributes are used to describe the work a LogRecord represents when this +// option is provided to a LogRecord's start or end events. Otherwise, these +// attributes provide additional information about the event being recorded +// (e.g. error, state change, processing progress, system event). +// +// If multiple of these options are passed the attributes of each successive +// option will extend the attributes instead of overwriting. There is no +// guarantee of uniqueness in the resulting attributes. +func WithAttributes(attributes ...attribute.KeyValue) LogRecordOption { + return attributeOption(attributes) +} + +type timestampOption time.Time + +func (o timestampOption) applyLogRecord(c LogRecordConfig) LogRecordConfig { + c.timestamp = time.Time(o) + return c +} + +// WithInstrumentationVersion sets the instrumentation version. +func WithInstrumentationVersion(version string) LoggerOption { + return loggerOptionFunc( + func(cfg LoggerConfig) LoggerConfig { + cfg.instrumentationVersion = version + return cfg + }, + ) +} + +// WithSchemaURL sets the schema URL for the Logger. +func WithSchemaURL(schemaURL string) LoggerOption { + return loggerOptionFunc( + func(cfg LoggerConfig) LoggerConfig { + cfg.schemaURL = schemaURL + return cfg + }, + ) +} diff --git a/log/config_test.go b/log/config_test.go new file mode 100644 index 00000000000..9837495f8f7 --- /dev/null +++ b/log/config_test.go @@ -0,0 +1,87 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoggerConfig(t *testing.T) { + v1 := "semver:0.0.1" + v2 := "semver:1.0.0" + schemaURL := "https://opentelemetry.io/schemas/1.2.0" + tests := []struct { + options []LoggerOption + expected LoggerConfig + }{ + { + // No non-zero-values should be set. + []LoggerOption{}, + LoggerConfig{}, + }, + { + []LoggerOption{ + WithInstrumentationVersion(v1), + }, + LoggerConfig{ + instrumentationVersion: v1, + }, + }, + { + []LoggerOption{ + // Multiple calls should overwrite. + WithInstrumentationVersion(v1), + WithInstrumentationVersion(v2), + }, + LoggerConfig{ + instrumentationVersion: v2, + }, + }, + { + []LoggerOption{ + WithSchemaURL(schemaURL), + }, + LoggerConfig{ + schemaURL: schemaURL, + }, + }, + } + for _, test := range tests { + config := NewLoggerConfig(test.options...) + assert.Equal(t, test.expected, config) + } +} + +// Save benchmark results to a file level var to avoid the compiler optimizing +// away the actual work. +var ( + loggerConfig LoggerConfig +) + +func BenchmarkNewTracerConfig(b *testing.B) { + opts := []LoggerOption{ + WithInstrumentationVersion("testing verion"), + WithSchemaURL("testing URL"), + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + loggerConfig = NewLoggerConfig(opts...) + } +} diff --git a/log/doc.go b/log/doc.go new file mode 100644 index 00000000000..39a443a093d --- /dev/null +++ b/log/doc.go @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package trace provides an implementation of the tracing part of the +OpenTelemetry API. + +To participate in distributed traces a LogRecord needs to be created for the +operation being performed as part of a traced workflow. In its simplest form: + + var tracer trace.Logger + + func init() { + tracer = otel.Tracer("instrumentation/package/name") + } + + func operation(ctx context.Context) { + var span trace.LogRecord + ctx, span = tracer.Start(ctx, "operation") + defer span.End() + // ... + } + +A Logger is unique to the instrumentation and is used to create Spans. +Instrumentation should be designed to accept a LoggerProvider from which it +can create its own unique Logger. Alternatively, the registered global +LoggerProvider from the go.opentelemetry.io/otel package can be used as +a default. + + const ( + name = "instrumentation/package/name" + version = "0.1.0" + ) + + type Instrumentation struct { + tracer trace.Logger + } + + func NewInstrumentation(tp trace.LoggerProvider) *Instrumentation { + if tp == nil { + tp = otel.LoggerProvider() + } + return &Instrumentation{ + tracer: tp.Logger(name, trace.WithInstrumentationVersion(version)), + } + } + + func operation(ctx context.Context, inst *Instrumentation) { + var span trace.LogRecord + ctx, span = inst.tracer.Start(ctx, "operation") + defer span.End() + // ... + } +*/ +package log // import "go.opentelemetry.io/otel/log" diff --git a/log/go.mod b/log/go.mod new file mode 100644 index 00000000000..f84e8bbea9b --- /dev/null +++ b/log/go.mod @@ -0,0 +1,79 @@ +module go.opentelemetry.io/otel/log + +go 1.18 + +require ( + github.com/google/go-cmp v0.5.9 + github.com/stretchr/testify v1.8.1 + go.opentelemetry.io/otel v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/otel => ../ + +replace go.opentelemetry.io/otel/bridge/opencensus => ../bridge/opencensus + +replace go.opentelemetry.io/otel/bridge/opencensus/test => ../bridge/opencensus/test + +replace go.opentelemetry.io/otel/bridge/opentracing => ../bridge/opentracing + +replace go.opentelemetry.io/otel/example/fib => ../example/fib + +replace go.opentelemetry.io/otel/example/jaeger => ../example/jaeger + +replace go.opentelemetry.io/otel/example/namedtracer => ../example/namedtracer + +replace go.opentelemetry.io/otel/example/opencensus => ../example/opencensus + +replace go.opentelemetry.io/otel/example/otel-collector => ../example/otel-collector + +replace go.opentelemetry.io/otel/example/passthrough => ../example/passthrough + +replace go.opentelemetry.io/otel/example/prometheus => ../example/prometheus + +replace go.opentelemetry.io/otel/example/view => ../example/view + +replace go.opentelemetry.io/otel/example/zipkin => ../example/zipkin + +replace go.opentelemetry.io/otel/exporters/jaeger => ../exporters/jaeger + +replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ../exporters/otlp/internal/retry + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric => ../exporters/otlp/otlpmetric + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../exporters/otlp/otlpmetric/otlpmetricgrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../exporters/otlp/otlpmetric/otlpmetrichttp + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../exporters/otlp/otlptrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../exporters/otlp/otlptrace/otlptracehttp + +replace go.opentelemetry.io/otel/exporters/prometheus => ../exporters/prometheus + +replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../exporters/stdout/stdoutmetric + +replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/zipkin => ../exporters/zipkin + +replace go.opentelemetry.io/otel/internal/tools => ../internal/tools + +replace go.opentelemetry.io/otel/metric => ../metric + +replace go.opentelemetry.io/otel/schema => ../schema + +replace go.opentelemetry.io/otel/sdk => ../sdk + +replace go.opentelemetry.io/otel/sdk/metric => ../sdk/metric + +replace go.opentelemetry.io/otel/trace => ../trace + +replace go.opentelemetry.io/otel/log => ./ diff --git a/log/go.sum b/log/go.sum new file mode 100644 index 00000000000..64456627351 --- /dev/null +++ b/log/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000000..c9d0526d90c --- /dev/null +++ b/log/log.go @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" +) + +// LogRecord is the individual component of a trace. It represents a single named +// and timed operation of a workflow that is traced. A Logger is used to +// create a LogRecord and it is then up to the operation the LogRecord represents to +// properly end the LogRecord when the operation itself ends. +// +// Warning: methods may be added to this interface in minor releases. +type LogRecord interface { + // IsRecording returns the recording state of the LogRecord. It will return + // true if the LogRecord is active and events can be recorded. + IsRecording() bool + + // SetAttributes sets kv as attributes of the LogRecord. If a key from kv + // already exists for an attribute of the LogRecord it will be overwritten with + // the value contained in kv. + SetAttributes(kv ...attribute.KeyValue) + + // LoggerProvider returns a LoggerProvider that can be used to generate + // additional Spans on the same telemetry pipeline as the current LogRecord. + LoggerProvider() LoggerProvider +} + +// Logger is the creator of Spans. +// +// Warning: methods may be added to this interface in minor releases. +type Logger interface { + // Emit creates a span and a context.Context containing the newly-created span. + // + // If the context.Context provided in `ctx` contains a LogRecord then the newly-created + // LogRecord will be a child of that span, otherwise it will be a root span. This behavior + // can be overridden by providing `WithNewRoot()` as a SpanOption, causing the + // newly-created LogRecord to be a root span even if `ctx` contains a LogRecord. + // + // When creating a LogRecord it is recommended to provide all known span attributes using + // the `WithAttributes()` SpanOption as samplers will only have access to the + // attributes provided when a LogRecord is created. + // + // Any LogRecord that is created MUST also be ended. This is the responsibility of the user. + // Implementations of this API may leak memory or other resources if Spans are not ended. + Emit(ctx context.Context, opts ...LogRecordOption) +} + +// LoggerProvider provides Tracers that are used by instrumentation code to +// trace computational workflows. +// +// A LoggerProvider is the collection destination of all Spans from Tracers it +// provides, it represents a unique telemetry collection pipeline. How that +// pipeline is defined, meaning how those Spans are collected, processed, and +// where they are exported, depends on its implementation. Instrumentation +// authors do not need to define this implementation, rather just use the +// provided Tracers to instrument code. +// +// Commonly, instrumentation code will accept a LoggerProvider implementation +// at runtime from its users or it can simply use the globally registered one +// (see https://pkg.go.dev/go.opentelemetry.io/otel#GetTracerProvider). +// +// Warning: methods may be added to this interface in minor releases. +type LoggerProvider interface { + // Logger returns a unique Logger scoped to be used by instrumentation code + // to trace computational workflows. The scope and identity of that + // instrumentation code is uniquely defined by the name and options passed. + // + // The passed name needs to uniquely identify instrumentation code. + // Therefore, it is recommended that name is the Go package name of the + // library providing instrumentation (note: not the code being + // instrumented). Instrumentation libraries can have multiple versions, + // therefore, the WithInstrumentationVersion option should be used to + // distinguish these different codebases. Additionally, instrumentation + // libraries may sometimes use traces to communicate different domains of + // workflow data (i.e. using spans to communicate workflow events only). If + // this is the case, the WithScopeAttributes option should be used to + // uniquely identify Tracers that handle the different domains of workflow + // data. + // + // If the same name and options are passed multiple times, the same Logger + // will be returned (it is up to the implementation if this will be the + // same underlying instance of that Logger or not). It is not necessary to + // call this multiple times with the same name and options to get an + // up-to-date Logger. All implementations will ensure any LoggerProvider + // configuration changes are propagated to all provided Tracers. + // + // If name is empty, then an implementation defined default name will be + // used instead. + // + // This method is safe to call concurrently. + Logger(name string, options ...LoggerOption) Logger +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 00000000000..80ea93d24bf --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log diff --git a/log/nonrecording.go b/log/nonrecording.go new file mode 100644 index 00000000000..d064d5347c3 --- /dev/null +++ b/log/nonrecording.go @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/log" + +// nonRecordingLogRecord is a minimal implementation of a LogRecord that wraps a +// SpanContext. It performs no operations other than to return the wrapped +// SpanContext. +type nonRecordingLogRecord struct { + noopLogRecord +} diff --git a/log/noop.go b/log/noop.go new file mode 100644 index 00000000000..6104fbecd58 --- /dev/null +++ b/log/noop.go @@ -0,0 +1,61 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" +) + +// NewNoopLoggerProvider returns an implementation of LoggerProvider that +// performs no operations. The Logger and Spans created from the returned +// LoggerProvider also perform no operations. +func NewNoopLoggerProvider() LoggerProvider { + return noopLoggerProvider{} +} + +type noopLoggerProvider struct{} + +var _ LoggerProvider = noopLoggerProvider{} + +// Logger returns noop implementation of Logger. +func (p noopLoggerProvider) Logger(string, ...LoggerOption) Logger { + return noopLogger{} +} + +// noopLogger is an implementation of Logger that preforms no operations. +type noopLogger struct{} + +var _ Logger = noopLogger{} + +// Emit carries forward a non-recording LogRecord, if one is present in the context, otherwise it +// creates a no-op LogRecord. +func (t noopLogger) Emit(ctx context.Context, _ ...LogRecordOption) { +} + +// noopLogRecord is an implementation of LogRecord that preforms no operations. +type noopLogRecord struct{} + +var _ LogRecord = noopLogRecord{} + +// IsRecording always returns false. +func (noopLogRecord) IsRecording() bool { return false } + +// SetAttributes does nothing. +func (noopLogRecord) SetAttributes(...attribute.KeyValue) {} + +// LoggerProvider returns a no-op LoggerProvider. +func (noopLogRecord) LoggerProvider() LoggerProvider { return noopLoggerProvider{} } diff --git a/log/noop_test.go b/log/noop_test.go new file mode 100644 index 00000000000..1a5d1b8436f --- /dev/null +++ b/log/noop_test.go @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "testing" +) + +func TestNewNoopLoggerProvider(t *testing.T) { + got, want := NewNoopLoggerProvider(), noopLoggerProvider{} + if got != want { + t.Errorf("NewNoopLoggerProvider() returned %#v, want %#v", got, want) + } +} + +func TestNoopLoggerProviderLogger(t *testing.T) { + tp := NewNoopLoggerProvider() + got, want := tp.Logger(""), noopLogger{} + if got != want { + t.Errorf("noopLoggerProvider.Logger() returned %#v, want %#v", got, want) + } +} From d8f6da41601979d4e0336946c6f46f322255f159 Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Thu, 19 Jan 2023 12:49:33 -0500 Subject: [PATCH 2/6] Add Log SDK --- go.mod | 62 +++ log/doc.go | 22 +- log/go.mod | 1 - log/go.sum | 1 - log/log.go | 4 - sdk/go.mod | 61 +++ sdk/log/batch_logrecord_processor.go | 433 +++++++++++++++++++++ sdk/log/doc.go | 21 + sdk/log/log.go | 358 +++++++++++++++++ sdk/log/log_test.go | 171 ++++++++ sdk/log/logger.go | 88 +++++ sdk/log/logrecord_exporter.go | 47 +++ sdk/log/logrecord_limits.go | 60 +++ sdk/log/logrecord_processor.go | 69 ++++ sdk/log/logrecord_processor_test.go | 240 ++++++++++++ sdk/log/provider.go | 382 ++++++++++++++++++ sdk/log/provider_test.go | 118 ++++++ sdk/log/simple_logrecord_processor.go | 126 ++++++ sdk/log/simple_logrecord_processor_test.go | 174 +++++++++ sdk/log/snapshot.go | 83 ++++ sdk/log/util_test.go | 26 ++ 21 files changed, 2528 insertions(+), 19 deletions(-) create mode 100644 sdk/log/batch_logrecord_processor.go create mode 100644 sdk/log/doc.go create mode 100644 sdk/log/log.go create mode 100644 sdk/log/log_test.go create mode 100644 sdk/log/logger.go create mode 100644 sdk/log/logrecord_exporter.go create mode 100644 sdk/log/logrecord_limits.go create mode 100644 sdk/log/logrecord_processor.go create mode 100644 sdk/log/logrecord_processor_test.go create mode 100644 sdk/log/provider.go create mode 100644 sdk/log/provider_test.go create mode 100644 sdk/log/simple_logrecord_processor.go create mode 100644 sdk/log/simple_logrecord_processor_test.go create mode 100644 sdk/log/snapshot.go create mode 100644 sdk/log/util_test.go diff --git a/go.mod b/go.mod index 767ce13fe78..ef3db4972a8 100644 --- a/go.mod +++ b/go.mod @@ -17,3 +17,65 @@ require ( ) replace go.opentelemetry.io/otel/trace => ./trace + +replace go.opentelemetry.io/otel => ./ + +replace go.opentelemetry.io/otel/bridge/opencensus => ./bridge/opencensus + +replace go.opentelemetry.io/otel/bridge/opencensus/test => ./bridge/opencensus/test + +replace go.opentelemetry.io/otel/bridge/opentracing => ./bridge/opentracing + +replace go.opentelemetry.io/otel/example/fib => ./example/fib + +replace go.opentelemetry.io/otel/example/jaeger => ./example/jaeger + +replace go.opentelemetry.io/otel/example/namedtracer => ./example/namedtracer + +replace go.opentelemetry.io/otel/example/opencensus => ./example/opencensus + +replace go.opentelemetry.io/otel/example/otel-collector => ./example/otel-collector + +replace go.opentelemetry.io/otel/example/passthrough => ./example/passthrough + +replace go.opentelemetry.io/otel/example/prometheus => ./example/prometheus + +replace go.opentelemetry.io/otel/example/view => ./example/view + +replace go.opentelemetry.io/otel/example/zipkin => ./example/zipkin + +replace go.opentelemetry.io/otel/exporters/jaeger => ./exporters/jaeger + +replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ./exporters/otlp/internal/retry + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric => ./exporters/otlp/otlpmetric + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ./exporters/otlp/otlpmetric/otlpmetricgrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ./exporters/otlp/otlpmetric/otlpmetrichttp + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ./exporters/otlp/otlptrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ./exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ./exporters/otlp/otlptrace/otlptracehttp + +replace go.opentelemetry.io/otel/exporters/prometheus => ./exporters/prometheus + +replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ./exporters/stdout/stdoutmetric + +replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ./exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/zipkin => ./exporters/zipkin + +replace go.opentelemetry.io/otel/internal/tools => ./internal/tools + +replace go.opentelemetry.io/otel/log => ./log + +replace go.opentelemetry.io/otel/metric => ./metric + +replace go.opentelemetry.io/otel/schema => ./schema + +replace go.opentelemetry.io/otel/sdk => ./sdk + +replace go.opentelemetry.io/otel/sdk/metric => ./sdk/metric diff --git a/log/doc.go b/log/doc.go index 39a443a093d..5074f51f6d1 100644 --- a/log/doc.go +++ b/log/doc.go @@ -13,26 +13,24 @@ // limitations under the License. /* -Package trace provides an implementation of the tracing part of the +Package log provides an implementation of the logging part of the OpenTelemetry API. To participate in distributed traces a LogRecord needs to be created for the operation being performed as part of a traced workflow. In its simplest form: - var tracer trace.Logger + var logger log.Logger func init() { - tracer = otel.Tracer("instrumentation/package/name") + logger = otel.Logger("instrumentation/package/name") } func operation(ctx context.Context) { - var span trace.LogRecord - ctx, span = tracer.Start(ctx, "operation") - defer span.End() + logger.Emit(ctx, log.WithAttributes(...)) // ... } -A Logger is unique to the instrumentation and is used to create Spans. +A Logger is unique to the instrumentation and is used to create LogRecords. Instrumentation should be designed to accept a LoggerProvider from which it can create its own unique Logger. Alternatively, the registered global LoggerProvider from the go.opentelemetry.io/otel package can be used as @@ -44,22 +42,20 @@ a default. ) type Instrumentation struct { - tracer trace.Logger + logger log.Logger } - func NewInstrumentation(tp trace.LoggerProvider) *Instrumentation { + func NewInstrumentation(tp log.LoggerProvider) *Instrumentation { if tp == nil { tp = otel.LoggerProvider() } return &Instrumentation{ - tracer: tp.Logger(name, trace.WithInstrumentationVersion(version)), + logger: tp.Logger(name, log.WithInstrumentationVersion(version)), } } func operation(ctx context.Context, inst *Instrumentation) { - var span trace.LogRecord - ctx, span = inst.tracer.Start(ctx, "operation") - defer span.End() + inst.tracer.Emit(ctx, log.WithAttributes(...)) // ... } */ diff --git a/log/go.mod b/log/go.mod index f84e8bbea9b..a4344d538aa 100644 --- a/log/go.mod +++ b/log/go.mod @@ -3,7 +3,6 @@ module go.opentelemetry.io/otel/log go 1.18 require ( - github.com/google/go-cmp v0.5.9 github.com/stretchr/testify v1.8.1 go.opentelemetry.io/otel v0.0.0-00010101000000-000000000000 ) diff --git a/log/go.sum b/log/go.sum index 64456627351..bce2202af2b 100644 --- a/log/go.sum +++ b/log/go.sum @@ -2,7 +2,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/log/log.go b/log/log.go index c9d0526d90c..47bcbb0c327 100644 --- a/log/log.go +++ b/log/log.go @@ -27,10 +27,6 @@ import ( // // Warning: methods may be added to this interface in minor releases. type LogRecord interface { - // IsRecording returns the recording state of the LogRecord. It will return - // true if the LogRecord is active and events can be recorded. - IsRecording() bool - // SetAttributes sets kv as attributes of the LogRecord. If a key from kv // already exists for an attribute of the LogRecord it will be overwritten with // the value contained in kv. diff --git a/sdk/go.mod b/sdk/go.mod index a0a6068ee9c..690847fe35f 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-cmp v0.5.9 github.com/stretchr/testify v1.8.1 go.opentelemetry.io/otel v1.11.2 + go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 go.opentelemetry.io/otel/trace v1.11.2 golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 ) @@ -21,3 +22,63 @@ require ( ) replace go.opentelemetry.io/otel/trace => ../trace + +replace go.opentelemetry.io/otel/log => ../log + +replace go.opentelemetry.io/otel/bridge/opencensus => ../bridge/opencensus + +replace go.opentelemetry.io/otel/bridge/opencensus/test => ../bridge/opencensus/test + +replace go.opentelemetry.io/otel/bridge/opentracing => ../bridge/opentracing + +replace go.opentelemetry.io/otel/example/fib => ../example/fib + +replace go.opentelemetry.io/otel/example/jaeger => ../example/jaeger + +replace go.opentelemetry.io/otel/example/namedtracer => ../example/namedtracer + +replace go.opentelemetry.io/otel/example/opencensus => ../example/opencensus + +replace go.opentelemetry.io/otel/example/otel-collector => ../example/otel-collector + +replace go.opentelemetry.io/otel/example/passthrough => ../example/passthrough + +replace go.opentelemetry.io/otel/example/prometheus => ../example/prometheus + +replace go.opentelemetry.io/otel/example/view => ../example/view + +replace go.opentelemetry.io/otel/example/zipkin => ../example/zipkin + +replace go.opentelemetry.io/otel/exporters/jaeger => ../exporters/jaeger + +replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ../exporters/otlp/internal/retry + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric => ../exporters/otlp/otlpmetric + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../exporters/otlp/otlpmetric/otlpmetricgrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../exporters/otlp/otlpmetric/otlpmetrichttp + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../exporters/otlp/otlptrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../exporters/otlp/otlptrace/otlptracehttp + +replace go.opentelemetry.io/otel/exporters/prometheus => ../exporters/prometheus + +replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../exporters/stdout/stdoutmetric + +replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/zipkin => ../exporters/zipkin + +replace go.opentelemetry.io/otel/internal/tools => ../internal/tools + +replace go.opentelemetry.io/otel/metric => ../metric + +replace go.opentelemetry.io/otel/schema => ../schema + +replace go.opentelemetry.io/otel/sdk => ./ + +replace go.opentelemetry.io/otel/sdk/metric => ./metric diff --git a/sdk/log/batch_logrecord_processor.go b/sdk/log/batch_logrecord_processor.go new file mode 100644 index 00000000000..b481fdff34d --- /dev/null +++ b/sdk/log/batch_logrecord_processor.go @@ -0,0 +1,433 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "context" + "runtime" + "sync" + "sync/atomic" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/internal/global" + "go.opentelemetry.io/otel/sdk/internal/env" + "go.opentelemetry.io/otel/trace" +) + +// Defaults for BatchLogRecordProcessorOptions. +const ( + DefaultMaxQueueSize = 2048 + DefaultScheduleDelay = 5000 + DefaultExportTimeout = 30000 + DefaultMaxExportBatchSize = 512 +) + +// BatchLogRecordProcessorOption configures a BatchLogRecordProcessor. +type BatchLogRecordProcessorOption func(o *BatchLogRecordProcessorOptions) + +// BatchLogRecordProcessorOptions is configuration settings for a +// BatchLogRecordProcessor. +type BatchLogRecordProcessorOptions struct { + // MaxQueueSize is the maximum queue size to buffer logRecords for delayed processing. If the + // queue gets full it drops the logRecords. Use BlockOnQueueFull to change this behavior. + // The default value of MaxQueueSize is 2048. + MaxQueueSize int + + // BatchTimeout is the maximum duration for constructing a batch. Processor + // forcefully sends available logRecords when timeout is reached. + // The default value of BatchTimeout is 5000 msec. + BatchTimeout time.Duration + + // ExportTimeout specifies the maximum duration for exporting logRecords. If the timeout + // is reached, the export will be cancelled. + // The default value of ExportTimeout is 30000 msec. + ExportTimeout time.Duration + + // MaxExportBatchSize is the maximum number of logRecords to process in a single batch. + // If there are more than one batch worth of logRecords then it processes multiple batches + // of logRecords one batch after the other without any delay. + // The default value of MaxExportBatchSize is 512. + MaxExportBatchSize int + + // BlockOnQueueFull blocks onEnd() and onStart() method if the queue is full + // AND if BlockOnQueueFull is set to true. + // Blocking option should be used carefully as it can severely affect the performance of an + // application. + BlockOnQueueFull bool +} + +// batchLogRecordProcessor is a LogRecordProcessor that batches asynchronously-received +// logRecords and sends them to a trace.Exporter when complete. +type batchLogRecordProcessor struct { + e LogRecordExporter + o BatchLogRecordProcessorOptions + + queue chan ReadOnlyLogRecord + dropped uint32 + + batch []ReadOnlyLogRecord + batchMutex sync.Mutex + timer *time.Timer + stopWait sync.WaitGroup + stopOnce sync.Once + stopCh chan struct{} +} + +var _ LogRecordProcessor = (*batchLogRecordProcessor)(nil) + +// NewBatchLogRecordProcessor creates a new LogRecordProcessor that will send completed +// span batches to the exporter with the supplied options. +// +// If the exporter is nil, the span processor will preform no action. +func NewBatchLogRecordProcessor( + exporter LogRecordExporter, options ...BatchLogRecordProcessorOption, +) LogRecordProcessor { + maxQueueSize := env.BatchSpanProcessorMaxQueueSize(DefaultMaxQueueSize) + maxExportBatchSize := env.BatchSpanProcessorMaxExportBatchSize(DefaultMaxExportBatchSize) + + if maxExportBatchSize > maxQueueSize { + if DefaultMaxExportBatchSize > maxQueueSize { + maxExportBatchSize = maxQueueSize + } else { + maxExportBatchSize = DefaultMaxExportBatchSize + } + } + + o := BatchLogRecordProcessorOptions{ + BatchTimeout: time.Duration(env.BatchSpanProcessorScheduleDelay(DefaultScheduleDelay)) * time.Millisecond, + ExportTimeout: time.Duration(env.BatchSpanProcessorExportTimeout(DefaultExportTimeout)) * time.Millisecond, + MaxQueueSize: maxQueueSize, + MaxExportBatchSize: maxExportBatchSize, + } + for _, opt := range options { + opt(&o) + } + bsp := &batchLogRecordProcessor{ + e: exporter, + o: o, + batch: make([]ReadOnlyLogRecord, 0, o.MaxExportBatchSize), + timer: time.NewTimer(o.BatchTimeout), + queue: make(chan ReadOnlyLogRecord, o.MaxQueueSize), + stopCh: make(chan struct{}), + } + + bsp.stopWait.Add(1) + go func() { + defer bsp.stopWait.Done() + bsp.processQueue() + bsp.drainQueue() + }() + + return bsp +} + +// OnStart method does nothing. +func (bsp *batchLogRecordProcessor) OnEmit(parent context.Context, s ReadWriteLogRecord) { + // Do not enqueue logRecords if we are just going to drop them. + if bsp.e == nil { + return + } + bsp.enqueue(s) +} + +// Shutdown flushes the queue and waits until all logRecords are processed. +// It only executes once. Subsequent call does nothing. +func (bsp *batchLogRecordProcessor) Shutdown(ctx context.Context) error { + var err error + bsp.stopOnce.Do( + func() { + wait := make(chan struct{}) + go func() { + close(bsp.stopCh) + bsp.stopWait.Wait() + if bsp.e != nil { + if err := bsp.e.Shutdown(ctx); err != nil { + otel.Handle(err) + } + } + close(wait) + }() + // Wait until the wait group is done or the context is cancelled + select { + case <-wait: + case <-ctx.Done(): + err = ctx.Err() + } + }, + ) + return err +} + +type forceFlushLogRecord struct { + ReadOnlyLogRecord + flushed chan struct{} +} + +func (f forceFlushLogRecord) SpanContext() trace.SpanContext { + return trace.NewSpanContext(trace.SpanContextConfig{TraceFlags: trace.FlagsSampled}) +} + +// ForceFlush exports all ended logRecords that have not yet been exported. +func (bsp *batchLogRecordProcessor) ForceFlush(ctx context.Context) error { + var err error + if bsp.e != nil { + flushCh := make(chan struct{}) + if bsp.enqueueBlockOnQueueFull(ctx, forceFlushLogRecord{flushed: flushCh}) { + select { + case <-flushCh: + // Processed any items in queue prior to ForceFlush being called + case <-ctx.Done(): + return ctx.Err() + } + } + + wait := make(chan error) + go func() { + wait <- bsp.exportLogRecords(ctx) + close(wait) + }() + // Wait until the export is finished or the context is cancelled/timed out + select { + case err = <-wait: + case <-ctx.Done(): + err = ctx.Err() + } + } + return err +} + +// WithMaxQueueSize returns a BatchLogRecordProcessorOption that configures the +// maximum queue size allowed for a BatchLogRecordProcessor. +func WithMaxQueueSize(size int) BatchLogRecordProcessorOption { + return func(o *BatchLogRecordProcessorOptions) { + o.MaxQueueSize = size + } +} + +// WithMaxExportBatchSize returns a BatchLogRecordProcessorOption that configures +// the maximum export batch size allowed for a BatchLogRecordProcessor. +func WithMaxExportBatchSize(size int) BatchLogRecordProcessorOption { + return func(o *BatchLogRecordProcessorOptions) { + o.MaxExportBatchSize = size + } +} + +// WithBatchTimeout returns a BatchLogRecordProcessorOption that configures the +// maximum delay allowed for a BatchLogRecordProcessor before it will export any +// held span (whether the queue is full or not). +func WithBatchTimeout(delay time.Duration) BatchLogRecordProcessorOption { + return func(o *BatchLogRecordProcessorOptions) { + o.BatchTimeout = delay + } +} + +// WithExportTimeout returns a BatchLogRecordProcessorOption that configures the +// amount of time a BatchLogRecordProcessor waits for an exporter to export before +// abandoning the export. +func WithExportTimeout(timeout time.Duration) BatchLogRecordProcessorOption { + return func(o *BatchLogRecordProcessorOptions) { + o.ExportTimeout = timeout + } +} + +// WithBlocking returns a BatchLogRecordProcessorOption that configures a +// BatchLogRecordProcessor to wait for enqueue operations to succeed instead of +// dropping data when the queue is full. +func WithBlocking() BatchLogRecordProcessorOption { + return func(o *BatchLogRecordProcessorOptions) { + o.BlockOnQueueFull = true + } +} + +// exportLogRecords is a subroutine of processing and draining the queue. +func (bsp *batchLogRecordProcessor) exportLogRecords(ctx context.Context) error { + bsp.timer.Reset(bsp.o.BatchTimeout) + + bsp.batchMutex.Lock() + defer bsp.batchMutex.Unlock() + + if bsp.o.ExportTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, bsp.o.ExportTimeout) + defer cancel() + } + + if l := len(bsp.batch); l > 0 { + global.Debug("exporting log records", "count", len(bsp.batch), "total_dropped", atomic.LoadUint32(&bsp.dropped)) + err := bsp.e.ExportLogRecords(ctx, bsp.batch) + + // A new batch is always created after exporting, even if the batch failed to be exported. + // + // It is up to the exporter to implement any type of retry logic if a batch is failing + // to be exported, since it is specific to the protocol and backend being sent to. + bsp.batch = bsp.batch[:0] + + if err != nil { + return err + } + } + return nil +} + +// processQueue removes logRecords from the `queue` channel until processor +// is shut down. It calls the exporter in batches of up to MaxExportBatchSize +// waiting up to BatchTimeout to form a batch. +func (bsp *batchLogRecordProcessor) processQueue() { + defer bsp.timer.Stop() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + for { + select { + case <-bsp.stopCh: + return + case <-bsp.timer.C: + if err := bsp.exportLogRecords(ctx); err != nil { + otel.Handle(err) + } + case sd := <-bsp.queue: + if ffs, ok := sd.(forceFlushLogRecord); ok { + close(ffs.flushed) + continue + } + bsp.batchMutex.Lock() + bsp.batch = append(bsp.batch, sd) + shouldExport := len(bsp.batch) >= bsp.o.MaxExportBatchSize + bsp.batchMutex.Unlock() + if shouldExport { + if !bsp.timer.Stop() { + <-bsp.timer.C + } + if err := bsp.exportLogRecords(ctx); err != nil { + otel.Handle(err) + } + } + } + } +} + +// drainQueue awaits the any caller that had added to bsp.stopWait +// to finish the enqueue, then exports the final batch. +func (bsp *batchLogRecordProcessor) drainQueue() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + for { + select { + case sd := <-bsp.queue: + if sd == nil { + if err := bsp.exportLogRecords(ctx); err != nil { + otel.Handle(err) + } + return + } + + bsp.batchMutex.Lock() + bsp.batch = append(bsp.batch, sd) + shouldExport := len(bsp.batch) == bsp.o.MaxExportBatchSize + bsp.batchMutex.Unlock() + + if shouldExport { + if err := bsp.exportLogRecords(ctx); err != nil { + otel.Handle(err) + } + } + default: + close(bsp.queue) + } + } +} + +func (bsp *batchLogRecordProcessor) enqueue(sd ReadOnlyLogRecord) { + ctx := context.TODO() + if bsp.o.BlockOnQueueFull { + bsp.enqueueBlockOnQueueFull(ctx, sd) + } else { + bsp.enqueueDrop(ctx, sd) + } +} + +func recoverSendOnClosedChan() { + x := recover() + switch err := x.(type) { + case nil: + return + case runtime.Error: + if err.Error() == "send on closed channel" { + return + } + } + panic(x) +} + +func (bsp *batchLogRecordProcessor) enqueueBlockOnQueueFull(ctx context.Context, sd ReadOnlyLogRecord) bool { + if !sd.SpanContext().IsSampled() { + return false + } + + // This ensures the bsp.queue<- below does not panic as the + // processor shuts down. + defer recoverSendOnClosedChan() + + select { + case <-bsp.stopCh: + return false + default: + } + + select { + case bsp.queue <- sd: + return true + case <-ctx.Done(): + return false + } +} + +func (bsp *batchLogRecordProcessor) enqueueDrop(ctx context.Context, sd ReadOnlyLogRecord) bool { + if !sd.SpanContext().IsSampled() { + return false + } + + // This ensures the bsp.queue<- below does not panic as the + // processor shuts down. + defer recoverSendOnClosedChan() + + select { + case <-bsp.stopCh: + return false + default: + } + + select { + case bsp.queue <- sd: + return true + default: + atomic.AddUint32(&bsp.dropped, 1) + } + return false +} + +// MarshalLog is the marshaling function used by the logging system to represent this exporter. +func (bsp *batchLogRecordProcessor) MarshalLog() interface{} { + return struct { + Type string + SpanExporter LogRecordExporter + Config BatchLogRecordProcessorOptions + }{ + Type: "BatchLogRecordProcessor", + SpanExporter: bsp.e, + Config: bsp.o, + } +} diff --git a/sdk/log/doc.go b/sdk/log/doc.go new file mode 100644 index 00000000000..212a8edd0cd --- /dev/null +++ b/sdk/log/doc.go @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package trace contains support for OpenTelemetry distributed tracing. + +The following assumes a basic familiarity with OpenTelemetry concepts. +See https://opentelemetry.io. +*/ +package log // import "go.opentelemetry.io/otel/sdk/log" diff --git a/sdk/log/log.go b/sdk/log/log.go new file mode 100644 index 00000000000..b888486f9eb --- /dev/null +++ b/sdk/log/log.go @@ -0,0 +1,358 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "strings" + "sync" + "time" + "unicode/utf8" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/trace" +) + +// ReadOnlyLogRecord allows reading information from the data structure underlying a +// log.LogRecord. It is used in places where reading information from a log record is +// necessary but changing the log record isn't necessary or allowed. +// +// Warning: methods may be added to this interface in minor releases. +type ReadOnlyLogRecord interface { + // SpanContext returns the unique SpanContext that identifies the span this log record is associated with. + SpanContext() trace.SpanContext + // Timestamp returns the time the log record started recording. + Timestamp() time.Time + // Attributes returns the defining attributes of the log record. + // The order of the returned attributes is not guaranteed to be stable across invocations. + Attributes() []attribute.KeyValue + // InstrumentationScope returns information about the instrumentation + // scope that created the log record. + InstrumentationScope() instrumentation.Scope + // InstrumentationLibrary returns information about the instrumentation + // library that created the log record. + // Deprecated: please use InstrumentationScope instead. + InstrumentationLibrary() instrumentation.Library + // Resource returns information about the entity that produced the log record. + Resource() *resource.Resource + // DroppedAttributes returns the number of attributes dropped by the log record + // due to limits being reached. + DroppedAttributes() int + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() +} + +// ReadWriteLogRecord exposes the same methods as log.LogRecord and in addition allows +// reading information from the underlying data structure. +// This interface exposes the union of the methods of log.LogRecord (which is a +// "write-only" log record) and ReadOnlyLogRecord. New methods for writing or reading log record +// information should be added under log.LogRecord or ReadOnlyLogRecord, respectively. +// +// Warning: methods may be added to this interface in minor releases. +type ReadWriteLogRecord interface { + log.LogRecord + ReadOnlyLogRecord +} + +// recordingLogRecord is an implementation of the OpenTelemetry Log API +// representing the individual component of a trace that is sampled. +type recordingLogRecord struct { + // mu protects the contents of this log record. + mu sync.Mutex + + // timestamp is the time at which this log record was started. + timestamp time.Time + + // spanContext holds the SpanContext of this log record. + spanContext trace.SpanContext + + // attributes is a collection of user provided key/values. The collection + // is constrained by a configurable maximum held by the parent + // LoggerProvider. When additional attributes are added after this maximum + // is reached these attributes the user is attempting to add are dropped. + // This dropped number of attributes is tracked and reported in the + // ReadOnlyLogRecord exported when the span ends. + attributes []attribute.KeyValue + droppedAttributes int + + // logger is the SDK logger that created this log record. + logger *logger +} + +var _ ReadWriteLogRecord = (*recordingLogRecord)(nil) + +// SpanContext returns the SpanContext of this log record. +func (s *recordingLogRecord) SpanContext() trace.SpanContext { + if s == nil { + return trace.SpanContext{} + } + return s.spanContext +} + +// SetAttributes sets attributes of this log record. +// +// If a key from attributes already exists the value associated with that key +// will be overwritten with the value contained in attributes. +// +// If adding attributes to the log record would exceed the maximum amount of +// attributes the span is configured to have, the last added attributes will +// be dropped. +func (s *recordingLogRecord) SetAttributes(attributes ...attribute.KeyValue) { + s.mu.Lock() + defer s.mu.Unlock() + + limit := s.logger.provider.logRecordLimits.AttributeCountLimit + if limit == 0 { + // No attributes allowed. + s.droppedAttributes += len(attributes) + return + } + + // If adding these attributes could exceed the capacity of s perform a + // de-duplication and truncation while adding to avoid over allocation. + if limit > 0 && len(s.attributes)+len(attributes) > limit { + s.addOverCapAttrs(limit, attributes) + return + } + + // Otherwise, add without deduplication. When attributes are read they + // will be deduplicated, optimizing the operation. + for _, a := range attributes { + if !a.Valid() { + // Drop all invalid attributes. + s.droppedAttributes++ + continue + } + a = truncateAttr(s.logger.provider.logRecordLimits.AttributeValueLengthLimit, a) + s.attributes = append(s.attributes, a) + } +} + +// addOverCapAttrs adds the attributes attrs to the span s while +// de-duplicating the attributes of s and attrs and dropping attributes that +// exceed the limit. +// +// This method assumes s.mu.Lock is held by the caller. +// +// This method should only be called when there is a possibility that adding +// attrs to s will exceed the limit. Otherwise, attrs should be added to s +// without checking for duplicates and all retrieval methods of the attributes +// for s will de-duplicate as needed. +// +// This method assumes limit is a value > 0. The argument should be validated +// by the caller. +func (s *recordingLogRecord) addOverCapAttrs(limit int, attrs []attribute.KeyValue) { + // In order to not allocate more capacity to s.attributes than needed, + // prune and truncate this addition of attributes while adding. + + // Do not set a capacity when creating this map. Benchmark testing has + // showed this to only add unused memory allocations in general use. + exists := make(map[attribute.Key]int) + s.dedupeAttrsFromRecord(&exists) + + // Now that s.attributes is deduplicated, adding unique attributes up to + // the capacity of s will not over allocate s.attributes. + for _, a := range attrs { + if !a.Valid() { + // Drop all invalid attributes. + s.droppedAttributes++ + continue + } + + if idx, ok := exists[a.Key]; ok { + // Perform all updates before dropping, even when at capacity. + s.attributes[idx] = a + continue + } + + if len(s.attributes) >= limit { + // Do not just drop all of the remaining attributes, make sure + // updates are checked and performed. + s.droppedAttributes++ + } else { + a = truncateAttr(s.logger.provider.logRecordLimits.AttributeValueLengthLimit, a) + s.attributes = append(s.attributes, a) + exists[a.Key] = len(s.attributes) - 1 + } + } +} + +// truncateAttr returns a truncated version of attr. Only string and string +// slice attribute values are truncated. String values are truncated to at +// most a length of limit. Each string slice value is truncated in this fashion +// (the slice length itself is unaffected). +// +// No truncation is perfromed for a negative limit. +func truncateAttr(limit int, attr attribute.KeyValue) attribute.KeyValue { + if limit < 0 { + return attr + } + switch attr.Value.Type() { + case attribute.STRING: + if v := attr.Value.AsString(); len(v) > limit { + return attr.Key.String(safeTruncate(v, limit)) + } + case attribute.STRINGSLICE: + v := attr.Value.AsStringSlice() + for i := range v { + if len(v[i]) > limit { + v[i] = safeTruncate(v[i], limit) + } + } + return attr.Key.StringSlice(v) + } + return attr +} + +// safeTruncate truncates the string and guarantees valid UTF-8 is returned. +func safeTruncate(input string, limit int) string { + if trunc, ok := safeTruncateValidUTF8(input, limit); ok { + return trunc + } + trunc, _ := safeTruncateValidUTF8(strings.ToValidUTF8(input, ""), limit) + return trunc +} + +// safeTruncateValidUTF8 returns a copy of the input string safely truncated to +// limit. The truncation is ensured to occur at the bounds of complete UTF-8 +// characters. If invalid encoding of UTF-8 is encountered, input is returned +// with false, otherwise, the truncated input will be returned with true. +func safeTruncateValidUTF8(input string, limit int) (string, bool) { + for cnt := 0; cnt <= limit; { + r, size := utf8.DecodeRuneInString(input[cnt:]) + if r == utf8.RuneError { + return input, false + } + + if cnt+size > limit { + return input[:cnt], true + } + cnt += size + } + return input, true +} + +// Timestamp returns the time this log record started. +func (s *recordingLogRecord) Timestamp() time.Time { + s.mu.Lock() + defer s.mu.Unlock() + return s.timestamp +} + +// Attributes returns the attributes of this span. +// +// The order of the returned attributes is not guaranteed to be stable. +func (s *recordingLogRecord) Attributes() []attribute.KeyValue { + s.mu.Lock() + defer s.mu.Unlock() + s.dedupeAttrs() + return s.attributes +} + +// dedupeAttrs deduplicates the attributes of s to fit capacity. +// +// This method assumes s.mu.Lock is held by the caller. +func (s *recordingLogRecord) dedupeAttrs() { + // Do not set a capacity when creating this map. Benchmark testing has + // showed this to only add unused memory allocations in general use. + exists := make(map[attribute.Key]int) + s.dedupeAttrsFromRecord(&exists) +} + +// dedupeAttrsFromRecord deduplicates the attributes of s to fit capacity +// using record as the record of unique attribute keys to their index. +// +// This method assumes s.mu.Lock is held by the caller. +func (s *recordingLogRecord) dedupeAttrsFromRecord(record *map[attribute.Key]int) { + // Use the fact that slices share the same backing array. + unique := s.attributes[:0] + for _, a := range s.attributes { + if idx, ok := (*record)[a.Key]; ok { + unique[idx] = a + } else { + unique = append(unique, a) + (*record)[a.Key] = len(unique) - 1 + } + } + // s.attributes have element types of attribute.KeyValue. These types are + // not pointers and they themselves do not contain pointer fields, + // therefore the duplicate values do not need to be zeroed for them to be + // garbage collected. + s.attributes = unique +} + +// InstrumentationScope returns the instrumentation.Scope associated with +// the Tracer that created this span. +func (s *recordingLogRecord) InstrumentationScope() instrumentation.Scope { + s.mu.Lock() + defer s.mu.Unlock() + return s.logger.instrumentationScope +} + +// InstrumentationLibrary returns the instrumentation.Library associated with +// the Tracer that created this span. +func (s *recordingLogRecord) InstrumentationLibrary() instrumentation.Library { + s.mu.Lock() + defer s.mu.Unlock() + return s.logger.instrumentationScope +} + +// Resource returns the Resource associated with the Tracer that created this +// span. +func (s *recordingLogRecord) Resource() *resource.Resource { + s.mu.Lock() + defer s.mu.Unlock() + return s.logger.provider.resource +} + +// DroppedAttributes returns the number of attributes dropped by the span +// due to limits being reached. +func (s *recordingLogRecord) DroppedAttributes() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.droppedAttributes +} + +// LoggerProvider returns a log.LoggerProvider that can be used to generate +// additional Spans on the same telemetry pipeline as the current Span. +func (s *recordingLogRecord) LoggerProvider() log.LoggerProvider { + return s.logger.provider +} + +// snapshot creates a read-only copy of the current state of the span. +func (s *recordingLogRecord) snapshot() ReadOnlyLogRecord { + var sd snapshot + s.mu.Lock() + defer s.mu.Unlock() + + sd.instrumentationScope = s.logger.instrumentationScope + sd.resource = s.logger.provider.resource + sd.spanContext = s.spanContext + sd.timestamp = s.timestamp + + if len(s.attributes) > 0 { + s.dedupeAttrs() + sd.attributes = s.attributes + } + sd.droppedAttributeCount = s.droppedAttributes + return &sd +} + +func (*recordingLogRecord) private() {} diff --git a/sdk/log/log_test.go b/sdk/log/log_test.go new file mode 100644 index 00000000000..c5079373bed --- /dev/null +++ b/sdk/log/log_test.go @@ -0,0 +1,171 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/attribute" +) + +func TestTruncateAttr(t *testing.T) { + const key = "key" + + strAttr := attribute.String(key, "value") + strSliceAttr := attribute.StringSlice(key, []string{"value-0", "value-1"}) + + tests := []struct { + limit int + attr, want attribute.KeyValue + }{ + { + limit: -1, + attr: strAttr, + want: strAttr, + }, + { + limit: -1, + attr: strSliceAttr, + want: strSliceAttr, + }, + { + limit: 0, + attr: attribute.Bool(key, true), + want: attribute.Bool(key, true), + }, + { + limit: 0, + attr: attribute.BoolSlice(key, []bool{true, false}), + want: attribute.BoolSlice(key, []bool{true, false}), + }, + { + limit: 0, + attr: attribute.Int(key, 42), + want: attribute.Int(key, 42), + }, + { + limit: 0, + attr: attribute.IntSlice(key, []int{42, -1}), + want: attribute.IntSlice(key, []int{42, -1}), + }, + { + limit: 0, + attr: attribute.Int64(key, 42), + want: attribute.Int64(key, 42), + }, + { + limit: 0, + attr: attribute.Int64Slice(key, []int64{42, -1}), + want: attribute.Int64Slice(key, []int64{42, -1}), + }, + { + limit: 0, + attr: attribute.Float64(key, 42), + want: attribute.Float64(key, 42), + }, + { + limit: 0, + attr: attribute.Float64Slice(key, []float64{42, -1}), + want: attribute.Float64Slice(key, []float64{42, -1}), + }, + { + limit: 0, + attr: strAttr, + want: attribute.String(key, ""), + }, + { + limit: 0, + attr: strSliceAttr, + want: attribute.StringSlice(key, []string{"", ""}), + }, + { + limit: 0, + attr: attribute.Stringer(key, bytes.NewBufferString("value")), + want: attribute.String(key, ""), + }, + { + limit: 1, + attr: strAttr, + want: attribute.String(key, "v"), + }, + { + limit: 1, + attr: strSliceAttr, + want: attribute.StringSlice(key, []string{"v", "v"}), + }, + { + limit: 5, + attr: strAttr, + want: strAttr, + }, + { + limit: 7, + attr: strSliceAttr, + want: strSliceAttr, + }, + { + limit: 6, + attr: attribute.StringSlice(key, []string{"value", "value-1"}), + want: attribute.StringSlice(key, []string{"value", "value-"}), + }, + { + limit: 128, + attr: strAttr, + want: strAttr, + }, + { + limit: 128, + attr: strSliceAttr, + want: strSliceAttr, + }, + { + // This tests the ordinary safeTruncate(). + limit: 10, + attr: attribute.String(key, "€€€€"), // 3 bytes each + want: attribute.String(key, "€€€"), + }, + { + // This tests truncation with an invalid UTF-8 input. + // + // Note that after removing the invalid rune, + // the string is over length and still has to + // be truncated on a code point boundary. + limit: 10, + attr: attribute.String(key, "€"[0:2]+"hello€€"), // corrupted first rune, then over limit + want: attribute.String(key, "hello€"), + }, + { + // This tests the fallback to invalidTruncate() + // where after validation the string does not require + // truncation. + limit: 6, + attr: attribute.String(key, "€"[0:2]+"hello"), // corrupted first rune, then not over limit + want: attribute.String(key, "hello"), + }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s->%s(limit:%d)", test.attr.Key, test.attr.Value.Emit(), test.limit) + t.Run( + name, func(t *testing.T) { + assert.Equal(t, test.want, truncateAttr(test.limit, test.attr)) + }, + ) + } +} diff --git a/sdk/log/logger.go b/sdk/log/logger.go new file mode 100644 index 00000000000..8b1f0e7ea1e --- /dev/null +++ b/sdk/log/logger.go @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "context" + "time" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/trace" +) + +type logger struct { + provider *LoggerProvider + instrumentationScope instrumentation.Scope +} + +var _ log.Logger = &logger{} + +// Emit starts a Span and returns it along with a context containing it. +// +// The Span is created with the provided name and as a child of any existing +// span context found in the passed context. The created Span will be +// configured appropriately by any SpanOption passed. +func (tr *logger) Emit(ctx context.Context, options ...log.LogRecordOption) { + config := log.NewLogRecordConfig(options...) + + if ctx == nil { + // Prevent log.ContextWithSpan from panicking. + ctx = context.Background() + } + + s := tr.newLogRecord(ctx, &config) + if rw, ok := s.(ReadWriteLogRecord); ok { + sps := tr.provider.logRecordProcessors.Load().(logRecordProcessorStates) + for _, sp := range sps { + sp.sp.OnEmit(ctx, rw) + } + } +} + +// newLogRecord returns a new configured span. +func (tr *logger) newLogRecord(ctx context.Context, config *log.LogRecordConfig) log.LogRecord { + // If told explicitly to make this a new root use a zero value SpanContext + // as a parent which contains an invalid trace ID and is not remote. + psc := trace.SpanContextFromContext(ctx) + return tr.newRecordingLogRecord(psc, config) +} + +// newRecordingLogRecord returns a new configured recordingLogRecord. +func (tr *logger) newRecordingLogRecord( + psc trace.SpanContext, config *log.LogRecordConfig, +) *recordingLogRecord { + timestamp := config.Timestamp() + if timestamp.IsZero() { + timestamp = time.Now() + } + + s := &recordingLogRecord{ + // Do not pre-allocate the attributes slice here! Doing so will + // allocate memory that is likely never going to be used, or if used, + // will be over-sized. The default Go compiler has been tested to + // dynamically allocate needed space very well. Benchmarking has shown + // it to be more performant than what we can predetermine here, + // especially for the common use case of few to no added + // attributes. + timestamp: timestamp, + logger: tr, + spanContext: psc, + } + + s.SetAttributes(config.Attributes()...) + + return s +} diff --git a/sdk/log/logrecord_exporter.go b/sdk/log/logrecord_exporter.go new file mode 100644 index 00000000000..fd80e493a39 --- /dev/null +++ b/sdk/log/logrecord_exporter.go @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import "context" + +// LogRecordExporter handles the delivery of logRecords to external receivers. This is +// the final component in the trace export pipeline. +type LogRecordExporter interface { + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. + + // ExportLogRecords exports a batch of logRecords. + // + // This function is called synchronously, so there is no concurrency + // safety requirement. However, due to the synchronous calling pattern, + // it is critical that all timeouts and cancellations contained in the + // passed context must be honored. + // + // Any retry logic must be contained in this function. The SDK that + // calls this function will not implement any retry logic. All errors + // returned by this function are considered unrecoverable and will be + // reported to a configured error Handler. + ExportLogRecords(ctx context.Context, spans []ReadOnlyLogRecord) error + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. + + // Shutdown notifies the exporter of a pending halt to operations. The + // exporter is expected to preform any cleanup or synchronization it + // requires while honoring all timeouts and cancellations contained in + // the passed context. + Shutdown(ctx context.Context) error + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. +} diff --git a/sdk/log/logrecord_limits.go b/sdk/log/logrecord_limits.go new file mode 100644 index 00000000000..74774b129b5 --- /dev/null +++ b/sdk/log/logrecord_limits.go @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import "go.opentelemetry.io/otel/sdk/internal/env" + +const ( + // DefaultAttributeValueLengthLimit is the default maximum allowed + // attribute value length, unlimited. + DefaultAttributeValueLengthLimit = -1 + + // DefaultAttributeCountLimit is the default maximum number of attributes + // a span can have. + DefaultAttributeCountLimit = 128 +) + +// LogRecordLimits represents the limits of a span. +type LogRecordLimits struct { + // AttributeValueLengthLimit is the maximum allowed attribute value length. + // + // This limit only applies to string and string slice attribute values. + // Any string longer than this value will be truncated to this length. + // + // Setting this to a negative value means no limit is applied. + AttributeValueLengthLimit int + + // AttributeCountLimit is the maximum allowed span attribute count. Any + // attribute added to a span once this limit is reached will be dropped. + // + // Setting this to zero means no attributes will be recorded. + // + // Setting this to a negative value means no limit is applied. + AttributeCountLimit int +} + +// NewLogRecordLimits returns a LogRecordLimits with all limits set to the value their +// corresponding environment variable holds, or the default if unset. +// +// • AttributeValueLengthLimit: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT +// (default: unlimited) +// +// • AttributeCountLimit: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT (default: 128) +func NewLogRecordLimits() LogRecordLimits { + return LogRecordLimits{ + AttributeValueLengthLimit: env.SpanAttributeValueLength(DefaultAttributeValueLengthLimit), + AttributeCountLimit: env.SpanAttributeCount(DefaultAttributeCountLimit), + } +} diff --git a/sdk/log/logrecord_processor.go b/sdk/log/logrecord_processor.go new file mode 100644 index 00000000000..40026e09e10 --- /dev/null +++ b/sdk/log/logrecord_processor.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "context" + "sync" +) + +// LogRecordProcessor is a processing pipeline for logRecords in the trace signal. +// LogRecordProcessors registered with a LoggerProvider and are called at the start +// and end of a Span's lifecycle, and are called in the order they are +// registered. +type LogRecordProcessor interface { + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. + + // OnEmit is called when a span is started. It is called synchronously + // and should not block. + OnEmit(parent context.Context, s ReadWriteLogRecord) + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. + + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. + + // Shutdown is called when the SDK shuts down. Any cleanup or release of + // resources held by the processor should be done in this call. + // + // Calls to OnStart, OnEnd, or ForceFlush after this has been called + // should be ignored. + // + // All timeouts and cancellations contained in ctx must be honored, this + // should not block indefinitely. + Shutdown(ctx context.Context) error + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. + + // ForceFlush exports all ended logRecords to the configured Exporter that have not yet + // been exported. It should only be called when absolutely necessary, such as when + // using a FaaS provider that may suspend the process after an invocation, but before + // the Processor can export the completed logRecords. + ForceFlush(ctx context.Context) error + // DO NOT CHANGE: any modification will not be backwards compatible and + // must never be done outside of a new major release. +} + +type logRecordProcessorState struct { + sp LogRecordProcessor + state *sync.Once +} + +func newLogRecordProcessorState(sp LogRecordProcessor) *logRecordProcessorState { + return &logRecordProcessorState{sp: sp, state: &sync.Once{}} +} + +type logRecordProcessorStates []*logRecordProcessorState diff --git a/sdk/log/logrecord_processor_test.go b/sdk/log/logrecord_processor_test.go new file mode 100644 index 00000000000..8e2a48354de --- /dev/null +++ b/sdk/log/logrecord_processor_test.go @@ -0,0 +1,240 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log_test + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/attribute" + sdklog "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/trace" +) + +type testLogRecordProcessor struct { + name string + logRecords []sdklog.ReadWriteLogRecord + shutdownCount int +} + +func (t *testLogRecordProcessor) OnEmit(parent context.Context, s sdklog.ReadWriteLogRecord) { + if t == nil { + return + } + psc := trace.SpanContextFromContext(parent) + kv := []attribute.KeyValue{ + { + Key: attribute.Key("SpanProcessorName" + t.name), + Value: attribute.StringValue(t.name), + }, + // Store parent trace ID and span ID as attributes to be read later in + // tests so that we "do something" with the parent argument. Real + // LogRecordProcessor implementations will likely use the parent argument in + // a more meaningful way. + { + Key: attribute.Key("ParentTraceID" + t.name), + Value: attribute.StringValue(psc.TraceID().String()), + }, + { + Key: attribute.Key("ParentSpanID" + t.name), + Value: attribute.StringValue(psc.SpanID().String()), + }, + } + s.SetAttributes(kv...) + t.logRecords = append(t.logRecords, s) +} + +func (t *testLogRecordProcessor) Shutdown(context.Context) error { + if t == nil { + return nil + } + t.shutdownCount++ + return nil +} + +func (t *testLogRecordProcessor) ForceFlush(context.Context) error { + if t == nil { + return nil + } + return nil +} + +func TestRegisterLogRecordProcessor(t *testing.T) { + name := "Register span processor before span starts" + tp := basicLoggerProvider(t) + spNames := []string{"sp1", "sp2", "sp3"} + sps := NewNamedTestLogRecordProcessors(spNames) + + for _, sp := range sps { + tp.RegisterLogRecordProcessor(sp) + } + + tid, _ := trace.TraceIDFromHex("01020304050607080102040810203040") + sid, _ := trace.SpanIDFromHex("0102040810203040") + parent := trace.NewSpanContext( + trace.SpanContextConfig{ + TraceID: tid, + SpanID: sid, + }, + ) + ctx := trace.ContextWithRemoteSpanContext(context.Background(), parent) + + tr := tp.Logger("LogRecordProcessor") + tr.Emit(ctx) + wantCount := 1 + + for _, sp := range sps { + gotCount := len(sp.logRecords) + if gotCount != wantCount { + t.Errorf("%s: started count: got %d, want %d\n", name, gotCount, wantCount) + } + + nameOK := false + tidOK := false + sidOK := false + for _, kv := range sp.logRecords[0].Attributes() { + switch kv.Key { + case attribute.Key("SpanProcessorName" + sp.name): + gotValue := kv.Value.AsString() + if gotValue != sp.name { + t.Errorf("%s: attributes: got %s, want %s\n", name, gotValue, sp.name) + } + nameOK = true + case attribute.Key("ParentTraceID" + sp.name): + gotValue := kv.Value.AsString() + if gotValue != parent.TraceID().String() { + t.Errorf("%s: attributes: got %s, want %s\n", name, gotValue, parent.TraceID()) + } + tidOK = true + case attribute.Key("ParentSpanID" + sp.name): + gotValue := kv.Value.AsString() + if gotValue != parent.SpanID().String() { + t.Errorf("%s: attributes: got %s, want %s\n", name, gotValue, parent.SpanID()) + } + sidOK = true + default: + continue + } + } + if !nameOK { + t.Errorf("%s: expected attributes(SpanProcessorName)\n", name) + } + if !tidOK { + t.Errorf("%s: expected attributes(ParentTraceID)\n", name) + } + if !sidOK { + t.Errorf("%s: expected attributes(ParentSpanID)\n", name) + } + } +} + +func TestUnregisterLogRecordProcessor(t *testing.T) { + name := "Start span after unregistering span processor" + tp := basicLoggerProvider(t) + spNames := []string{"sp1", "sp2", "sp3"} + sps := NewNamedTestLogRecordProcessors(spNames) + + for _, sp := range sps { + tp.RegisterLogRecordProcessor(sp) + } + + tr := tp.Logger("LogRecordProcessor") + tr.Emit(context.Background()) + for _, sp := range sps { + tp.UnregisterLogRecordProcessor(sp) + } + + // start another span after unregistering span processor. + tr.Emit(context.Background()) + + for _, sp := range sps { + wantCount := 1 + gotCount := len(sp.logRecords) + if gotCount != wantCount { + t.Errorf("%s: started count: got %d, want %d\n", name, gotCount, wantCount) + } + } +} + +func TestUnregisterLogRecordProcessorWhileSpanIsActive(t *testing.T) { + name := "Unregister span processor while span is active" + tp := basicLoggerProvider(t) + sp := NewTestLogRecordProcessor("sp") + tp.RegisterLogRecordProcessor(sp) + + tr := tp.Logger("LogRecordProcessor") + tr.Emit(context.Background()) + tp.UnregisterLogRecordProcessor(sp) + + wantCount := 1 + gotCount := len(sp.logRecords) + if gotCount != wantCount { + t.Errorf("%s: started count: got %d, want %d\n", name, gotCount, wantCount) + } +} + +func TestSpanProcessorShutdown(t *testing.T) { + name := "Increment shutdown counter of a span processor" + tp := basicLoggerProvider(t) + sp := NewTestLogRecordProcessor("sp") + tp.RegisterLogRecordProcessor(sp) + + wantCount := 1 + err := sp.Shutdown(context.Background()) + if err != nil { + t.Error("Error shutting the testLogRecordProcessor down\n") + } + + gotCount := sp.shutdownCount + if wantCount != gotCount { + t.Errorf("%s: wrong counter: got %d, want %d\n", name, gotCount, wantCount) + } +} + +func TestMultipleUnregisterLogRecordProcessorCalls(t *testing.T) { + name := "Increment shutdown counter after first UnregisterLogRecordProcessor call" + tp := basicLoggerProvider(t) + sp := NewTestLogRecordProcessor("sp") + + wantCount := 1 + + tp.RegisterLogRecordProcessor(sp) + tp.UnregisterLogRecordProcessor(sp) + + gotCount := sp.shutdownCount + if wantCount != gotCount { + t.Errorf("%s: wrong counter: got %d, want %d\n", name, gotCount, wantCount) + } + + // Multiple UnregisterLogRecordProcessor should not trigger multiple Shutdown calls. + tp.UnregisterLogRecordProcessor(sp) + + gotCount = sp.shutdownCount + if wantCount != gotCount { + t.Errorf("%s: wrong counter: got %d, want %d\n", name, gotCount, wantCount) + } +} + +func NewTestLogRecordProcessor(name string) *testLogRecordProcessor { + return &testLogRecordProcessor{name: name} +} + +func NewNamedTestLogRecordProcessors(names []string) []*testLogRecordProcessor { + tsp := []*testLogRecordProcessor{} + for _, n := range names { + tsp = append(tsp, NewTestLogRecordProcessor(n)) + } + return tsp +} diff --git a/sdk/log/provider.go b/sdk/log/provider.go new file mode 100644 index 00000000000..78fd01c0905 --- /dev/null +++ b/sdk/log/provider.go @@ -0,0 +1,382 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/internal/global" + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" +) + +const ( + defaultLoggerName = "go.opentelemetry.io/otel/sdk/logger" +) + +// loggerProviderConfig. +type loggerProviderConfig struct { + // processors contains collection of LogRecordProcessors that are processing pipeline + // for logRecords in the trace signal. + // LogRecordProcessors registered with a LoggerProvider and are called at the start + // and end of a Span's lifecycle, and are called in the order they are + // registered. + processors []LogRecordProcessor + + // logRecordLimits defines the attribute, event, and link limits for logRecords. + logRecordLimits LogRecordLimits + + // resource contains attributes representing an entity that produces telemetry. + resource *resource.Resource +} + +// MarshalLog is the marshaling function used by the logging system to represent this exporter. +func (cfg loggerProviderConfig) MarshalLog() interface{} { + return struct { + LogRecordProcessors []LogRecordProcessor + LogRecordLimits LogRecordLimits + Resource *resource.Resource + }{ + LogRecordProcessors: cfg.processors, + LogRecordLimits: cfg.logRecordLimits, + Resource: cfg.resource, + } +} + +// LoggerProvider is an OpenTelemetry LoggerProvider. It provides Tracers to +// instrumentation so it can trace operational flow through a system. +type LoggerProvider struct { + mu sync.Mutex + namedLogger map[instrumentation.Scope]*logger + logRecordProcessors atomic.Value + isShutdown bool + + // These fields are not protected by the lock mu. They are assumed to be + // immutable after creation of the LoggerProvider. + logRecordLimits LogRecordLimits + resource *resource.Resource +} + +var _ log.LoggerProvider = &LoggerProvider{} + +// NewLoggerProvider returns a new and configured LoggerProvider. +// +// By default the returned LoggerProvider is configured with: +// - a ParentBased(AlwaysSample) Sampler +// - a random number IDGenerator +// - the resource.Default() Resource +// - the default LogRecordLimits. +// +// The passed opts are used to override these default values and configure the +// returned LoggerProvider appropriately. +func NewLoggerProvider(opts ...LoggerProviderOption) *LoggerProvider { + o := loggerProviderConfig{ + logRecordLimits: NewLogRecordLimits(), + } + + for _, opt := range opts { + o = opt.apply(o) + } + + o = ensureValidLoggerProviderConfig(o) + + tp := &LoggerProvider{ + namedLogger: make(map[instrumentation.Scope]*logger), + logRecordLimits: o.logRecordLimits, + resource: o.resource, + } + global.Info("LoggerProvider created", "config", o) + + spss := logRecordProcessorStates{} + for _, sp := range o.processors { + spss = append(spss, newLogRecordProcessorState(sp)) + } + tp.logRecordProcessors.Store(spss) + + return tp +} + +// Logger returns a Logger with the given name and options. If a Logger for +// the given name and options does not exist it is created, otherwise the +// existing Logger is returned. +// +// If name is empty, DefaultLoggerName is used instead. +// +// This method is safe to be called concurrently. +func (p *LoggerProvider) Logger(name string, opts ...log.LoggerOption) log.Logger { + c := log.NewLoggerConfig(opts...) + + p.mu.Lock() + defer p.mu.Unlock() + if name == "" { + name = defaultLoggerName + } + is := instrumentation.Scope{ + Name: name, + Version: c.InstrumentationVersion(), + SchemaURL: c.SchemaURL(), + } + t, ok := p.namedLogger[is] + if !ok { + t = &logger{ + provider: p, + instrumentationScope: is, + } + p.namedLogger[is] = t + global.Info("Logger created", "name", name, "version", c.InstrumentationVersion(), "schemaURL", c.SchemaURL()) + } + return t +} + +// RegisterLogRecordProcessor adds the given LogRecordProcessor to the list of LogRecordProcessors. +func (p *LoggerProvider) RegisterLogRecordProcessor(sp LogRecordProcessor) { + p.mu.Lock() + defer p.mu.Unlock() + if p.isShutdown { + return + } + newSPS := logRecordProcessorStates{} + newSPS = append(newSPS, p.logRecordProcessors.Load().(logRecordProcessorStates)...) + newSPS = append(newSPS, newLogRecordProcessorState(sp)) + p.logRecordProcessors.Store(newSPS) +} + +// UnregisterLogRecordProcessor removes the given LogRecordProcessor from the list of LogRecordProcessors. +func (p *LoggerProvider) UnregisterLogRecordProcessor(sp LogRecordProcessor) { + p.mu.Lock() + defer p.mu.Unlock() + if p.isShutdown { + return + } + old := p.logRecordProcessors.Load().(logRecordProcessorStates) + if len(old) == 0 { + return + } + spss := logRecordProcessorStates{} + spss = append(spss, old...) + + // stop the span processor if it is started and remove it from the list + var stopOnce *logRecordProcessorState + var idx int + for i, sps := range spss { + if sps.sp == sp { + stopOnce = sps + idx = i + } + } + if stopOnce != nil { + stopOnce.state.Do( + func() { + if err := sp.Shutdown(context.Background()); err != nil { + otel.Handle(err) + } + }, + ) + } + if len(spss) > 1 { + copy(spss[idx:], spss[idx+1:]) + } + spss[len(spss)-1] = nil + spss = spss[:len(spss)-1] + + p.logRecordProcessors.Store(spss) +} + +// ForceFlush immediately exports all logRecords that have not yet been exported for +// all the registered span processors. +func (p *LoggerProvider) ForceFlush(ctx context.Context) error { + spss := p.logRecordProcessors.Load().(logRecordProcessorStates) + if len(spss) == 0 { + return nil + } + + for _, sps := range spss { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + if err := sps.sp.ForceFlush(ctx); err != nil { + return err + } + } + return nil +} + +// Shutdown shuts down LoggerProvider. All registered span processors are shut down +// in the order they were registered and any held computational resources are released. +func (p *LoggerProvider) Shutdown(ctx context.Context) error { + spss := p.logRecordProcessors.Load().(logRecordProcessorStates) + if len(spss) == 0 { + return nil + } + + p.mu.Lock() + defer p.mu.Unlock() + p.isShutdown = true + + var retErr error + for _, sps := range spss { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + var err error + sps.state.Do( + func() { + err = sps.sp.Shutdown(ctx) + }, + ) + if err != nil { + if retErr == nil { + retErr = err + } else { + // Poor man's list of errors + retErr = fmt.Errorf("%v; %v", retErr, err) + } + } + } + p.logRecordProcessors.Store(logRecordProcessorStates{}) + return retErr +} + +// LoggerProviderOption configures a LoggerProvider. +type LoggerProviderOption interface { + apply(loggerProviderConfig) loggerProviderConfig +} + +type loggerProviderOptionFunc func(loggerProviderConfig) loggerProviderConfig + +func (fn loggerProviderOptionFunc) apply(cfg loggerProviderConfig) loggerProviderConfig { + return fn(cfg) +} + +// WithSyncer registers the exporter with the LoggerProvider using a +// SimpleSpanProcessor. +// +// This is not recommended for production use. The synchronous nature of the +// SimpleSpanProcessor that will wrap the exporter make it good for testing, +// debugging, or showing examples of other feature, but it will be slow and +// have a high computation resource usage overhead. The WithBatcher option is +// recommended for production use instead. +func WithSyncer(e LogRecordExporter) LoggerProviderOption { + return WithLogRecordProcessor(NewSimpleLogRecordProcessor(e)) +} + +// WithBatcher registers the exporter with the LoggerProvider using a +// BatchSpanProcessor configured with the passed opts. +func WithBatcher(e LogRecordExporter, opts ...BatchLogRecordProcessorOption) LoggerProviderOption { + return WithLogRecordProcessor(NewBatchLogRecordProcessor(e, opts...)) +} + +// WithLogRecordProcessor registers the LogRecordProcessor with a LoggerProvider. +func WithLogRecordProcessor(sp LogRecordProcessor) LoggerProviderOption { + return loggerProviderOptionFunc( + func(cfg loggerProviderConfig) loggerProviderConfig { + cfg.processors = append(cfg.processors, sp) + return cfg + }, + ) +} + +// WithResource returns a LoggerProviderOption that will configure the +// Resource r as a LoggerProvider's Resource. The configured Resource is +// referenced by all the Tracers the LoggerProvider creates. It represents the +// entity producing telemetry. +// +// If this option is not used, the LoggerProvider will use the +// resource.Default() Resource by default. +func WithResource(r *resource.Resource) LoggerProviderOption { + return loggerProviderOptionFunc( + func(cfg loggerProviderConfig) loggerProviderConfig { + var err error + cfg.resource, err = resource.Merge(resource.Environment(), r) + if err != nil { + otel.Handle(err) + } + return cfg + }, + ) +} + +// WithLogRecordLimits returns a LoggerProviderOption that configures a +// LoggerProvider to use the LogRecordLimits sl. These LogRecordLimits bound any Span +// created by a Tracer from the LoggerProvider. +// +// If any field of sl is zero or negative it will be replaced with the default +// value for that field. +// +// If this or WithRawLogRecordLimits are not provided, the LoggerProvider will use +// the limits defined by environment variables, or the defaults if unset. +// Refer to the NewLogRecordLimits documentation for information about this +// relationship. +// +// Deprecated: Use WithRawLogRecordLimits instead which allows setting unlimited +// and zero limits. This option will be kept until the next major version +// incremented release. +func WithLogRecordLimits(sl LogRecordLimits) LoggerProviderOption { + if sl.AttributeValueLengthLimit <= 0 { + sl.AttributeValueLengthLimit = DefaultAttributeValueLengthLimit + } + if sl.AttributeCountLimit <= 0 { + sl.AttributeCountLimit = DefaultAttributeCountLimit + } + return loggerProviderOptionFunc( + func(cfg loggerProviderConfig) loggerProviderConfig { + cfg.logRecordLimits = sl + return cfg + }, + ) +} + +// WithRawLogRecordLimits returns a LoggerProviderOption that configures a +// LoggerProvider to use these limits. These limits bound any Span created by +// a Tracer from the LoggerProvider. +// +// The limits will be used as-is. Zero or negative values will not be changed +// to the default value like WithLogRecordLimits does. Setting a limit to zero will +// effectively disable the related resource it limits and setting to a +// negative value will mean that resource is unlimited. Consequentially, this +// means that the zero-value LogRecordLimits will disable all span resources. +// Because of this, limits should be constructed using NewLogRecordLimits and +// updated accordingly. +// +// If this or WithLogRecordLimits are not provided, the LoggerProvider will use the +// limits defined by environment variables, or the defaults if unset. Refer to +// the NewLogRecordLimits documentation for information about this relationship. +func WithRawLogRecordLimits(limits LogRecordLimits) LoggerProviderOption { + return loggerProviderOptionFunc( + func(cfg loggerProviderConfig) loggerProviderConfig { + cfg.logRecordLimits = limits + return cfg + }, + ) +} + +// ensureValidLoggerProviderConfig ensures that given TracerProviderConfig is valid. +func ensureValidLoggerProviderConfig(cfg loggerProviderConfig) loggerProviderConfig { + if cfg.resource == nil { + cfg.resource = resource.Default() + } + return cfg +} diff --git a/sdk/log/provider_test.go b/sdk/log/provider_test.go new file mode 100644 index 00000000000..55327ce1716 --- /dev/null +++ b/sdk/log/provider_test.go @@ -0,0 +1,118 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +type storingHandler struct { + errs []error +} + +func (s *storingHandler) Handle(err error) { + s.errs = append(s.errs, err) +} + +func (s *storingHandler) Reset() { + s.errs = nil +} + +var ( + handler = &storingHandler{} +) + +type basicLogRecordProcessor struct { + flushed bool + closed bool + injectShutdownError error +} + +func (t *basicLogRecordProcessor) Shutdown(context.Context) error { + t.closed = true + return t.injectShutdownError +} + +func (t *basicLogRecordProcessor) OnEmit(context.Context, ReadWriteLogRecord) {} +func (t *basicLogRecordProcessor) ForceFlush(context.Context) error { + t.flushed = true + return nil +} + +func TestForceFlushAndShutdownTraceProviderWithoutProcessor(t *testing.T) { + stp := NewLoggerProvider() + assert.NoError(t, stp.ForceFlush(context.Background())) + assert.NoError(t, stp.Shutdown(context.Background())) +} + +func TestShutdownTraceProvider(t *testing.T) { + stp := NewLoggerProvider() + sp := &basicLogRecordProcessor{} + stp.RegisterLogRecordProcessor(sp) + + assert.NoError(t, stp.ForceFlush(context.Background())) + assert.True(t, sp.flushed, "error ForceFlush basicLogRecordProcessor") + assert.NoError(t, stp.Shutdown(context.Background())) + assert.True(t, sp.closed, "error Shutdown basicLogRecordProcessor") +} + +func TestFailedProcessorShutdown(t *testing.T) { + stp := NewLoggerProvider() + spErr := errors.New("basic span processor shutdown failure") + sp := &basicLogRecordProcessor{ + injectShutdownError: spErr, + } + stp.RegisterLogRecordProcessor(sp) + + err := stp.Shutdown(context.Background()) + assert.Error(t, err) + assert.Equal(t, err, spErr) +} + +func TestFailedProcessorsShutdown(t *testing.T) { + stp := NewLoggerProvider() + spErr1 := errors.New("basic span processor shutdown failure1") + spErr2 := errors.New("basic span processor shutdown failure2") + sp1 := &basicLogRecordProcessor{ + injectShutdownError: spErr1, + } + sp2 := &basicLogRecordProcessor{ + injectShutdownError: spErr2, + } + stp.RegisterLogRecordProcessor(sp1) + stp.RegisterLogRecordProcessor(sp2) + + err := stp.Shutdown(context.Background()) + assert.Error(t, err) + assert.EqualError(t, err, "basic span processor shutdown failure1; basic span processor shutdown failure2") + assert.True(t, sp1.closed) + assert.True(t, sp2.closed) +} + +func TestSchemaURL(t *testing.T) { + stp := NewLoggerProvider() + schemaURL := "https://opentelemetry.io/schemas/1.2.0" + tracerIface := stp.Logger("tracername", log.WithSchemaURL(schemaURL)) + + // Verify that the SchemaURL of the constructed Tracer is correctly populated. + tracerStruct := tracerIface.(*logger) + assert.EqualValues(t, schemaURL, tracerStruct.instrumentationScope.SchemaURL) +} diff --git a/sdk/log/simple_logrecord_processor.go b/sdk/log/simple_logrecord_processor.go new file mode 100644 index 00000000000..9b8899e6061 --- /dev/null +++ b/sdk/log/simple_logrecord_processor.go @@ -0,0 +1,126 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel" +) + +// simpleLogRecordProcessor is a LogRecordProcessor that synchronously sends all +// completed Spans to a trace.Exporter immediately. +type simpleLogRecordProcessor struct { + exporterMu sync.RWMutex + exporter LogRecordExporter + stopOnce sync.Once +} + +var _ LogRecordProcessor = (*simpleLogRecordProcessor)(nil) + +// NewSimpleLogRecordProcessor returns a new LogRecordProcessor that will synchronously +// send completed logRecords to the exporter immediately. +// +// This LogRecordProcessor is not recommended for production use. The synchronous +// nature of this LogRecordProcessor make it good for testing, debugging, or +// showing examples of other feature, but it will be slow and have a high +// computation resource usage overhead. The BatchSpanProcessor is recommended +// for production use instead. +func NewSimpleLogRecordProcessor(exporter LogRecordExporter) LogRecordProcessor { + ssp := &simpleLogRecordProcessor{ + exporter: exporter, + } + return ssp +} + +func (ssp *simpleLogRecordProcessor) OnEmit(ctx context.Context, s ReadWriteLogRecord) { + ssp.exporterMu.RLock() + defer ssp.exporterMu.RUnlock() + + if ssp.exporter != nil { + if err := ssp.exporter.ExportLogRecords(context.Background(), []ReadOnlyLogRecord{s}); err != nil { + otel.Handle(err) + } + } +} + +// Shutdown shuts down the exporter this SimpleSpanProcessor exports to. +func (ssp *simpleLogRecordProcessor) Shutdown(ctx context.Context) error { + var err error + ssp.stopOnce.Do( + func() { + stopFunc := func(exp LogRecordExporter) (<-chan error, func()) { + done := make(chan error) + return done, func() { done <- exp.Shutdown(ctx) } + } + + // The exporter field of the simpleLogRecordProcessor needs to be zeroed to + // signal it is shut down, meaning all subsequent calls to OnEnd will + // be gracefully ignored. This needs to be done synchronously to avoid + // any race condition. + // + // A closure is used to keep reference to the exporter and then the + // field is zeroed. This ensures the simpleLogRecordProcessor is shut down + // before the exporter. This order is important as it avoids a + // potential deadlock. If the exporter shut down operation generates a + // span, that span would need to be exported. Meaning, OnEnd would be + // called and try acquiring the lock that is held here. + ssp.exporterMu.Lock() + done, shutdown := stopFunc(ssp.exporter) + ssp.exporter = nil + ssp.exporterMu.Unlock() + + go shutdown() + + // Wait for the exporter to shut down or the deadline to expire. + select { + case err = <-done: + case <-ctx.Done(): + // It is possible for the exporter to have immediately shut down + // and the context to be done simultaneously. In that case this + // outer select statement will randomly choose a case. This will + // result in a different returned error for similar scenarios. + // Instead, double check if the exporter shut down at the same + // time and return that error if so. This will ensure consistency + // as well as ensure the caller knows the exporter shut down + // successfully (they can already determine if the deadline is + // expired given they passed the context). + select { + case err = <-done: + default: + err = ctx.Err() + } + } + }, + ) + return err +} + +// ForceFlush does nothing as there is no data to flush. +func (ssp *simpleLogRecordProcessor) ForceFlush(context.Context) error { + return nil +} + +// MarshalLog is the marshaling function used by the logging system to represent this Span Processor. +func (ssp *simpleLogRecordProcessor) MarshalLog() interface{} { + return struct { + Type string + Exporter LogRecordExporter + }{ + Type: "SimpleLogRecordProcessor", + Exporter: ssp.exporter, + } +} diff --git a/sdk/log/simple_logrecord_processor_test.go b/sdk/log/simple_logrecord_processor_test.go new file mode 100644 index 00000000000..4991d3c4020 --- /dev/null +++ b/sdk/log/simple_logrecord_processor_test.go @@ -0,0 +1,174 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log_test + +import ( + "context" + "errors" + "testing" + "time" + + "go.opentelemetry.io/otel/log" + sdklog "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/trace" +) + +var ( + tid, _ = trace.TraceIDFromHex("01020304050607080102040810203040") + sid, _ = trace.SpanIDFromHex("0102040810203040") +) + +type testExporter struct { + logRecords []sdklog.ReadOnlyLogRecord + shutdown bool +} + +func (t *testExporter) ExportLogRecords(ctx context.Context, logRecords []sdklog.ReadOnlyLogRecord) error { + t.logRecords = append(t.logRecords, logRecords...) + return nil +} + +func (t *testExporter) Shutdown(ctx context.Context) error { + t.shutdown = true + select { + case <-ctx.Done(): + // Ensure context deadline tests receive the expected error. + return ctx.Err() + default: + return nil + } +} + +var _ sdklog.LogRecordExporter = (*testExporter)(nil) + +func TestNewSimpleLogRecordProcessor(t *testing.T) { + if ssp := sdklog.NewSimpleLogRecordProcessor(&testExporter{}); ssp == nil { + t.Error("failed to create new SimpleLogRecordProcessor") + } +} + +func TestNewSimpleLogRecordProcessorWithNilExporter(t *testing.T) { + if ssp := sdklog.NewSimpleLogRecordProcessor(nil); ssp == nil { + t.Error("failed to create new SimpleLogRecordProcessor with nil exporter") + } +} + +func emitLogRecord(tp log.LoggerProvider) { + tr := tp.Logger("SimpleLogRecordProcessor") + sc := trace.NewSpanContext( + trace.SpanContextConfig{ + TraceID: tid, + SpanID: sid, + TraceFlags: 0x1, + }, + ) + ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc) + tr.Emit(ctx) +} + +func TestSimpleLogRecordProcessorOnEnd(t *testing.T) { + tp := basicLoggerProvider(t) + te := testExporter{} + ssp := sdklog.NewSimpleLogRecordProcessor(&te) + + tp.RegisterLogRecordProcessor(ssp) + emitLogRecord(tp) + + wantTraceID := tid + gotTraceID := te.logRecords[0].SpanContext().TraceID() + if wantTraceID != gotTraceID { + t.Errorf("SimpleLogRecordProcessor OnEmit() check: got %+v, want %+v\n", gotTraceID, wantTraceID) + } +} + +func TestSimpleLogRecordProcessorShutdown(t *testing.T) { + exporter := &testExporter{} + ssp := sdklog.NewSimpleLogRecordProcessor(exporter) + + // Ensure we can export a span before we test we cannot after shutdown. + tp := basicLoggerProvider(t) + tp.RegisterLogRecordProcessor(ssp) + emitLogRecord(tp) + nExported := len(exporter.logRecords) + if nExported != 1 { + t.Error("failed to verify span export") + } + + if err := ssp.Shutdown(context.Background()); err != nil { + t.Errorf("shutting the SimpleLogRecordProcessor down: %v", err) + } + if !exporter.shutdown { + t.Error("SimpleLogRecordProcessor.Shutdown did not shut down exporter") + } + + emitLogRecord(tp) + if len(exporter.logRecords) > nExported { + t.Error("exported span to shutdown exporter") + } +} + +func TestSimpleLogRecordProcessorShutdownOnEndConcurrency(t *testing.T) { + exporter := &testExporter{} + ssp := sdklog.NewSimpleLogRecordProcessor(exporter) + tp := basicLoggerProvider(t) + tp.RegisterLogRecordProcessor(ssp) + + stop := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer func() { + done <- struct{}{} + }() + for { + select { + case <-stop: + return + default: + emitLogRecord(tp) + } + } + }() + + if err := ssp.Shutdown(context.Background()); err != nil { + t.Errorf("shutting the SimpleLogRecordProcessor down: %v", err) + } + if !exporter.shutdown { + t.Error("SimpleLogRecordProcessor.Shutdown did not shut down exporter") + } + + stop <- struct{}{} + <-done +} + +func TestSimpleLogRecordProcessorShutdownHonorsContextDeadline(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) + defer cancel() + <-ctx.Done() + + ssp := sdklog.NewSimpleLogRecordProcessor(&testExporter{}) + if got, want := ssp.Shutdown(ctx), context.DeadlineExceeded; !errors.Is(got, want) { + t.Errorf("SimpleLogRecordProcessor.Shutdown did not return %v, got %v", want, got) + } +} + +func TestSimpleLogRecordProcessorShutdownHonorsContextCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + ssp := sdklog.NewSimpleLogRecordProcessor(&testExporter{}) + if got, want := ssp.Shutdown(ctx), context.Canceled; !errors.Is(got, want) { + t.Errorf("SimpleLogRecordProcessor.Shutdown did not return %v, got %v", want, got) + } +} diff --git a/sdk/log/snapshot.go b/sdk/log/snapshot.go new file mode 100644 index 00000000000..b9a76fd4840 --- /dev/null +++ b/sdk/log/snapshot.go @@ -0,0 +1,83 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/trace" +) + +// snapshot is an record of a logRecords state at a particular checkpointed time. +// It is used as a read-only representation of that state. +type snapshot struct { + name string + spanContext trace.SpanContext + timestamp time.Time + attributes []attribute.KeyValue + droppedAttributeCount int + resource *resource.Resource + instrumentationScope instrumentation.Scope +} + +var _ ReadOnlyLogRecord = snapshot{} + +func (s snapshot) private() {} + +// Name returns the name of the span. +func (s snapshot) Name() string { + return s.name +} + +// SpanContext returns the unique SpanContext that identifies the span. +func (s snapshot) SpanContext() trace.SpanContext { + return s.spanContext +} + +// Timestamp returns the time the span started recording. +func (s snapshot) Timestamp() time.Time { + return s.timestamp +} + +// Attributes returns the defining attributes of the span. +func (s snapshot) Attributes() []attribute.KeyValue { + return s.attributes +} + +// InstrumentationScope returns information about the instrumentation +// scope that created the span. +func (s snapshot) InstrumentationScope() instrumentation.Scope { + return s.instrumentationScope +} + +// InstrumentationLibrary returns information about the instrumentation +// library that created the span. +func (s snapshot) InstrumentationLibrary() instrumentation.Library { + return s.instrumentationScope +} + +// Resource returns information about the entity that produced the span. +func (s snapshot) Resource() *resource.Resource { + return s.resource +} + +// DroppedAttributes returns the number of attributes dropped by the span +// due to limits being reached. +func (s snapshot) DroppedAttributes() int { + return s.droppedAttributeCount +} diff --git a/sdk/log/util_test.go b/sdk/log/util_test.go new file mode 100644 index 00000000000..82051c5edec --- /dev/null +++ b/sdk/log/util_test.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log_test + +import ( + "testing" + + sdklog "go.opentelemetry.io/otel/sdk/log" +) + +func basicLoggerProvider(t *testing.T) *sdklog.LoggerProvider { + tp := sdklog.NewLoggerProvider() + return tp +} From 7dea179297d9c0d58b09125b4be1c1969b8c68f7 Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Thu, 19 Jan 2023 16:26:35 -0500 Subject: [PATCH 3/6] Add stdlogout exporter --- exporters/stdout/stdoutlog/config.go | 96 ++++++++++++++++++++++ exporters/stdout/stdoutlog/doc.go | 17 ++++ exporters/stdout/stdoutlog/go.mod | 23 ++++++ exporters/stdout/stdoutlog/go.sum | 14 ++++ exporters/stdout/stdoutlog/log.go | 114 ++++++++++++++++++++++++++ sdk/log/logtest/exporter.go | 87 ++++++++++++++++++++ sdk/log/logtest/exporter_test.go | 59 ++++++++++++++ sdk/log/logtest/log.go | 118 +++++++++++++++++++++++++++ sdk/log/logtest/recorder.go | 92 +++++++++++++++++++++ 9 files changed, 620 insertions(+) create mode 100644 exporters/stdout/stdoutlog/config.go create mode 100644 exporters/stdout/stdoutlog/doc.go create mode 100644 exporters/stdout/stdoutlog/go.mod create mode 100644 exporters/stdout/stdoutlog/go.sum create mode 100644 exporters/stdout/stdoutlog/log.go create mode 100644 sdk/log/logtest/exporter.go create mode 100644 sdk/log/logtest/exporter_test.go create mode 100644 sdk/log/logtest/log.go create mode 100644 sdk/log/logtest/recorder.go diff --git a/exporters/stdout/stdoutlog/config.go b/exporters/stdout/stdoutlog/config.go new file mode 100644 index 00000000000..d84b8471d01 --- /dev/null +++ b/exporters/stdout/stdoutlog/config.go @@ -0,0 +1,96 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + +import ( + "io" + "os" +) + +var ( + defaultWriter = os.Stdout + defaultPrettyPrint = false + defaultTimestamps = true +) + +// config contains options for the STDOUT exporter. +type config struct { + // Writer is the destination. If not set, os.Stdout is used. + Writer io.Writer + + // PrettyPrint will encode the output into readable JSON. Default is + // false. + PrettyPrint bool + + // Timestamps specifies if timestamps should be printed. Default is + // true. + Timestamps bool +} + +// newConfig creates a validated Config configured with options. +func newConfig(options ...Option) (config, error) { + cfg := config{ + Writer: defaultWriter, + PrettyPrint: defaultPrettyPrint, + Timestamps: defaultTimestamps, + } + for _, opt := range options { + cfg = opt.apply(cfg) + } + return cfg, nil +} + +// Option sets the value of an option for a Config. +type Option interface { + apply(config) config +} + +// WithWriter sets the export stream destination. +func WithWriter(w io.Writer) Option { + return writerOption{w} +} + +type writerOption struct { + W io.Writer +} + +func (o writerOption) apply(cfg config) config { + cfg.Writer = o.W + return cfg +} + +// WithPrettyPrint sets the export stream format to use JSON. +func WithPrettyPrint() Option { + return prettyPrintOption(true) +} + +type prettyPrintOption bool + +func (o prettyPrintOption) apply(cfg config) config { + cfg.PrettyPrint = bool(o) + return cfg +} + +// WithoutTimestamps sets the export stream to not include timestamps. +func WithoutTimestamps() Option { + return timestampsOption(false) +} + +type timestampsOption bool + +func (o timestampsOption) apply(cfg config) config { + cfg.Timestamps = bool(o) + return cfg +} diff --git a/exporters/stdout/stdoutlog/doc.go b/exporters/stdout/stdoutlog/doc.go new file mode 100644 index 00000000000..d6e4e058cb1 --- /dev/null +++ b/exporters/stdout/stdoutlog/doc.go @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package stdouttrace contains an OpenTelemetry exporter for tracing +// telemetry to be written to an output destination as JSON. +package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" diff --git a/exporters/stdout/stdoutlog/go.mod b/exporters/stdout/stdoutlog/go.mod new file mode 100644 index 00000000000..7dba78413af --- /dev/null +++ b/exporters/stdout/stdoutlog/go.mod @@ -0,0 +1,23 @@ +module go.opentelemetry.io/otel/exporters/stdout/stdoutlog + +go 1.18 + +replace ( + go.opentelemetry.io/otel => ../../.. + go.opentelemetry.io/otel/sdk => ../../../sdk +) + +require go.opentelemetry.io/otel/sdk v1.11.2 + +require ( + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/otel v1.11.2 // indirect + go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 // indirect + go.opentelemetry.io/otel/trace v1.11.2 // indirect + golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect +) + +replace go.opentelemetry.io/otel/log => ../../../log + +replace go.opentelemetry.io/otel/sdk/log => ../../../sdk/log diff --git a/exporters/stdout/stdoutlog/go.sum b/exporters/stdout/stdoutlog/go.sum new file mode 100644 index 00000000000..91360f415c1 --- /dev/null +++ b/exporters/stdout/stdoutlog/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0= +go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/exporters/stdout/stdoutlog/log.go b/exporters/stdout/stdoutlog/log.go new file mode 100644 index 00000000000..af1bf84969a --- /dev/null +++ b/exporters/stdout/stdoutlog/log.go @@ -0,0 +1,114 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + +import ( + "context" + "encoding/json" + "sync" + "time" + + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/log/logtest" +) + +var zeroTime time.Time + +var _ log.LogRecordExporter = &Exporter{} + +// New creates an Exporter with the passed options. +func New(options ...Option) (*Exporter, error) { + cfg, err := newConfig(options...) + if err != nil { + return nil, err + } + + enc := json.NewEncoder(cfg.Writer) + if cfg.PrettyPrint { + enc.SetIndent("", "\t") + } + + return &Exporter{ + encoder: enc, + timestamps: cfg.Timestamps, + }, nil +} + +// Exporter is an implementation of trace.SpanSyncer that writes spans to stdout. +type Exporter struct { + encoder *json.Encoder + encoderMu sync.Mutex + timestamps bool + + stoppedMu sync.RWMutex + stopped bool +} + +// ExportLogRecords writes spans in json format to stdout. +func (e *Exporter) ExportLogRecords(ctx context.Context, spans []log.ReadOnlyLogRecord) error { + e.stoppedMu.RLock() + stopped := e.stopped + e.stoppedMu.RUnlock() + if stopped { + return nil + } + + if len(spans) == 0 { + return nil + } + + stubs := logtest.LogRecordStubsFromReadOnlyLogRecords(spans) + + e.encoderMu.Lock() + defer e.encoderMu.Unlock() + for i := range stubs { + stub := &stubs[i] + // Remove timestamps + if !e.timestamps { + stub.Timestamp = zeroTime + } + + // Encode span stubs, one by one + if err := e.encoder.Encode(stub); err != nil { + return err + } + } + return nil +} + +// Shutdown is called to stop the exporter, it preforms no action. +func (e *Exporter) Shutdown(ctx context.Context) error { + e.stoppedMu.Lock() + e.stopped = true + e.stoppedMu.Unlock() + + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + return nil +} + +// MarshalLog is the marshaling function used by the logging system to represent this exporter. +func (e *Exporter) MarshalLog() interface{} { + return struct { + Type string + WithTimestamps bool + }{ + Type: "stdout", + WithTimestamps: e.timestamps, + } +} diff --git a/sdk/log/logtest/exporter.go b/sdk/log/logtest/exporter.go new file mode 100644 index 00000000000..1068335c989 --- /dev/null +++ b/sdk/log/logtest/exporter.go @@ -0,0 +1,87 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tracetest is a testing helper package for the SDK. User can +// configure no-op or in-memory exporters to verify different SDK behaviors or +// custom instrumentation. +package logtest // import "go.opentelemetry.io/otel/sdk/trace/tracetest" + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel/sdk/log" +) + +var _ log.LogRecordExporter = (*NoopExporter)(nil) + +// NewNoopExporter returns a new no-op exporter. +func NewNoopExporter() *NoopExporter { + return new(NoopExporter) +} + +// NoopExporter is an exporter that drops all received spans and performs no +// action. +type NoopExporter struct{} + +// ExportSpans handles export of spans by dropping them. +func (nsb *NoopExporter) ExportLogRecords(ctx context.Context, spans []log.ReadOnlyLogRecord) error { + return nil +} + +// Shutdown stops the exporter by doing nothing. +func (nsb *NoopExporter) Shutdown(context.Context) error { return nil } + +var _ log.LogRecordExporter = (*InMemoryExporter)(nil) + +// NewInMemoryExporter returns a new InMemoryExporter. +func NewInMemoryExporter() *InMemoryExporter { + return new(InMemoryExporter) +} + +// InMemoryExporter is an exporter that stores all received spans in-memory. +type InMemoryExporter struct { + mu sync.Mutex + ss LogRecordStubs +} + +// ExportSpans handles export of spans by storing them in memory. +func (imsb *InMemoryExporter) ExportLogRecords(ctx context.Context, spans []log.ReadOnlyLogRecord) error { + imsb.mu.Lock() + defer imsb.mu.Unlock() + imsb.ss = append(imsb.ss, LogRecordStubsFromReadOnlyLogRecords(spans)...) + return nil +} + +// Shutdown stops the exporter by clearing spans held in memory. +func (imsb *InMemoryExporter) Shutdown(context.Context) error { + imsb.Reset() + return nil +} + +// Reset the current in-memory storage. +func (imsb *InMemoryExporter) Reset() { + imsb.mu.Lock() + defer imsb.mu.Unlock() + imsb.ss = nil +} + +// GetSpans returns the current in-memory stored spans. +func (imsb *InMemoryExporter) GetSpans() LogRecordStubs { + imsb.mu.Lock() + defer imsb.mu.Unlock() + ret := make(LogRecordStubs, len(imsb.ss)) + copy(ret, imsb.ss) + return ret +} diff --git a/sdk/log/logtest/exporter_test.go b/sdk/log/logtest/exporter_test.go new file mode 100644 index 00000000000..d5fd5a8eae5 --- /dev/null +++ b/sdk/log/logtest/exporter_test.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logtest + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestNoop tests only that the no-op does not crash in different scenarios. +func TestNoop(t *testing.T) { + nsb := NewNoopExporter() + + require.NoError(t, nsb.ExportLogRecords(context.Background(), nil)) + require.NoError(t, nsb.ExportLogRecords(context.Background(), make(LogRecordStubs, 10).Snapshots())) + require.NoError(t, nsb.ExportLogRecords(context.Background(), make(LogRecordStubs, 0, 10).Snapshots())) +} + +func TestNewInMemoryExporter(t *testing.T) { + imsb := NewInMemoryExporter() + + require.NoError(t, imsb.ExportLogRecords(context.Background(), nil)) + assert.Len(t, imsb.GetSpans(), 0) + + input := make(LogRecordStubs, 10) + for i := 0; i < 10; i++ { + input[i] = LogRecordStub{} + } + require.NoError(t, imsb.ExportLogRecords(context.Background(), input.Snapshots())) + sds := imsb.GetSpans() + assert.Len(t, sds, 10) + for i, sd := range sds { + assert.Equal(t, input[i], sd) + } + imsb.Reset() + // Ensure that operations on the internal storage does not change the previously returned value. + assert.Len(t, sds, 10) + assert.Len(t, imsb.GetSpans(), 0) + + require.NoError(t, imsb.ExportLogRecords(context.Background(), input.Snapshots()[0:1])) + sds = imsb.GetSpans() + assert.Len(t, sds, 1) + assert.Equal(t, input[0], sds[0]) +} diff --git a/sdk/log/logtest/log.go b/sdk/log/logtest/log.go new file mode 100644 index 00000000000..7ec6d13b835 --- /dev/null +++ b/sdk/log/logtest/log.go @@ -0,0 +1,118 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logtest // import "go.opentelemetry.io/otel/sdk/trace/logtest" + +import ( + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + logsdk "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/trace" +) + +// LogRecordStubs is a slice of LogRecordStub use for testing an SDK. +type LogRecordStubs []LogRecordStub + +// LogRecordStubsFromReadOnlyLogRecords returns LogRecordStubs populated from ro. +func LogRecordStubsFromReadOnlyLogRecords(ro []logsdk.ReadOnlyLogRecord) LogRecordStubs { + if len(ro) == 0 { + return nil + } + + s := make(LogRecordStubs, 0, len(ro)) + for _, r := range ro { + s = append(s, SpanStubFromReadOnlySpan(r)) + } + + return s +} + +// Snapshots returns s as a slice of ReadOnlySpans. +func (s LogRecordStubs) Snapshots() []logsdk.ReadOnlyLogRecord { + if len(s) == 0 { + return nil + } + + ro := make([]logsdk.ReadOnlyLogRecord, len(s)) + for i := 0; i < len(s); i++ { + ro[i] = s[i].Snapshot() + } + return ro +} + +// LogRecordStub is a stand-in for a Span. +type LogRecordStub struct { + SpanContext trace.SpanContext + Timestamp time.Time + EndTime time.Time + Attributes []attribute.KeyValue + DroppedAttributes int + Resource *resource.Resource + InstrumentationLibrary instrumentation.Library +} + +// SpanStubFromReadOnlySpan returns a LogRecordStub populated from ro. +func SpanStubFromReadOnlySpan(ro logsdk.ReadOnlyLogRecord) LogRecordStub { + if ro == nil { + return LogRecordStub{} + } + + return LogRecordStub{ + SpanContext: ro.SpanContext(), + Timestamp: ro.Timestamp(), + Attributes: ro.Attributes(), + DroppedAttributes: ro.DroppedAttributes(), + Resource: ro.Resource(), + InstrumentationLibrary: ro.InstrumentationScope(), + } +} + +// Snapshot returns a read-only copy of the LogRecordStub. +func (s LogRecordStub) Snapshot() logsdk.ReadOnlyLogRecord { + return logRecordSnapshot{ + spanContext: s.SpanContext, + timestamp: s.Timestamp, + attributes: s.Attributes, + droppedAttributes: s.DroppedAttributes, + resource: s.Resource, + instrumentationScope: s.InstrumentationLibrary, + } +} + +type logRecordSnapshot struct { + // Embed the interface to implement the private method. + logsdk.ReadOnlyLogRecord + + spanContext trace.SpanContext + timestamp time.Time + attributes []attribute.KeyValue + droppedAttributes int + resource *resource.Resource + instrumentationScope instrumentation.Scope +} + +func (s logRecordSnapshot) SpanContext() trace.SpanContext { return s.spanContext } +func (s logRecordSnapshot) Timestamp() time.Time { return s.timestamp } +func (s logRecordSnapshot) Attributes() []attribute.KeyValue { return s.attributes } +func (s logRecordSnapshot) DroppedAttributes() int { return s.droppedAttributes } +func (s logRecordSnapshot) Resource() *resource.Resource { return s.resource } +func (s logRecordSnapshot) InstrumentationScope() instrumentation.Scope { + return s.instrumentationScope +} +func (s logRecordSnapshot) InstrumentationLibrary() instrumentation.Library { + return s.instrumentationScope +} diff --git a/sdk/log/logtest/recorder.go b/sdk/log/logtest/recorder.go new file mode 100644 index 00000000000..6183a49c559 --- /dev/null +++ b/sdk/log/logtest/recorder.go @@ -0,0 +1,92 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logtest // import "go.opentelemetry.io/otel/sdk/trace/tracetest" + +import ( + "context" + "sync" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +// SpanRecorder records started and ended spans. +type SpanRecorder struct { + startedMu sync.RWMutex + started []sdktrace.ReadWriteSpan + + endedMu sync.RWMutex + ended []sdktrace.ReadOnlySpan +} + +var _ sdktrace.SpanProcessor = (*SpanRecorder)(nil) + +// NewSpanRecorder returns a new initialized SpanRecorder. +func NewSpanRecorder() *SpanRecorder { + return new(SpanRecorder) +} + +// OnStart records started spans. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) OnStart(_ context.Context, s sdktrace.ReadWriteSpan) { + sr.startedMu.Lock() + defer sr.startedMu.Unlock() + sr.started = append(sr.started, s) +} + +// OnEnd records completed spans. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) OnEnd(s sdktrace.ReadOnlySpan) { + sr.endedMu.Lock() + defer sr.endedMu.Unlock() + sr.ended = append(sr.ended, s) +} + +// Shutdown does nothing. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) Shutdown(context.Context) error { + return nil +} + +// ForceFlush does nothing. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) ForceFlush(context.Context) error { + return nil +} + +// Started returns a copy of all started spans that have been recorded. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) Started() []sdktrace.ReadWriteSpan { + sr.startedMu.RLock() + defer sr.startedMu.RUnlock() + dst := make([]sdktrace.ReadWriteSpan, len(sr.started)) + copy(dst, sr.started) + return dst +} + +// Ended returns a copy of all ended spans that have been recorded. +// +// This method is safe to be called concurrently. +func (sr *SpanRecorder) Ended() []sdktrace.ReadOnlySpan { + sr.endedMu.RLock() + defer sr.endedMu.RUnlock() + dst := make([]sdktrace.ReadOnlySpan, len(sr.ended)) + copy(dst, sr.ended) + return dst +} From 7db21cf90670c91d4db8f8186720062265995585 Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Thu, 19 Jan 2023 16:53:46 -0500 Subject: [PATCH 4/6] Add logs in namedtracer example --- example/namedtracer/foo/foo.go | 7 +++++-- example/namedtracer/go.mod | 13 +++++++++---- example/namedtracer/main.go | 29 ++++++++++++++++++++++++++++- sdk/log/logtest/log.go | 1 - 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/example/namedtracer/foo/foo.go b/example/namedtracer/foo/foo.go index 1193f8ad018..47627fc85ff 100644 --- a/example/namedtracer/foo/foo.go +++ b/example/namedtracer/foo/foo.go @@ -19,6 +19,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/trace" ) @@ -28,16 +29,18 @@ var ( // SubOperation is an example to demonstrate the use of named tracer. // It creates a named tracer with its package path. -func SubOperation(ctx context.Context) error { +func SubOperation(ctx context.Context, logger log.Logger) error { // Using global provider. Alternative is to have application provide a getter // for its component to get the instance of the provider. tr := otel.Tracer("example/namedtracer/foo") var span trace.Span - _, span = tr.Start(ctx, "Sub operation...") + ctx, span = tr.Start(ctx, "Sub operation...") defer span.End() span.SetAttributes(lemonsKey.String("five")) span.AddEvent("Sub span event") + logger.Emit(ctx, log.WithAttributes(attribute.String("operation", "suboperation"))) + return nil } diff --git a/example/namedtracer/go.mod b/example/namedtracer/go.mod index a9bd8db480e..de64ef06587 100644 --- a/example/namedtracer/go.mod +++ b/example/namedtracer/go.mod @@ -2,10 +2,7 @@ module go.opentelemetry.io/otel/example/namedtracer go 1.18 -replace ( - go.opentelemetry.io/otel => ../.. - go.opentelemetry.io/otel/sdk => ../../sdk -) +replace go.opentelemetry.io/otel => ../.. require ( github.com/go-logr/stdr v1.2.2 @@ -17,9 +14,17 @@ require ( require ( github.com/go-logr/logr v1.2.3 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.0.0-00010101000000-000000000000 // indirect + go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 // indirect golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect ) replace go.opentelemetry.io/otel/trace => ../../trace +replace go.opentelemetry.io/otel/log => ../../log + +replace go.opentelemetry.io/otel/sdk => ../../sdk + replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/stdout/stdoutlog => ../../exporters/stdout/stdoutlog diff --git a/example/namedtracer/main.go b/example/namedtracer/main.go index 68c51ce45c8..d2ac48f07cb 100644 --- a/example/namedtracer/main.go +++ b/example/namedtracer/main.go @@ -25,7 +25,10 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/example/namedtracer/foo" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + otellog "go.opentelemetry.io/otel/log" + sdklog "go.opentelemetry.io/otel/sdk/log" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" ) @@ -37,6 +40,7 @@ var ( ) var tp *sdktrace.TracerProvider +var lp *sdklog.LoggerProvider // initTracer creates and registers trace provider instance. func initTracer() error { @@ -53,6 +57,20 @@ func initTracer() error { return nil } +// initLogger creates and registers logger provider instance. +func initLogger() error { + exp, err := stdoutlog.New(stdoutlog.WithPrettyPrint()) + if err != nil { + return fmt.Errorf("failed to initialize stdoutlog exporter: %w", err) + } + bsp := sdklog.NewBatchLogRecordProcessor(exp) + lp = sdklog.NewLoggerProvider( + sdklog.WithLogRecordProcessor(bsp), + ) + //otel.SetTracerProvider(tp) + return nil +} + func main() { // Set logging level to info to see SDK status messages stdr.SetVerbosity(5) @@ -61,11 +79,16 @@ func main() { if err := initTracer(); err != nil { log.Panic(err) } + // initialize logger provider. + if err := initLogger(); err != nil { + log.Panic(err) + } // Create a named tracer with package path as its name. tracer := tp.Tracer("example/namedtracer/main") ctx := context.Background() defer func() { _ = tp.Shutdown(ctx) }() + defer func() { _ = lp.Shutdown(ctx) }() m0, _ := baggage.NewMember(string(fooKey), "foo1") m1, _ := baggage.NewMember(string(barKey), "bar1") @@ -77,7 +100,11 @@ func main() { defer span.End() span.AddEvent("Nice operation!", trace.WithAttributes(attribute.Int("bogons", 100))) span.SetAttributes(anotherKey.String("yes")) - if err := foo.SubOperation(ctx); err != nil { + + logger := lp.Logger("example/namedtracer/main") + logger.Emit(ctx, otellog.WithAttributes(attribute.String("operation", "main"))) + + if err := foo.SubOperation(ctx, logger); err != nil { panic(err) } } diff --git a/sdk/log/logtest/log.go b/sdk/log/logtest/log.go index 7ec6d13b835..d37c9e110bb 100644 --- a/sdk/log/logtest/log.go +++ b/sdk/log/logtest/log.go @@ -58,7 +58,6 @@ func (s LogRecordStubs) Snapshots() []logsdk.ReadOnlyLogRecord { type LogRecordStub struct { SpanContext trace.SpanContext Timestamp time.Time - EndTime time.Time Attributes []attribute.KeyValue DroppedAttributes int Resource *resource.Resource From d66ebd852326955585e652609f9181019d1d6565 Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Fri, 20 Jan 2023 15:44:16 -0500 Subject: [PATCH 5/6] Begin adding slog Handler --- example/namedtracer/go.mod | 7 +++--- example/namedtracer/go.sum | 6 +++-- example/namedtracer/slogotel/slogotel.go | 28 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 example/namedtracer/slogotel/slogotel.go diff --git a/example/namedtracer/go.mod b/example/namedtracer/go.mod index de64ef06587..b897143afef 100644 --- a/example/namedtracer/go.mod +++ b/example/namedtracer/go.mod @@ -7,16 +7,17 @@ replace go.opentelemetry.io/otel => ../.. require ( github.com/go-logr/stdr v1.2.2 go.opentelemetry.io/otel v1.11.2 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.0.0-00010101000000-000000000000 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.2 + go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 go.opentelemetry.io/otel/sdk v1.11.2 go.opentelemetry.io/otel/trace v1.11.2 + golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 ) require ( github.com/go-logr/logr v1.2.3 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.0.0-00010101000000-000000000000 // indirect - go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 // indirect - golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect + golang.org/x/sys v0.1.0 // indirect ) replace go.opentelemetry.io/otel/trace => ../../trace diff --git a/example/namedtracer/go.sum b/example/namedtracer/go.sum index 924770f5797..a6946f23c54 100644 --- a/example/namedtracer/go.sum +++ b/example/namedtracer/go.sum @@ -7,6 +7,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= -golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY= +golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/example/namedtracer/slogotel/slogotel.go b/example/namedtracer/slogotel/slogotel.go new file mode 100644 index 00000000000..8baf5a692f9 --- /dev/null +++ b/example/namedtracer/slogotel/slogotel.go @@ -0,0 +1,28 @@ +package slogotel + +import "golang.org/x/exp/slog" + +type OtelSlogHandler struct { +} + +func (o OtelSlogHandler) Enabled(level slog.Level) bool { + //TODO implement me + panic("implement me") +} + +func (o OtelSlogHandler) Handle(r slog.Record) error { + //TODO implement me + panic("implement me") +} + +func (o OtelSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + //TODO implement me + panic("implement me") +} + +func (o OtelSlogHandler) WithGroup(name string) slog.Handler { + //TODO implement me + panic("implement me") +} + +var _ slog.Handler = (*OtelSlogHandler)(nil) From 4d11d613c6650eff439c66c8f1200d15a3f63906 Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Wed, 1 Mar 2023 21:17:23 -0500 Subject: [PATCH 6/6] Implement slog Handler and use in the example --- example/namedtracer/foo/foo.go | 7 ++++ example/namedtracer/go.mod | 2 +- example/namedtracer/go.sum | 2 ++ example/namedtracer/main.go | 6 ++++ example/namedtracer/slogotel/slogotel.go | 43 ++++++++++++++++++++---- 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/example/namedtracer/foo/foo.go b/example/namedtracer/foo/foo.go index 47627fc85ff..3e3dffc8f79 100644 --- a/example/namedtracer/foo/foo.go +++ b/example/namedtracer/foo/foo.go @@ -17,6 +17,8 @@ package foo // import "go.opentelemetry.io/otel/example/namedtracer/foo" import ( "context" + "golang.org/x/exp/slog" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" @@ -40,7 +42,12 @@ func SubOperation(ctx context.Context, logger log.Logger) error { span.SetAttributes(lemonsKey.String("five")) span.AddEvent("Sub span event") + // Log via Otel Logger API logger.Emit(ctx, log.WithAttributes(attribute.String("operation", "suboperation"))) + // Log via slog API. Note this will correctly output Span Context (traceid,spanid) in the final output + // if Otel slog Handler is used. + slog.InfoCtx(ctx, "Otel logs via slog and with context!", "source", "slog") + return nil } diff --git a/example/namedtracer/go.mod b/example/namedtracer/go.mod index b897143afef..fde36034020 100644 --- a/example/namedtracer/go.mod +++ b/example/namedtracer/go.mod @@ -12,7 +12,7 @@ require ( go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 go.opentelemetry.io/otel/sdk v1.11.2 go.opentelemetry.io/otel/trace v1.11.2 - golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 ) require ( diff --git a/example/namedtracer/go.sum b/example/namedtracer/go.sum index a6946f23c54..1c94b236fa5 100644 --- a/example/namedtracer/go.sum +++ b/example/namedtracer/go.sum @@ -9,6 +9,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY= golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/example/namedtracer/main.go b/example/namedtracer/main.go index d2ac48f07cb..87667a0a638 100644 --- a/example/namedtracer/main.go +++ b/example/namedtracer/main.go @@ -20,11 +20,13 @@ import ( "log" "github.com/go-logr/stdr" + "golang.org/x/exp/slog" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/example/namedtracer/foo" + "go.opentelemetry.io/otel/example/namedtracer/slogotel" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" otellog "go.opentelemetry.io/otel/log" @@ -68,6 +70,10 @@ func initLogger() error { sdklog.WithLogRecordProcessor(bsp), ) //otel.SetTracerProvider(tp) + + // Configure all slog logs to also go via Otel Logs Bridge API + slog.SetDefault(slog.New(slogotel.NewOtelSlogHandler(lp.Logger("namedtracer")))) + return nil } diff --git a/example/namedtracer/slogotel/slogotel.go b/example/namedtracer/slogotel/slogotel.go index 8baf5a692f9..4e03d620f45 100644 --- a/example/namedtracer/slogotel/slogotel.go +++ b/example/namedtracer/slogotel/slogotel.go @@ -1,18 +1,47 @@ package slogotel -import "golang.org/x/exp/slog" +import ( + "context" + + "golang.org/x/exp/slog" + + "go.opentelemetry.io/otel/attribute" + otellog "go.opentelemetry.io/otel/log" +) type OtelSlogHandler struct { + logger otellog.Logger } -func (o OtelSlogHandler) Enabled(level slog.Level) bool { - //TODO implement me - panic("implement me") +func NewOtelSlogHandler(logger otellog.Logger) *OtelSlogHandler { + return &OtelSlogHandler{logger: logger} } -func (o OtelSlogHandler) Handle(r slog.Record) error { - //TODO implement me - panic("implement me") +func (o OtelSlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return true +} + +func (o OtelSlogHandler) Handle(ctx context.Context, record slog.Record) error { + attrs := make([]attribute.KeyValue, 0, record.NumAttrs()) + record.Attrs( + func(attr slog.Attr) { + attrs = append(attrs, slogToOtelAttr(attr)) + }, + ) + + o.logger.Emit(ctx, otellog.WithAttributes(attrs...)) + return nil +} + +func slogToOtelAttr(attr slog.Attr) (r attribute.KeyValue) { + r.Key = attribute.Key(attr.Key) + switch attr.Value.Kind() { + case slog.KindString: + r.Value = attribute.StringValue(attr.Value.String()) + default: + panic("implement other cases") + } + return r } func (o OtelSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {