Skip to content

Commit 07deb53

Browse files
committed
feat: @bentocache/otel package
1 parent 4426d7d commit 07deb53

26 files changed

Lines changed: 1696 additions & 218 deletions

.changeset/tidy-experts-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bentocache/otel': patch
3+
---
4+
5+
Initial release

compose.yml

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -97,53 +97,23 @@ services:
9797
ports:
9898
- "3306:3306"
9999

100-
loki:
101-
image: grafana/loki:2.8.0
100+
lgtm:
101+
image: grafana/otel-lgtm:latest
102+
extra_hosts:
103+
- "host.docker.internal:host-gateway"
102104
ports:
103-
- "3100:3100"
104-
command: -config.file=/etc/loki/local-config.yaml
105-
networks:
106-
- loki
107-
108-
prometheus:
109-
image: prom/prometheus:v2.45.2
110-
ports:
111-
- "9090:9090"
112-
volumes:
113-
- ./docker/prometheus.yml:/etc/prometheus/prometheus.yml
114-
networks:
115-
- loki
116-
117-
grafana:
105+
- "3001:3000" # Grafana
106+
- "3100:3100" # Loki HTTP API
107+
- "3200:3200" # Tempo HTTP API
108+
- "4317:4317" # OTLP gRPC
109+
- "4318:4318" # OTLP HTTP
110+
- "9090:9090" # Prometheus
118111
environment:
119-
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
112+
- GF_SECURITY_ADMIN_USER=admin
113+
- GF_SECURITY_ADMIN_PASSWORD=admin
120114
- GF_AUTH_ANONYMOUS_ENABLED=true
121115
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
122-
entrypoint:
123-
- sh
124-
- -euc
125-
- |
126-
mkdir -p /etc/grafana/provisioning/datasources
127-
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
128-
apiVersion: 1
129-
datasources:
130-
- name: Loki
131-
type: loki
132-
access: proxy
133-
orgId: 1
134-
url: http://loki:3100
135-
basicAuth: false
136-
isDefault: true
137-
version: 1
138-
editable: false
139-
EOF
140-
/run.sh
141-
image: grafana/grafana:latest
142-
ports:
143-
- "3000:3000"
144-
networks:
145-
- loki
116+
- GF_AUTH_DISABLE_LOGIN_FORM=true
146117

147118
networks:
148-
loki:
149119
valkey-cluster:

docs/content/docs/db.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
"contentPath": "./stampede_protection.md",
7878
"category": "Guides"
7979
},
80+
{
81+
"permalink": "telemetry",
82+
"title": "Telemetry",
83+
"contentPath": "./telemetry.md",
84+
"category": "Digging Deeper"
85+
},
8086
{
8187
"permalink": "adaptive-caching",
8288
"title": "Adaptive caching",

docs/content/docs/digging_deeper/events.md

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -140,105 +140,3 @@ Emitted when the application receives a message instructing it to update its cac
140140
Payload: `{ message }`
141141

142142
- `message`: The message that was received
143-
144-
---
145-
146-
## Tracing Channels (Experimental)
147-
148-
:::warning
149-
This API is experimental and may change in future versions.
150-
:::
151-
152-
Bentocache also exposes [Node.js Diagnostic Channels](https://nodejs.org/api/diagnostics_channel.html#tracingchannel) for more advanced instrumentation needs. Unlike events, tracing channels provide timing information and are designed for APM tools and OpenTelemetry integration.
153-
154-
### Why Tracing Channels?
155-
156-
- **Built-in**: No external dependencies, uses Node.js native `diagnostics_channel`
157-
- **Zero overhead**: When no subscribers are attached, there's virtually no performance impact
158-
- **Timing information**: Automatically tracks start/end of async operations
159-
- **APM integration**: Works seamlessly with OpenTelemetry and other APM tools
160-
161-
### Available Channels
162-
163-
#### `bentocache.cache.operation`
164-
165-
Traces all cache operations with timing information.
166-
167-
```ts
168-
import { tracingChannels } from 'bentocache'
169-
170-
tracingChannels.cacheOperation.subscribe({
171-
start(message) {
172-
// Called when operation starts
173-
console.log(`Starting ${message.operation} on ${message.key}`)
174-
},
175-
asyncEnd(message) {
176-
// Called when async operation completes
177-
console.log(`Completed ${message.operation}`, {
178-
key: message.key,
179-
store: message.store,
180-
hit: message.hit,
181-
tier: message.tier,
182-
graced: message.graced,
183-
})
184-
},
185-
error({ error, ...message }) {
186-
// Called when operation fails
187-
console.error(`Failed ${message.operation}:`, error)
188-
},
189-
})
190-
```
191-
192-
### Message Properties
193-
194-
| Property | Type | Description |
195-
| ----------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
196-
| `operation` | `'get' \| 'set' \| 'delete' \| 'deleteMany' \| 'clear' \| 'expire'` | The operation type |
197-
| `key` | `string` | Cache key with full prefix (e.g., `'users:123'`) |
198-
| `keys` | `string[]` | Multiple keys for `deleteMany` operation |
199-
| `store` | `string` | Store name |
200-
| `hit` | `boolean` | Whether the key was found (only for `get`) |
201-
| `tier` | `'l1' \| 'l2'` | Which tier served the value (only for `get` hits) |
202-
| `graced` | `boolean` | Whether value came from grace period (only for `get`) |
203-
204-
### OpenTelemetry Integration Example
205-
206-
```ts
207-
import { tracingChannels, type CacheOperationMessage } from 'bentocache'
208-
import { trace } from '@opentelemetry/api'
209-
210-
const tracer = trace.getTracer('bentocache')
211-
const spans = new WeakMap()
212-
213-
tracingChannels.cacheOperation.subscribe({
214-
start(message: CacheOperationMessage) {
215-
const span = tracer.startSpan(`cache.${message.operation}`)
216-
span.setAttribute('cache.key', message.key ?? '')
217-
span.setAttribute('cache.store', message.store)
218-
spans.set(message, span)
219-
},
220-
asyncEnd(message: CacheOperationMessage) {
221-
const span = spans.get(message)
222-
if (!span) return
223-
224-
if (message.hit !== undefined) {
225-
span.setAttribute('cache.hit', message.hit)
226-
}
227-
if (message.tier) {
228-
span.setAttribute('cache.tier', message.tier)
229-
}
230-
if (message.graced) {
231-
span.setAttribute('cache.graced', message.graced)
232-
}
233-
234-
span.end()
235-
},
236-
error({ error, ...message }) {
237-
const span = spans.get(message)
238-
if (!span) return
239-
240-
span.recordException(error)
241-
span.end()
242-
},
243-
})
244-
```

docs/content/docs/options.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,21 @@ Levels: `global`
204204
Only configurable at the BentoCache level.
205205

206206
See [events](./digging_deeper/events.md) for more details.
207+
208+
### `internalOperationWrapper`
209+
210+
Default: `undefined`
211+
212+
Levels: `global`
213+
214+
Wrap internal Bentocache operations (L2 cache + bus) to suppress or customize instrumentation.
215+
This does not affect userland factory execution.
216+
217+
```ts
218+
import { context } from '@opentelemetry/api'
219+
import { suppressTracing } from '@opentelemetry/core'
220+
221+
const bento = new BentoCache({
222+
internalOperationWrapper: (fn) => context.with(suppressTracing(context.active()), fn),
223+
})
224+
```

docs/content/docs/telemetry.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
summary: Instrument Bentocache with OpenTelemetry using @bentocache/otel
3+
---
4+
5+
# Telemetry (OpenTelemetry)
6+
7+
:::warning
8+
This package is experimental and may change in future versions.
9+
:::
10+
11+
Bentocache provides an official OpenTelemetry instrumentation package: `@bentocache/otel`.
12+
13+
14+
It listens to Bentocache tracing channels and emits spans like:
15+
16+
- `cache.get`
17+
- `cache.getOrSet`
18+
- `cache.set`
19+
- `cache.delete`
20+
- `cache.deleteMany`
21+
- `cache.clear`
22+
- `cache.expire`
23+
- `cache.factory`
24+
25+
## Tracing Channels
26+
27+
Bentocache exposes [Node.js Diagnostic Channels](https://nodejs.org/api/diagnostics_channel.html#tracingchannel) for advanced instrumentation and APM integration, which `@bentocache/otel` utilizes for OpenTelemetry spans. You can also subscribe to these channels directly for custom telemetry or monitoring solutions.
28+
29+
### Available channels
30+
31+
#### `bentocache.cache.operation`
32+
33+
Traces cache operations with timing information.
34+
35+
```ts
36+
import { tracingChannels } from 'bentocache'
37+
38+
tracingChannels.cacheOperation.subscribe({
39+
start(message) {
40+
// Called when operation starts
41+
console.log(`Starting ${message.operation} on ${message.key}`)
42+
},
43+
asyncEnd(message) {
44+
// Called when async operation completes
45+
console.log(`Completed ${message.operation}`, {
46+
key: message.key,
47+
store: message.store,
48+
hit: message.hit,
49+
tier: message.tier,
50+
graced: message.graced,
51+
})
52+
},
53+
error({ error, ...message }) {
54+
// Called when operation fails
55+
console.error(`Failed ${message.operation}:`, error)
56+
},
57+
})
58+
```
59+
60+
### Message properties
61+
62+
| Property | Type | Description |
63+
| ----------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
64+
| `operation` | `'get' \| 'set' \| 'delete' \| 'deleteMany' \| 'clear' \| 'expire'` | The operation type |
65+
| `key` | `string` | Cache key with full prefix (e.g., `'users:123'`) |
66+
| `keys` | `string[]` | Multiple keys for `deleteMany` operation |
67+
| `store` | `string` | Store name |
68+
| `hit` | `boolean` | Whether the key was found (only for `get`) |
69+
| `tier` | `'l1' \| 'l2'` | Which tier served the value (only for `get` hits) |
70+
| `graced` | `boolean` | Whether value came from grace period (only for `get`) |
71+
72+
## Install
73+
74+
:::codegroup
75+
```sh
76+
// title: npm
77+
npm i @bentocache/otel
78+
```
79+
80+
```sh
81+
// title: pnpm
82+
pnpm add @bentocache/otel
83+
```
84+
85+
```sh
86+
// title: yarn
87+
yarn add @bentocache/otel
88+
```
89+
:::
90+
91+
## Basic setup
92+
93+
```ts
94+
import { NodeSDK } from '@opentelemetry/sdk-node'
95+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
96+
import { BentoCacheInstrumentation } from '@bentocache/otel'
97+
98+
const traceExporter = new OTLPTraceExporter({
99+
url: 'http://localhost:4318/v1/traces',
100+
})
101+
102+
const sdk = new NodeSDK({
103+
serviceName: 'my-app',
104+
traceExporter,
105+
instrumentations: [
106+
new BentoCacheInstrumentation({
107+
requireParentSpan: true,
108+
includeKeys: false,
109+
suppressInternalOperations: true,
110+
}),
111+
],
112+
})
113+
114+
sdk.start()
115+
```
116+
117+
## Parent spans and `requireParentSpan`
118+
119+
By default, `requireParentSpan` is `true`.
120+
This means Bentocache spans are created only when a parent span is active.
121+
122+
If you do not use HTTP/framework instrumentation yet, you can:
123+
124+
- Set `requireParentSpan: false`, or
125+
- Create parent spans manually around your application logic.
126+
127+
```ts
128+
import { trace } from '@opentelemetry/api'
129+
130+
const tracer = trace.getTracer('app')
131+
132+
await tracer.startActiveSpan('request', async (span) => {
133+
await bento.getOrSet({
134+
key: 'user:1',
135+
ttl: '5m',
136+
factory: () => fetchUser(),
137+
})
138+
139+
span.end()
140+
})
141+
```
142+
143+
## Span naming and attributes
144+
145+
Default span names use `cache.<operation>`.
146+
You can customize naming with `spanName` or `spanNamePrefix`.
147+
148+
Common attributes:
149+
150+
- `cache.operation`
151+
- `cache.store`
152+
- `cache.key`
153+
- `cache.keys`
154+
- `cache.hit`
155+
- `cache.tier`
156+
- `cache.graced`
157+
158+
## Configuration options
159+
160+
- `requireParentSpan` (default: `true`): Create spans only when a parent span exists.
161+
- `includeKeys` (default: `false`): Include key/keys attributes on spans.
162+
- `keySanitizer`: Sanitize keys before adding them to span attributes.
163+
- `spanName`: Provide a custom span-name factory.
164+
- `spanNamePrefix` (default: `cache`): Prefix for default span names.
165+
- `suppressInternalOperations` (default: `true`): Suppress internal L2/bus operations.

packages/bentocache/src/bento_cache_options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class BentoCacheOptions {
7979
*/
8080
serializeL1: boolean = true
8181
onFactoryError?: (error: FactoryError) => void
82+
internalOperationWrapper?: RawBentoCacheOptions['internalOperationWrapper']
8283

8384
constructor(options: RawBentoCacheOptions) {
8485
this.#options = { ...this, ...options }
@@ -98,6 +99,7 @@ export class BentoCacheOptions {
9899

99100
this.logger = new Logger(this.#options.logger ?? noopLogger())
100101
this.onFactoryError = this.#options.onFactoryError
102+
this.internalOperationWrapper = this.#options.internalOperationWrapper
101103
}
102104

103105
serializeL1Cache(shouldSerialize: boolean = true) {

0 commit comments

Comments
 (0)