Skip to content

Commit 79e0cc8

Browse files
committed
feat: add support for otel context bags
1 parent 2450725 commit 79e0cc8

4 files changed

Lines changed: 137 additions & 7 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@athenna/otel",
3-
"version": "5.10.0",
3+
"version": "5.12.0",
44
"description": "OpenTelemetry package for Athenna.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",

src/otel/OtelImpl.ts

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import {
1313
createContextKey,
1414
SpanStatusCode,
1515
type Span,
16-
type Context,
16+
type Context
1717
} from '@opentelemetry/api'
1818

1919
import { Config } from '@athenna/config'
2020
import { NodeSDK } from '@opentelemetry/sdk-node'
2121
import { Options, Macroable } from '@athenna/common'
2222
import { getRPCMetadata, RPCType } from '@opentelemetry/core'
2323

24+
const otelCurrentContextBagKey = Symbol.for('athenna.otel.currentContextBag')
25+
2426
export class OtelImpl extends Macroable {
2527
/**
2628
* Holds the OpenTelemetry SDK instance.
@@ -181,7 +183,10 @@ export class OtelImpl extends Macroable {
181183
* const tenantId = Otel.getContextValue(tenantIdKey)
182184
* ```
183185
*/
184-
public getContextValue<T = any>(key: string | symbol, ctx?: Context): T | undefined {
186+
public getContextValue<T = any>(
187+
key: string | symbol,
188+
ctx?: Context
189+
): T | undefined {
185190
return (ctx || context.active()).getValue(key as any) as T | undefined
186191
}
187192

@@ -193,7 +198,11 @@ export class OtelImpl extends Macroable {
193198
* const nextContext = Otel.setContextValue(tenantIdKey, 'tenant-1')
194199
* ```
195200
*/
196-
public setContextValue<T = any>(key: string | symbol, value: T, ctx?: Context) {
201+
public setContextValue<T = any>(
202+
key: string | symbol,
203+
value: T,
204+
ctx?: Context
205+
) {
197206
return (ctx || context.active()).setValue(key as any, value)
198207
}
199208

@@ -207,10 +216,84 @@ export class OtelImpl extends Macroable {
207216
* })
208217
* ```
209218
*/
210-
public withContextValue<T = any, Result = any>(key: string | symbol, value: T, callback: () => Result, ctx?: Context) {
219+
public withContextValue<T = any, Result = any>(
220+
key: string | symbol,
221+
value: T,
222+
callback: () => Result,
223+
ctx?: Context
224+
) {
211225
return context.with(this.setContextValue(key, value, ctx), callback)
212226
}
213227

228+
/**
229+
* Get a value from the mutable request-scoped context store.
230+
*
231+
* @example
232+
* ```ts
233+
* const exampleId = Otel.getCurrentContextValue('exampleId')
234+
* ```
235+
*/
236+
public getCurrentContextValue<T = any>(
237+
key: string | symbol,
238+
ctx?: Context
239+
): T | undefined {
240+
return this.getCurrentContextStore(ctx).get(key) as T | undefined
241+
}
242+
243+
/**
244+
* Set a value inside the mutable request-scoped context store.
245+
*
246+
* @example
247+
* ```ts
248+
* Otel.setCurrentContextValue('exampleId', 'example-id-from-controller')
249+
* ```
250+
*/
251+
public setCurrentContextValue<T = any>(
252+
key: string | symbol,
253+
value: T,
254+
ctx?: Context
255+
) {
256+
this.getCurrentContextStore(ctx).set(key, value)
257+
258+
return this
259+
}
260+
261+
/**
262+
* Delete a value from the mutable request-scoped context store.
263+
*
264+
* @example
265+
* ```ts
266+
* Otel.deleteCurrentContextValue('exampleId')
267+
* ```
268+
*/
269+
public deleteCurrentContextValue(
270+
key: string | symbol,
271+
ctx?: Context
272+
): boolean {
273+
return this.getCurrentContextStore(ctx).delete(key)
274+
}
275+
276+
/**
277+
* Set multiple values inside the mutable request-scoped context store.
278+
*
279+
* @example
280+
* ```ts
281+
* Otel.setCurrentContextValues({ tenantId: 'tenant-1', requestId: 'req-1' })
282+
* ```
283+
*/
284+
public setCurrentContextValues(
285+
values: Record<string | symbol, unknown>,
286+
ctx?: Context
287+
) {
288+
const store = this.getCurrentContextStore(ctx)
289+
290+
for (const key of Reflect.ownKeys(values)) {
291+
store.set(key as string | symbol, values[key as keyof typeof values])
292+
}
293+
294+
return this
295+
}
296+
214297
/**
215298
* Get the HTTP RPC metadata for the current active context.
216299
*
@@ -258,6 +341,21 @@ export class OtelImpl extends Macroable {
258341
return new NodeSDK(options)
259342
}
260343

344+
/**
345+
* Return the mutable request-scoped context store from the active context.
346+
*/
347+
private getCurrentContextStore(ctx?: Context) {
348+
const store = (ctx || context.active()).getValue(
349+
otelCurrentContextBagKey as any
350+
)
351+
352+
if (!store || !(store instanceof Map)) {
353+
throw new Error('Current request context store is not initialized')
354+
}
355+
356+
return store as Map<string | symbol, unknown>
357+
}
358+
261359
/**
262360
* Handle the error that occurs when recording a span.
263361
*/

tests/unit/otel/OtelImplTest.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { context } from '@opentelemetry/api'
1212
import { Test, BeforeEach, AfterEach, type Context } from '@athenna/test'
1313
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
1414

15+
const otelCurrentContextBagKey = Symbol.for('athenna.otel.currentContextBag')
16+
1517
export default class OtelImplTest {
1618
@BeforeEach()
1719
public async beforeEach() {
@@ -48,4 +50,34 @@ export default class OtelImplTest {
4850

4951
assert.isUndefined(otel.getContextValue(tenantIdKey))
5052
}
53+
54+
@Test()
55+
public async shouldBeAbleToMutateTheCurrentRequestContextStore({ assert }: Context) {
56+
const otel = new OtelImpl()
57+
const bag = new Map<string | symbol, unknown>()
58+
59+
context.with(context.active().setValue(otelCurrentContextBagKey as any, bag), () => {
60+
otel.setCurrentContextValue('exampleId', 'example-id-from-controller')
61+
62+
assert.equal(otel.getCurrentContextValue('exampleId'), 'example-id-from-controller')
63+
assert.equal(bag.get('exampleId'), 'example-id-from-controller')
64+
65+
otel.setCurrentContextValues({ tenantId: 'tenant-1' })
66+
67+
assert.equal(otel.getCurrentContextValue('tenantId'), 'tenant-1')
68+
assert.isTrue(otel.deleteCurrentContextValue('tenantId'))
69+
assert.isUndefined(otel.getCurrentContextValue('tenantId'))
70+
})
71+
}
72+
73+
@Test()
74+
public async shouldThrowAClearErrorWhenCurrentRequestContextStoreIsMissing({ assert }: Context) {
75+
const otel = new OtelImpl()
76+
77+
assert.throws(() => otel.getCurrentContextValue('exampleId'), 'Current request context store is not initialized')
78+
assert.throws(
79+
() => otel.setCurrentContextValue('exampleId', 'example-id-from-controller'),
80+
'Current request context store is not initialized'
81+
)
82+
}
5183
}

0 commit comments

Comments
 (0)