From ba8379d2144dd3661c4e1a7a08b924d4ec1e98ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Fri, 6 Sep 2024 14:43:58 +0800 Subject: [PATCH] feat: add Tracer and Meter for components --- example_test.go | 58 +++++++++++++++++++++++++++++----- internal/kslog/log_observer.go | 48 +++++++++++++++++----------- kod.go | 54 +++++++++++++++++++------------ registry.go | 9 +++--- registry_test.go | 2 +- 5 files changed, 118 insertions(+), 53 deletions(-) diff --git a/example_test.go b/example_test.go index 2945beb..facb6ef 100644 --- a/example_test.go +++ b/example_test.go @@ -15,7 +15,7 @@ import ( ) // This example demonstrates how to use [kod.Run] and [kod.Implements] to run a simple application. -func Example_mainComponent() { +func Example_componentMain() { kod.Run(context.Background(), func(ctx context.Context, app *helloworld.App) error { fmt.Println("Hello, World!") return nil @@ -92,8 +92,8 @@ func Example_configGlobal() { // helloWorld shutdown } -// This example demonstrates how to use [kod.WithLogger] to provide a custom logger to the application. -func Example_log() { +// This example demonstrates how to use logging with OpenTelemetry. +func Example_openTelemetryLog() { logger, observer := kod.NewTestLogger() kod.RunTest(&testing.T{}, func(ctx context.Context, app *helloworld.App) { @@ -104,15 +104,57 @@ func Example_log() { app.HelloWorld.Get().SayHello(ctx) }, kod.WithLogger(logger)) - fmt.Println(observer) + fmt.Println(observer.RemoveKeys("trace_id", "span_id", "time")) + + // Output: + // helloWorld init + // Hello, World! + // helloWorld shutdown + // {"component":"github.com/go-kod/kod/Main","level":"INFO","msg":"Hello, World!"} + // {"component":"github.com/go-kod/kod/Main","level":"WARN","msg":"Hello, World!"} + // {"component":"github.com/go-kod/kod/Main","level":"ERROR","msg":"Hello, World!"} + // {"component":"github.com/go-kod/kod/examples/helloworld/HelloWorld","level":"INFO","msg":"Hello, World!"} +} + +// This example demonstrates how to use tracing with OpenTelemetry. +func Example_openTelemetryTrace() { + logger, observer := kod.NewTestLogger() + + kod.Run(context.Background(), func(ctx context.Context, app *helloworld.App) error { + ctx, span := app.Tracer().Start(ctx, "example") + defer span.End() + app.L(ctx).Info("Hello, World!") + app.L(ctx).WarnContext(ctx, "Hello, World!") + + app.HelloWorld.Get().SayHello(ctx) + return nil + }, kod.WithInterceptors(ktrace.Interceptor()), kod.WithLogger(logger)) + + fmt.Println(observer.Filter(func(m map[string]any) bool { + return m["trace_id"] != nil && m["span_id"] != nil + }).RemoveKeys("trace_id", "span_id", "time")) + // Output: // helloWorld init // Hello, World! // helloWorld shutdown - // {"level":"INFO","msg":"Hello, World!","component":"github.com/go-kod/kod/Main"} - // {"level":"WARN","msg":"Hello, World!","component":"github.com/go-kod/kod/Main"} - // {"level":"ERROR","msg":"Hello, World!","component":"github.com/go-kod/kod/Main"} - // {"level":"INFO","msg":"Hello, World!","component":"github.com/go-kod/kod/examples/helloworld/HelloWorld"} + // {"component":"github.com/go-kod/kod/Main","level":"INFO","msg":"Hello, World!"} + // {"component":"github.com/go-kod/kod/Main","level":"WARN","msg":"Hello, World!"} + // {"component":"github.com/go-kod/kod/examples/helloworld/HelloWorld","level":"INFO","msg":"Hello, World!"} +} + +// This example demonstrates how to use metrics with OpenTelemetry. +func Example_openTelemetryMetric() { + kod.Run(context.Background(), func(ctx context.Context, app *helloworld.App) error { + metric, _ := app.Meter().Int64Counter("example") + metric.Add(ctx, 1) + + return nil + }) + + // Output: + // helloWorld init + // helloWorld shutdown } // This example demonstrates how to use [kod.WithInterceptors] to provide a custom interceptor to the application. diff --git a/internal/kslog/log_observer.go b/internal/kslog/log_observer.go index c62a805..ac5f48f 100644 --- a/internal/kslog/log_observer.go +++ b/internal/kslog/log_observer.go @@ -9,14 +9,15 @@ import ( "github.com/samber/lo" ) -// removeTime removes the top-level time attribute. -// It is intended to be used as a ReplaceAttr function, -// to make example output deterministic. -func removeTime(groups []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey && len(groups) == 0 { - return slog.Attr{} +// NewTestLogger returns a new test logger. +func NewTestLogger() (*slog.Logger, *observer) { + observer := &observer{ + buf: new(bytes.Buffer), } - return a + log := slog.New(slog.NewJSONHandler(observer.buf, nil)) + slog.SetDefault(log) + + return log, observer } type observer struct { @@ -77,21 +78,30 @@ func (b *observer) Filter(filter func(map[string]any) bool) *observer { } } +// RemoveKeys removes the provided keys from the observed logs. +func (b *observer) RemoveKeys(keys ...string) *observer { + filtered := make([]map[string]any, 0) + for _, line := range b.parse() { + for _, key := range keys { + delete(line, key) + } + + filtered = append(filtered, line) + } + + buf := new(bytes.Buffer) + for _, line := range filtered { + lo.Must0(json.NewEncoder(buf).Encode(line)) + } + + return &observer{ + buf: buf, + } +} + // Clean clears the observed logs. func (b *observer) Clean() *observer { b.buf.Reset() return b } - -func NewTestLogger() (*slog.Logger, *observer) { - observer := &observer{ - buf: new(bytes.Buffer), - } - log := slog.New(slog.NewJSONHandler(observer.buf, &slog.HandlerOptions{ - ReplaceAttr: removeTime, - })) - slog.SetDefault(log) - - return log, observer -} diff --git a/kod.go b/kod.go index 032a5b7..b7de33f 100644 --- a/kod.go +++ b/kod.go @@ -20,12 +20,14 @@ import ( "go.opentelemetry.io/contrib/instrumentation/runtime" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/log" - "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/sdk/trace" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + sdkresource "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + "go.opentelemetry.io/otel/trace" "github.com/go-kod/kod/interceptor" "github.com/go-kod/kod/internal/hooks" @@ -41,7 +43,8 @@ const ( // Implements[T any] provides a common structure for components, // with logging capabilities and a reference to the component's interface. type Implements[T any] struct { - log *slog.Logger + name string + log *slog.Logger //nolint component_interface_type T } @@ -51,10 +54,21 @@ func (i *Implements[T]) L(ctx context.Context) *slog.Logger { return kslog.LogWithContext(ctx, i.log) } +// T return the associated tracer. +func (i *Implements[T]) Tracer(opts ...trace.TracerOption) trace.Tracer { + return otel.Tracer(i.name, opts...) +} + +// M return the associated meter. +func (i *Implements[T]) Meter(opts ...metric.MeterOption) metric.Meter { + return otel.GetMeterProvider().Meter(i.name, opts...) +} + // setLogger sets the logger for the component. // nolint -func (i *Implements[T]) setLogger(log *slog.Logger) { - i.log = log +func (i *Implements[T]) setLogger(name string, log *slog.Logger) { + i.name = name + i.log = log.With("component", name) } // implements is a marker method to assert implementation of an interface. @@ -461,11 +475,11 @@ func (k *Kod) initOpenTelemetry(ctx context.Context) { lo.Must0(host.Start()) lo.Must0(runtime.Start()) - res := lo.Must(resource.New(ctx, - resource.WithFromEnv(), - resource.WithTelemetrySDK(), - resource.WithHost(), - resource.WithAttributes( + res := lo.Must(sdkresource.New(ctx, + sdkresource.WithFromEnv(), + sdkresource.WithTelemetrySDK(), + sdkresource.WithHost(), + sdkresource.WithAttributes( semconv.ServiceNameKey.String(k.config.Name), semconv.ServiceVersionKey.String(k.config.Version), semconv.DeploymentEnvironmentKey.String(k.config.Env), @@ -478,11 +492,11 @@ func (k *Kod) initOpenTelemetry(ctx context.Context) { } // configureTrace configures the trace provider with the provided context and resource. -func (k *Kod) configureTrace(ctx context.Context, res *resource.Resource) { +func (k *Kod) configureTrace(ctx context.Context, res *sdkresource.Resource) { spanExporter := lo.Must(autoexport.NewSpanExporter(ctx)) - spanProvider := trace.NewTracerProvider( - trace.WithBatcher(spanExporter), - trace.WithResource(res), + spanProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(spanExporter), + sdktrace.WithResource(res), ) otel.SetTextMapPropagator( @@ -499,11 +513,11 @@ func (k *Kod) configureTrace(ctx context.Context, res *resource.Resource) { } // configureMetric configures the metric provider with the provided context and resource. -func (k *Kod) configureMetric(ctx context.Context, res *resource.Resource) { +func (k *Kod) configureMetric(ctx context.Context, res *sdkresource.Resource) { metricReader := lo.Must(autoexport.NewMetricReader(ctx)) - metricProvider := metric.NewMeterProvider( - metric.WithReader(metricReader), - metric.WithResource(res), + metricProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(metricReader), + sdkmetric.WithResource(res), ) otel.SetMeterProvider(metricProvider) @@ -515,7 +529,7 @@ func (k *Kod) configureMetric(ctx context.Context, res *resource.Resource) { } // configureLog configures the log provider with the provided context and resource. -func (k *Kod) configureLog(ctx context.Context, res *resource.Resource) { +func (k *Kod) configureLog(ctx context.Context, res *sdkresource.Resource) { logExporter := lo.Must(autoexport.NewLogExporter(ctx)) loggerProvider := log.NewLoggerProvider( log.WithProcessor( diff --git a/registry.go b/registry.go index 37c8b28..6f23426 100644 --- a/registry.go +++ b/registry.go @@ -101,8 +101,7 @@ func (k *Kod) get(ctx context.Context, reg *Registration) (any, error) { } // Fill logger. - logger := k.log.With(slog.String("component", reg.Name)) - if err := fillLog(obj, logger); err != nil { + if err := fillLog(reg.Name, obj, k.log); err != nil { return nil, err } @@ -136,13 +135,13 @@ func (k *Kod) get(ctx context.Context, reg *Registration) (any, error) { return obj, nil } -func fillLog(obj any, log *slog.Logger) error { - x, ok := obj.(interface{ setLogger(*slog.Logger) }) +func fillLog(name string, obj any, log *slog.Logger) error { + x, ok := obj.(interface{ setLogger(string, *slog.Logger) }) if !ok { return fmt.Errorf("fillLog: %T does not implement kod.Implements", obj) } - x.setLogger(log) + x.setLogger(name, log) return nil } diff --git a/registry_test.go b/registry_test.go index 04924ea..33169e9 100644 --- a/registry_test.go +++ b/registry_test.go @@ -13,7 +13,7 @@ import ( func TestFill(t *testing.T) { t.Run("case 1", func(t *testing.T) { - assert.NotNil(t, fillLog(nil, nil)) + assert.NotNil(t, fillLog("", nil, nil)) }) t.Run("case 2", func(t *testing.T) {