Skip to content

Commit 82bed8d

Browse files
authored
Merge pull request #42 from hyp3rd/feat/observability
feat(observability): add OpenTelemetry tracing and metrics middleware…
2 parents c8ab2a2 + ffc5df6 commit 82bed8d

7 files changed

Lines changed: 420 additions & 0 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ make run-example group=eviction # or any other example
6969

7070
For a complete list of examples, refer to the [examples](./__examples/README.md) directory.
7171

72+
### Observability (OpenTelemetry)
73+
74+
HyperCache provides optional OpenTelemetry middleware for tracing and metrics.
75+
76+
- Tracing: wrap the service with `middleware.NewOTelTracingMiddleware` using a `trace.Tracer`.
77+
- Metrics: wrap with `middleware.NewOTelMetricsMiddleware` using a `metric.Meter`.
78+
79+
Example wiring (see `__examples/observability/otel.go`):
80+
81+
```go
82+
svc := hypercache.ApplyMiddleware(svc,
83+
func(next hypercache.Service) hypercache.Service { return middleware.NewOTelTracingMiddleware(next, tracer) },
84+
func(next hypercache.Service) hypercache.Service { mw, _ := middleware.NewOTelMetricsMiddleware(next, meter); return mw },
85+
)
86+
```
87+
88+
Use your preferred OpenTelemetry SDK setup for exporters and processors in production; the example uses no-op providers for simplicity.
89+
7290
## API
7391

7492
The `NewInMemoryWithDefaults` function creates a new `HyperCache` instance with the defaults:

__examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ All the code in this directory is for demonstration purposes only.
2121
8. [`Middleware`](./middleware/middleware.go) - An example of implementing a custom middleware and register it with the `HyperCacheService`.
2222

2323
9. [`Size`](./size/size.go) - An example of using the HyperCache package to store a list of items and limit the cache based on size.
24+
25+
10. [`Observability (OpenTelemetry)`](./observability/otel.go) - Demonstrates wrapping the service with tracing and metrics middleware using OpenTelemetry.

