@unrdf/observability 26.4.2
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/.eslintrc.cjs +10 -0
- package/IMPLEMENTATION-SUMMARY.md +478 -0
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/capability-map.md +90 -0
- package/config/alert-rules.yml +269 -0
- package/config/prometheus.yml +136 -0
- package/dashboards/grafana-unrdf.json +798 -0
- package/dashboards/unrdf-workflow-dashboard.json +295 -0
- package/docs/OBSERVABILITY-PATTERNS.md +681 -0
- package/docs/OBSERVABILITY-RUNBOOK.md +554 -0
- package/examples/observability-demo.mjs +334 -0
- package/package.json +46 -0
- package/src/advanced-metrics.mjs +413 -0
- package/src/alerts/alert-manager.mjs +436 -0
- package/src/custom-events.mjs +558 -0
- package/src/distributed-tracing.mjs +352 -0
- package/src/exporters/grafana-exporter.mjs +415 -0
- package/src/index.mjs +61 -0
- package/src/metrics/workflow-metrics.mjs +346 -0
- package/src/receipts/anchor.mjs +155 -0
- package/src/receipts/index.mjs +62 -0
- package/src/receipts/merkle-tree.mjs +188 -0
- package/src/receipts/receipt-chain.mjs +209 -0
- package/src/receipts/receipt-schema.mjs +128 -0
- package/src/receipts/tamper-detection.mjs +219 -0
- package/test/advanced-metrics.test.mjs +302 -0
- package/test/custom-events.test.mjs +387 -0
- package/test/distributed-tracing.test.mjs +314 -0
- package/validation/observability-validation.mjs +366 -0
- package/vitest.config.mjs +25 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Custom Event System
|
|
3
|
+
* @module observability/custom-events
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Security, performance, and business event tracking with structured
|
|
7
|
+
* event correlation and alerting integration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { trace, SpanKind, SpanStatusCode } from '@opentelemetry/api';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Event severity levels
|
|
15
|
+
*/
|
|
16
|
+
export const EventSeverity = {
|
|
17
|
+
DEBUG: 'debug',
|
|
18
|
+
INFO: 'info',
|
|
19
|
+
WARNING: 'warning',
|
|
20
|
+
ERROR: 'error',
|
|
21
|
+
CRITICAL: 'critical',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Event types
|
|
26
|
+
*/
|
|
27
|
+
export const EventType = {
|
|
28
|
+
// Security events
|
|
29
|
+
SECURITY_AUTH_FAILURE: 'security.auth.failure',
|
|
30
|
+
SECURITY_INJECTION_ATTEMPT: 'security.injection.attempt',
|
|
31
|
+
SECURITY_RATE_LIMIT_EXCEEDED: 'security.rate_limit.exceeded',
|
|
32
|
+
SECURITY_UNAUTHORIZED_ACCESS: 'security.unauthorized_access',
|
|
33
|
+
|
|
34
|
+
// Performance events
|
|
35
|
+
PERFORMANCE_SLOW_QUERY: 'performance.slow_query',
|
|
36
|
+
PERFORMANCE_TIMEOUT_WARNING: 'performance.timeout.warning',
|
|
37
|
+
PERFORMANCE_MEMORY_HIGH: 'performance.memory.high',
|
|
38
|
+
PERFORMANCE_CPU_HIGH: 'performance.cpu.high',
|
|
39
|
+
|
|
40
|
+
// Business events
|
|
41
|
+
BUSINESS_WORKFLOW_COMPLETE: 'business.workflow.complete',
|
|
42
|
+
BUSINESS_STATE_CHANGE: 'business.state.change',
|
|
43
|
+
BUSINESS_VALIDATION_FAILURE: 'business.validation.failure',
|
|
44
|
+
BUSINESS_TRANSACTION_COMPLETE: 'business.transaction.complete',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom event schema
|
|
49
|
+
*/
|
|
50
|
+
export const CustomEventSchema = z.object({
|
|
51
|
+
type: z.string(),
|
|
52
|
+
severity: z.enum([
|
|
53
|
+
EventSeverity.DEBUG,
|
|
54
|
+
EventSeverity.INFO,
|
|
55
|
+
EventSeverity.WARNING,
|
|
56
|
+
EventSeverity.ERROR,
|
|
57
|
+
EventSeverity.CRITICAL,
|
|
58
|
+
]),
|
|
59
|
+
message: z.string(),
|
|
60
|
+
timestamp: z.number(),
|
|
61
|
+
attributes: z.record(z.string(), z.any()),
|
|
62
|
+
correlationId: z.string().optional(),
|
|
63
|
+
userId: z.string().optional(),
|
|
64
|
+
spanId: z.string().optional(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Custom events manager
|
|
69
|
+
*
|
|
70
|
+
* Provides structured event tracking for:
|
|
71
|
+
* - Security incidents (auth failures, injection attempts)
|
|
72
|
+
* - Performance anomalies (slow queries, timeouts)
|
|
73
|
+
* - Business events (workflow completion, state changes)
|
|
74
|
+
*/
|
|
75
|
+
export class CustomEvents {
|
|
76
|
+
/**
|
|
77
|
+
* Create custom events manager
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} [config] - Configuration
|
|
80
|
+
* @param {string} [config.serviceName='unrdf'] - Service name
|
|
81
|
+
* @param {boolean} [config.enabled=true] - Enable events
|
|
82
|
+
* @param {Function} [config.eventHandler] - Custom event handler
|
|
83
|
+
*/
|
|
84
|
+
constructor(config = {}) {
|
|
85
|
+
this.serviceName = config.serviceName || 'unrdf';
|
|
86
|
+
this.enabled = config.enabled !== false;
|
|
87
|
+
this.eventHandler = config.eventHandler;
|
|
88
|
+
this.tracer = trace.getTracer(this.serviceName);
|
|
89
|
+
|
|
90
|
+
// Event storage (last 1000 events)
|
|
91
|
+
this.events = [];
|
|
92
|
+
this.maxEvents = 1000;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Emit a security event
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} options - Event options
|
|
99
|
+
* @param {string} options.type - Event type
|
|
100
|
+
* @param {string} options.message - Event message
|
|
101
|
+
* @param {Object} [options.attributes] - Additional attributes
|
|
102
|
+
* @param {string} [options.userId] - User ID if applicable
|
|
103
|
+
* @param {string} [options.correlationId] - Correlation ID
|
|
104
|
+
*/
|
|
105
|
+
emitSecurityEvent({ type, message, attributes = {}, userId, correlationId }) {
|
|
106
|
+
return this._emitEvent({
|
|
107
|
+
type,
|
|
108
|
+
severity: this._getSeverityForSecurityEvent(type),
|
|
109
|
+
message,
|
|
110
|
+
attributes: {
|
|
111
|
+
...attributes,
|
|
112
|
+
category: 'security',
|
|
113
|
+
},
|
|
114
|
+
userId,
|
|
115
|
+
correlationId,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Emit authentication failure event
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} options - Event options
|
|
123
|
+
* @param {string} options.userId - User ID that failed auth
|
|
124
|
+
* @param {string} options.reason - Failure reason
|
|
125
|
+
* @param {string} [options.ip] - IP address
|
|
126
|
+
* @param {Object} [options.metadata] - Additional metadata
|
|
127
|
+
*/
|
|
128
|
+
emitAuthFailure({ userId, reason, ip, metadata = {} }) {
|
|
129
|
+
return this.emitSecurityEvent({
|
|
130
|
+
type: EventType.SECURITY_AUTH_FAILURE,
|
|
131
|
+
message: `Authentication failure for user: ${userId}`,
|
|
132
|
+
attributes: {
|
|
133
|
+
'auth.user_id': userId,
|
|
134
|
+
'auth.failure_reason': reason,
|
|
135
|
+
'auth.ip_address': ip,
|
|
136
|
+
...metadata,
|
|
137
|
+
},
|
|
138
|
+
userId,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Emit injection attempt event
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} options - Event options
|
|
146
|
+
* @param {string} options.attackType - Type of injection (SQL, SPARQL, command)
|
|
147
|
+
* @param {string} options.payload - Attack payload (sanitized)
|
|
148
|
+
* @param {string} [options.userId] - User ID if authenticated
|
|
149
|
+
* @param {string} [options.ip] - IP address
|
|
150
|
+
*/
|
|
151
|
+
emitInjectionAttempt({ attackType, payload, userId, ip }) {
|
|
152
|
+
return this.emitSecurityEvent({
|
|
153
|
+
type: EventType.SECURITY_INJECTION_ATTEMPT,
|
|
154
|
+
message: `${attackType} injection attempt detected`,
|
|
155
|
+
attributes: {
|
|
156
|
+
'injection.type': attackType,
|
|
157
|
+
'injection.payload_hash': this._hashPayload(payload),
|
|
158
|
+
'injection.ip_address': ip,
|
|
159
|
+
},
|
|
160
|
+
userId,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Emit performance event
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} options - Event options
|
|
168
|
+
* @param {string} options.type - Event type
|
|
169
|
+
* @param {string} options.message - Event message
|
|
170
|
+
* @param {Object} [options.attributes] - Additional attributes
|
|
171
|
+
* @param {string} [options.correlationId] - Correlation ID
|
|
172
|
+
*/
|
|
173
|
+
emitPerformanceEvent({ type, message, attributes = {}, correlationId }) {
|
|
174
|
+
return this._emitEvent({
|
|
175
|
+
type,
|
|
176
|
+
severity: this._getSeverityForPerformanceEvent(type, attributes),
|
|
177
|
+
message,
|
|
178
|
+
attributes: {
|
|
179
|
+
...attributes,
|
|
180
|
+
category: 'performance',
|
|
181
|
+
},
|
|
182
|
+
correlationId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Emit slow query event
|
|
188
|
+
*
|
|
189
|
+
* @param {Object} options - Event options
|
|
190
|
+
* @param {string} options.query - Query (sanitized)
|
|
191
|
+
* @param {number} options.duration - Query duration in ms
|
|
192
|
+
* @param {number} options.threshold - Slow query threshold in ms
|
|
193
|
+
* @param {Object} [options.metadata] - Additional metadata
|
|
194
|
+
*/
|
|
195
|
+
emitSlowQuery({ query, duration, threshold, metadata = {} }) {
|
|
196
|
+
return this.emitPerformanceEvent({
|
|
197
|
+
type: EventType.PERFORMANCE_SLOW_QUERY,
|
|
198
|
+
message: `Slow query detected: ${duration}ms (threshold: ${threshold}ms)`,
|
|
199
|
+
attributes: {
|
|
200
|
+
'query.duration_ms': duration,
|
|
201
|
+
'query.threshold_ms': threshold,
|
|
202
|
+
'query.hash': this._hashPayload(query),
|
|
203
|
+
...metadata,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Emit timeout warning event
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} options - Event options
|
|
212
|
+
* @param {string} options.operation - Operation name
|
|
213
|
+
* @param {number} options.elapsed - Elapsed time in ms
|
|
214
|
+
* @param {number} options.timeout - Timeout threshold in ms
|
|
215
|
+
*/
|
|
216
|
+
emitTimeoutWarning({ operation, elapsed, timeout }) {
|
|
217
|
+
return this.emitPerformanceEvent({
|
|
218
|
+
type: EventType.PERFORMANCE_TIMEOUT_WARNING,
|
|
219
|
+
message: `Operation approaching timeout: ${elapsed}ms / ${timeout}ms`,
|
|
220
|
+
attributes: {
|
|
221
|
+
'operation.name': operation,
|
|
222
|
+
'operation.elapsed_ms': elapsed,
|
|
223
|
+
'operation.timeout_ms': timeout,
|
|
224
|
+
'operation.remaining_ms': timeout - elapsed,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Emit high memory usage event
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} options - Event options
|
|
233
|
+
* @param {number} options.heapUsed - Heap used in bytes
|
|
234
|
+
* @param {number} options.heapTotal - Heap total in bytes
|
|
235
|
+
* @param {number} options.threshold - Threshold as fraction (0-1)
|
|
236
|
+
*/
|
|
237
|
+
emitHighMemory({ heapUsed, heapTotal, threshold }) {
|
|
238
|
+
const usageRatio = heapUsed / heapTotal;
|
|
239
|
+
|
|
240
|
+
return this.emitPerformanceEvent({
|
|
241
|
+
type: EventType.PERFORMANCE_MEMORY_HIGH,
|
|
242
|
+
message: `High memory usage: ${Math.round(usageRatio * 100)}%`,
|
|
243
|
+
attributes: {
|
|
244
|
+
'memory.heap_used': heapUsed,
|
|
245
|
+
'memory.heap_total': heapTotal,
|
|
246
|
+
'memory.usage_ratio': usageRatio,
|
|
247
|
+
'memory.threshold': threshold,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Emit business event
|
|
254
|
+
*
|
|
255
|
+
* @param {Object} options - Event options
|
|
256
|
+
* @param {string} options.type - Event type
|
|
257
|
+
* @param {string} options.message - Event message
|
|
258
|
+
* @param {Object} [options.attributes] - Additional attributes
|
|
259
|
+
* @param {string} [options.userId] - User ID
|
|
260
|
+
* @param {string} [options.correlationId] - Correlation ID
|
|
261
|
+
*/
|
|
262
|
+
emitBusinessEvent({ type, message, attributes = {}, userId, correlationId }) {
|
|
263
|
+
return this._emitEvent({
|
|
264
|
+
type,
|
|
265
|
+
severity: EventSeverity.INFO,
|
|
266
|
+
message,
|
|
267
|
+
attributes: {
|
|
268
|
+
...attributes,
|
|
269
|
+
category: 'business',
|
|
270
|
+
},
|
|
271
|
+
userId,
|
|
272
|
+
correlationId,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Emit workflow completion event
|
|
278
|
+
*
|
|
279
|
+
* @param {Object} options - Event options
|
|
280
|
+
* @param {string} options.workflowId - Workflow ID
|
|
281
|
+
* @param {string} options.workflowType - Workflow type
|
|
282
|
+
* @param {number} options.duration - Workflow duration in ms
|
|
283
|
+
* @param {boolean} options.success - Whether workflow succeeded
|
|
284
|
+
* @param {Object} [options.metadata] - Additional metadata
|
|
285
|
+
*/
|
|
286
|
+
emitWorkflowComplete({ workflowId, workflowType, duration, success, metadata = {} }) {
|
|
287
|
+
return this.emitBusinessEvent({
|
|
288
|
+
type: EventType.BUSINESS_WORKFLOW_COMPLETE,
|
|
289
|
+
message: `Workflow ${workflowId} completed: ${success ? 'success' : 'failure'}`,
|
|
290
|
+
attributes: {
|
|
291
|
+
'workflow.id': workflowId,
|
|
292
|
+
'workflow.type': workflowType,
|
|
293
|
+
'workflow.duration_ms': duration,
|
|
294
|
+
'workflow.success': success,
|
|
295
|
+
...metadata,
|
|
296
|
+
},
|
|
297
|
+
correlationId: workflowId,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Emit state change event
|
|
303
|
+
*
|
|
304
|
+
* @param {Object} options - Event options
|
|
305
|
+
* @param {string} options.entity - Entity type
|
|
306
|
+
* @param {string} options.entityId - Entity ID
|
|
307
|
+
* @param {string} options.fromState - Previous state
|
|
308
|
+
* @param {string} options.toState - New state
|
|
309
|
+
* @param {string} [options.userId] - User who triggered change
|
|
310
|
+
*/
|
|
311
|
+
emitStateChange({ entity, entityId, fromState, toState, userId }) {
|
|
312
|
+
return this.emitBusinessEvent({
|
|
313
|
+
type: EventType.BUSINESS_STATE_CHANGE,
|
|
314
|
+
message: `${entity} ${entityId} state changed: ${fromState} → ${toState}`,
|
|
315
|
+
attributes: {
|
|
316
|
+
'state.entity': entity,
|
|
317
|
+
'state.entity_id': entityId,
|
|
318
|
+
'state.from': fromState,
|
|
319
|
+
'state.to': toState,
|
|
320
|
+
},
|
|
321
|
+
userId,
|
|
322
|
+
correlationId: entityId,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Emit event (internal)
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} eventData - Event data
|
|
330
|
+
* @returns {Object} Created event
|
|
331
|
+
* @private
|
|
332
|
+
*/
|
|
333
|
+
_emitEvent(eventData) {
|
|
334
|
+
if (!this.enabled) return null;
|
|
335
|
+
|
|
336
|
+
// Create span for event
|
|
337
|
+
const span = this.tracer.startSpan(`event.${eventData.type}`, {
|
|
338
|
+
kind: SpanKind.INTERNAL,
|
|
339
|
+
attributes: {
|
|
340
|
+
'event.type': eventData.type,
|
|
341
|
+
'event.severity': eventData.severity,
|
|
342
|
+
'event.category': eventData.attributes.category,
|
|
343
|
+
...eventData.attributes,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Create event object
|
|
348
|
+
const event = CustomEventSchema.parse({
|
|
349
|
+
...eventData,
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
spanId: span.spanContext().spanId,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Store event
|
|
355
|
+
this.events.push(event);
|
|
356
|
+
if (this.events.length > this.maxEvents) {
|
|
357
|
+
this.events = this.events.slice(-this.maxEvents);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Call custom handler if provided
|
|
361
|
+
if (this.eventHandler) {
|
|
362
|
+
try {
|
|
363
|
+
this.eventHandler(event);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('[CustomEvents] Handler error:', error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Log event
|
|
370
|
+
const logLevel = this._getLogLevel(event.severity);
|
|
371
|
+
console[logLevel](`[CustomEvents] ${event.type}: ${event.message}`, event.attributes);
|
|
372
|
+
|
|
373
|
+
// End span
|
|
374
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
375
|
+
span.end();
|
|
376
|
+
|
|
377
|
+
return event;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get severity for security event
|
|
382
|
+
*
|
|
383
|
+
* @param {string} type - Event type
|
|
384
|
+
* @returns {string} Severity level
|
|
385
|
+
* @private
|
|
386
|
+
*/
|
|
387
|
+
_getSeverityForSecurityEvent(type) {
|
|
388
|
+
const severityMap = {
|
|
389
|
+
[EventType.SECURITY_AUTH_FAILURE]: EventSeverity.WARNING,
|
|
390
|
+
[EventType.SECURITY_INJECTION_ATTEMPT]: EventSeverity.CRITICAL,
|
|
391
|
+
[EventType.SECURITY_RATE_LIMIT_EXCEEDED]: EventSeverity.WARNING,
|
|
392
|
+
[EventType.SECURITY_UNAUTHORIZED_ACCESS]: EventSeverity.ERROR,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
return severityMap[type] || EventSeverity.WARNING;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get severity for performance event
|
|
400
|
+
*
|
|
401
|
+
* @param {string} type - Event type
|
|
402
|
+
* @param {Object} attributes - Event attributes
|
|
403
|
+
* @returns {string} Severity level
|
|
404
|
+
* @private
|
|
405
|
+
*/
|
|
406
|
+
_getSeverityForPerformanceEvent(type, attributes) {
|
|
407
|
+
// Timeout warnings are critical if very close to timeout
|
|
408
|
+
if (type === EventType.PERFORMANCE_TIMEOUT_WARNING) {
|
|
409
|
+
const remaining = attributes['operation.remaining_ms'] || 0;
|
|
410
|
+
return remaining < 1000 ? EventSeverity.CRITICAL : EventSeverity.WARNING;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// High memory is critical if > 90%
|
|
414
|
+
if (type === EventType.PERFORMANCE_MEMORY_HIGH) {
|
|
415
|
+
const ratio = attributes['memory.usage_ratio'] || 0;
|
|
416
|
+
return ratio > 0.9 ? EventSeverity.CRITICAL : EventSeverity.WARNING;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return EventSeverity.WARNING;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get log level for severity
|
|
424
|
+
*
|
|
425
|
+
* @param {string} severity - Event severity
|
|
426
|
+
* @returns {string} Console log level
|
|
427
|
+
* @private
|
|
428
|
+
*/
|
|
429
|
+
_getLogLevel(severity) {
|
|
430
|
+
const levelMap = {
|
|
431
|
+
[EventSeverity.DEBUG]: 'debug',
|
|
432
|
+
[EventSeverity.INFO]: 'info',
|
|
433
|
+
[EventSeverity.WARNING]: 'warn',
|
|
434
|
+
[EventSeverity.ERROR]: 'error',
|
|
435
|
+
[EventSeverity.CRITICAL]: 'error',
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
return levelMap[severity] || 'info';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Hash payload for storage (to avoid storing sensitive data)
|
|
443
|
+
*
|
|
444
|
+
* @param {string} payload - Payload to hash
|
|
445
|
+
* @returns {string} Hash
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
_hashPayload(payload) {
|
|
449
|
+
// Simple hash for demonstration (use crypto in production)
|
|
450
|
+
let hash = 0;
|
|
451
|
+
for (let i = 0; i < payload.length; i++) {
|
|
452
|
+
const char = payload.charCodeAt(i);
|
|
453
|
+
hash = (hash << 5) - hash + char;
|
|
454
|
+
hash = hash & hash;
|
|
455
|
+
}
|
|
456
|
+
return hash.toString(16);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get events by type
|
|
461
|
+
*
|
|
462
|
+
* @param {string} type - Event type
|
|
463
|
+
* @param {Object} [options] - Filter options
|
|
464
|
+
* @param {number} [options.limit=100] - Max events to return
|
|
465
|
+
* @param {number} [options.since] - Timestamp to filter from
|
|
466
|
+
* @returns {Array} Filtered events
|
|
467
|
+
*/
|
|
468
|
+
getEventsByType(type, options = {}) {
|
|
469
|
+
const { limit = 100, since } = options;
|
|
470
|
+
|
|
471
|
+
let filtered = this.events.filter(e => e.type === type);
|
|
472
|
+
|
|
473
|
+
if (since) {
|
|
474
|
+
filtered = filtered.filter(e => e.timestamp >= since);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return filtered.slice(-limit);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get events by severity
|
|
482
|
+
*
|
|
483
|
+
* @param {string} severity - Severity level
|
|
484
|
+
* @param {Object} [options] - Filter options
|
|
485
|
+
* @returns {Array} Filtered events
|
|
486
|
+
*/
|
|
487
|
+
getEventsBySeverity(severity, options = {}) {
|
|
488
|
+
const { limit = 100, since } = options;
|
|
489
|
+
|
|
490
|
+
let filtered = this.events.filter(e => e.severity === severity);
|
|
491
|
+
|
|
492
|
+
if (since) {
|
|
493
|
+
filtered = filtered.filter(e => e.timestamp >= since);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return filtered.slice(-limit);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get events by correlation ID
|
|
501
|
+
*
|
|
502
|
+
* @param {string} correlationId - Correlation ID
|
|
503
|
+
* @returns {Array} Correlated events
|
|
504
|
+
*/
|
|
505
|
+
getEventsByCorrelationId(correlationId) {
|
|
506
|
+
return this.events.filter(e => e.correlationId === correlationId);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Clear stored events
|
|
511
|
+
*/
|
|
512
|
+
clearEvents() {
|
|
513
|
+
this.events = [];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get event statistics
|
|
518
|
+
*
|
|
519
|
+
* @returns {Object} Event stats
|
|
520
|
+
*/
|
|
521
|
+
getStats() {
|
|
522
|
+
const stats = {
|
|
523
|
+
total: this.events.length,
|
|
524
|
+
bySeverity: {},
|
|
525
|
+
byType: {},
|
|
526
|
+
byCategory: {},
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
for (const event of this.events) {
|
|
530
|
+
// Count by severity
|
|
531
|
+
stats.bySeverity[event.severity] = (stats.bySeverity[event.severity] || 0) + 1;
|
|
532
|
+
|
|
533
|
+
// Count by type
|
|
534
|
+
stats.byType[event.type] = (stats.byType[event.type] || 0) + 1;
|
|
535
|
+
|
|
536
|
+
// Count by category
|
|
537
|
+
const category = event.attributes.category || 'unknown';
|
|
538
|
+
stats.byCategory[category] = (stats.byCategory[category] || 0) + 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return stats;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Create custom events instance
|
|
547
|
+
*
|
|
548
|
+
* @param {Object} [config] - Configuration
|
|
549
|
+
* @returns {CustomEvents} Events instance
|
|
550
|
+
*/
|
|
551
|
+
export function createCustomEvents(config = {}) {
|
|
552
|
+
return new CustomEvents(config);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Default custom events instance
|
|
557
|
+
*/
|
|
558
|
+
export const defaultCustomEvents = createCustomEvents();
|