@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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper Detection - Verify receipt integrity and detect modifications
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive verification:
|
|
5
|
+
* - Individual receipt hash verification
|
|
6
|
+
* - Chain link verification (previousHash matches)
|
|
7
|
+
* - Temporal ordering verification
|
|
8
|
+
* - Merkle proof verification
|
|
9
|
+
* - Complete chain verification
|
|
10
|
+
*
|
|
11
|
+
* @module @unrdf/observability/receipts/tamper-detection
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { blake3 } from 'hash-wasm';
|
|
15
|
+
import { VerificationResultSchema } from './receipt-schema.mjs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* TamperDetector - Verify receipt integrity
|
|
19
|
+
*/
|
|
20
|
+
export class TamperDetector {
|
|
21
|
+
/**
|
|
22
|
+
* Verify a single receipt's hash integrity
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} receipt - Receipt to verify
|
|
25
|
+
* @returns {Promise<Object>} Verification result
|
|
26
|
+
*/
|
|
27
|
+
async verifyReceipt(receipt) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
const checks = {};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Check required fields
|
|
33
|
+
const required = [
|
|
34
|
+
'id',
|
|
35
|
+
'hash',
|
|
36
|
+
'timestamp_ns',
|
|
37
|
+
'timestamp_iso',
|
|
38
|
+
'operation',
|
|
39
|
+
'payload',
|
|
40
|
+
'previousHash',
|
|
41
|
+
];
|
|
42
|
+
for (const field of required) {
|
|
43
|
+
if (!(field in receipt)) {
|
|
44
|
+
errors.push(`Missing required field: ${field}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (errors.length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
receiptId: receipt.id,
|
|
52
|
+
errors,
|
|
53
|
+
checks,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Recompute hash
|
|
58
|
+
const canonicalContent = {
|
|
59
|
+
id: receipt.id,
|
|
60
|
+
timestamp_ns: receipt.timestamp_ns,
|
|
61
|
+
timestamp_iso: receipt.timestamp_iso,
|
|
62
|
+
operation: receipt.operation,
|
|
63
|
+
payload: receipt.payload,
|
|
64
|
+
previousHash: receipt.previousHash,
|
|
65
|
+
...(receipt.actor && { actor: receipt.actor }),
|
|
66
|
+
...(receipt.metadata && { metadata: receipt.metadata }),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const canonical = JSON.stringify(canonicalContent, Object.keys(canonicalContent).sort());
|
|
70
|
+
const computedHash = await blake3(canonical);
|
|
71
|
+
|
|
72
|
+
checks.hashIntegrity = computedHash === receipt.hash;
|
|
73
|
+
if (!checks.hashIntegrity) {
|
|
74
|
+
errors.push(`Hash mismatch: expected ${receipt.hash}, got ${computedHash}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return VerificationResultSchema.parse({
|
|
78
|
+
valid: errors.length === 0,
|
|
79
|
+
receiptId: receipt.id,
|
|
80
|
+
errors,
|
|
81
|
+
checks,
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
valid: false,
|
|
86
|
+
receiptId: receipt.id,
|
|
87
|
+
errors: [`Verification exception: ${err.message}`],
|
|
88
|
+
checks,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Verify chain link between two receipts
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} current - Current receipt
|
|
97
|
+
* @param {Object} previous - Previous receipt
|
|
98
|
+
* @returns {Promise<Object>} Verification result
|
|
99
|
+
*/
|
|
100
|
+
async verifyChainLink(current, previous) {
|
|
101
|
+
const errors = [];
|
|
102
|
+
const checks = {};
|
|
103
|
+
|
|
104
|
+
// Verify previousHash matches
|
|
105
|
+
checks.chainLink = current.previousHash === previous.hash;
|
|
106
|
+
if (!checks.chainLink) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`Chain broken: current.previousHash (${current.previousHash}) !== previous.hash (${previous.hash})`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Verify temporal ordering
|
|
113
|
+
const currentTime = BigInt(current.timestamp_ns);
|
|
114
|
+
const previousTime = BigInt(previous.timestamp_ns);
|
|
115
|
+
checks.temporalOrder = currentTime > previousTime;
|
|
116
|
+
if (!checks.temporalOrder) {
|
|
117
|
+
errors.push(`Temporal ordering violated: ${currentTime} <= ${previousTime}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return VerificationResultSchema.parse({
|
|
121
|
+
valid: errors.length === 0,
|
|
122
|
+
receiptId: current.id,
|
|
123
|
+
errors,
|
|
124
|
+
checks,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Verify entire receipt chain
|
|
130
|
+
*
|
|
131
|
+
* @param {Array<Object>} receipts - Array of receipts to verify
|
|
132
|
+
* @returns {Promise<Object>} Verification result
|
|
133
|
+
*/
|
|
134
|
+
async verifyChain(receipts) {
|
|
135
|
+
const errors = [];
|
|
136
|
+
const checks = {};
|
|
137
|
+
|
|
138
|
+
if (receipts.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
valid: true,
|
|
141
|
+
errors: [],
|
|
142
|
+
checks: {},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Verify first receipt (genesis)
|
|
147
|
+
const firstResult = await this.verifyReceipt(receipts[0]);
|
|
148
|
+
if (!firstResult.valid) {
|
|
149
|
+
errors.push(`Receipt 0 invalid: ${firstResult.errors.join(', ')}`);
|
|
150
|
+
return { valid: false, errors, checks };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (receipts[0].previousHash !== null) {
|
|
154
|
+
errors.push('Genesis receipt must have null previousHash');
|
|
155
|
+
return { valid: false, errors, checks };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Verify remaining receipts and chain links
|
|
159
|
+
for (let i = 1; i < receipts.length; i++) {
|
|
160
|
+
const receiptResult = await this.verifyReceipt(receipts[i]);
|
|
161
|
+
if (!receiptResult.valid) {
|
|
162
|
+
errors.push(`Receipt ${i} invalid: ${receiptResult.errors.join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const linkResult = await this.verifyChainLink(receipts[i], receipts[i - 1]);
|
|
166
|
+
if (!linkResult.valid) {
|
|
167
|
+
errors.push(`Chain link ${i - 1} -> ${i} invalid: ${linkResult.errors.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return VerificationResultSchema.parse({
|
|
172
|
+
valid: errors.length === 0,
|
|
173
|
+
errors,
|
|
174
|
+
checks,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detect tampering by comparing original and suspect receipts
|
|
180
|
+
*
|
|
181
|
+
* @param {Object} original - Original receipt
|
|
182
|
+
* @param {Object} suspect - Suspect receipt
|
|
183
|
+
* @returns {Promise<Object>} Tampering analysis
|
|
184
|
+
*/
|
|
185
|
+
async detectTampering(original, suspect) {
|
|
186
|
+
const analysis = {
|
|
187
|
+
tampered: false,
|
|
188
|
+
changes: [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Compare all fields
|
|
192
|
+
const fields = [
|
|
193
|
+
'id',
|
|
194
|
+
'hash',
|
|
195
|
+
'timestamp_ns',
|
|
196
|
+
'timestamp_iso',
|
|
197
|
+
'operation',
|
|
198
|
+
'payload',
|
|
199
|
+
'previousHash',
|
|
200
|
+
'actor',
|
|
201
|
+
];
|
|
202
|
+
for (const field of fields) {
|
|
203
|
+
const origValue = JSON.stringify(original[field]);
|
|
204
|
+
const suspValue = JSON.stringify(suspect[field]);
|
|
205
|
+
if (origValue !== suspValue) {
|
|
206
|
+
analysis.tampered = true;
|
|
207
|
+
analysis.changes.push({
|
|
208
|
+
field,
|
|
209
|
+
original: original[field],
|
|
210
|
+
suspect: suspect[field],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return analysis;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default TamperDetector;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Advanced Metrics Tests
|
|
3
|
+
* @module observability/test/advanced-metrics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import { createAdvancedMetrics } from '../src/advanced-metrics.mjs';
|
|
8
|
+
|
|
9
|
+
describe('AdvancedMetrics', () => {
|
|
10
|
+
let metrics;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
metrics = createAdvancedMetrics({
|
|
14
|
+
serviceName: 'test-service',
|
|
15
|
+
enabled: true,
|
|
16
|
+
samplingRate: 1.0, // 100% for testing
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Configuration', () => {
|
|
21
|
+
it('should create metrics with default config', () => {
|
|
22
|
+
const defaultMetrics = createAdvancedMetrics();
|
|
23
|
+
expect(defaultMetrics.config.serviceName).toBe('unrdf');
|
|
24
|
+
expect(defaultMetrics.config.enabled).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should accept custom configuration', () => {
|
|
28
|
+
const customMetrics = createAdvancedMetrics({
|
|
29
|
+
serviceName: 'custom-service',
|
|
30
|
+
samplingRate: 0.5,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(customMetrics.config.serviceName).toBe('custom-service');
|
|
34
|
+
expect(customMetrics.config.samplingRate).toBe(0.5);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should validate configuration with Zod schema', () => {
|
|
38
|
+
expect(() => {
|
|
39
|
+
createAdvancedMetrics({
|
|
40
|
+
samplingRate: 1.5, // Invalid: > 1
|
|
41
|
+
});
|
|
42
|
+
}).toThrow();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('Business Metrics', () => {
|
|
47
|
+
it('should record successful operation', () => {
|
|
48
|
+
const spy = vi.spyOn(metrics.businessMetrics.operations, 'add');
|
|
49
|
+
|
|
50
|
+
metrics.recordOperation({
|
|
51
|
+
operation: 'sparql-query',
|
|
52
|
+
success: true,
|
|
53
|
+
duration: 50,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(spy).toHaveBeenCalledWith(1, {
|
|
57
|
+
operation: 'sparql-query',
|
|
58
|
+
result: 'success',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should record failed operation with error type', () => {
|
|
63
|
+
const spy = vi.spyOn(metrics.businessMetrics.failuresByType, 'add');
|
|
64
|
+
|
|
65
|
+
metrics.recordOperation({
|
|
66
|
+
operation: 'triple-insert',
|
|
67
|
+
success: false,
|
|
68
|
+
duration: 100,
|
|
69
|
+
errorType: 'ValidationError',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(spy).toHaveBeenCalledWith(1, {
|
|
73
|
+
operation: 'triple-insert',
|
|
74
|
+
error_type: 'ValidationError',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should record SLA violations', () => {
|
|
79
|
+
const spy = vi.spyOn(metrics.businessMetrics.slaViolations, 'add');
|
|
80
|
+
|
|
81
|
+
metrics.recordOperation({
|
|
82
|
+
operation: 'query-execution',
|
|
83
|
+
success: true,
|
|
84
|
+
duration: 150,
|
|
85
|
+
slaThreshold: 100,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(spy).toHaveBeenCalledWith(1, {
|
|
89
|
+
operation: 'query-execution',
|
|
90
|
+
threshold: '100',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should not record SLA violation when under threshold', () => {
|
|
95
|
+
const spy = vi.spyOn(metrics.businessMetrics.slaViolations, 'add');
|
|
96
|
+
|
|
97
|
+
metrics.recordOperation({
|
|
98
|
+
operation: 'query-execution',
|
|
99
|
+
success: true,
|
|
100
|
+
duration: 50,
|
|
101
|
+
slaThreshold: 100,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(spy).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should record success rate', () => {
|
|
108
|
+
const spy = vi.spyOn(metrics.businessMetrics.successRate, 'add');
|
|
109
|
+
|
|
110
|
+
metrics.recordSuccessRate('query-execution', 0.95);
|
|
111
|
+
|
|
112
|
+
expect(spy).toHaveBeenCalledWith(0.95, { operation: 'query-execution' });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Latency Metrics', () => {
|
|
117
|
+
it('should record operation latency in histogram', () => {
|
|
118
|
+
const spy = vi.spyOn(metrics.latencyMetrics.histogram, 'record');
|
|
119
|
+
|
|
120
|
+
metrics.recordOperation({
|
|
121
|
+
operation: 'parse-turtle',
|
|
122
|
+
success: true,
|
|
123
|
+
duration: 75,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(spy).toHaveBeenCalledWith(75, { operation: 'parse-turtle' });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should record latency percentiles', () => {
|
|
130
|
+
const p50Spy = vi.spyOn(metrics.latencyMetrics.p50, 'add');
|
|
131
|
+
const p95Spy = vi.spyOn(metrics.latencyMetrics.p95, 'add');
|
|
132
|
+
const p99Spy = vi.spyOn(metrics.latencyMetrics.p99, 'add');
|
|
133
|
+
|
|
134
|
+
metrics.recordLatencyPercentiles('query', {
|
|
135
|
+
p50: 25,
|
|
136
|
+
p90: 50,
|
|
137
|
+
p95: 100,
|
|
138
|
+
p99: 500,
|
|
139
|
+
max: 1000,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(p50Spy).toHaveBeenCalledWith(25, { operation: 'query' });
|
|
143
|
+
expect(p95Spy).toHaveBeenCalledWith(100, { operation: 'query' });
|
|
144
|
+
expect(p99Spy).toHaveBeenCalledWith(500, { operation: 'query' });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Throughput Metrics', () => {
|
|
149
|
+
it('should record throughput', () => {
|
|
150
|
+
const spy = vi.spyOn(metrics.throughputMetrics.opsPerSecond, 'add');
|
|
151
|
+
|
|
152
|
+
metrics.recordThroughput('insert', 125);
|
|
153
|
+
|
|
154
|
+
expect(spy).toHaveBeenCalledWith(125, { operation: 'insert' });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should update throughput automatically', async () => {
|
|
158
|
+
// Record operations
|
|
159
|
+
for (let i = 0; i < 10; i++) {
|
|
160
|
+
metrics.recordOperation({
|
|
161
|
+
operation: 'test-op',
|
|
162
|
+
success: true,
|
|
163
|
+
duration: 10,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Wait for throughput calculation (>1 second)
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
169
|
+
|
|
170
|
+
// Throughput should be calculated
|
|
171
|
+
expect(metrics.operationCounts.size).toBe(0); // Cleared after calculation
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Resource Metrics', () => {
|
|
176
|
+
it('should record memory utilization', () => {
|
|
177
|
+
const spy = vi.spyOn(metrics.resourceMetrics.heapUsed, 'add');
|
|
178
|
+
|
|
179
|
+
metrics.recordResourceUtilization();
|
|
180
|
+
|
|
181
|
+
expect(spy).toHaveBeenCalled();
|
|
182
|
+
const callArgs = spy.mock.calls[0][0];
|
|
183
|
+
expect(callArgs).toBeGreaterThan(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should record event loop lag', () => {
|
|
187
|
+
const lagSpy = vi.spyOn(metrics.resourceMetrics.eventLoopLag, 'record');
|
|
188
|
+
const cpuSpy = vi.spyOn(metrics.resourceMetrics.cpuLoad, 'add');
|
|
189
|
+
|
|
190
|
+
metrics.recordEventLoopLag(15);
|
|
191
|
+
|
|
192
|
+
expect(lagSpy).toHaveBeenCalledWith(15);
|
|
193
|
+
expect(cpuSpy).toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should estimate CPU load from event loop lag', () => {
|
|
197
|
+
const spy = vi.spyOn(metrics.resourceMetrics.cpuLoad, 'add');
|
|
198
|
+
|
|
199
|
+
// High lag = high CPU estimate
|
|
200
|
+
metrics.recordEventLoopLag(150);
|
|
201
|
+
|
|
202
|
+
const cpuLoad = spy.mock.calls[0][0];
|
|
203
|
+
expect(cpuLoad).toBeGreaterThan(0);
|
|
204
|
+
expect(cpuLoad).toBeLessThanOrEqual(1);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('Sampling', () => {
|
|
209
|
+
it('should sample at configured rate', () => {
|
|
210
|
+
const sampledMetrics = createAdvancedMetrics({
|
|
211
|
+
enabled: true,
|
|
212
|
+
samplingRate: 0.5, // 50%
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const spy = vi.spyOn(sampledMetrics.businessMetrics.operations, 'add');
|
|
216
|
+
|
|
217
|
+
// Record 100 operations
|
|
218
|
+
for (let i = 0; i < 100; i++) {
|
|
219
|
+
sampledMetrics.recordOperation({
|
|
220
|
+
operation: 'test',
|
|
221
|
+
success: true,
|
|
222
|
+
duration: 10,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Should sample ~50 (with some variance)
|
|
227
|
+
const callCount = spy.mock.calls.length;
|
|
228
|
+
expect(callCount).toBeGreaterThan(30);
|
|
229
|
+
expect(callCount).toBeLessThan(70);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should not record when disabled', () => {
|
|
233
|
+
const disabledMetrics = createAdvancedMetrics({
|
|
234
|
+
enabled: false,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const spy = vi.spyOn(disabledMetrics.businessMetrics?.operations || {}, 'add', () => {});
|
|
238
|
+
|
|
239
|
+
disabledMetrics.recordOperation({
|
|
240
|
+
operation: 'test',
|
|
241
|
+
success: true,
|
|
242
|
+
duration: 10,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(spy).not.toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('Performance', () => {
|
|
250
|
+
it('should record operation in <0.1ms', () => {
|
|
251
|
+
const start = performance.now();
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < 1000; i++) {
|
|
254
|
+
metrics.recordOperation({
|
|
255
|
+
operation: 'perf-test',
|
|
256
|
+
success: true,
|
|
257
|
+
duration: 10,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const elapsed = performance.now() - start;
|
|
262
|
+
const avgTime = elapsed / 1000;
|
|
263
|
+
|
|
264
|
+
expect(avgTime).toBeLessThan(0.1); // <0.1ms per operation
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should have minimal memory overhead', () => {
|
|
268
|
+
const before = process.memoryUsage().heapUsed;
|
|
269
|
+
|
|
270
|
+
// Record 10,000 operations
|
|
271
|
+
for (let i = 0; i < 10000; i++) {
|
|
272
|
+
metrics.recordOperation({
|
|
273
|
+
operation: 'memory-test',
|
|
274
|
+
success: true,
|
|
275
|
+
duration: 10,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const after = process.memoryUsage().heapUsed;
|
|
280
|
+
const overhead = (after - before) / 1024 / 1024; // MB
|
|
281
|
+
|
|
282
|
+
expect(overhead).toBeLessThan(10); // <10MB overhead
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('Summary', () => {
|
|
287
|
+
it('should provide metrics summary', () => {
|
|
288
|
+
metrics.recordOperation({
|
|
289
|
+
operation: 'test-op',
|
|
290
|
+
success: true,
|
|
291
|
+
duration: 10,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const summary = metrics.getSummary();
|
|
295
|
+
|
|
296
|
+
expect(summary).toHaveProperty('enabled');
|
|
297
|
+
expect(summary).toHaveProperty('samplingRate');
|
|
298
|
+
expect(summary).toHaveProperty('operationTypes');
|
|
299
|
+
expect(summary.enabled).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|