__examples/observability/otel.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"go.opentelemetry.io/otel/attribute"
10+
"go.opentelemetry.io/otel/metric/noop"
11+
"go.opentelemetry.io/otel/trace"
12+
13+
"github.com/hyp3rd/hypercache"
14+
"github.com/hyp3rd/hypercache/pkg/middleware"
15+
)
16+
17+
// This example shows how to wrap HyperCache with OpenTelemetry middleware.
18+
func main() {
19+
cache, err := hypercache.NewInMemoryWithDefaults(16)
20+
if err != nil {
21+
fmt.Fprintln(os.Stderr, err)
22+
return
23+
}
24+
25+
// Build a service from the cache to apply middleware.
26+
svc := hypercache.Service(cache)
27+
28+
// Use noop providers for a minimal example. Replace with real SDK providers in production.
29+
meter := noop.NewMeterProvider().Meter("hypercache/examples")
30+
tracer := trace.NewNoopTracerProvider().Tracer("hypercache/examples")
31+
32+
// Apply OTel tracing and metrics middleware.
33+
svc = hypercache.ApplyMiddleware(svc,
34+
func(next hypercache.Service) hypercache.Service {
35+
return middleware.NewOTelTracingMiddleware(next, tracer, middleware.WithCommonAttributes(
36+
attribute.String("component", "hypercache"),
37+
))
38+
},
39+
func(next hypercache.Service) hypercache.Service {
40+
mw, _ := middleware.NewOTelMetricsMiddleware(next, meter)
41+
return mw
42+
},
43+
)
44+
defer svc.Stop()
45+
46+
_ = svc.Set(context.Background(), "key", "value", time.Minute)
47+
if v, ok := svc.Get(context.Background(), "key"); ok {
48+
fmt.Println("got:", v)
49+
}
50+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ require (
99
github.com/redis/go-redis/v9 v9.12.1
1010
github.com/shamaton/msgpack/v2 v2.3.0
1111
github.com/ugorji/go/codec v1.3.0
12+
go.opentelemetry.io/otel v1.37.0
13+
go.opentelemetry.io/otel/metric v1.37.0
14+
go.opentelemetry.io/otel/trace v1.37.0
1215
)
1316

1417
require (

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
88
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
99
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
1010
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
11+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
12+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
13+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
14+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
1115
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
1216
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
1317
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -28,6 +32,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
2832
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2933
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
3034
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
35+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
36+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
37+
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
38+
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
39+
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
40+
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
41+
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
42+
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
3143
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
3244
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3345
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

pkg/middleware/otel_metrics.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package middleware
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"go.opentelemetry.io/otel/attribute"
9+
"go.opentelemetry.io/otel/metric"
10+
11+
"github.com/hyp3rd/hypercache"
12+
"github.com/hyp3rd/hypercache/pkg/backend"
13+
"github.com/hyp3rd/hypercache/pkg/cache"
14+
"github.com/hyp3rd/hypercache/pkg/stats"
15+
)
16+
17+
// OTelMetricsMiddleware emits OpenTelemetry metrics for service methods.
18+
type OTelMetricsMiddleware struct {
19+
next hypercache.Service
20+
meter metric.Meter
21+
22+
// instruments
23+
calls metric.Int64Counter
24+
durations metric.Float64Histogram
25+
}
26+
27+
// NewOTelMetricsMiddleware constructs a metrics middleware using the provided meter.
28+
func NewOTelMetricsMiddleware(next hypercache.Service, meter metric.Meter) (hypercache.Service, error) {
29+
calls, err := meter.Int64Counter("hypercache.calls")
30+
if err != nil {
31+
return nil, fmt.Errorf("create counter: %w", err)
32+
}
33+
34+
durations, err := meter.Float64Histogram("hypercache.duration.ms")
35+
if err != nil {
36+
return nil, fmt.Errorf("create histogram: %w", err)
37+
}
38+
39+
return &OTelMetricsMiddleware{next: next, meter: meter, calls: calls, durations: durations}, nil
40+
}
41+
42+
// Get implements Service.Get with metrics.
43+
func (mw *OTelMetricsMiddleware) Get(ctx context.Context, key string) (any, bool) {
44+
start := time.Now()
45+
v, ok := mw.next.Get(ctx, key)
46+
mw.rec(ctx, "Get", start, attribute.Int("key.len", len(key)), attribute.Bool("hit", ok))
47+
48+
return v, ok
49+
}
50+
51+
// Set implements Service.Set with metrics.
52+
func (mw *OTelMetricsMiddleware) Set(ctx context.Context, key string, value any, expiration time.Duration) error {
53+
start := time.Now()
54+
err := mw.next.Set(ctx, key, value, expiration)
55+
mw.rec(ctx, "Set", start, attribute.Int("key.len", len(key)))
56+
57+
return err
58+
}
59+
60+
// GetOrSet implements Service.GetOrSet with metrics.
61+
func (mw *OTelMetricsMiddleware) GetOrSet(ctx context.Context, key string, value any, expiration time.Duration) (any, error) {
62+
start := time.Now()
63+
v, err := mw.next.GetOrSet(ctx, key, value, expiration)
64+
mw.rec(ctx, "GetOrSet", start, attribute.Int("key.len", len(key)))
65+
66+
return v, err
67+
}
68+
69+
// GetWithInfo implements Service.GetWithInfo with metrics.
70+
func (mw *OTelMetricsMiddleware) GetWithInfo(ctx context.Context, key string) (*cache.Item, bool) {
71+
start := time.Now()
72+
it, ok := mw.next.GetWithInfo(ctx, key)
73+
mw.rec(ctx, "GetWithInfo", start, attribute.Int("key.len", len(key)), attribute.Bool("hit", ok))
74+
75+
return it, ok
76+
}
77+
78+
// GetMultiple implements Service.GetMultiple with metrics.
79+
func (mw *OTelMetricsMiddleware) GetMultiple(ctx context.Context, keys ...string) (map[string]any, map[string]error) {
80+
start := time.Now()
81+
res, failed := mw.next.GetMultiple(ctx, keys...)
82+
mw.rec(ctx, "GetMultiple", start, attribute.Int("keys.count", len(keys)), attribute.Int("result.count", len(res)), attribute.Int("failed.count", len(failed)))
83+
84+
return res, failed
85+
}
86+
87+
// List implements Service.List with metrics.
88+
func (mw *OTelMetricsMiddleware) List(ctx context.Context, filters ...backend.IFilter) ([]*cache.Item, error) {
89+
start := time.Now()
90+
items, err := mw.next.List(ctx, filters...)
91+
92+
n := 0
93+
if items != nil {
94+
n = len(items)
95+
}
96+
97+
mw.rec(ctx, "List", start, attribute.Int("items.count", n))
98+
99+
return items, err
100+
}
101+
102+
// Remove implements Service.Remove with metrics.
103+
func (mw *OTelMetricsMiddleware) Remove(ctx context.Context, keys ...string) error {
104+
start := time.Now()
105+
err := mw.next.Remove(ctx, keys...)
106+
mw.rec(ctx, "Remove", start, attribute.Int("keys.count", len(keys)))
107+
108+
return err
109+
}
110+
111+
// Clear implements Service.Clear with metrics.
112+
func (mw *OTelMetricsMiddleware) Clear(ctx context.Context) error {
113+
start := time.Now()
114+
err := mw.next.Clear(ctx)
115+
mw.rec(ctx, "Clear", start)
116+
117+
return err
118+
}
119+
120+
// Capacity returns cache capacity.
121+
func (mw *OTelMetricsMiddleware) Capacity() int { return mw.next.Capacity() }
122+
123+
// Allocation returns allocated size.
124+
func (mw *OTelMetricsMiddleware) Allocation() int64 { return mw.next.Allocation() }
125+
126+
// Count returns items count.
127+
func (mw *OTelMetricsMiddleware) Count(ctx context.Context) int { return mw.next.Count(ctx) }
128+
129+
// TriggerEviction triggers eviction.
130+
func (mw *OTelMetricsMiddleware) TriggerEviction() { mw.next.TriggerEviction() }
131+
132+
// Stop stops the underlying service.
133+
func (mw *OTelMetricsMiddleware) Stop() { mw.next.Stop() }
134+
135+
// GetStats returns stats.
136+
func (mw *OTelMetricsMiddleware) GetStats() stats.Stats { return mw.next.GetStats() }
137+
138+
// rec records call count and duration with attributes.
139+
// Moved to the end to satisfy funcorder linters.
140+
func (mw *OTelMetricsMiddleware) rec(ctx context.Context, method string, start time.Time, attrs ...attribute.KeyValue) {
141+
base := []attribute.KeyValue{attribute.String("method", method)}
142+
if len(attrs) > 0 {
143+
base = append(base, attrs...)
144+
}
145+
146+
mw.calls.Add(ctx, 1, metric.WithAttributes(base...))
147+
mw.durations.Record(ctx, float64(time.Since(start).Milliseconds()), metric.WithAttributes(base...))
148+
}
149+
150+
// keep helpers at end of file

0 commit comments

Comments
 (0)