Skip to content

Commit

Permalink
feat: add Tracer and Meter for components
Browse files Browse the repository at this point in the history
  • Loading branch information
sysulq committed Sep 6, 2024
1 parent 80d1102 commit ba8379d
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 53 deletions.
58 changes: 50 additions & 8 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
48 changes: 29 additions & 19 deletions internal/kslog/log_observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
54 changes: 34 additions & 20 deletions kod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -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(
Expand Down
9 changes: 4 additions & 5 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit ba8379d

Please sign in to comment.