@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.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +64 -0
- package/src/browser-shims.mjs +343 -0
- package/src/browser.mjs +910 -0
- package/src/canonicalize.mjs +414 -0
- package/src/condition-cache.mjs +109 -0
- package/src/condition-evaluator.mjs +722 -0
- package/src/dark-matter-core.mjs +742 -0
- package/src/define-hook.mjs +213 -0
- package/src/effect-sandbox-browser.mjs +283 -0
- package/src/effect-sandbox-worker.mjs +170 -0
- package/src/effect-sandbox.mjs +517 -0
- package/src/engines/index.mjs +11 -0
- package/src/engines/rdf-engine.mjs +299 -0
- package/src/file-resolver.mjs +387 -0
- package/src/hook-executor-batching.mjs +277 -0
- package/src/hook-executor.mjs +870 -0
- package/src/hook-management.mjs +150 -0
- package/src/index.mjs +93 -0
- package/src/ken-parliment.mjs +119 -0
- package/src/ken.mjs +149 -0
- package/src/knowledge-engine/builtin-rules.mjs +190 -0
- package/src/knowledge-engine/inference-engine.mjs +418 -0
- package/src/knowledge-engine/knowledge-engine.mjs +317 -0
- package/src/knowledge-engine/pattern-dsl.mjs +142 -0
- package/src/knowledge-engine/pattern-matcher.mjs +215 -0
- package/src/knowledge-engine/rules.mjs +184 -0
- package/src/knowledge-engine.mjs +319 -0
- package/src/knowledge-hook-engine.mjs +360 -0
- package/src/knowledge-hook-manager.mjs +469 -0
- package/src/knowledge-substrate-core.mjs +927 -0
- package/src/lite.mjs +222 -0
- package/src/lockchain-writer-browser.mjs +414 -0
- package/src/lockchain-writer.mjs +602 -0
- package/src/monitoring/andon-signals.mjs +775 -0
- package/src/observability.mjs +531 -0
- package/src/parse.mjs +290 -0
- package/src/performance-optimizer.mjs +678 -0
- package/src/policy-pack.mjs +572 -0
- package/src/query-cache.mjs +116 -0
- package/src/query-optimizer.mjs +1051 -0
- package/src/query.mjs +306 -0
- package/src/reason.mjs +350 -0
- package/src/resolution-layer.mjs +506 -0
- package/src/schemas.mjs +1063 -0
- package/src/security/error-sanitizer.mjs +257 -0
- package/src/security/path-validator.mjs +194 -0
- package/src/security/sandbox-restrictions.mjs +331 -0
- package/src/security-validator.mjs +389 -0
- package/src/store-cache.mjs +137 -0
- package/src/telemetry.mjs +167 -0
- package/src/transaction.mjs +810 -0
- package/src/utils/adaptive-monitor.mjs +746 -0
- package/src/utils/circuit-breaker.mjs +513 -0
- package/src/utils/edge-case-handler.mjs +503 -0
- package/src/utils/memory-manager.mjs +498 -0
- package/src/utils/ring-buffer.mjs +282 -0
- package/src/validate.mjs +319 -0
- 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();
|