diff --git a/app/app.go b/app/app.go index 8b589de..7710ff6 100644 --- a/app/app.go +++ b/app/app.go @@ -79,7 +79,7 @@ func Run(f func(ctx context.Context, lg *zap.Logger, m *Metrics) error, op ...Op // Update root logger after autologs setup. lg = zctx.From(ctx) - m, err := newMetrics(ctx, lg.Named("metrics"), res, opts.meterOptions, opts.tracerOptions) + m, err := newMetrics(ctx, lg.Named("metrics"), res, opts.meterOptions, opts.tracerOptions, opts.loggerOptions) if err != nil { panic(err) } diff --git a/app/metrics.go b/app/metrics.go index 7d913c1..25ab93b 100644 --- a/app/metrics.go +++ b/app/metrics.go @@ -16,6 +16,8 @@ import ( "go.opentelemetry.io/contrib/instrumentation/runtime" "go.opentelemetry.io/contrib/propagators/autoprop" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/noop" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" @@ -23,6 +25,7 @@ import ( "go.uber.org/zap" "golang.org/x/sync/errgroup" + "github.com/go-faster/sdk/autologs" "github.com/go-faster/sdk/autometer" "github.com/go-faster/sdk/autotracer" ) @@ -43,6 +46,7 @@ type Metrics struct { tracerProvider trace.TracerProvider meterProvider metric.MeterProvider + loggerProvider log.LoggerProvider resource *resource.Resource propagator propagation.TextMapPropagator @@ -137,6 +141,13 @@ func (m *Metrics) TracerProvider() trace.TracerProvider { return m.tracerProvider } +func (m *Metrics) LoggerProvider() log.LoggerProvider { + if m.loggerProvider == nil { + return noop.NewLoggerProvider() + } + return m.loggerProvider +} + func (m *Metrics) TextMapPropagator() propagation.TextMapPropagator { return m.propagator } @@ -167,6 +178,7 @@ func newMetrics( res *resource.Resource, meterOptions []autometer.Option, tracerOptions []autotracer.Option, + logsOptions []autologs.Option, ) (*Metrics, error) { { // Setup global OTEL logger and error handler. @@ -178,6 +190,18 @@ func newMetrics( lg: lg, resource: res, } + { + provider, stop, err := autologs.NewLoggerProvider(ctx, + include(logsOptions, + autologs.WithResource(res), + )..., + ) + if err != nil { + return nil, errors.Wrap(err, "logger provider") + } + m.loggerProvider = provider + m.registerShutdown("logger", stop) + } { provider, stop, err := autotracer.NewTracerProvider(ctx, include(tracerOptions, diff --git a/app/option.go b/app/option.go index 3c2dc6d..442f416 100644 --- a/app/option.go +++ b/app/option.go @@ -6,6 +6,7 @@ import ( "go.opentelemetry.io/otel/sdk/resource" "go.uber.org/zap" + "github.com/go-faster/sdk/autologs" "github.com/go-faster/sdk/autometer" "github.com/go-faster/sdk/autotracer" ) @@ -17,6 +18,7 @@ type options struct { meterOptions []autometer.Option tracerOptions []autotracer.Option + loggerOptions []autologs.Option resourceFn func(ctx context.Context) (*resource.Resource, error) } diff --git a/autologs/autologs.go b/autologs/autologs.go index 01ca2d9..7a7c928 100644 --- a/autologs/autologs.go +++ b/autologs/autologs.go @@ -2,51 +2,134 @@ package autologs import ( "context" + "io" "os" "strings" "github.com/go-faster/errors" - "go.opentelemetry.io/collector/pdata/plog/plogotlp" - "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/noop" + sdklog "go.opentelemetry.io/otel/sdk/log" "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "github.com/go-faster/sdk/zapotel" "github.com/go-faster/sdk/zctx" ) -// Setup OTLP log exporter if configured. -func Setup(ctx context.Context, res *resource.Resource) (context.Context, error) { - if os.Getenv("OTEL_LOGS_EXPORTER") != "otlp" { - return ctx, nil - } +const ( + expOTLP = "otlp" + expNone = "none" // no-op - endpoint := os.Getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") - if endpoint == "" { - endpoint = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") - } - if endpoint == "" { - endpoint = "localhost:4317" + protoHTTP = "http" + protoGRPC = "grpc" + defaultProto = protoGRPC +) + +const ( + writerStdout = "stdout" + writerStderr = "stderr" +) + +func writerByName(name string) io.Writer { + switch name { + case writerStdout: + return os.Stdout + case writerStderr: + return os.Stderr + default: + return io.Discard } +} - endpoint = strings.TrimPrefix(endpoint, "http://") - conn, err := grpc.NewClient(endpoint, - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return ctx, errors.Wrap(err, "create grpc client") +func getEnvOr(name, def string) string { + if v := os.Getenv(name); v != "" { + return v } + return def +} + +func nop(_ context.Context) error { return nil } +// ShutdownFunc is a function that shuts down the MeterProvider. +type ShutdownFunc func(ctx context.Context) error + +// NewLoggerProvider initializes new [log.LoggerProvider] with the given options from environment variables. +func NewLoggerProvider(ctx context.Context, options ...Option) ( + meterProvider log.LoggerProvider, + meterShutdown ShutdownFunc, + err error, +) { + cfg := newConfig(options) lg := zctx.From(ctx) - otelCore := zapotel.New(lg.Level(), res, plogotlp.NewGRPCClient(conn)) - // Update logger down the stack. - lg.Info("Setting up OTLP log exporter") - lg = lg.WithOptions( - zap.WrapCore(func(core zapcore.Core) zapcore.Core { - return zapcore.NewTee(core, otelCore) - }), - ) - return zctx.Base(ctx, lg), nil + var logOptions []sdklog.LoggerProviderOption + if cfg.res != nil { + logOptions = append(logOptions, sdklog.WithResource(cfg.res)) + } + ret := func(e sdklog.Exporter) (log.LoggerProvider, func(ctx context.Context) error, error) { + logOptions = append(logOptions, sdklog.WithProcessor( + sdklog.NewBatchProcessor(e), + )) + return sdklog.NewLoggerProvider(logOptions...), e.Shutdown, nil + } + exporter := strings.TrimSpace(getEnvOr("OTEL_LOGS_EXPORTER", expOTLP)) + switch exporter { + case expOTLP: + proto := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") + if proto == "" { + proto = os.Getenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL") + } + if proto == "" { + proto = defaultProto + } + lg.Debug("Using OTLP logs exporter", zap.String("protocol", proto)) + switch proto { + case protoHTTP: + exp, err := otlploghttp.New(ctx) + if err != nil { + return nil, nil, errors.Wrap(err, "create OTLP HTTP logs exporter") + } + return ret(exp) + case protoGRPC: + exp, err := otlploggrpc.New(ctx) + if err != nil { + return nil, nil, errors.Wrap(err, "create OTLP gRPC logs exporter") + } + return ret(exp) + default: + return nil, nil, errors.Errorf("unsupported logs otlp protocol %q", proto) + } + case writerStdout, writerStderr: + lg.Debug("Using stdout log exporter", zap.String("writer", exporter)) + writer := cfg.writer + if writer == nil { + writer = writerByName(exporter) + } + exp, err := stdoutlog.New(stdoutlog.WithWriter(writer)) + if err != nil { + return nil, nil, errors.Wrapf(err, "create %q logs exporter", exporter) + } + return ret(exp) + case expNone: + lg.Debug("Using no-op logs exporter") + return noop.NewLoggerProvider(), nop, nil + default: + lookup := cfg.lookup + if lookup == nil { + break + } + lg.Debug("Looking for logs exporter", zap.String("exporter", exporter)) + exp, ok, err := lookup(ctx, exporter) + if err != nil { + return nil, nil, errors.Wrapf(err, "create %q", exporter) + } + if !ok { + break + } + + lg.Debug("Using user-defined log exporter", zap.String("exporter", exporter)) + return ret(exp) + } + return nil, nil, errors.Errorf("unsupported OTEL_LOGS_EXPORTER %q", exporter) } diff --git a/autologs/config.go b/autologs/config.go new file mode 100644 index 0000000..2033f58 --- /dev/null +++ b/autologs/config.go @@ -0,0 +1,70 @@ +package autologs + +import ( + "context" + "io" + + sdklog "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/resource" +) + +// config contains configuration options for a LoggerProvider. +type config struct { + res *resource.Resource + writer io.Writer + lookup LookupExporter +} + +// newConfig returns a config configured with options. +func newConfig(options []Option) config { + conf := config{res: resource.Default()} + for _, o := range options { + conf = o.apply(conf) + } + return conf +} + +// Option applies a configuration option value to a LoggerProvider. +type Option interface { + apply(config) config +} + +// optionFunc applies a set of options to a config. +type optionFunc func(config) config + +// apply returns a config with option(s) applied. +func (o optionFunc) apply(conf config) config { + return o(conf) +} + +// WithResource associates a Resource with a LoggerProvider. This Resource +// represents the entity producing telemetry and is associated with all Meters +// the LoggerProvider will create. +// +// By default, if this Option is not used, the default Resource from the +// go.opentelemetry.io/otel/sdk/resource package will be used. +func WithResource(res *resource.Resource) Option { + return optionFunc(func(conf config) config { + conf.res = res + return conf + }) +} + +// WithWriter sets writer for the stderr, stdout exporters. +func WithWriter(out io.Writer) Option { + return optionFunc(func(conf config) config { + conf.writer = out + return conf + }) +} + +// LookupExporter creates exporter by name. +type LookupExporter func(ctx context.Context, name string) (sdklog.Exporter, bool, error) + +// WithLookupExporter sets exporter lookup function. +func WithLookupExporter(lookup LookupExporter) Option { + return optionFunc(func(conf config) config { + conf.lookup = lookup + return conf + }) +} diff --git a/autologs/config_test.go b/autologs/config_test.go new file mode 100644 index 0000000..39755a7 --- /dev/null +++ b/autologs/config_test.go @@ -0,0 +1,49 @@ +package autologs + +import ( + "context" + "errors" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/sdk/log" +) + +func TestWithLookupExporter(t *testing.T) { + var lookup LookupExporter = func(ctx context.Context, name string) (log.Exporter, bool, error) { + switch name { + case "return_something": + e, err := stdoutlog.New(stdoutlog.WithWriter(io.Discard)) + return e, true, err + case "return_error": + return nil, false, errors.New("test error") + default: + return nil, false, nil + } + } + + for i, tt := range []struct { + name string + containsErr string + }{ + {"return_something", ``}, + {"return_error", `test error`}, + {"return_not_exist", `unsupported OTEL_LOGS_EXPORTER "return_not_exist"`}, + } { + tt := tt + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", tt.name) + ctx := context.Background() + + _, _, err := NewLoggerProvider(ctx, WithLookupExporter(lookup)) + if tt.containsErr != "" { + require.ErrorContains(t, err, tt.containsErr) + return + } + require.NoError(t, err) + }) + } +} diff --git a/autologs/setup.go b/autologs/setup.go new file mode 100644 index 0000000..01ca2d9 --- /dev/null +++ b/autologs/setup.go @@ -0,0 +1,52 @@ +package autologs + +import ( + "context" + "os" + "strings" + + "github.com/go-faster/errors" + "go.opentelemetry.io/collector/pdata/plog/plogotlp" + "go.opentelemetry.io/otel/sdk/resource" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/go-faster/sdk/zapotel" + "github.com/go-faster/sdk/zctx" +) + +// Setup OTLP log exporter if configured. +func Setup(ctx context.Context, res *resource.Resource) (context.Context, error) { + if os.Getenv("OTEL_LOGS_EXPORTER") != "otlp" { + return ctx, nil + } + + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") + if endpoint == "" { + endpoint = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + } + if endpoint == "" { + endpoint = "localhost:4317" + } + + endpoint = strings.TrimPrefix(endpoint, "http://") + conn, err := grpc.NewClient(endpoint, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return ctx, errors.Wrap(err, "create grpc client") + } + + lg := zctx.From(ctx) + otelCore := zapotel.New(lg.Level(), res, plogotlp.NewGRPCClient(conn)) + // Update logger down the stack. + lg.Info("Setting up OTLP log exporter") + lg = lg.WithOptions( + zap.WrapCore(func(core zapcore.Core) zapcore.Core { + return zapcore.NewTee(core, otelCore) + }), + ) + return zctx.Base(ctx, lg), nil +} diff --git a/go.mod b/go.mod index faa37ad..14efdd4 100644 --- a/go.mod +++ b/go.mod @@ -11,15 +11,20 @@ require ( go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0 go.opentelemetry.io/contrib/propagators/autoprop v0.58.0 go.opentelemetry.io/otel v1.33.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 go.opentelemetry.io/otel/exporters/prometheus v0.55.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 + go.opentelemetry.io/otel/log v0.9.0 go.opentelemetry.io/otel/metric v1.33.0 go.opentelemetry.io/otel/sdk v1.33.0 + go.opentelemetry.io/otel/sdk/log v0.9.0 go.opentelemetry.io/otel/sdk/metric v1.33.0 go.opentelemetry.io/otel/trace v1.33.0 go.uber.org/automaxprocs v1.6.0 diff --git a/go.sum b/go.sum index 338c40e..7284ada 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,10 @@ go.opentelemetry.io/contrib/propagators/ot v1.33.0 h1:xj/pQFKo4ROsx0v129KpLgFwaY go.opentelemetry.io/contrib/propagators/ot v1.33.0/go.mod h1:/xxHCLhTmaypEFwMViRGROj2qgrGiFrkxIlATt0rddc= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 h1:gA2gh+3B3NDvRFP30Ufh7CC3TtJRbUSf2TTD0LbCagw= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0/go.mod h1:smRTR+02OtrVGjvWE1sQxhuazozKc/BXvvqqnmOxy+s= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 h1:Za0Z/j9Gf3Z9DKQ1choU9xI2noCxlkcyFFP2Ob3miEQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0/go.mod h1:jMRB8N75meTNjDFQyJBA/2Z9en21CsxwMctn08NHY6c= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8= @@ -96,14 +100,20 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfg go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo= go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 h1:iI15wfQb5ZtAVTdS5WROxpYmw6Kjez3hT9SuzXhrgGQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0/go.mod h1:yepwlNzVVxHWR5ugHIrll+euPQPq4pvysHTDr/daV9o= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= +go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY= +go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU= go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk/log v0.9.0 h1:YPCi6W1Eg0vwT/XJWsv2/PaQ2nyAJYuF7UUjQSBe3bc= +go.opentelemetry.io/otel/sdk/log v0.9.0/go.mod h1:y0HdrOz7OkXQBuc2yjiqnEHc+CRKeVhRE3hx4RwTmV4= go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=