@unrdf/knowledge-engine 5.0.1

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/package.json +64 -0
  4. package/src/browser-shims.mjs +343 -0
  5. package/src/browser.mjs +910 -0
  6. package/src/canonicalize.mjs +414 -0
  7. package/src/condition-cache.mjs +109 -0
  8. package/src/condition-evaluator.mjs +722 -0
  9. package/src/dark-matter-core.mjs +742 -0
  10. package/src/define-hook.mjs +213 -0
  11. package/src/effect-sandbox-browser.mjs +283 -0
  12. package/src/effect-sandbox-worker.mjs +170 -0
  13. package/src/effect-sandbox.mjs +517 -0
  14. package/src/engines/index.mjs +11 -0
  15. package/src/engines/rdf-engine.mjs +299 -0
  16. package/src/file-resolver.mjs +387 -0
  17. package/src/hook-executor-batching.mjs +277 -0
  18. package/src/hook-executor.mjs +870 -0
  19. package/src/hook-management.mjs +150 -0
  20. package/src/index.mjs +93 -0
  21. package/src/ken-parliment.mjs +119 -0
  22. package/src/ken.mjs +149 -0
  23. package/src/knowledge-engine/builtin-rules.mjs +190 -0
  24. package/src/knowledge-engine/inference-engine.mjs +418 -0
  25. package/src/knowledge-engine/knowledge-engine.mjs +317 -0
  26. package/src/knowledge-engine/pattern-dsl.mjs +142 -0
  27. package/src/knowledge-engine/pattern-matcher.mjs +215 -0
  28. package/src/knowledge-engine/rules.mjs +184 -0
  29. package/src/knowledge-engine.mjs +319 -0
  30. package/src/knowledge-hook-engine.mjs +360 -0
  31. package/src/knowledge-hook-manager.mjs +469 -0
  32. package/src/knowledge-substrate-core.mjs +927 -0
  33. package/src/lite.mjs +222 -0
  34. package/src/lockchain-writer-browser.mjs +414 -0
  35. package/src/lockchain-writer.mjs +602 -0
  36. package/src/monitoring/andon-signals.mjs +775 -0
  37. package/src/observability.mjs +531 -0
  38. package/src/parse.mjs +290 -0
  39. package/src/performance-optimizer.mjs +678 -0
  40. package/src/policy-pack.mjs +572 -0
  41. package/src/query-cache.mjs +116 -0
  42. package/src/query-optimizer.mjs +1051 -0
  43. package/src/query.mjs +306 -0
  44. package/src/reason.mjs +350 -0
  45. package/src/resolution-layer.mjs +506 -0
  46. package/src/schemas.mjs +1063 -0
  47. package/src/security/error-sanitizer.mjs +257 -0
  48. package/src/security/path-validator.mjs +194 -0
  49. package/src/security/sandbox-restrictions.mjs +331 -0
  50. package/src/security-validator.mjs +389 -0
  51. package/src/store-cache.mjs +137 -0
  52. package/src/telemetry.mjs +167 -0
  53. package/src/transaction.mjs +810 -0
  54. package/src/utils/adaptive-monitor.mjs +746 -0
  55. package/src/utils/circuit-breaker.mjs +513 -0
  56. package/src/utils/edge-case-handler.mjs +503 -0
  57. package/src/utils/memory-manager.mjs +498 -0
  58. package/src/utils/ring-buffer.mjs +282 -0
  59. package/src/validate.mjs +319 -0
  60. package/src/validators/index.mjs +338 -0
