mcp-event-intelligence 0.8.1 → 0.9.0
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/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/conformance/README.md +59 -0
- package/conformance/fixtures/composite-trigger.valid.json +70 -0
- package/conformance/fixtures/event-lineage.valid.json +26 -0
- package/conformance/reference-host.mjs +145 -0
- package/package.json +15 -3
- package/scripts/embedded-host-kit.d.mts +67 -1
- package/scripts/embedded-host-kit.mjs +218 -11
- package/scripts/host-conformance.d.mts +75 -0
- package/scripts/host-conformance.mjs +238 -0
- package/scripts/host-integration.d.mts +5 -0
- package/scripts/host-integration.mjs +23 -4
- package/scripts/lib/composite-event-consumer.mjs +28 -0
- package/scripts/lib/composite-wake-coordinator.mjs +64 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +39 -0
- package/scripts/lib/mcp-events-client.mjs +36 -0
- package/scripts/lib/observability.d.mts +77 -0
- package/scripts/lib/observability.mjs +134 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +10 -0
- package/scripts/lib/trigger-control-plane.mjs +26 -0
- package/scripts/lib/wake-retry-scheduler.mjs +10 -0
- package/server.json +2 -2
|
@@ -127,6 +127,7 @@ export async function createEventIntelligenceHost({
|
|
|
127
127
|
wake,
|
|
128
128
|
wakeHandlers = {},
|
|
129
129
|
semanticEvaluator,
|
|
130
|
+
observability,
|
|
130
131
|
} = {}) {
|
|
131
132
|
const explicitConnections = mcpClients.map(normalizeMcpConnection);
|
|
132
133
|
const runtime = await createLocalEventIntelligenceRuntime({
|
|
@@ -140,7 +141,9 @@ export async function createEventIntelligenceHost({
|
|
|
140
141
|
wakeHandlers,
|
|
141
142
|
semanticEvaluator,
|
|
142
143
|
store,
|
|
144
|
+
observability,
|
|
143
145
|
});
|
|
146
|
+
const observer = runtime.observability;
|
|
144
147
|
|
|
145
148
|
const registryManaged = new Set();
|
|
146
149
|
let unsubscribe = null;
|
|
@@ -212,6 +215,21 @@ export async function createEventIntelligenceHost({
|
|
|
212
215
|
}
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
await Promise.all(outcomes.map((outcome) => observer.emit({
|
|
219
|
+
event: 'ei.source.registry_update',
|
|
220
|
+
level:
|
|
221
|
+
outcome.status === 'error'
|
|
222
|
+
? 'error'
|
|
223
|
+
: outcome.status === 'ignored_no_events' ||
|
|
224
|
+
outcome.status === 'already_attached'
|
|
225
|
+
? 'debug'
|
|
226
|
+
: 'info',
|
|
227
|
+
connectionId: outcome.connectionId,
|
|
228
|
+
status: outcome.status,
|
|
229
|
+
...(outcome.error ? { error: new Error(outcome.error) } : {}),
|
|
230
|
+
...(outcome.events ? { metadata: { eventNames: outcome.events } } : {}),
|
|
231
|
+
})));
|
|
232
|
+
|
|
215
233
|
return outcomes;
|
|
216
234
|
};
|
|
217
235
|
|
|
@@ -241,10 +259,11 @@ export async function createEventIntelligenceHost({
|
|
|
241
259
|
if (typeof mcpRegistry.subscribe === 'function') {
|
|
242
260
|
const maybeUnsubscribe = mcpRegistry.subscribe(() => {
|
|
243
261
|
refreshMcpRegistry().catch((error) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
262
|
+
void observer.emit({
|
|
263
|
+
event: 'ei.source.refresh_failed',
|
|
264
|
+
level: 'error',
|
|
265
|
+
error,
|
|
266
|
+
});
|
|
248
267
|
});
|
|
249
268
|
});
|
|
250
269
|
if (typeof maybeUnsubscribe === 'function') {
|
|
@@ -10,6 +10,7 @@ export class CompositeEventConsumer {
|
|
|
10
10
|
wakeCoordinators = null,
|
|
11
11
|
derivedEventCoordinator = null,
|
|
12
12
|
maxDerivedDepth = 16,
|
|
13
|
+
observability = null,
|
|
13
14
|
}) {
|
|
14
15
|
this.store = store;
|
|
15
16
|
this.triggerEngine = triggerEngine;
|
|
@@ -19,6 +20,7 @@ export class CompositeEventConsumer {
|
|
|
19
20
|
: new Map();
|
|
20
21
|
this.derivedEventCoordinator = derivedEventCoordinator;
|
|
21
22
|
this.maxDerivedDepth = Math.max(1, Number(maxDerivedDepth) || 16);
|
|
23
|
+
this.observability = observability;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
coordinatorFor(runtime) {
|
|
@@ -58,6 +60,15 @@ export class CompositeEventConsumer {
|
|
|
58
60
|
for (const result of results) {
|
|
59
61
|
if (!result.matched || !result.match) continue;
|
|
60
62
|
|
|
63
|
+
await this.observability?.emit({
|
|
64
|
+
event: 'ei.match.matched',
|
|
65
|
+
level: 'info',
|
|
66
|
+
traceId: event.traceId,
|
|
67
|
+
triggerId: result.match.triggerId,
|
|
68
|
+
matchId: result.match.matchId,
|
|
69
|
+
status: result.match.status,
|
|
70
|
+
});
|
|
71
|
+
|
|
61
72
|
const definition = (await this.store.listTriggers()).find(
|
|
62
73
|
(candidate) =>
|
|
63
74
|
candidate.triggerId === result.match.triggerId &&
|
|
@@ -72,6 +83,14 @@ export class CompositeEventConsumer {
|
|
|
72
83
|
status: 'trigger_definition_missing',
|
|
73
84
|
runtimeReceiptId: null,
|
|
74
85
|
});
|
|
86
|
+
await this.observability?.emit({
|
|
87
|
+
event: 'ei.match.definition_missing',
|
|
88
|
+
level: 'error',
|
|
89
|
+
traceId: event.traceId,
|
|
90
|
+
triggerId: result.triggerId,
|
|
91
|
+
matchId: result.match.matchId,
|
|
92
|
+
status: 'trigger_definition_missing',
|
|
93
|
+
});
|
|
75
94
|
continue;
|
|
76
95
|
}
|
|
77
96
|
|
|
@@ -131,6 +150,15 @@ export class CompositeEventConsumer {
|
|
|
131
150
|
status: 'runtime_unconfigured',
|
|
132
151
|
runtimeReceiptId: null,
|
|
133
152
|
});
|
|
153
|
+
await this.observability?.emit({
|
|
154
|
+
event: 'ei.wake.runtime_unconfigured',
|
|
155
|
+
level: 'warn',
|
|
156
|
+
traceId: event.traceId,
|
|
157
|
+
triggerId: result.triggerId,
|
|
158
|
+
matchId: result.match.matchId,
|
|
159
|
+
status: 'runtime_unconfigured',
|
|
160
|
+
metadata: { runtime: definition.target.runtime },
|
|
161
|
+
});
|
|
134
162
|
continue;
|
|
135
163
|
}
|
|
136
164
|
|
|
@@ -31,6 +31,7 @@ export class CompositeWakeCoordinator {
|
|
|
31
31
|
maxAttempts = 5,
|
|
32
32
|
retryBaseDelayMs = 1000,
|
|
33
33
|
retryMaxDelayMs = 60000,
|
|
34
|
+
observability = null,
|
|
34
35
|
}) {
|
|
35
36
|
this.store = store;
|
|
36
37
|
this.triggerEngine = triggerEngine;
|
|
@@ -45,6 +46,7 @@ export class CompositeWakeCoordinator {
|
|
|
45
46
|
this.retryBaseDelayMs,
|
|
46
47
|
Number(retryMaxDelayMs) || 60000,
|
|
47
48
|
);
|
|
49
|
+
this.observability = observability;
|
|
48
50
|
|
|
49
51
|
for (const method of [
|
|
50
52
|
'ensureWakeDelivery',
|
|
@@ -101,6 +103,16 @@ export class CompositeWakeCoordinator {
|
|
|
101
103
|
composite: true,
|
|
102
104
|
},
|
|
103
105
|
});
|
|
106
|
+
await this.observability?.emit({
|
|
107
|
+
event: 'ei.wake.delivered',
|
|
108
|
+
level: 'info',
|
|
109
|
+
traceId: delivered.traceId,
|
|
110
|
+
triggerId: match.triggerId,
|
|
111
|
+
matchId: match.matchId,
|
|
112
|
+
wakeId,
|
|
113
|
+
status: 'wake_delivered',
|
|
114
|
+
metadata: { recoveredFromDeliveryState: true },
|
|
115
|
+
});
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
if (match.status !== 'fired') {
|
|
@@ -193,6 +205,15 @@ export class CompositeWakeCoordinator {
|
|
|
193
205
|
composite: true,
|
|
194
206
|
},
|
|
195
207
|
});
|
|
208
|
+
await this.observability?.emit({
|
|
209
|
+
event: 'ei.wake.queued',
|
|
210
|
+
level: 'debug',
|
|
211
|
+
traceId: queued.traceId,
|
|
212
|
+
triggerId: match.triggerId,
|
|
213
|
+
matchId: match.matchId,
|
|
214
|
+
wakeId,
|
|
215
|
+
status: 'wake_queued',
|
|
216
|
+
});
|
|
196
217
|
}
|
|
197
218
|
return {
|
|
198
219
|
status: 'wake_queued',
|
|
@@ -272,6 +293,15 @@ export class CompositeWakeCoordinator {
|
|
|
272
293
|
composite: true,
|
|
273
294
|
},
|
|
274
295
|
});
|
|
296
|
+
await this.observability?.emit({
|
|
297
|
+
event: 'ei.wake.queued',
|
|
298
|
+
level: 'debug',
|
|
299
|
+
traceId: queued.traceId,
|
|
300
|
+
triggerId: match.triggerId,
|
|
301
|
+
matchId: match.matchId,
|
|
302
|
+
wakeId,
|
|
303
|
+
status: 'wake_queued',
|
|
304
|
+
});
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
try {
|
|
@@ -322,6 +352,17 @@ export class CompositeWakeCoordinator {
|
|
|
322
352
|
composite: true,
|
|
323
353
|
},
|
|
324
354
|
});
|
|
355
|
+
await this.observability?.emit({
|
|
356
|
+
event: 'ei.wake.delivered',
|
|
357
|
+
level: 'info',
|
|
358
|
+
traceId: delivered.traceId,
|
|
359
|
+
triggerId: match.triggerId,
|
|
360
|
+
matchId: match.matchId,
|
|
361
|
+
wakeId,
|
|
362
|
+
status: 'wake_delivered',
|
|
363
|
+
attempt: delivery?.attemptCount ?? claim.attemptCount,
|
|
364
|
+
metadata: { duplicate: receipt.duplicate === true },
|
|
365
|
+
});
|
|
325
366
|
|
|
326
367
|
await this.triggerEngine.markFired(match.matchId, wakeId);
|
|
327
368
|
|
|
@@ -365,6 +406,18 @@ export class CompositeWakeCoordinator {
|
|
|
365
406
|
composite: true,
|
|
366
407
|
},
|
|
367
408
|
});
|
|
409
|
+
await this.observability?.emit({
|
|
410
|
+
event: 'ei.wake.retry_scheduled',
|
|
411
|
+
level: 'warn',
|
|
412
|
+
traceId: queued.traceId,
|
|
413
|
+
triggerId: match.triggerId,
|
|
414
|
+
matchId: match.matchId,
|
|
415
|
+
wakeId,
|
|
416
|
+
status: 'retry_scheduled',
|
|
417
|
+
attempt: delivery?.attemptCount ?? claim.attemptCount,
|
|
418
|
+
nextAttemptAt: delivery?.nextAttemptAt ?? undefined,
|
|
419
|
+
error,
|
|
420
|
+
});
|
|
368
421
|
return {
|
|
369
422
|
status: 'retry_scheduled',
|
|
370
423
|
wake: persistedQueued,
|
|
@@ -398,6 +451,17 @@ export class CompositeWakeCoordinator {
|
|
|
398
451
|
composite: true,
|
|
399
452
|
},
|
|
400
453
|
});
|
|
454
|
+
await this.observability?.emit({
|
|
455
|
+
event: 'ei.wake.dead_letter',
|
|
456
|
+
level: 'error',
|
|
457
|
+
traceId: deadLetter.traceId,
|
|
458
|
+
triggerId: match.triggerId,
|
|
459
|
+
matchId: match.matchId,
|
|
460
|
+
wakeId,
|
|
461
|
+
status: 'dead_letter',
|
|
462
|
+
attempt: delivery?.attemptCount ?? claim.attemptCount,
|
|
463
|
+
error,
|
|
464
|
+
});
|
|
401
465
|
return {
|
|
402
466
|
status: 'dead_letter',
|
|
403
467
|
wake: deadLetter,
|
|
@@ -48,6 +48,9 @@ import {
|
|
|
48
48
|
import {
|
|
49
49
|
ActivationHydrator,
|
|
50
50
|
} from './activation-hydrator.mjs';
|
|
51
|
+
import {
|
|
52
|
+
createObservabilityEmitter,
|
|
53
|
+
} from './observability.mjs';
|
|
51
54
|
|
|
52
55
|
const REQUIRED_STORE_METHODS = [
|
|
53
56
|
'putTrigger',
|
|
@@ -114,7 +117,9 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
114
117
|
wake,
|
|
115
118
|
semanticEvaluator,
|
|
116
119
|
store: providedStore,
|
|
120
|
+
observability,
|
|
117
121
|
} = {}) {
|
|
122
|
+
const observer = createObservabilityEmitter(observability);
|
|
118
123
|
const dataDir = env.DATA_DIR ?? './data';
|
|
119
124
|
const rootStore = providedStore ?? new PersistentEventStore(dataDir);
|
|
120
125
|
const restored = await initializeStore(rootStore);
|
|
@@ -212,6 +217,7 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
212
217
|
triggerEngine,
|
|
213
218
|
deliverer: createSignedRuntimeWakeDeliverer(target),
|
|
214
219
|
packetBuilder,
|
|
220
|
+
observability: observer,
|
|
215
221
|
...deliveryOptions,
|
|
216
222
|
}),
|
|
217
223
|
);
|
|
@@ -224,6 +230,7 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
224
230
|
store,
|
|
225
231
|
triggerEngine,
|
|
226
232
|
packetBuilder,
|
|
233
|
+
observability: observer,
|
|
227
234
|
...deliveryOptions,
|
|
228
235
|
deliverer: async (packet) => {
|
|
229
236
|
const activation = await activationHydrator.hydrateWake(packet.wake_id);
|
|
@@ -253,6 +260,7 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
253
260
|
store,
|
|
254
261
|
triggerEngine,
|
|
255
262
|
packetBuilder,
|
|
263
|
+
observability: observer,
|
|
256
264
|
...deliveryOptions,
|
|
257
265
|
deliverer: async (packet) => {
|
|
258
266
|
const activation = await activationHydrator.hydrateWake(packet.wake_id);
|
|
@@ -281,18 +289,21 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
281
289
|
wakeCoordinators,
|
|
282
290
|
derivedEventCoordinator,
|
|
283
291
|
maxDerivedDepth: Number(env.MAX_DERIVED_EVENT_DEPTH ?? 16),
|
|
292
|
+
observability: observer,
|
|
284
293
|
});
|
|
285
294
|
|
|
286
295
|
const temporalScheduler = new TemporalDeadlineScheduler({
|
|
287
296
|
store,
|
|
288
297
|
compositeEventConsumer,
|
|
289
298
|
intervalMs: Number(env.TEMPORAL_TICK_MS ?? 1000),
|
|
299
|
+
observability: observer,
|
|
290
300
|
});
|
|
291
301
|
temporalScheduler.start();
|
|
292
302
|
|
|
293
303
|
const triggerControl = new TriggerControlPlane({
|
|
294
304
|
store,
|
|
295
305
|
triggerEngine,
|
|
306
|
+
observability: observer,
|
|
296
307
|
});
|
|
297
308
|
const triggerInspector = new TriggerInspector({ store });
|
|
298
309
|
|
|
@@ -302,6 +313,7 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
302
313
|
store,
|
|
303
314
|
resolveCoordinator: coordinatorFor,
|
|
304
315
|
intervalMs: Number(env.WAKE_RETRY_TICK_MS ?? 1000),
|
|
316
|
+
observability: observer,
|
|
305
317
|
});
|
|
306
318
|
wakeRetryScheduler.start();
|
|
307
319
|
|
|
@@ -354,13 +366,38 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
354
366
|
principal_id: 'event-intelligence:host-mcp-client',
|
|
355
367
|
},
|
|
356
368
|
),
|
|
369
|
+
observability: observer,
|
|
357
370
|
});
|
|
358
371
|
|
|
359
372
|
const discovery = await mcpEventsClient.discoverAll();
|
|
360
373
|
mcpEventsClient.start();
|
|
361
374
|
|
|
375
|
+
for (const source of discovery) {
|
|
376
|
+
await observer.emit({
|
|
377
|
+
event: source.status === 'error'
|
|
378
|
+
? 'ei.source.discovery_failed'
|
|
379
|
+
: 'ei.source.discovered',
|
|
380
|
+
level: source.status === 'error' ? 'warn' : 'info',
|
|
381
|
+
connectionId: source.connectionId,
|
|
382
|
+
status: source.status,
|
|
383
|
+
...(source.error ? { error: new Error(source.error) } : {}),
|
|
384
|
+
...(source.events ? { metadata: { eventNames: source.events } } : {}),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
await observer.emit({
|
|
388
|
+
event: 'ei.lifecycle.started',
|
|
389
|
+
level: 'info',
|
|
390
|
+
metadata: {
|
|
391
|
+
restoredScopes:
|
|
392
|
+
typeof rootStore.listScopeIds === 'function'
|
|
393
|
+
? await rootStore.listScopeIds()
|
|
394
|
+
: [DEFAULT_EVENT_SCOPE_ID],
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
362
398
|
return {
|
|
363
399
|
restored,
|
|
400
|
+
observability: observer,
|
|
364
401
|
storeCapabilities,
|
|
365
402
|
store: defaultContext.store,
|
|
366
403
|
evaluator,
|
|
@@ -378,6 +415,7 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
378
415
|
return [...scopeContexts.keys()].sort();
|
|
379
416
|
},
|
|
380
417
|
async close() {
|
|
418
|
+
await observer.emit({ event: 'ei.lifecycle.closing', level: 'debug' });
|
|
381
419
|
await mcpEventsClient.close();
|
|
382
420
|
const contexts = [...scopeContexts.values()];
|
|
383
421
|
await Promise.all(contexts.map((context) => context.close()));
|
|
@@ -390,6 +428,7 @@ export async function createLocalEventIntelligenceRuntime({
|
|
|
390
428
|
await store.drain();
|
|
391
429
|
}
|
|
392
430
|
}
|
|
431
|
+
await observer.emit({ event: 'ei.lifecycle.closed', level: 'info' });
|
|
393
432
|
},
|
|
394
433
|
};
|
|
395
434
|
}
|
|
@@ -286,6 +286,7 @@ export class McpEventsClientManager {
|
|
|
286
286
|
resolveScope = null,
|
|
287
287
|
connections = [],
|
|
288
288
|
now = () => new Date(),
|
|
289
|
+
observability = null,
|
|
289
290
|
}) {
|
|
290
291
|
this.store = store;
|
|
291
292
|
this.compositeEventConsumer = compositeEventConsumer;
|
|
@@ -293,6 +294,7 @@ export class McpEventsClientManager {
|
|
|
293
294
|
this.resolveScope = resolveScope;
|
|
294
295
|
this.connections = new Map();
|
|
295
296
|
this.now = now;
|
|
297
|
+
this.observability = observability;
|
|
296
298
|
this.descriptors = new Map();
|
|
297
299
|
this.profiles = new Map();
|
|
298
300
|
this.timers = new Map();
|
|
@@ -877,6 +879,15 @@ export class McpEventsClientManager {
|
|
|
877
879
|
connection.connectionId,
|
|
878
880
|
error instanceof Error ? error.message : String(error),
|
|
879
881
|
);
|
|
882
|
+
void this.observability?.emit({
|
|
883
|
+
event: 'ei.source.delivery_failed',
|
|
884
|
+
level: 'warn',
|
|
885
|
+
scopeId: connection.scopeId,
|
|
886
|
+
connectionId: connection.connectionId,
|
|
887
|
+
serverId: connection.serverId,
|
|
888
|
+
eventName: subscription.eventName,
|
|
889
|
+
error,
|
|
890
|
+
});
|
|
880
891
|
};
|
|
881
892
|
|
|
882
893
|
const onTerminated = async (detail) => {
|
|
@@ -887,6 +898,15 @@ export class McpEventsClientManager {
|
|
|
887
898
|
typeof detail === 'string' ? detail : JSON.stringify(detail),
|
|
888
899
|
);
|
|
889
900
|
}
|
|
901
|
+
void this.observability?.emit({
|
|
902
|
+
event: 'ei.source.delivery_terminated',
|
|
903
|
+
level: detail ? 'warn' : 'debug',
|
|
904
|
+
scopeId: connection.scopeId,
|
|
905
|
+
connectionId: connection.connectionId,
|
|
906
|
+
serverId: connection.serverId,
|
|
907
|
+
eventName: subscription.eventName,
|
|
908
|
+
status: 'terminated',
|
|
909
|
+
});
|
|
890
910
|
};
|
|
891
911
|
|
|
892
912
|
const open =
|
|
@@ -1054,6 +1074,14 @@ export class McpEventsClientManager {
|
|
|
1054
1074
|
} catch (error) {
|
|
1055
1075
|
const message = error instanceof Error ? error.message : String(error);
|
|
1056
1076
|
this.lastErrors.set(connection.connectionId, message);
|
|
1077
|
+
await this.observability?.emit({
|
|
1078
|
+
event: 'ei.source.poll_failed',
|
|
1079
|
+
level: 'warn',
|
|
1080
|
+
scopeId: connection.scopeId,
|
|
1081
|
+
connectionId: connection.connectionId,
|
|
1082
|
+
serverId: connection.serverId,
|
|
1083
|
+
error,
|
|
1084
|
+
});
|
|
1057
1085
|
results.push({
|
|
1058
1086
|
connectionId: connection.connectionId,
|
|
1059
1087
|
error: message,
|
|
@@ -1097,6 +1125,14 @@ export class McpEventsClientManager {
|
|
|
1097
1125
|
connection.connectionId,
|
|
1098
1126
|
error instanceof Error ? error.message : String(error),
|
|
1099
1127
|
);
|
|
1128
|
+
await this.observability?.emit({
|
|
1129
|
+
event: 'ei.source.poll_failed',
|
|
1130
|
+
level: 'warn',
|
|
1131
|
+
scopeId: connection.scopeId,
|
|
1132
|
+
connectionId: connection.connectionId,
|
|
1133
|
+
serverId: connection.serverId,
|
|
1134
|
+
error,
|
|
1135
|
+
});
|
|
1100
1136
|
} finally {
|
|
1101
1137
|
if (
|
|
1102
1138
|
this.started &&
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type EventIntelligenceObservabilityLevel =
|
|
2
|
+
| 'debug'
|
|
3
|
+
| 'info'
|
|
4
|
+
| 'warn'
|
|
5
|
+
| 'error';
|
|
6
|
+
|
|
7
|
+
export interface EventIntelligenceObservabilityEvent {
|
|
8
|
+
schema: 'event-intelligence.observability.v1' | string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
level: EventIntelligenceObservabilityLevel;
|
|
11
|
+
event: `ei.${string}`;
|
|
12
|
+
component: string;
|
|
13
|
+
traceId?: string;
|
|
14
|
+
scopeId?: string;
|
|
15
|
+
triggerId?: string;
|
|
16
|
+
matchId?: string;
|
|
17
|
+
wakeId?: string;
|
|
18
|
+
connectionId?: string;
|
|
19
|
+
serverId?: string;
|
|
20
|
+
eventName?: string;
|
|
21
|
+
status?: string;
|
|
22
|
+
attempt?: number;
|
|
23
|
+
nextAttemptAt?: string;
|
|
24
|
+
error?: {
|
|
25
|
+
name?: string;
|
|
26
|
+
code?: string;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type EventIntelligenceObservabilitySink =
|
|
33
|
+
| ((event: EventIntelligenceObservabilityEvent) => void | Promise<void>)
|
|
34
|
+
| {
|
|
35
|
+
emit(
|
|
36
|
+
event: EventIntelligenceObservabilityEvent,
|
|
37
|
+
): void | Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface EventIntelligenceObservabilityEmitter {
|
|
41
|
+
readonly enabled: boolean;
|
|
42
|
+
emit(input: {
|
|
43
|
+
event: `ei.${string}`;
|
|
44
|
+
level?: EventIntelligenceObservabilityLevel;
|
|
45
|
+
timestamp?: string;
|
|
46
|
+
component?: string;
|
|
47
|
+
traceId?: string;
|
|
48
|
+
scopeId?: string;
|
|
49
|
+
triggerId?: string;
|
|
50
|
+
matchId?: string;
|
|
51
|
+
wakeId?: string;
|
|
52
|
+
connectionId?: string;
|
|
53
|
+
serverId?: string;
|
|
54
|
+
eventName?: string;
|
|
55
|
+
status?: string;
|
|
56
|
+
attempt?: number;
|
|
57
|
+
nextAttemptAt?: string;
|
|
58
|
+
error?: unknown;
|
|
59
|
+
metadata?: Record<string, unknown>;
|
|
60
|
+
}): Promise<boolean>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const EI_OBSERVABILITY_SCHEMA_VERSION: '1';
|
|
64
|
+
|
|
65
|
+
export function serializeObservabilityError(error: unknown): Readonly<{
|
|
66
|
+
name?: string;
|
|
67
|
+
code?: string;
|
|
68
|
+
message: string;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
export function createObservabilityEmitter(
|
|
72
|
+
sink?: EventIntelligenceObservabilitySink | null,
|
|
73
|
+
options?: {
|
|
74
|
+
now?: () => Date;
|
|
75
|
+
component?: string;
|
|
76
|
+
},
|
|
77
|
+
): EventIntelligenceObservabilityEmitter;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const LEVELS = new Set(['debug', 'info', 'warn', 'error']);
|
|
2
|
+
const SENSITIVE_KEY = /(authorization|cookie|credential|password|secret|token|payload|instruction|context|evidence|body|content|data)/i;
|
|
3
|
+
|
|
4
|
+
export const EI_OBSERVABILITY_SCHEMA_VERSION = '1';
|
|
5
|
+
|
|
6
|
+
function truncate(value, max = 1000) {
|
|
7
|
+
const text = String(value ?? '')
|
|
8
|
+
.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [redacted]')
|
|
9
|
+
.replace(
|
|
10
|
+
/((?:api[_-]?key|authorization|password|secret|token)\\s*[=:]\\s*)[^\\s,;]+/gi,
|
|
11
|
+
'$1[redacted]',
|
|
12
|
+
);
|
|
13
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sanitize(value, depth = 0) {
|
|
17
|
+
if (value == null || typeof value === 'boolean' || typeof value === 'number') {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'string') return truncate(value);
|
|
21
|
+
if (depth >= 3) return '[truncated]';
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.slice(0, 20).map((item) => sanitize(item, depth + 1));
|
|
24
|
+
}
|
|
25
|
+
if (value instanceof Error) {
|
|
26
|
+
return {
|
|
27
|
+
name: truncate(value.name, 120),
|
|
28
|
+
message: truncate(value.message),
|
|
29
|
+
...(value.code == null ? {} : { code: truncate(value.code, 200) }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === 'object') {
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, item] of Object.entries(value).slice(0, 40)) {
|
|
35
|
+
if (SENSITIVE_KEY.test(key)) continue;
|
|
36
|
+
const sanitized = sanitize(item, depth + 1);
|
|
37
|
+
if (sanitized !== undefined) result[key] = sanitized;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeSink(input) {
|
|
45
|
+
if (input == null) return null;
|
|
46
|
+
if (typeof input === 'function') return { emit: input };
|
|
47
|
+
if (typeof input === 'object' && typeof input.emit === 'function') {
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('observability must be a function or { emit(event) }');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function serializeObservabilityError(error) {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
name: truncate(error.name, 120),
|
|
57
|
+
message: truncate(error.message),
|
|
58
|
+
...(error.code == null ? {} : { code: truncate(error.code, 200) }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze({ message: truncate(error) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Runtime-neutral, best-effort observability boundary.
|
|
66
|
+
*
|
|
67
|
+
* Async sinks are detached after invocation, so slow telemetry cannot stall
|
|
68
|
+
* event processing. Sink failures are intentionally swallowed: telemetry must never
|
|
69
|
+
* change Event Intelligence delivery semantics. Event payloads, continuation
|
|
70
|
+
* instructions, evidence and credentials are not accepted as first-class
|
|
71
|
+
* fields and sensitive keys in metadata are dropped defensively.
|
|
72
|
+
*/
|
|
73
|
+
export function createObservabilityEmitter(
|
|
74
|
+
sinkInput,
|
|
75
|
+
{ now = () => new Date(), component = 'event-intelligence' } = {},
|
|
76
|
+
) {
|
|
77
|
+
const sink = normalizeSink(sinkInput);
|
|
78
|
+
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
enabled: Boolean(sink),
|
|
81
|
+
async emit(input = {}) {
|
|
82
|
+
if (!sink) return false;
|
|
83
|
+
|
|
84
|
+
const event = String(input.event ?? '').trim();
|
|
85
|
+
if (!event.startsWith('ei.')) {
|
|
86
|
+
throw new Error('observability event must use the ei.* namespace');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const level = String(input.level ?? 'info').toLowerCase();
|
|
90
|
+
if (!LEVELS.has(level)) {
|
|
91
|
+
throw new Error(`unsupported observability level: ${level}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const record = {
|
|
95
|
+
schema: `event-intelligence.observability.v${EI_OBSERVABILITY_SCHEMA_VERSION}`,
|
|
96
|
+
timestamp: input.timestamp
|
|
97
|
+
? String(input.timestamp)
|
|
98
|
+
: now().toISOString(),
|
|
99
|
+
level,
|
|
100
|
+
event,
|
|
101
|
+
component: String(input.component ?? component),
|
|
102
|
+
...(input.traceId ? { traceId: String(input.traceId) } : {}),
|
|
103
|
+
...(input.scopeId ? { scopeId: String(input.scopeId) } : {}),
|
|
104
|
+
...(input.triggerId ? { triggerId: String(input.triggerId) } : {}),
|
|
105
|
+
...(input.matchId ? { matchId: String(input.matchId) } : {}),
|
|
106
|
+
...(input.wakeId ? { wakeId: String(input.wakeId) } : {}),
|
|
107
|
+
...(input.connectionId ? { connectionId: String(input.connectionId) } : {}),
|
|
108
|
+
...(input.serverId ? { serverId: String(input.serverId) } : {}),
|
|
109
|
+
...(input.eventName ? { eventName: String(input.eventName) } : {}),
|
|
110
|
+
...(input.status ? { status: String(input.status) } : {}),
|
|
111
|
+
...(input.attempt != null ? { attempt: Number(input.attempt) } : {}),
|
|
112
|
+
...(input.nextAttemptAt
|
|
113
|
+
? { nextAttemptAt: String(input.nextAttemptAt) }
|
|
114
|
+
: {}),
|
|
115
|
+
...(input.error
|
|
116
|
+
? { error: serializeObservabilityError(input.error) }
|
|
117
|
+
: {}),
|
|
118
|
+
...(input.metadata && typeof input.metadata === 'object'
|
|
119
|
+
? { metadata: sanitize(input.metadata) }
|
|
120
|
+
: {}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const pending = sink.emit(Object.freeze(record));
|
|
125
|
+
if (pending && typeof pending.then === 'function') {
|
|
126
|
+
void Promise.resolve(pending).catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -4,11 +4,13 @@ export class TemporalDeadlineScheduler {
|
|
|
4
4
|
compositeEventConsumer,
|
|
5
5
|
now = () => new Date(),
|
|
6
6
|
intervalMs = 1000,
|
|
7
|
+
observability = null,
|
|
7
8
|
}) {
|
|
8
9
|
this.store = store;
|
|
9
10
|
this.compositeEventConsumer = compositeEventConsumer;
|
|
10
11
|
this.now = now;
|
|
11
12
|
this.intervalMs = Math.max(250, Number(intervalMs) || 1000);
|
|
13
|
+
this.observability = observability;
|
|
12
14
|
this.timer = null;
|
|
13
15
|
this.running = false;
|
|
14
16
|
}
|
|
@@ -94,6 +96,14 @@ export class TemporalDeadlineScheduler {
|
|
|
94
96
|
if (this.timer) return;
|
|
95
97
|
this.timer = setInterval(() => {
|
|
96
98
|
this.runDue().catch((error) => {
|
|
99
|
+
if (this.observability?.enabled) {
|
|
100
|
+
void this.observability.emit({
|
|
101
|
+
event: 'ei.scheduler.temporal_deadline_failed',
|
|
102
|
+
level: 'error',
|
|
103
|
+
error,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
97
107
|
console.error(JSON.stringify({
|
|
98
108
|
message: 'temporal_deadline_scheduler_error',
|
|
99
109
|
error: error instanceof Error ? error.message : String(error),
|