Skip to content

Commit

Permalink
Merge pull request #162 from go-faster/feat/add-logger-provider
Browse files Browse the repository at this point in the history
feat(app, autologs): add logger provider
  • Loading branch information
ernado authored Jan 6, 2025
2 parents de2451f + 09abcce commit 45bf6e5
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 33 deletions.
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
24 changes: 24 additions & 0 deletions app/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ 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"
"go.opentelemetry.io/otel/trace"
"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"
)
Expand All @@ -43,6 +46,7 @@ type Metrics struct {

tracerProvider trace.TracerProvider
meterProvider metric.MeterProvider
loggerProvider log.LoggerProvider

resource *resource.Resource
propagator propagation.TextMapPropagator
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions app/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -17,6 +18,7 @@ type options struct {

meterOptions []autometer.Option
tracerOptions []autotracer.Option
loggerOptions []autologs.Option
resourceFn func(ctx context.Context) (*resource.Resource, error)
}

Expand Down
147 changes: 115 additions & 32 deletions autologs/autologs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
70 changes: 70 additions & 0 deletions autologs/config.go
Original file line number Diff line number Diff line change
@@ -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
})
}
49 changes: 49 additions & 0 deletions autologs/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
Loading

0 comments on commit 45bf6e5

Please sign in to comment.