@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.
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts +72 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +203 -0
- package/dist/instrument.js.map +1 -0
- package/dist/redactingEventListener.d.ts +120 -0
- package/dist/redactingEventListener.d.ts.map +1 -0
- package/dist/redactingEventListener.js +174 -0
- package/dist/redactingEventListener.js.map +1 -0
- package/dist/redactingInstrumentation.d.ts +120 -0
- package/dist/redactingInstrumentation.d.ts.map +1 -0
- package/dist/redactingInstrumentation.js +174 -0
- package/dist/redactingInstrumentation.js.map +1 -0
- package/dist/redaction.d.ts +91 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +109 -0
- package/dist/redaction.js.map +1 -0
- package/dist/serviceInstrumentation.d.ts +151 -0
- package/dist/serviceInstrumentation.d.ts.map +1 -0
- package/dist/serviceInstrumentation.js +3 -0
- package/dist/serviceInstrumentation.js.map +1 -0
- package/package.json +44 -0
- package/src/index.ts +3 -0
- package/src/instrument.ts +285 -0
- package/src/redaction.ts +173 -0
- package/src/serviceInstrumentation.ts +158 -0
|
@@ -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 @@
|
|
|
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,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
|
+
}
|
package/src/redaction.ts
ADDED
|
@@ -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
|
+
}
|