@composed-di/instrumentation-core 0.6.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceInstrumentation.d.ts","sourceRoot":"","sources":["../src/serviceInstrumentation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;;;;;;;;OAaG;IACH,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IAEtB;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAA;CACjC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAEvC;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAExB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAA;IAEpB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,YAAY,CAAC,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,CAAA;IAE3D;;;;;OAKG;IACH,SAAS,CAAC,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAAA;IAErD;;;;;;OAMG;IACH,YAAY,CAAC,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,CAAA;CAC5D"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=serviceInstrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceInstrumentation.js","sourceRoot":"","sources":["../src/serviceInstrumentation.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@composed-di/instrumentation-core",
3
+ "version": "0.6.0-alpha",
4
+ "private": false,
5
+ "keywords": [
6
+ "dependency injection",
7
+ "di",
8
+ "instrumentation",
9
+ "observability",
10
+ "tracing"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Juan Herrera juanhr454@gmail.com",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/imherrera/composed-di.git",
17
+ "directory": "packages/instrumentation-core"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "type": "commonjs",
24
+ "sideEffects": false,
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "require": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@composed-di/core": "^0.6.0-alpha"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc --build",
42
+ "clean": "tsc --build --clean"
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './serviceInstrumentation'
2
+ export * from './instrument'
3
+ export * from './redaction'
@@ -0,0 +1,285 @@
1
+ import { ServiceFactory, ServiceKey, ServiceModule } from '@composed-di/core'
2
+ import type {
3
+ EventOutcome,
4
+ EventSpan,
5
+ ServiceInstrumentation,
6
+ } from './serviceInstrumentation'
7
+ import type { RedactionRule } from './redaction'
8
+
9
+ type GenericFactory = ServiceFactory<unknown, readonly ServiceKey<any>[]>
10
+
11
+ /**
12
+ * Configuration for {@link instrument}. Capture policy lives here, not in
13
+ * the ServiceInstrumentation implementations: what an implementation
14
+ * receives is exactly what it is allowed to record, so no implementation
15
+ * carries its own capture flags or redaction logic.
16
+ */
17
+ export interface InstrumentOptions {
18
+ /**
19
+ * The instrumentation notified of service lifecycle events and method
20
+ * calls.
21
+ */
22
+ instrumentation: ServiceInstrumentation
23
+
24
+ /**
25
+ * Deliver method arguments to the instrumentation (as
26
+ * MethodCallContext.args). Off by default: arguments may be large or
27
+ * contain secrets, and they end up wherever the instrumentation exports
28
+ * them. When off, the instrumentation never sees the arguments at all.
29
+ */
30
+ captureArguments?: boolean
31
+
32
+ /**
33
+ * Deliver method call return / resolved values to the instrumentation
34
+ * (as the success outcome's `value`). Off by default, for the same
35
+ * reasons as `captureArguments`. When off, the instrumentation never
36
+ * sees the values at all. Initialize and dispose outcomes never carry a
37
+ * value: the service instance is not useful information to report.
38
+ */
39
+ captureResults?: boolean
40
+
41
+ /**
42
+ * Per-service redaction applied to whatever the capture flags let
43
+ * through: matched arguments and success values are blanked (or run
44
+ * through the rule's custom mask) before the instrumentation sees them.
45
+ * The capture flags are the primary gate — when capture is off there is
46
+ * nothing to redact, and rules cannot re-enable delivery.
47
+ */
48
+ redactionRules?: readonly RedactionRule<any>[]
49
+ }
50
+
51
+ /**
52
+ * Wraps service factories with instrumentation, so the given
53
+ * ServiceInstrumentation is notified when a service is initialized or
54
+ * disposed and when a method is called on a service instance, and may
55
+ * return an EventSpan per operation to observe its completion. Service
56
+ * instances are wrapped in a Proxy to observe method calls, and errors
57
+ * are rethrown after being reported, so behavior is otherwise unchanged.
58
+ *
59
+ * ServiceModule entries are flattened into their factories, so an
60
+ * already-built module can be instrumented as a whole. Compose the result
61
+ * with `ServiceModule.from`:
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const module = ServiceModule.from(
66
+ * instrument([db, cache, api], {
67
+ * instrumentation: otel,
68
+ * captureArguments: true,
69
+ * captureResults: true,
70
+ * redactionRules: [redactionRule(VaultKey).redact('getSecret').build()],
71
+ * }),
72
+ * );
73
+ * ```
74
+ *
75
+ * @param entries - An array of ServiceModule or factory instances to wrap.
76
+ * @param options - The instrumentation to notify, plus the capture and
77
+ * redaction policy applied before anything reaches it.
78
+ * @return The wrapped factories, ready to be passed to ServiceModule.from.
79
+ */
80
+ export function instrument(
81
+ entries: (ServiceModule | GenericFactory)[],
82
+ options: InstrumentOptions,
83
+ ): GenericFactory[] {
84
+ return entries
85
+ .flatMap((e) => (e instanceof ServiceModule ? e.factories : [e]))
86
+ .map((factory) => makeObservable(options, factory))
87
+ }
88
+
89
+ /**
90
+ * The capture policy of one instrumented factory: resolves what to report
91
+ * for a call's arguments and a success outcome, combining the capture
92
+ * flags with the factory's redaction rule (if any). This is the single
93
+ * place that decides visibility — instrumentations record what they
94
+ * receive and nothing else.
95
+ */
96
+ interface Capture {
97
+ /**
98
+ * The arguments to deliver in MethodCallContext, or undefined when
99
+ * argument capture is off.
100
+ */
101
+ args(
102
+ functionName: string,
103
+ args: readonly unknown[],
104
+ ): readonly unknown[] | undefined
105
+
106
+ /**
107
+ * The success outcome to deliver to EventSpan.end for a method call:
108
+ * carries the (possibly redacted) value when result capture is on, no
109
+ * value otherwise.
110
+ */
111
+ success(functionName: string, value: unknown): EventOutcome
112
+ }
113
+
114
+ function makeCapture(
115
+ options: InstrumentOptions,
116
+ key: ServiceKey<unknown>,
117
+ ): Capture {
118
+ const rule = options.redactionRules?.find((r) => r.key === key)
119
+ const captureArguments = options.captureArguments ?? false
120
+ const captureResults = options.captureResults ?? false
121
+
122
+ return {
123
+ args: (functionName, args) =>
124
+ captureArguments
125
+ ? rule
126
+ ? rule.maskArgs(functionName, args)
127
+ : args
128
+ : undefined,
129
+ success: (functionName, value) =>
130
+ captureResults
131
+ ? {
132
+ type: 'success',
133
+ value: rule ? rule.maskResult(functionName, value) : value,
134
+ }
135
+ : { type: 'success' },
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Wraps a given service factory with instrumentation to notify a
141
+ * ServiceInstrumentation of lifecycle events and method calls.
142
+ *
143
+ * For each of initialize, dispose, and method calls, the instrumentation
144
+ * is invoked at the start of the operation and may return an EventSpan
145
+ * whose `end` is called with the outcome when the operation finishes.
146
+ * Errors are rethrown after being reported.
147
+ *
148
+ * @param options The instrumentation to notify and the capture policy.
149
+ * @param delegate The original service factory to be instrumented.
150
+ * @return A new service factory that provides the same dependencies but includes event notification logic.
151
+ */
152
+ function makeObservable<T, D extends readonly ServiceKey<any>[]>(
153
+ options: InstrumentOptions,
154
+ delegate: ServiceFactory<any, D>,
155
+ ): ServiceFactory<T, D> {
156
+ const instrumentation = options.instrumentation
157
+ const key = delegate.provides
158
+ const capture = makeCapture(options, key)
159
+
160
+ return ServiceFactory.singleton({
161
+ scope: delegate.scope,
162
+ provides: delegate.provides,
163
+ dependsOn: delegate.dependsOn,
164
+ initialize: async (...args) => {
165
+ const span = instrumentation.onInitialize?.({ key })
166
+ if (!span) {
167
+ return delegate.initialize(...args)
168
+ }
169
+
170
+ try {
171
+ const instance = await span.run(() => delegate.initialize(...args))
172
+ span.end({ type: 'success' })
173
+ return observeMethodCalls(instance, instrumentation, key, capture)
174
+ } catch (error) {
175
+ span.end({ type: 'failure', error })
176
+ throw error
177
+ }
178
+ },
179
+ dispose: () => {
180
+ const dispose = delegate.dispose
181
+ if (dispose) {
182
+ const span = instrumentation.onDispose?.({ key })
183
+ if (span) {
184
+ try {
185
+ span.run(dispose)
186
+ span.end({ type: 'success' })
187
+ } catch (error) {
188
+ span.end({ type: 'failure', error })
189
+ throw error
190
+ }
191
+ }
192
+ }
193
+ },
194
+ })
195
+ }
196
+
197
+ /**
198
+ * Wraps an object with a Proxy to notify the instrumentation of method calls.
199
+ *
200
+ * Methods returning a promise report their outcome when the promise
201
+ * settles, not when the method returns.
202
+ *
203
+ * @param thing The object whose method calls need to be observed.
204
+ * @param instrumentation The instrumentation notified of method call events.
205
+ * @param key The service key used to identify the service in events.
206
+ * @param capture The capture policy deciding what argument and result
207
+ * values (if any) are delivered with each event.
208
+ * @return A Proxy wrapping the input object, with all method calls being reported.
209
+ */
210
+ function observeMethodCalls(
211
+ thing: any,
212
+ instrumentation: ServiceInstrumentation,
213
+ key: ServiceKey<unknown>,
214
+ capture: Capture,
215
+ ): any {
216
+ if (typeof thing !== 'object' || thing === null) {
217
+ return thing
218
+ }
219
+
220
+ const className = classNameOf(thing)
221
+
222
+ return new Proxy(thing, {
223
+ get(target, prop) {
224
+ const value = Reflect.get(target, prop)
225
+ if (typeof value === 'function' && typeof prop === 'string') {
226
+ return (...args: unknown[]) => {
227
+ const span = instrumentation.onMethodCall?.({
228
+ key,
229
+ className,
230
+ functionName: prop,
231
+ args: capture.args(prop, args),
232
+ })
233
+ try {
234
+ const result = invokeWithin(span, () => value.apply(target, args))
235
+ if (result instanceof Promise) {
236
+ return result.then(
237
+ (resolved) => {
238
+ span?.end(capture.success(prop, resolved))
239
+ return resolved
240
+ },
241
+ (error) => {
242
+ span?.end({ type: 'failure', error })
243
+ throw error
244
+ },
245
+ )
246
+ }
247
+ span?.end(capture.success(prop, result))
248
+ return result
249
+ } catch (error) {
250
+ span?.end({ type: 'failure', error })
251
+ throw error
252
+ }
253
+ }
254
+ }
255
+ return value
256
+ },
257
+ })
258
+ }
259
+
260
+ /**
261
+ * Invokes an operation through the EventSpan's `run` wrapper when the
262
+ * instrumentation returned a span, so it can establish ambient state
263
+ * (tracing context) around the operation; invokes the operation directly
264
+ * otherwise.
265
+ *
266
+ * @param span The EventSpan returned by the instrumentation, if any.
267
+ * @param fn The thunk performing the operation.
268
+ * @returns The value returned by `fn`.
269
+ */
270
+ function invokeWithin<T>(span: EventSpan | void, fn: () => T): T {
271
+ return span ? span.run(fn) : fn()
272
+ }
273
+
274
+ /**
275
+ * Resolves the class name of a service instance, or undefined for values
276
+ * that are not instances of a named class (plain object literals,
277
+ * null-prototype objects).
278
+ *
279
+ * @param thing The service instance to inspect.
280
+ * @returns The constructor name, or undefined when there is none to report.
281
+ */
282
+ function classNameOf(thing: object): string | undefined {
283
+ const name = thing.constructor?.name
284
+ return name && name !== 'Object' ? name : undefined
285
+ }
@@ -0,0 +1,173 @@
1
+ import type { ServiceKey } from '@composed-di/core'
2
+
3
+ /**
4
+ * The placeholder that replaces a redacted value when no custom
5
+ * transform is given for it.
6
+ */
7
+ export const REDACTED_VALUE = '[REDACTED]'
8
+
9
+ /**
10
+ * Custom masking for one included property, narrowed to the exact
11
+ * method named when {@link RedactionRuleBuilder.redact} is called.
12
+ * Omitting a side fully blanks it with {@link REDACTED_VALUE}; providing
13
+ * one lets you report a partial mask instead (e.g. the last 4 digits of
14
+ * a card number) — both always return a `string`, the masked
15
+ * representation to report in place of the real value.
16
+ */
17
+ export type Mask<T, K extends Extract<keyof T, string>> = T[K] extends (
18
+ ...args: infer A
19
+ ) => infer R
20
+ ? {
21
+ maskArgs?: (...args: A) => string
22
+ maskResult?: (result: R) => string
23
+ }
24
+ : never
25
+
26
+ /**
27
+ * Per-property override, layered on top of a rule's `redactAll` default.
28
+ * `redacted: true` (from {@link RedactionRuleBuilder.redact}) redacts
29
+ * that property, optionally with a custom mask; `redacted: false` (from
30
+ * {@link RedactionRuleBuilder.exclude}) forces it to be left untouched
31
+ * regardless of `redactAll`.
32
+ */
33
+ interface PropertyOverride {
34
+ redacted: boolean
35
+ maskArgs?: (...args: any[]) => string
36
+ maskResult?: (result: any) => string
37
+ }
38
+
39
+ /**
40
+ * Marks a service — or specific properties of it — as sensitive, so the
41
+ * values flowing through its events are redacted. Passed to
42
+ * {@link instrument} via InstrumentOptions.redactionRules and applied
43
+ * centrally, after the capture flags: values a rule matches are scrubbed
44
+ * before the instrumentation ever sees them, and when capture is off
45
+ * there is nothing to redact. Returned by
46
+ * {@link RedactionRuleBuilder.build}, which is the only way to construct
47
+ * one — the `redactAll`/per-property merge logic lives entirely inside
48
+ * `maskArgs`/`maskResult`, so callers never need to know how a rule
49
+ * reached its decision, only what to do with it.
50
+ */
51
+ export interface RedactionRule<T> {
52
+ readonly key: ServiceKey<T>
53
+
54
+ /**
55
+ * The args to report for a call to `functionName`: unchanged if not
56
+ * redacted, otherwise blanked or run through a custom `maskArgs`.
57
+ */
58
+ maskArgs(functionName: string, args: readonly unknown[]): readonly unknown[]
59
+
60
+ /**
61
+ * The value to report for a method call's success outcome: unchanged
62
+ * if not redacted, otherwise blanked or run through a custom
63
+ * `maskResult`.
64
+ */
65
+ maskResult(functionName: string, result: unknown): unknown
66
+ }
67
+
68
+ /**
69
+ * Fluent, single-key rule builder returned by {@link redactionRule}.
70
+ * `redactAll`, `redact`, and `exclude` all merge into the same rule —
71
+ * call them in any combination, in any order; the more specific
72
+ * per-property calls (`redact`/`exclude`) always win over the blanket
73
+ * `redactAll` default for the properties they name.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const rules = [
78
+ * redactionRule(SecretClientKey)
79
+ * .redactAll()
80
+ * .build(), // whole service is sensitive
81
+ * redactionRule(BillingKey)
82
+ * .redactAll()
83
+ * .redact('chargeCard', { maskResult: (card) => `card ending in ${card.number.slice(-4)}` })
84
+ * .exclude('ping') // redact everything except this, with one custom mask
85
+ * .build(),
86
+ * redactionRule(VaultKey)
87
+ * .redact('getSecret')
88
+ * .build(), // only this call, nothing else
89
+ * ];
90
+ * ```
91
+ */
92
+ export class RedactionRuleBuilder<T> {
93
+ private redactAllFlag = false
94
+ private hasRedact = false
95
+ private readonly overrides: Record<string, PropertyOverride> = {}
96
+
97
+ constructor(private readonly key: ServiceKey<T>) {}
98
+
99
+ /** Redacts every property by default. */
100
+ redactAll(): this {
101
+ this.redactAllFlag = true
102
+ this.hasRedact = true
103
+ return this
104
+ }
105
+
106
+ /**
107
+ * Marks one property (method) as redacted, with optional custom
108
+ * masking. Call repeatedly for several properties. Overrides
109
+ * `redactAll`/`exclude` for this specific property.
110
+ */
111
+ redact<K extends Extract<keyof T, string>>(name: K, mask?: Mask<T, K>): this {
112
+ this.overrides[name] = { redacted: true, ...mask }
113
+ this.hasRedact = true
114
+ return this
115
+ }
116
+
117
+ /**
118
+ * Marks one or more properties as explicitly NOT redacted, overriding
119
+ * `redactAll` for just these.
120
+ */
121
+ exclude(...names: Extract<keyof T, string>[]): this {
122
+ for (const name of names) {
123
+ this.overrides[name] = { redacted: false }
124
+ }
125
+ return this
126
+ }
127
+
128
+ build(): RedactionRule<any> {
129
+ // `exclude` alone never redacts anything — at least one call to
130
+ // `redactAll`/`redact` is required for this rule to have any effect.
131
+ if (!this.hasRedact) {
132
+ throw new Error(
133
+ `redactionRule(${this.key.name}) has no effect: call .redactAll() and/or .redact(...) ` +
134
+ 'before .build() — .exclude() alone never redacts anything.',
135
+ )
136
+ }
137
+
138
+ const redactAllFlag = this.redactAllFlag
139
+ const overrides = this.overrides
140
+
141
+ return {
142
+ key: this.key,
143
+ maskArgs(functionName, args) {
144
+ const override = overrides[functionName]
145
+ const redacted = override ? override.redacted : redactAllFlag
146
+ if (!redacted) {
147
+ return args
148
+ }
149
+ return override?.maskArgs
150
+ ? // A custom mask reports one string for the whole call,
151
+ // rather than a value per argument.
152
+ [override.maskArgs(...args)]
153
+ : // Replace each argument rather than the whole array, so the
154
+ // delegate still sees the call's arity.
155
+ args.map(() => REDACTED_VALUE)
156
+ },
157
+ maskResult(functionName, result) {
158
+ const override = overrides[functionName]
159
+ const redacted = override ? override.redacted : redactAllFlag
160
+ if (!redacted) {
161
+ return result
162
+ }
163
+ return override?.maskResult
164
+ ? override.maskResult(result)
165
+ : REDACTED_VALUE
166
+ },
167
+ } as RedactionRule<any>
168
+ }
169
+ }
170
+
171
+ export function redactionRule<T>(key: ServiceKey<T>): RedactionRuleBuilder<T> {
172
+ return new RedactionRuleBuilder(key)
173
+ }
@@ -0,0 +1,158 @@
1
+ import type { ServiceKey } from '@composed-di/core'
2
+
3
+ /**
4
+ * A handle representing a single in-flight operation (initialization,
5
+ * disposal, or method call), returned by a ServiceInstrumentation when the
6
+ * operation starts.
7
+ *
8
+ * `end` is invoked exactly once when the operation finishes, so
9
+ * implementations can close over per-call state (a start time, a span
10
+ * handle, a correlation id) without any bookkeeping to pair concurrent
11
+ * start/finish events.
12
+ */
13
+ export interface EventSpan {
14
+ /**
15
+ * Wrapper around the operation itself. The instrumented factory invokes
16
+ * the operation as `run(() => operation())`, so the instrumentation can
17
+ * establish ambient state that the operation body and its async
18
+ * continuations inherit — this is what lets spans of nested service
19
+ * calls form a parent-child hierarchy.
20
+ *
21
+ * Implementations must invoke `fn` exactly once, synchronously, and
22
+ * return its result unchanged (for async operations, the promise itself).
23
+ * `end` is still delivered separately when the operation finishes.
24
+ *
25
+ * @param fn - A thunk that performs the operation.
26
+ * @return The value returned by `fn`.
27
+ */
28
+ run<T>(fn: () => T): T
29
+
30
+ /**
31
+ * Invoked exactly once when the operation finishes, whether it succeeded
32
+ * or failed. For methods that return a promise, this fires when the
33
+ * promise settles, not when the method returns. On failure, the error is
34
+ * rethrown to the caller after this is invoked.
35
+ *
36
+ * Whether to retain or log the outcome is the implementation's choice;
37
+ * values are passed by reference, so implementations must not mutate them.
38
+ *
39
+ * @param outcome - How the operation finished, and its value or error.
40
+ * @return void
41
+ */
42
+ end(outcome: EventOutcome): void
43
+ }
44
+
45
+ /**
46
+ * How an operation finished, delivered to EventSpan.end: `success` may
47
+ * carry the return or resolved value of a method call (initialize and
48
+ * dispose outcomes never carry one); `failure` carries the error that was
49
+ * thrown or rejected.
50
+ *
51
+ * Whether `value` is present is decided by {@link instrument}, not by the
52
+ * implementation: it is absent unless result capture is enabled in the
53
+ * InstrumentOptions, and holds the redacted value when a redaction rule
54
+ * matches. Implementations must record the value exactly when it is
55
+ * present (`'value' in outcome` — a captured `undefined` return is
56
+ * delivered as a present `value: undefined`) and must not record any
57
+ * result when it is absent.
58
+ */
59
+ export type EventOutcome =
60
+ | { type: 'success'; value?: unknown }
61
+ | { type: 'failure'; error: unknown }
62
+
63
+ /**
64
+ * Context of a service initialization, delivered to onInitialize.
65
+ * Future fields are added here rather than as extra parameters.
66
+ */
67
+ export interface InitializeContext {
68
+ /**
69
+ * The unique identifier of the service that is being initialized.
70
+ */
71
+ key: ServiceKey<unknown>
72
+ }
73
+
74
+ /**
75
+ * Context of a service disposal, delivered to onDispose.
76
+ * Future fields are added here rather than as extra parameters.
77
+ */
78
+ export interface DisposeContext {
79
+ /**
80
+ * The unique identifier of the service that is being disposed.
81
+ */
82
+ key: ServiceKey<unknown>
83
+ }
84
+
85
+ /**
86
+ * Context of a method invocation, delivered to onMethodCall.
87
+ * Future fields are added here rather than as extra parameters.
88
+ */
89
+ export interface MethodCallContext {
90
+ /**
91
+ * The unique identifier of the service the method belongs to.
92
+ */
93
+ key: ServiceKey<unknown>
94
+
95
+ /**
96
+ * The name of the class implementing the service (the instance's
97
+ * constructor name), which may differ from `key.name`. Undefined for
98
+ * services that are not instances of a named class, such as plain
99
+ * object literals.
100
+ */
101
+ className?: string
102
+
103
+ /**
104
+ * The name of the method that is being called.
105
+ */
106
+ functionName: string
107
+
108
+ /**
109
+ * The arguments to report for this call, passed by reference;
110
+ * implementations must not mutate them.
111
+ *
112
+ * Whether they are present is decided by {@link instrument}, not by the
113
+ * implementation: absent unless argument capture is enabled in the
114
+ * InstrumentOptions, and already redacted when a redaction rule matches.
115
+ * Implementations must record the arguments exactly when present and
116
+ * must not record any arguments when absent.
117
+ */
118
+ args?: readonly unknown[]
119
+ }
120
+
121
+ /**
122
+ * Interface for instrumenting services wrapped by {@link instrument}.
123
+ * Implement this interface to observe lifecycle events and method calls
124
+ * of the instrumented services.
125
+ *
126
+ * Instrumentation is strictly observational: implementations see every
127
+ * operation but must never alter it — see the EventSpan contract.
128
+ *
129
+ * Each method is invoked when the corresponding operation starts and may
130
+ * return an EventSpan that is notified when that operation finishes.
131
+ * Returning nothing opts out of completion tracking for that call.
132
+ */
133
+ export interface ServiceInstrumentation {
134
+ /**
135
+ * Invoked at the start of the initialization process for a specific service.
136
+ *
137
+ * @param context - Context of the initialization, including the service key.
138
+ * @return An EventSpan notified when initialization finishes, or void.
139
+ */
140
+ onInitialize?(context: InitializeContext): EventSpan | void
141
+
142
+ /**
143
+ * Invoked when the disposal process for a service starts.
144
+ *
145
+ * @param context - Context of the disposal, including the service key.
146
+ * @return An EventSpan notified when disposal finishes, or void.
147
+ */
148
+ onDispose?(context: DisposeContext): EventSpan | void
149
+
150
+ /**
151
+ * Invoked when a method call starts on a service instance.
152
+ *
153
+ * @param context - Context of the invocation, including the service key,
154
+ * the method name, and its arguments.
155
+ * @return An EventSpan notified when the call finishes, or void.
156
+ */
157
+ onMethodCall?(context: MethodCallContext): EventSpan | void
158
+ }