Skip to content

Commit ba8379d

Browse files
committed
feat: add Tracer and Meter for components
1 parent 80d1102 commit ba8379d

File tree

5 files changed

+118
-53
lines changed

5 files changed

+118
-53
lines changed

example_test.go

+50-8
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
// This example demonstrates how to use [kod.Run] and [kod.Implements] to run a simple application.
18-
func Example_mainComponent() {
18+
func Example_componentMain() {
1919
kod.Run(context.Background(), func(ctx context.Context, app *helloworld.App) error {
2020
fmt.Println("Hello, World!")
2121
return nil
@@ -92,8 +92,8 @@ func Example_configGlobal() {
9292
// helloWorld shutdown
9393
}
9494

95-
// This example demonstrates how to use [kod.WithLogger] to provide a custom logger to the application.
96-
func Example_log() {
95+
// This example demonstrates how to use logging with OpenTelemetry.
96+
func Example_openTelemetryLog() {
9797
logger, observer := kod.NewTestLogger()
9898

9999
kod.RunTest(&testing.T{}, func(ctx context.Context, app *helloworld.App) {
@@ -104,15 +104,57 @@ func Example_log() {
104104
app.HelloWorld.Get().SayHello(ctx)
105105
}, kod.WithLogger(logger))
106106

107-
fmt.Println(observer)
107+
fmt.Println(observer.RemoveKeys("trace_id", "span_id", "time"))
108+
109+
// Output:
110+
// helloWorld init
111+
// Hello, World!
112+
// helloWorld shutdown
113+
// {"component":"github.com/go-kod/kod/Main","level":"INFO","msg":"Hello, World!"}
114+
// {"component":"github.com/go-kod/kod/Main","level":"WARN","msg":"Hello, World!"}
115+
// {"component":"github.com/go-kod/kod/Main","level":"ERROR","msg":"Hello, World!"}
116+
// {"component":"github.com/go-kod/kod/examples/helloworld/HelloWorld","level":"INFO","msg":"Hello, World!"}
117+
}
118+
119+
// This example demonstrates how to use tracing with OpenTelemetry.
120+
func Example_openTelemetryTrace() {
121+
logger, observer := kod.NewTestLogger()
122+
123+
kod.Run(context.Background(), func(ctx context.Context, app *helloworld.App) error {
124+
ctx, span := app.Tracer().Start(ctx, "example")
125+
defer span.End()
126+
app.L(ctx).Info("Hello, World!")
127+
app.L(ctx).WarnContext(ctx, "Hello, World!")
128+
129+
app.HelloWorld.Get().SayHello(ctx)
130+
return nil
131+
}, kod.WithInterceptors(ktrace.Interceptor()), kod.WithLogger(logger))
132+
133+
fmt.Println(observer.Filter(func(m map[string]any) bool {
134+
return m["trace_id"] != nil && m["span_id"] != nil
135+
}).RemoveKeys("trace_id", "span_id", "time"))
136+
108137
// Output:
109138
// helloWorld init
110139
// Hello, World!
111140
// helloWorld shutdown
112-
// {"level":"INFO","msg":"Hello, World!","component":"github.com/go-kod/kod/Main"}
113-
// {"level":"WARN","msg":"Hello, World!","component":"github.com/go-kod/kod/Main"}
114-
// {"level":"ERROR","msg":"Hello, World!","component":"github.com/go-kod/kod/Main"}
115-
// {"level":"INFO","msg":"Hello, World!","component":"github.com/go-kod/kod/examples/helloworld/HelloWorld"}
141+
// {"component":"github.com/go-kod/kod/Main","level":"INFO","msg":"Hello, World!"}
142+
// {"component":"github.com/go-kod/kod/Main","level":"WARN","msg":"Hello, World!"}
143+
// {"component":"github.com/go-kod/kod/examples/helloworld/HelloWorld","level":"INFO","msg":"Hello, World!"}
144+
}
145+
146+
// This example demonstrates how to use metrics with OpenTelemetry.
147+
func Example_openTelemetryMetric() {
148+
kod.Run(context.Background(), func(ctx context.Context, app *helloworld.App) error {
149+
metric, _ := app.Meter().Int64Counter("example")
150+
metric.Add(ctx, 1)
151+
152+
return nil
153+
})
154+
155+
// Output:
156+
// helloWorld init
157+
// helloWorld shutdown
116158
}
117159

118160
// This example demonstrates how to use [kod.WithInterceptors] to provide a custom interceptor to the application.

internal/kslog/log_observer.go

+29-19
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import (
99
"github.com/samber/lo"
1010
)
1111

12-
// removeTime removes the top-level time attribute.
13-
// It is intended to be used as a ReplaceAttr function,
14-
// to make example output deterministic.
15-
func removeTime(groups []string, a slog.Attr) slog.Attr {
16-
if a.Key == slog.TimeKey && len(groups) == 0 {
17-
return slog.Attr{}
12+
// NewTestLogger returns a new test logger.
13+
func NewTestLogger() (*slog.Logger, *observer) {
14+
observer := &observer{
15+
buf: new(bytes.Buffer),
1816
}
19-
return a
17+
log := slog.New(slog.NewJSONHandler(observer.buf, nil))
18+
slog.SetDefault(log)
19+
20+
return log, observer
2021
}
2122

2223
type observer struct {
@@ -77,21 +78,30 @@ func (b *observer) Filter(filter func(map[string]any) bool) *observer {
7778
}
7879
}
7980

81+
// RemoveKeys removes the provided keys from the observed logs.
82+
func (b *observer) RemoveKeys(keys ...string) *observer {
83+
filtered := make([]map[string]any, 0)
84+
for _, line := range b.parse() {
85+
for _, key := range keys {
86+
delete(line, key)
87+
}
88+
89+
filtered = append(filtered, line)
90+
}
91+
92+
buf := new(bytes.Buffer)
93+
for _, line := range filtered {
94+
lo.Must0(json.NewEncoder(buf).Encode(line))
95+
}
96+
97+
return &observer{
98+
buf: buf,
99+
}
100+
}
101+
80102
// Clean clears the observed logs.
81103
func (b *observer) Clean() *observer {
82104
b.buf.Reset()
83105

84106
return b
85107
}
86-
87-
func NewTestLogger() (*slog.Logger, *observer) {
88-
observer := &observer{
89-
buf: new(bytes.Buffer),
90-
}
91-
log := slog.New(slog.NewJSONHandler(observer.buf, &slog.HandlerOptions{
92-
ReplaceAttr: removeTime,
93-
}))
94-
slog.SetDefault(log)
95-
96-
return log, observer
97-
}

kod.go

+34-20
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import (
2020
"go.opentelemetry.io/contrib/instrumentation/runtime"
2121
"go.opentelemetry.io/otel"
2222
"go.opentelemetry.io/otel/log/global"
23+
"go.opentelemetry.io/otel/metric"
2324
"go.opentelemetry.io/otel/propagation"
2425
"go.opentelemetry.io/otel/sdk/log"
25-
"go.opentelemetry.io/otel/sdk/metric"
26-
"go.opentelemetry.io/otel/sdk/resource"
27-
"go.opentelemetry.io/otel/sdk/trace"
26+
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
27+
sdkresource "go.opentelemetry.io/otel/sdk/resource"
28+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
2829
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
30+
"go.opentelemetry.io/otel/trace"
2931

3032
"github.com/go-kod/kod/interceptor"
3133
"github.com/go-kod/kod/internal/hooks"
@@ -41,7 +43,8 @@ const (
4143
// Implements[T any] provides a common structure for components,
4244
// with logging capabilities and a reference to the component's interface.
4345
type Implements[T any] struct {
44-
log *slog.Logger
46+
name string
47+
log *slog.Logger
4548
//nolint
4649
component_interface_type T
4750
}
@@ -51,10 +54,21 @@ func (i *Implements[T]) L(ctx context.Context) *slog.Logger {
5154
return kslog.LogWithContext(ctx, i.log)
5255
}
5356

57+
// T return the associated tracer.
58+
func (i *Implements[T]) Tracer(opts ...trace.TracerOption) trace.Tracer {
59+
return otel.Tracer(i.name, opts...)
60+
}
61+
62+
// M return the associated meter.
63+
func (i *Implements[T]) Meter(opts ...metric.MeterOption) metric.Meter {
64+
return otel.GetMeterProvider().Meter(i.name, opts...)
65+
}
66+
5467
// setLogger sets the logger for the component.
5568
// nolint
56-
func (i *Implements[T]) setLogger(log *slog.Logger) {
57-
i.log = log
69+
func (i *Implements[T]) setLogger(name string, log *slog.Logger) {
70+
i.name = name
71+
i.log = log.With("component", name)
5872
}
5973

6074
// implements is a marker method to assert implementation of an interface.
@@ -461,11 +475,11 @@ func (k *Kod) initOpenTelemetry(ctx context.Context) {
461475
lo.Must0(host.Start())
462476
lo.Must0(runtime.Start())
463477

464-
res := lo.Must(resource.New(ctx,
465-
resource.WithFromEnv(),
466-
resource.WithTelemetrySDK(),
467-
resource.WithHost(),
468-
resource.WithAttributes(
478+
res := lo.Must(sdkresource.New(ctx,
479+
sdkresource.WithFromEnv(),
480+
sdkresource.WithTelemetrySDK(),
481+
sdkresource.WithHost(),
482+
sdkresource.WithAttributes(
469483
semconv.ServiceNameKey.String(k.config.Name),
470484
semconv.ServiceVersionKey.String(k.config.Version),
471485
semconv.DeploymentEnvironmentKey.String(k.config.Env),
@@ -478,11 +492,11 @@ func (k *Kod) initOpenTelemetry(ctx context.Context) {
478492
}
479493

480494
// configureTrace configures the trace provider with the provided context and resource.
481-
func (k *Kod) configureTrace(ctx context.Context, res *resource.Resource) {
495+
func (k *Kod) configureTrace(ctx context.Context, res *sdkresource.Resource) {
482496
spanExporter := lo.Must(autoexport.NewSpanExporter(ctx))
483-
spanProvider := trace.NewTracerProvider(
484-
trace.WithBatcher(spanExporter),
485-
trace.WithResource(res),
497+
spanProvider := sdktrace.NewTracerProvider(
498+
sdktrace.WithBatcher(spanExporter),
499+
sdktrace.WithResource(res),
486500
)
487501

488502
otel.SetTextMapPropagator(
@@ -499,11 +513,11 @@ func (k *Kod) configureTrace(ctx context.Context, res *resource.Resource) {
499513
}
500514

501515
// configureMetric configures the metric provider with the provided context and resource.
502-
func (k *Kod) configureMetric(ctx context.Context, res *resource.Resource) {
516+
func (k *Kod) configureMetric(ctx context.Context, res *sdkresource.Resource) {
503517
metricReader := lo.Must(autoexport.NewMetricReader(ctx))
504-
metricProvider := metric.NewMeterProvider(
505-
metric.WithReader(metricReader),
506-
metric.WithResource(res),
518+
metricProvider := sdkmetric.NewMeterProvider(
519+
sdkmetric.WithReader(metricReader),
520+
sdkmetric.WithResource(res),
507521
)
508522

509523
otel.SetMeterProvider(metricProvider)
@@ -515,7 +529,7 @@ func (k *Kod) configureMetric(ctx context.Context, res *resource.Resource) {
515529
}
516530

517531
// configureLog configures the log provider with the provided context and resource.
518-
func (k *Kod) configureLog(ctx context.Context, res *resource.Resource) {
532+
func (k *Kod) configureLog(ctx context.Context, res *sdkresource.Resource) {
519533
logExporter := lo.Must(autoexport.NewLogExporter(ctx))
520534
loggerProvider := log.NewLoggerProvider(
521535
log.WithProcessor(

registry.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ func (k *Kod) get(ctx context.Context, reg *Registration) (any, error) {
101101
}
102102

103103
// Fill logger.
104-
logger := k.log.With(slog.String("component", reg.Name))
105-
if err := fillLog(obj, logger); err != nil {
104+
if err := fillLog(reg.Name, obj, k.log); err != nil {
106105
return nil, err
107106
}
108107

@@ -136,13 +135,13 @@ func (k *Kod) get(ctx context.Context, reg *Registration) (any, error) {
136135
return obj, nil
137136
}
138137

139-
func fillLog(obj any, log *slog.Logger) error {
140-
x, ok := obj.(interface{ setLogger(*slog.Logger) })
138+
func fillLog(name string, obj any, log *slog.Logger) error {
139+
x, ok := obj.(interface{ setLogger(string, *slog.Logger) })
141140
if !ok {
142141
return fmt.Errorf("fillLog: %T does not implement kod.Implements", obj)
143142
}
144143

145-
x.setLogger(log)
144+
x.setLogger(name, log)
146145
return nil
147146
}
148147

registry_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
func TestFill(t *testing.T) {
1515
t.Run("case 1", func(t *testing.T) {
16-
assert.NotNil(t, fillLog(nil, nil))
16+
assert.NotNil(t, fillLog("", nil, nil))
1717
})
1818

1919
t.Run("case 2", func(t *testing.T) {

0 commit comments

Comments
 (0)