@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.
@@ -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();