@@ -0,0 +1,531 @@
1
+ /**
2
+ * @file OpenTelemetry Observability for UNRDF
3
+ * @module observability
4
+ *
5
+ * @description
6
+ * Implements comprehensive observability with OpenTelemetry traces, metrics,
7
+ * and logging for the UNRDF Knowledge Engine. Provides backpressure monitoring,
8
+ * error isolation, and performance tracking.
9
+ */
10
+
11
+ import { _randomUUID } from 'crypto';
12
+ import { _z } from 'zod';
13
+ import { ObservabilityConfigSchema, PerformanceMetricsSchema } from './schemas.mjs';
14
+ import { getRuntimeConfig } from '../context/config.mjs';
15
+
16
+ /**
17
+ * OpenTelemetry observability manager
18
+ */
19
+ export class ObservabilityManager {
20
+ /**
21
+ * Create a new observability manager
22
+ * @param {Object} [config] - Observability configuration
23
+ */
24
+ constructor(config = {}) {
25
+ this.config = ObservabilityConfigSchema.parse(config);
26
+ this.tracer = null;
27
+ this.meter = null;
28
+ this.logger = null;
29
+ this.metrics = {
30
+ transactionLatency: [],
31
+ hookExecutionRate: 0,
32
+ errorCount: 0,
33
+ totalTransactions: 0,
34
+ memoryUsage: [],
35
+ cacheStats: { hits: 0, misses: 0, size: 0 },
36
+ backpressure: { queueDepth: 0, watermarks: { high: 1000, low: 100 } },
37
+ };
38
+ this.activeSpans = new Map();
39
+ this.initialized = false;
40
+ this._lastMetrics = null; // for smoothing fallback
41
+ }
42
+
43
+ /**
44
+ * Initialize OpenTelemetry components
45
+ * @returns {Promise<void>}
46
+ */
47
+ async initialize() {
48
+ if (this.initialized) return;
49
+
50
+ try {
51
+ // Dynamic import of OpenTelemetry packages
52
+ const { NodeSDK } = await import('@opentelemetry/sdk-node');
53
+ const { getNodeAutoInstrumentations } =
54
+ await import('@opentelemetry/auto-instrumentations-node');
55
+ const { Resource } = await import('@opentelemetry/resources');
56
+ const { SemanticResourceAttributes } = await import('@opentelemetry/semantic-conventions');
57
+ const { OTLPTraceExporter } = await import('@opentelemetry/exporter-otlp-http');
58
+ const { OTLPMetricExporter } = await import('@opentelemetry/exporter-otlp-http');
59
+ const { PeriodicExportingMetricReader } = await import('@opentelemetry/sdk-metrics');
60
+ const { trace, metrics } = await import('@opentelemetry/api');
61
+
62
+ // Create resource
63
+ const resource = new Resource({
64
+ [SemanticResourceAttributes.SERVICE_NAME]: this.config.serviceName,
65
+ [SemanticResourceAttributes.SERVICE_VERSION]: this.config.serviceVersion,
66
+ ...this.config.resourceAttributes,
67
+ });
68
+
69
+ // Create exporters
70
+ const traceExporter = new OTLPTraceExporter({
71
+ url: this.config.endpoint ? `${this.config.endpoint}/v1/traces` : undefined,
72
+ headers: this.config.headers,
73
+ });
74
+
75
+ const metricExporter = new OTLPMetricExporter({
76
+ url: this.config.endpoint ? `${this.config.endpoint}/v1/metrics` : undefined,
77
+ headers: this.config.headers,
78
+ });
79
+
80
+ // Create metric reader
81
+ const metricReader = new PeriodicExportingMetricReader({
82
+ exporter: metricExporter,
83
+ exportIntervalMillis: this.config.scheduledDelayMillis,
84
+ exportTimeoutMillis: this.config.exportTimeoutMillis,
85
+ });
86
+
87
+ // Initialize SDK
88
+ const sdk = new NodeSDK({
89
+ resource,
90
+ traceExporter: this.config.enableTracing ? traceExporter : undefined,
91
+ metricReader: this.config.enableMetrics ? metricReader : undefined,
92
+ instrumentations: this.config.enableTracing ? [getNodeAutoInstrumentations()] : [],
93
+ });
94
+
95
+ await sdk.start();
96
+
97
+ // Get tracer and meter
98
+ this.tracer = trace.getTracer(this.config.serviceName, this.config.serviceVersion);
99
+ this.meter = metrics.getMeter(this.config.serviceName, this.config.serviceVersion);
100
+
101
+ // Create custom metrics
102
+ this._createCustomMetrics();
103
+
104
+ this.initialized = true;
105
+ console.log(`[Observability] Initialized with service: ${this.config.serviceName}`);
106
+ } catch (error) {
107
+ console.warn(`[Observability] Failed to initialize OpenTelemetry: ${error.message}`);
108
+ // Fallback to console logging
109
+ this._initializeFallback();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Create custom metrics
115
+ * @private
116
+ */
117
+ _createCustomMetrics() {
118
+ if (!this.meter) return;
119
+
120
+ // Transaction metrics
121
+ this.transactionCounter = this.meter.createCounter('kgc_transactions_total', {
122
+ description: 'Total number of transactions processed',
123
+ });
124
+
125
+ this.transactionDuration = this.meter.createHistogram('kgc_transaction_duration_ms', {
126
+ description: 'Transaction processing duration in milliseconds',
127
+ unit: 'ms',
128
+ });
129
+
130
+ this.hookExecutionCounter = this.meter.createCounter('kgc_hooks_executed_total', {
131
+ description: 'Total number of hooks executed',
132
+ });
133
+
134
+ this.hookDuration = this.meter.createHistogram('kgc_hook_duration_ms', {
135
+ description: 'Hook execution duration in milliseconds',
136
+ unit: 'ms',
137
+ });
138
+
139
+ this.errorCounter = this.meter.createCounter('kgc_errors_total', {
140
+ description: 'Total number of errors',
141
+ });
142
+
143
+ this.memoryGauge = this.meter.createUpDownCounter('kgc_memory_usage_bytes', {
144
+ description: 'Memory usage in bytes',
145
+ });
146
+
147
+ this.cacheHitCounter = this.meter.createCounter('kgc_cache_hits_total', {
148
+ description: 'Total cache hits',
149
+ });
150
+
151
+ this.cacheMissCounter = this.meter.createCounter('kgc_cache_misses_total', {
152
+ description: 'Total cache misses',
153
+ });
154
+
155
+ this.queueDepthGauge = this.meter.createUpDownCounter('kgc_queue_depth', {
156
+ description: 'Current queue depth',
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Initialize fallback observability
162
+ * @private
163
+ */
164
+ _initializeFallback() {
165
+ console.log('[Observability] Using fallback console logging');
166
+ this.initialized = true;
167
+ }
168
+
169
+ /**
170
+ * Start a transaction span
171
+ * @param {string} transactionId - Transaction ID
172
+ * @param {Object} [attributes] - Span attributes
173
+ * @returns {Object} Span context
174
+ */
175
+ startTransactionSpan(transactionId, attributes = {}) {
176
+ if (!this.tracer) {
177
+ return { transactionId, startTime: Date.now() };
178
+ }
179
+
180
+ const span = this.tracer.startSpan('kgc.transaction', {
181
+ attributes: {
182
+ 'kgc.transaction.id': transactionId,
183
+ 'kgc.service.name': this.config.serviceName,
184
+ ...attributes,
185
+ },
186
+ });
187
+
188
+ const spanContext = { transactionId, span, startTime: Date.now() };
189
+ this.activeSpans.set(transactionId, spanContext);
190
+ return spanContext;
191
+ }
192
+
193
+ /**
194
+ * End a transaction span
195
+ * @param {string} transactionId - Transaction ID
196
+ * @param {Object} [attributes] - Final span attributes
197
+ * @param {Error} [error] - Error if transaction failed
198
+ */
199
+ endTransactionSpan(transactionId, attributes = {}, error = null) {
200
+ const spanContext = this.activeSpans.get(transactionId);
201
+ if (!spanContext) return;
202
+
203
+ const { span, startTime } = spanContext;
204
+ const duration = Date.now() - startTime;
205
+
206
+ if (span) {
207
+ span.setAttributes({
208
+ 'kgc.transaction.duration_ms': duration,
209
+ 'kgc.transaction.success': !error,
210
+ ...attributes,
211
+ });
212
+
213
+ if (error) {
214
+ span.recordException(error);
215
+ span.setStatus({ code: 2, message: error.message }); // ERROR
216
+ } else {
217
+ span.setStatus({ code: 1 }); // OK
218
+ }
219
+
220
+ span.end();
221
+ }
222
+
223
+ this.activeSpans.delete(transactionId);
224
+
225
+ // Update metrics
226
+ this._updateTransactionMetrics(duration, !error);
227
+ }
228
+
229
+ /**
230
+ * Start a hook execution span
231
+ * @param {string} hookId - Hook ID
232
+ * @param {string} transactionId - Parent transaction ID
233
+ * @param {Object} [attributes] - Span attributes
234
+ * @returns {Object} Span context
235
+ */
236
+ startHookSpan(hookId, transactionId, attributes = {}) {
237
+ if (!this.tracer) {
238
+ return { hookId, startTime: Date.now() };
239
+ }
240
+
241
+ const parentSpan = this.activeSpans.get(transactionId)?.span;
242
+ const span = this.tracer.startSpan('kgc.hook', {
243
+ parent: parentSpan,
244
+ attributes: {
245
+ 'kgc.hook.id': hookId,
246
+ 'kgc.transaction.id': transactionId,
247
+ ...attributes,
248
+ },
249
+ });
250
+
251
+ const spanKey = `${transactionId}:${hookId}`;
252
+ const spanContext = { hookId, span, startTime: Date.now() };
253
+ this.activeSpans.set(spanKey, spanContext);
254
+ return spanContext;
255
+ }
256
+
257
+ /**
258
+ * End a hook execution span
259
+ * @param {string} hookId - Hook ID
260
+ * @param {string} transactionId - Parent transaction ID
261
+ * @param {Object} [attributes] - Final span attributes
262
+ * @param {Error} [error] - Error if hook failed
263
+ */
264
+ endHookSpan(hookId, transactionId, attributes = {}, error = null) {
265
+ const spanKey = `${transactionId}:${hookId}`;
266
+ const spanContext = this.activeSpans.get(spanKey);
267
+ if (!spanContext) return;
268
+
269
+ const { span, startTime } = spanContext;
270
+ const duration = Date.now() - startTime;
271
+
272
+ if (span) {
273
+ span.setAttributes({
274
+ 'kgc.hook.duration_ms': duration,
275
+ 'kgc.hook.success': !error,
276
+ ...attributes,
277
+ });
278
+
279
+ if (error) {
280
+ span.recordException(error);
281
+ span.setStatus({ code: 2, message: error.message }); // ERROR
282
+ } else {
283
+ span.setStatus({ code: 1 }); // OK
284
+ }
285
+
286
+ span.end();
287
+ }
288
+
289
+ this.activeSpans.delete(spanKey);
290
+
291
+ // Update metrics
292
+ this._updateHookMetrics(duration, !error);
293
+ }
294
+
295
+ /**
296
+ * Record an error
297
+ * @param {Error} error - Error to record
298
+ * @param {Object} [attributes] - Error attributes
299
+ */
300
+ recordError(error, attributes = {}) {
301
+ this.metrics.errorCount++;
302
+
303
+ if (this.errorCounter) {
304
+ this.errorCounter.add(1, {
305
+ 'error.type': error.constructor.name,
306
+ 'error.message': error.message,
307
+ ...attributes,
308
+ });
309
+ }
310
+
311
+ console.error(`[Observability] Error recorded:`, error.message, attributes);
312
+ }
313
+
314
+ /**
315
+ * Update cache statistics
316
+ * @param {boolean} hit - Whether it was a cache hit
317
+ */
318
+ updateCacheStats(hit) {
319
+ if (hit) {
320
+ this.metrics.cacheStats.hits++;
321
+ if (this.cacheHitCounter) {
322
+ this.cacheHitCounter.add(1);
323
+ }
324
+ } else {
325
+ this.metrics.cacheStats.misses++;
326
+ if (this.cacheMissCounter) {
327
+ this.cacheMissCounter.add(1);
328
+ }
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Update queue depth
334
+ * @param {number} depth - Current queue depth
335
+ */
336
+ updateQueueDepth(depth) {
337
+ this.metrics.backpressure.queueDepth = depth;
338
+
339
+ if (this.queueDepthGauge) {
340
+ this.queueDepthGauge.add(depth - this.metrics.backpressure.queueDepth);
341
+ }
342
+
343
+ // Check watermarks
344
+ if (depth > this.metrics.backpressure.watermarks.high) {
345
+ console.warn(
346
+ `[Observability] High queue depth: ${depth} > ${this.metrics.backpressure.watermarks.high}`
347
+ );
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Update memory usage
353
+ */
354
+ updateMemoryUsage() {
355
+ const memUsage = process.memoryUsage();
356
+ this.metrics.memoryUsage.push({
357
+ timestamp: Date.now(),
358
+ rss: memUsage.rss,
359
+ heapUsed: memUsage.heapUsed,
360
+ heapTotal: memUsage.heapTotal,
361
+ external: memUsage.external,
362
+ });
363
+
364
+ // Keep only last 100 measurements
365
+ if (this.metrics.memoryUsage.length > 100) {
366
+ this.metrics.memoryUsage = this.metrics.memoryUsage.slice(-100);
367
+ }
368
+
369
+ if (this.memoryGauge) {
370
+ this.memoryGauge.add(memUsage.heapUsed);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Get performance metrics
376
+ * @returns {Object} Performance metrics
377
+ */
378
+ getPerformanceMetrics() {
379
+ const now = Date.now();
380
+ const oneMinuteAgo = now - 60000;
381
+
382
+ // Calculate transaction latency percentiles
383
+ const recentLatencies = this.metrics.transactionLatency
384
+ .filter(l => l.timestamp > oneMinuteAgo)
385
+ .map(l => l.duration)
386
+ .sort((a, b) => a - b);
387
+
388
+ let p50 = this._calculatePercentile(recentLatencies, 0.5);
389
+ let p95 = this._calculatePercentile(recentLatencies, 0.95);
390
+ let p99 = this._calculatePercentile(recentLatencies, 0.99);
391
+ let max = recentLatencies.length > 0 ? recentLatencies[recentLatencies.length - 1] : 0;
392
+
393
+ // Calculate hook execution rate (per minute)
394
+ const recentHooks = this.metrics.hookExecutionRate;
395
+ let hookRate = recentHooks; // Already per minute
396
+
397
+ // Calculate error rate
398
+ const totalTransactions = this.metrics.totalTransactions;
399
+ let errorRate = totalTransactions > 0 ? this.metrics.errorCount / totalTransactions : 0;
400
+
401
+ // Get current memory usage
402
+ const currentMemory = process.memoryUsage();
403
+
404
+ // Calculate cache hit rate
405
+ const totalCacheOps = this.metrics.cacheStats.hits + this.metrics.cacheStats.misses;
406
+ let cacheHitRate = totalCacheOps > 0 ? this.metrics.cacheStats.hits / totalCacheOps : 0;
407
+
408
+ // Apply min-sample gate and EWMA smoothing to reduce noise/false positives
409
+ const minSamples = this.config.minSamples;
410
+ const alpha = this.config.ewmaAlpha;
411
+ if (recentLatencies.length < minSamples && this._lastMetrics) {
412
+ // Fall back partially to previous metrics
413
+ p50 = alpha * p50 + (1 - alpha) * this._lastMetrics.transactionLatency.p50;
414
+ p95 = alpha * p95 + (1 - alpha) * this._lastMetrics.transactionLatency.p95;
415
+ p99 = alpha * p99 + (1 - alpha) * this._lastMetrics.transactionLatency.p99;
416
+ max = Math.max(max, this._lastMetrics.transactionLatency.max);
417
+ hookRate = alpha * hookRate + (1 - alpha) * this._lastMetrics.hookExecutionRate;
418
+ errorRate = alpha * errorRate + (1 - alpha) * this._lastMetrics.errorRate;
419
+ cacheHitRate = alpha * cacheHitRate + (1 - alpha) * this._lastMetrics.cacheStats.hitRate;
420
+ }
421
+
422
+ const computed = PerformanceMetricsSchema.parse({
423
+ transactionLatency: { p50, p95, p99, max },
424
+ hookExecutionRate: hookRate,
425
+ errorRate,
426
+ memoryUsage: currentMemory,
427
+ cacheStats: {
428
+ hitRate: cacheHitRate,
429
+ size: this.metrics.cacheStats.size,
430
+ maxSize:
431
+ typeof this.config.cacheMaxSize === 'number'
432
+ ? this.config.cacheMaxSize
433
+ : (getRuntimeConfig().cacheMaxSize ?? this.metrics.cacheStats.size),
434
+ },
435
+ backpressure: this.metrics.backpressure,
436
+ });
437
+
438
+ this._lastMetrics = computed;
439
+ return computed;
440
+ }
441
+
442
+ /**
443
+ * Update transaction metrics
444
+ * @param {number} duration - Transaction duration
445
+ * @param {boolean} success - Whether transaction succeeded
446
+ * @private
447
+ */
448
+ _updateTransactionMetrics(duration, success) {
449
+ this.metrics.transactionLatency.push({
450
+ timestamp: Date.now(),
451
+ duration,
452
+ success,
453
+ });
454
+
455
+ // Keep only last 1000 measurements
456
+ if (this.metrics.transactionLatency.length > 1000) {
457
+ this.metrics.transactionLatency = this.metrics.transactionLatency.slice(-1000);
458
+ }
459
+
460
+ this.metrics.totalTransactions++;
461
+
462
+ if (this.transactionCounter) {
463
+ this.transactionCounter.add(1, { success: success.toString() });
464
+ }
465
+
466
+ if (this.transactionDuration) {
467
+ this.transactionDuration.record(duration);
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Update hook metrics
473
+ * @param {number} duration - Hook duration
474
+ * @param {boolean} success - Whether hook succeeded
475
+ * @private
476
+ */
477
+ _updateHookMetrics(duration, success) {
478
+ this.metrics.hookExecutionRate++;
479
+
480
+ if (this.hookExecutionCounter) {
481
+ this.hookExecutionCounter.add(1, { success: success.toString() });
482
+ }
483
+
484
+ if (this.hookDuration) {
485
+ this.hookDuration.record(duration);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Calculate percentile
491
+ * @param {Array<number>} values - Sorted values
492
+ * @param {number} percentile - Percentile (0-1)
493
+ * @returns {number} Percentile value
494
+ * @private
495
+ */
496
+ _calculatePercentile(values, percentile) {
497
+ if (values.length === 0) return 0;
498
+ const index = Math.ceil(values.length * percentile) - 1;
499
+ return values[Math.max(0, index)];
500
+ }
501
+
502
+ /**
503
+ * Shutdown observability
504
+ * @returns {Promise<void>}
505
+ */
506
+ async shutdown() {
507
+ // End all active spans
508
+ for (const [_key, spanContext] of this.activeSpans) {
509
+ if (spanContext.span) {
510
+ spanContext.span.end();
511
+ }
512
+ }
513
+ this.activeSpans.clear();
514
+
515
+ console.log('[Observability] Shutdown complete');
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Create an observability manager instance
521
+ * @param {Object} [config] - Configuration
522
+ * @returns {ObservabilityManager} Observability manager
523
+ */
524
+ export function createObservabilityManager(config = {}) {
525
+ return new ObservabilityManager(config);
526
+ }
527
+
528
+ /**
529
+ * Default observability manager instance
530
+ */
531
+ export const defaultObservabilityManager = createObservabilityManager();