musubi-sdd 3.0.1 → 3.5.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/bin/musubi-change.js +623 -10
- package/bin/musubi-orchestrate.js +456 -0
- package/bin/musubi-trace.js +393 -0
- package/package.json +3 -2
- package/src/analyzers/impact-analyzer.js +682 -0
- package/src/integrations/cicd.js +782 -0
- package/src/integrations/documentation.js +740 -0
- package/src/integrations/examples.js +789 -0
- package/src/integrations/index.js +23 -0
- package/src/integrations/platforms.js +929 -0
- package/src/managers/delta-spec.js +484 -0
- package/src/monitoring/incident-manager.js +890 -0
- package/src/monitoring/index.js +633 -0
- package/src/monitoring/observability.js +938 -0
- package/src/monitoring/release-manager.js +622 -0
- package/src/orchestration/index.js +168 -0
- package/src/orchestration/orchestration-engine.js +409 -0
- package/src/orchestration/pattern-registry.js +319 -0
- package/src/orchestration/patterns/auto.js +386 -0
- package/src/orchestration/patterns/group-chat.js +395 -0
- package/src/orchestration/patterns/human-in-loop.js +506 -0
- package/src/orchestration/patterns/nested.js +322 -0
- package/src/orchestration/patterns/sequential.js +278 -0
- package/src/orchestration/patterns/swarm.js +395 -0
- package/src/orchestration/workflow-orchestrator.js +738 -0
- package/src/reporters/coverage-report.js +452 -0
- package/src/reporters/traceability-matrix-report.js +684 -0
- package/src/steering/advanced-validation.js +812 -0
- package/src/steering/auto-updater.js +670 -0
- package/src/steering/index.js +119 -0
- package/src/steering/quality-metrics.js +650 -0
- package/src/steering/template-constraints.js +789 -0
- package/src/templates/agents/claude-code/skills/agent-assistant/SKILL.md +22 -0
- package/src/templates/agents/claude-code/skills/issue-resolver/SKILL.md +21 -0
- package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +90 -28
- package/src/templates/agents/claude-code/skills/project-manager/SKILL.md +32 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/SKILL.md +27 -0
- package/src/templates/agents/claude-code/skills/steering/SKILL.md +30 -0
- package/src/templates/agents/claude-code/skills/test-engineer/SKILL.md +21 -0
- package/src/templates/agents/claude-code/skills/ui-ux-designer/SKILL.md +27 -0
- package/src/templates/agents/codex/AGENTS.md +36 -1
- package/src/templates/agents/cursor/AGENTS.md +36 -1
- package/src/templates/agents/gemini-cli/GEMINI.md +36 -1
- package/src/templates/agents/github-copilot/AGENTS.md +65 -1
- package/src/templates/agents/qwen-code/QWEN.md +36 -1
- package/src/templates/agents/windsurf/AGENTS.md +36 -1
- package/src/templates/shared/delta-spec-template.md +246 -0
- package/src/validators/delta-format.js +474 -0
- package/src/validators/traceability-validator.js +561 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Module - Logs, Metrics, and Traces
|
|
3
|
+
*
|
|
4
|
+
* Provides unified observability capabilities:
|
|
5
|
+
* - Structured logging
|
|
6
|
+
* - Metrics collection
|
|
7
|
+
* - Distributed tracing
|
|
8
|
+
* - Correlation IDs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { EventEmitter } = require('events');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Log Levels
|
|
15
|
+
*/
|
|
16
|
+
const LogLevel = {
|
|
17
|
+
TRACE: 'trace',
|
|
18
|
+
DEBUG: 'debug',
|
|
19
|
+
INFO: 'info',
|
|
20
|
+
WARN: 'warn',
|
|
21
|
+
ERROR: 'error',
|
|
22
|
+
FATAL: 'fatal'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Log level priorities
|
|
27
|
+
*/
|
|
28
|
+
const LOG_PRIORITY = {
|
|
29
|
+
[LogLevel.TRACE]: 0,
|
|
30
|
+
[LogLevel.DEBUG]: 1,
|
|
31
|
+
[LogLevel.INFO]: 2,
|
|
32
|
+
[LogLevel.WARN]: 3,
|
|
33
|
+
[LogLevel.ERROR]: 4,
|
|
34
|
+
[LogLevel.FATAL]: 5
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Trace Status
|
|
39
|
+
*/
|
|
40
|
+
const TraceStatus = {
|
|
41
|
+
OK: 'ok',
|
|
42
|
+
ERROR: 'error',
|
|
43
|
+
UNSET: 'unset'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Span Kind
|
|
48
|
+
*/
|
|
49
|
+
const SpanKind = {
|
|
50
|
+
INTERNAL: 'internal',
|
|
51
|
+
SERVER: 'server',
|
|
52
|
+
CLIENT: 'client',
|
|
53
|
+
PRODUCER: 'producer',
|
|
54
|
+
CONSUMER: 'consumer'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate unique ID
|
|
59
|
+
*/
|
|
60
|
+
function generateId(length = 16) {
|
|
61
|
+
let result = '';
|
|
62
|
+
const chars = '0123456789abcdef';
|
|
63
|
+
for (let i = 0; i < length; i++) {
|
|
64
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Structured Logger
|
|
71
|
+
*/
|
|
72
|
+
class Logger extends EventEmitter {
|
|
73
|
+
constructor(options = {}) {
|
|
74
|
+
super();
|
|
75
|
+
this.name = options.name || 'default';
|
|
76
|
+
this.level = options.level || LogLevel.INFO;
|
|
77
|
+
this.context = options.context || {};
|
|
78
|
+
this.outputs = options.outputs || [new ConsoleOutput()];
|
|
79
|
+
this.parent = options.parent || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a child logger with additional context
|
|
84
|
+
*/
|
|
85
|
+
child(context) {
|
|
86
|
+
return new Logger({
|
|
87
|
+
name: this.name,
|
|
88
|
+
level: this.level,
|
|
89
|
+
context: { ...this.context, ...context },
|
|
90
|
+
outputs: this.outputs,
|
|
91
|
+
parent: this
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a level is enabled
|
|
97
|
+
*/
|
|
98
|
+
isLevelEnabled(level) {
|
|
99
|
+
return LOG_PRIORITY[level] >= LOG_PRIORITY[this.level];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Log at a specific level
|
|
104
|
+
*/
|
|
105
|
+
log(level, message, meta = {}) {
|
|
106
|
+
if (!this.isLevelEnabled(level)) return;
|
|
107
|
+
|
|
108
|
+
const entry = {
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
level,
|
|
111
|
+
logger: this.name,
|
|
112
|
+
message,
|
|
113
|
+
...this.context,
|
|
114
|
+
...meta
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
for (const output of this.outputs) {
|
|
118
|
+
output.write(entry);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.emit('log', entry);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
trace(message, meta) { this.log(LogLevel.TRACE, message, meta); }
|
|
125
|
+
debug(message, meta) { this.log(LogLevel.DEBUG, message, meta); }
|
|
126
|
+
info(message, meta) { this.log(LogLevel.INFO, message, meta); }
|
|
127
|
+
warn(message, meta) { this.log(LogLevel.WARN, message, meta); }
|
|
128
|
+
error(message, meta) { this.log(LogLevel.ERROR, message, meta); }
|
|
129
|
+
fatal(message, meta) { this.log(LogLevel.FATAL, message, meta); }
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Add an output
|
|
133
|
+
*/
|
|
134
|
+
addOutput(output) {
|
|
135
|
+
this.outputs.push(output);
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set log level
|
|
141
|
+
*/
|
|
142
|
+
setLevel(level) {
|
|
143
|
+
this.level = level;
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Console output for logger
|
|
150
|
+
*/
|
|
151
|
+
class ConsoleOutput {
|
|
152
|
+
constructor(options = {}) {
|
|
153
|
+
this.format = options.format || 'json';
|
|
154
|
+
this.pretty = options.pretty || false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
write(entry) {
|
|
158
|
+
if (this.format === 'json') {
|
|
159
|
+
const output = this.pretty
|
|
160
|
+
? JSON.stringify(entry, null, 2)
|
|
161
|
+
: JSON.stringify(entry);
|
|
162
|
+
|
|
163
|
+
if (entry.level === LogLevel.ERROR || entry.level === LogLevel.FATAL) {
|
|
164
|
+
console.error(output);
|
|
165
|
+
} else if (entry.level === LogLevel.WARN) {
|
|
166
|
+
console.warn(output);
|
|
167
|
+
} else {
|
|
168
|
+
console.log(output);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
const timestamp = entry.timestamp;
|
|
172
|
+
const level = entry.level.toUpperCase().padEnd(5);
|
|
173
|
+
console.log(`${timestamp} [${level}] ${entry.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* File output for logger
|
|
180
|
+
*/
|
|
181
|
+
class FileOutput {
|
|
182
|
+
constructor(options = {}) {
|
|
183
|
+
this.path = options.path || 'app.log';
|
|
184
|
+
this.buffer = [];
|
|
185
|
+
this.bufferSize = options.bufferSize || 100;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
write(entry) {
|
|
189
|
+
this.buffer.push(JSON.stringify(entry) + '\n');
|
|
190
|
+
|
|
191
|
+
if (this.buffer.length >= this.bufferSize) {
|
|
192
|
+
this.flush();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
flush() {
|
|
197
|
+
// In real implementation, would write to file
|
|
198
|
+
this.buffer = [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getBuffer() {
|
|
202
|
+
return [...this.buffer];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Metric Collector
|
|
208
|
+
*/
|
|
209
|
+
class MetricsCollector extends EventEmitter {
|
|
210
|
+
constructor(options = {}) {
|
|
211
|
+
super();
|
|
212
|
+
this.name = options.name || 'default';
|
|
213
|
+
this.prefix = options.prefix || '';
|
|
214
|
+
this.metrics = new Map();
|
|
215
|
+
this.labels = options.labels || {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Format metric name
|
|
220
|
+
*/
|
|
221
|
+
_formatName(name) {
|
|
222
|
+
return this.prefix ? `${this.prefix}_${name}` : name;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create or get a counter
|
|
227
|
+
*/
|
|
228
|
+
counter(name, help = '') {
|
|
229
|
+
const fullName = this._formatName(name);
|
|
230
|
+
if (!this.metrics.has(fullName)) {
|
|
231
|
+
this.metrics.set(fullName, {
|
|
232
|
+
type: 'counter',
|
|
233
|
+
name: fullName,
|
|
234
|
+
help,
|
|
235
|
+
values: new Map()
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return new CounterMetric(this.metrics.get(fullName), this.labels);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create or get a gauge
|
|
243
|
+
*/
|
|
244
|
+
gauge(name, help = '') {
|
|
245
|
+
const fullName = this._formatName(name);
|
|
246
|
+
if (!this.metrics.has(fullName)) {
|
|
247
|
+
this.metrics.set(fullName, {
|
|
248
|
+
type: 'gauge',
|
|
249
|
+
name: fullName,
|
|
250
|
+
help,
|
|
251
|
+
values: new Map()
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return new GaugeMetric(this.metrics.get(fullName), this.labels);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create or get a histogram
|
|
259
|
+
*/
|
|
260
|
+
histogram(name, help = '', buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]) {
|
|
261
|
+
const fullName = this._formatName(name);
|
|
262
|
+
if (!this.metrics.has(fullName)) {
|
|
263
|
+
this.metrics.set(fullName, {
|
|
264
|
+
type: 'histogram',
|
|
265
|
+
name: fullName,
|
|
266
|
+
help,
|
|
267
|
+
buckets,
|
|
268
|
+
values: new Map()
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return new HistogramMetric(this.metrics.get(fullName), this.labels);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Export metrics in Prometheus format
|
|
276
|
+
*/
|
|
277
|
+
toPrometheus() {
|
|
278
|
+
let output = '';
|
|
279
|
+
|
|
280
|
+
for (const metric of this.metrics.values()) {
|
|
281
|
+
output += `# HELP ${metric.name} ${metric.help}\n`;
|
|
282
|
+
output += `# TYPE ${metric.name} ${metric.type}\n`;
|
|
283
|
+
|
|
284
|
+
if (metric.type === 'histogram') {
|
|
285
|
+
for (const [labelsKey, data] of metric.values) {
|
|
286
|
+
const labels = labelsKey ? `{${labelsKey}}` : '';
|
|
287
|
+
for (let i = 0; i < metric.buckets.length; i++) {
|
|
288
|
+
output += `${metric.name}_bucket{le="${metric.buckets[i]}"${labelsKey ? ',' + labelsKey : ''}} ${data.buckets[i]}\n`;
|
|
289
|
+
}
|
|
290
|
+
output += `${metric.name}_bucket{le="+Inf"${labelsKey ? ',' + labelsKey : ''}} ${data.count}\n`;
|
|
291
|
+
output += `${metric.name}_sum${labels} ${data.sum}\n`;
|
|
292
|
+
output += `${metric.name}_count${labels} ${data.count}\n`;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
for (const [labelsKey, value] of metric.values) {
|
|
296
|
+
const labels = labelsKey ? `{${labelsKey}}` : '';
|
|
297
|
+
output += `${metric.name}${labels} ${value}\n`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return output;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Export metrics as JSON
|
|
307
|
+
*/
|
|
308
|
+
toJSON() {
|
|
309
|
+
const result = [];
|
|
310
|
+
|
|
311
|
+
for (const metric of this.metrics.values()) {
|
|
312
|
+
const metricData = {
|
|
313
|
+
name: metric.name,
|
|
314
|
+
type: metric.type,
|
|
315
|
+
help: metric.help,
|
|
316
|
+
values: []
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
for (const [labelsKey, value] of metric.values) {
|
|
320
|
+
metricData.values.push({
|
|
321
|
+
labels: labelsKey,
|
|
322
|
+
value: metric.type === 'histogram' ? { ...value } : value
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
result.push(metricData);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Counter metric
|
|
335
|
+
*/
|
|
336
|
+
class CounterMetric {
|
|
337
|
+
constructor(metric, defaultLabels = {}) {
|
|
338
|
+
this.metric = metric;
|
|
339
|
+
this.defaultLabels = defaultLabels;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_labelKey(labels = {}) {
|
|
343
|
+
const all = { ...this.defaultLabels, ...labels };
|
|
344
|
+
return Object.entries(all)
|
|
345
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
346
|
+
.sort()
|
|
347
|
+
.join(',');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
inc(labels = {}, value = 1) {
|
|
351
|
+
const key = this._labelKey(labels);
|
|
352
|
+
const current = this.metric.values.get(key) || 0;
|
|
353
|
+
this.metric.values.set(key, current + value);
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
get(labels = {}) {
|
|
358
|
+
const key = this._labelKey(labels);
|
|
359
|
+
return this.metric.values.get(key) || 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gauge metric
|
|
365
|
+
*/
|
|
366
|
+
class GaugeMetric {
|
|
367
|
+
constructor(metric, defaultLabels = {}) {
|
|
368
|
+
this.metric = metric;
|
|
369
|
+
this.defaultLabels = defaultLabels;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_labelKey(labels = {}) {
|
|
373
|
+
const all = { ...this.defaultLabels, ...labels };
|
|
374
|
+
return Object.entries(all)
|
|
375
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
376
|
+
.sort()
|
|
377
|
+
.join(',');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
set(value, labels = {}) {
|
|
381
|
+
const key = this._labelKey(labels);
|
|
382
|
+
this.metric.values.set(key, value);
|
|
383
|
+
return this;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
inc(labels = {}, value = 1) {
|
|
387
|
+
const key = this._labelKey(labels);
|
|
388
|
+
const current = this.metric.values.get(key) || 0;
|
|
389
|
+
this.metric.values.set(key, current + value);
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
dec(labels = {}, value = 1) {
|
|
394
|
+
return this.inc(labels, -value);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get(labels = {}) {
|
|
398
|
+
const key = this._labelKey(labels);
|
|
399
|
+
return this.metric.values.get(key) || 0;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Histogram metric
|
|
405
|
+
*/
|
|
406
|
+
class HistogramMetric {
|
|
407
|
+
constructor(metric, defaultLabels = {}) {
|
|
408
|
+
this.metric = metric;
|
|
409
|
+
this.defaultLabels = defaultLabels;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_labelKey(labels = {}) {
|
|
413
|
+
const all = { ...this.defaultLabels, ...labels };
|
|
414
|
+
return Object.entries(all)
|
|
415
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
416
|
+
.sort()
|
|
417
|
+
.join(',');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
observe(value, labels = {}) {
|
|
421
|
+
const key = this._labelKey(labels);
|
|
422
|
+
let data = this.metric.values.get(key);
|
|
423
|
+
|
|
424
|
+
if (!data) {
|
|
425
|
+
data = {
|
|
426
|
+
buckets: this.metric.buckets.map(() => 0),
|
|
427
|
+
sum: 0,
|
|
428
|
+
count: 0
|
|
429
|
+
};
|
|
430
|
+
this.metric.values.set(key, data);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
data.sum += value;
|
|
434
|
+
data.count += 1;
|
|
435
|
+
|
|
436
|
+
for (let i = 0; i < this.metric.buckets.length; i++) {
|
|
437
|
+
if (value <= this.metric.buckets[i]) {
|
|
438
|
+
data.buckets[i] += 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
get(labels = {}) {
|
|
446
|
+
const key = this._labelKey(labels);
|
|
447
|
+
return this.metric.values.get(key) || { buckets: [], sum: 0, count: 0 };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Span for distributed tracing
|
|
453
|
+
*/
|
|
454
|
+
class Span {
|
|
455
|
+
constructor(options) {
|
|
456
|
+
this.traceId = options.traceId || generateId(32);
|
|
457
|
+
this.spanId = options.spanId || generateId(16);
|
|
458
|
+
this.parentSpanId = options.parentSpanId || null;
|
|
459
|
+
this.name = options.name;
|
|
460
|
+
this.kind = options.kind || SpanKind.INTERNAL;
|
|
461
|
+
this.startTime = options.startTime || Date.now();
|
|
462
|
+
this.endTime = null;
|
|
463
|
+
this.status = TraceStatus.UNSET;
|
|
464
|
+
this.statusMessage = '';
|
|
465
|
+
this.attributes = options.attributes || {};
|
|
466
|
+
this.events = [];
|
|
467
|
+
this.links = options.links || [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Set an attribute
|
|
472
|
+
*/
|
|
473
|
+
setAttribute(key, value) {
|
|
474
|
+
this.attributes[key] = value;
|
|
475
|
+
return this;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Set multiple attributes
|
|
480
|
+
*/
|
|
481
|
+
setAttributes(attributes) {
|
|
482
|
+
Object.assign(this.attributes, attributes);
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Add an event
|
|
488
|
+
*/
|
|
489
|
+
addEvent(name, attributes = {}) {
|
|
490
|
+
this.events.push({
|
|
491
|
+
name,
|
|
492
|
+
timestamp: Date.now(),
|
|
493
|
+
attributes
|
|
494
|
+
});
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Set status
|
|
500
|
+
*/
|
|
501
|
+
setStatus(status, message = '') {
|
|
502
|
+
this.status = status;
|
|
503
|
+
this.statusMessage = message;
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* End the span
|
|
509
|
+
*/
|
|
510
|
+
end() {
|
|
511
|
+
this.endTime = Date.now();
|
|
512
|
+
return this;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get duration in milliseconds
|
|
517
|
+
*/
|
|
518
|
+
getDuration() {
|
|
519
|
+
if (!this.endTime) return null;
|
|
520
|
+
return this.endTime - this.startTime;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get span context for propagation
|
|
525
|
+
*/
|
|
526
|
+
getContext() {
|
|
527
|
+
return {
|
|
528
|
+
traceId: this.traceId,
|
|
529
|
+
spanId: this.spanId
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
toJSON() {
|
|
534
|
+
return {
|
|
535
|
+
traceId: this.traceId,
|
|
536
|
+
spanId: this.spanId,
|
|
537
|
+
parentSpanId: this.parentSpanId,
|
|
538
|
+
name: this.name,
|
|
539
|
+
kind: this.kind,
|
|
540
|
+
startTime: this.startTime,
|
|
541
|
+
endTime: this.endTime,
|
|
542
|
+
duration: this.getDuration(),
|
|
543
|
+
status: this.status,
|
|
544
|
+
statusMessage: this.statusMessage,
|
|
545
|
+
attributes: this.attributes,
|
|
546
|
+
events: this.events,
|
|
547
|
+
links: this.links
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Tracer for distributed tracing
|
|
554
|
+
*/
|
|
555
|
+
class Tracer extends EventEmitter {
|
|
556
|
+
constructor(options = {}) {
|
|
557
|
+
super();
|
|
558
|
+
this.serviceName = options.serviceName || 'unknown';
|
|
559
|
+
this.version = options.version || '1.0.0';
|
|
560
|
+
this.spans = [];
|
|
561
|
+
this.exporters = options.exporters || [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Start a new span
|
|
566
|
+
*/
|
|
567
|
+
startSpan(name, options = {}) {
|
|
568
|
+
const span = new Span({
|
|
569
|
+
traceId: options.traceId,
|
|
570
|
+
parentSpanId: options.parentSpanId,
|
|
571
|
+
name,
|
|
572
|
+
kind: options.kind,
|
|
573
|
+
attributes: {
|
|
574
|
+
'service.name': this.serviceName,
|
|
575
|
+
'service.version': this.version,
|
|
576
|
+
...options.attributes
|
|
577
|
+
},
|
|
578
|
+
links: options.links
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
this.spans.push(span);
|
|
582
|
+
this.emit('spanStarted', span);
|
|
583
|
+
return span;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Create a child span
|
|
588
|
+
*/
|
|
589
|
+
startChildSpan(parent, name, options = {}) {
|
|
590
|
+
return this.startSpan(name, {
|
|
591
|
+
traceId: parent.traceId,
|
|
592
|
+
parentSpanId: parent.spanId,
|
|
593
|
+
...options
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* End and export a span
|
|
599
|
+
*/
|
|
600
|
+
endSpan(span) {
|
|
601
|
+
span.end();
|
|
602
|
+
this.emit('spanEnded', span);
|
|
603
|
+
|
|
604
|
+
for (const exporter of this.exporters) {
|
|
605
|
+
exporter.export(span);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return span;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get all spans for a trace
|
|
613
|
+
*/
|
|
614
|
+
getTrace(traceId) {
|
|
615
|
+
return this.spans.filter(s => s.traceId === traceId);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Add an exporter
|
|
620
|
+
*/
|
|
621
|
+
addExporter(exporter) {
|
|
622
|
+
this.exporters.push(exporter);
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Get all spans
|
|
628
|
+
*/
|
|
629
|
+
getAllSpans() {
|
|
630
|
+
return this.spans.map(s => s.toJSON());
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Console exporter for traces
|
|
636
|
+
*/
|
|
637
|
+
class ConsoleExporter {
|
|
638
|
+
constructor(options = {}) {
|
|
639
|
+
this.format = options.format || 'json';
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export(span) {
|
|
643
|
+
if (this.format === 'json') {
|
|
644
|
+
console.log(JSON.stringify(span.toJSON()));
|
|
645
|
+
} else {
|
|
646
|
+
const duration = span.getDuration() || 0;
|
|
647
|
+
console.log(`[${span.traceId.slice(0, 8)}] ${span.name} (${duration}ms) - ${span.status}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Memory exporter for testing
|
|
654
|
+
*/
|
|
655
|
+
class MemoryExporter {
|
|
656
|
+
constructor() {
|
|
657
|
+
this.spans = [];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export(span) {
|
|
661
|
+
this.spans.push(span.toJSON());
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
getSpans() {
|
|
665
|
+
return this.spans;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
clear() {
|
|
669
|
+
this.spans = [];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Context propagation for correlation
|
|
675
|
+
*/
|
|
676
|
+
class CorrelationContext {
|
|
677
|
+
constructor() {
|
|
678
|
+
this.values = new Map();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Set a value
|
|
683
|
+
*/
|
|
684
|
+
set(key, value) {
|
|
685
|
+
this.values.set(key, value);
|
|
686
|
+
return this;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Get a value
|
|
691
|
+
*/
|
|
692
|
+
get(key) {
|
|
693
|
+
return this.values.get(key);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Check if key exists
|
|
698
|
+
*/
|
|
699
|
+
has(key) {
|
|
700
|
+
return this.values.has(key);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Get all values
|
|
705
|
+
*/
|
|
706
|
+
getAll() {
|
|
707
|
+
return Object.fromEntries(this.values);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Create headers for HTTP propagation
|
|
712
|
+
*/
|
|
713
|
+
toHeaders() {
|
|
714
|
+
const headers = {};
|
|
715
|
+
|
|
716
|
+
if (this.has('traceId')) {
|
|
717
|
+
headers['x-trace-id'] = this.get('traceId');
|
|
718
|
+
}
|
|
719
|
+
if (this.has('spanId')) {
|
|
720
|
+
headers['x-span-id'] = this.get('spanId');
|
|
721
|
+
}
|
|
722
|
+
if (this.has('correlationId')) {
|
|
723
|
+
headers['x-correlation-id'] = this.get('correlationId');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return headers;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Create from headers
|
|
731
|
+
*/
|
|
732
|
+
static fromHeaders(headers) {
|
|
733
|
+
const ctx = new CorrelationContext();
|
|
734
|
+
|
|
735
|
+
if (headers['x-trace-id']) {
|
|
736
|
+
ctx.set('traceId', headers['x-trace-id']);
|
|
737
|
+
}
|
|
738
|
+
if (headers['x-span-id']) {
|
|
739
|
+
ctx.set('spanId', headers['x-span-id']);
|
|
740
|
+
}
|
|
741
|
+
if (headers['x-correlation-id']) {
|
|
742
|
+
ctx.set('correlationId', headers['x-correlation-id']);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return ctx;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Unified Observability Provider
|
|
751
|
+
*/
|
|
752
|
+
class ObservabilityProvider extends EventEmitter {
|
|
753
|
+
constructor(options = {}) {
|
|
754
|
+
super();
|
|
755
|
+
this.serviceName = options.serviceName || 'musubi-service';
|
|
756
|
+
this.version = options.version || '1.0.0';
|
|
757
|
+
|
|
758
|
+
this.logger = new Logger({
|
|
759
|
+
name: this.serviceName,
|
|
760
|
+
level: options.logLevel || LogLevel.INFO
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
this.metrics = new MetricsCollector({
|
|
764
|
+
name: this.serviceName,
|
|
765
|
+
prefix: options.metricsPrefix || ''
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
this.tracer = new Tracer({
|
|
769
|
+
serviceName: this.serviceName,
|
|
770
|
+
version: this.version
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Standard metrics
|
|
774
|
+
this._setupStandardMetrics();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Setup standard metrics
|
|
779
|
+
*/
|
|
780
|
+
_setupStandardMetrics() {
|
|
781
|
+
// Request metrics
|
|
782
|
+
this.requestCounter = this.metrics.counter('http_requests_total', 'Total HTTP requests');
|
|
783
|
+
this.requestDuration = this.metrics.histogram('http_request_duration_seconds', 'HTTP request duration');
|
|
784
|
+
this.requestErrors = this.metrics.counter('http_request_errors_total', 'Total HTTP errors');
|
|
785
|
+
|
|
786
|
+
// Resource metrics
|
|
787
|
+
this.activeConnections = this.metrics.gauge('active_connections', 'Active connections');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Get the logger
|
|
792
|
+
*/
|
|
793
|
+
getLogger(name) {
|
|
794
|
+
return this.logger.child({ component: name });
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Get the metrics collector
|
|
799
|
+
*/
|
|
800
|
+
getMetrics() {
|
|
801
|
+
return this.metrics;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Get the tracer
|
|
806
|
+
*/
|
|
807
|
+
getTracer() {
|
|
808
|
+
return this.tracer;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Start a traced operation
|
|
813
|
+
*/
|
|
814
|
+
trace(name, fn, options = {}) {
|
|
815
|
+
const span = this.tracer.startSpan(name, options);
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
const result = fn(span);
|
|
819
|
+
|
|
820
|
+
if (result && typeof result.then === 'function') {
|
|
821
|
+
return result
|
|
822
|
+
.then(r => {
|
|
823
|
+
span.setStatus(TraceStatus.OK);
|
|
824
|
+
this.tracer.endSpan(span);
|
|
825
|
+
return r;
|
|
826
|
+
})
|
|
827
|
+
.catch(err => {
|
|
828
|
+
span.setStatus(TraceStatus.ERROR, err.message);
|
|
829
|
+
span.addEvent('exception', {
|
|
830
|
+
'exception.type': err.name,
|
|
831
|
+
'exception.message': err.message
|
|
832
|
+
});
|
|
833
|
+
this.tracer.endSpan(span);
|
|
834
|
+
throw err;
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
span.setStatus(TraceStatus.OK);
|
|
839
|
+
this.tracer.endSpan(span);
|
|
840
|
+
return result;
|
|
841
|
+
} catch (err) {
|
|
842
|
+
span.setStatus(TraceStatus.ERROR, err.message);
|
|
843
|
+
span.addEvent('exception', {
|
|
844
|
+
'exception.type': err.name,
|
|
845
|
+
'exception.message': err.message
|
|
846
|
+
});
|
|
847
|
+
this.tracer.endSpan(span);
|
|
848
|
+
throw err;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Record an HTTP request
|
|
854
|
+
*/
|
|
855
|
+
recordRequest(method, path, statusCode, duration) {
|
|
856
|
+
const labels = { method, path, status_code: statusCode.toString() };
|
|
857
|
+
|
|
858
|
+
this.requestCounter.inc(labels);
|
|
859
|
+
this.requestDuration.observe(duration / 1000, { method, path }); // Convert to seconds
|
|
860
|
+
|
|
861
|
+
if (statusCode >= 400) {
|
|
862
|
+
this.requestErrors.inc(labels);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Export all telemetry data
|
|
868
|
+
*/
|
|
869
|
+
exportTelemetry() {
|
|
870
|
+
return {
|
|
871
|
+
service: {
|
|
872
|
+
name: this.serviceName,
|
|
873
|
+
version: this.version
|
|
874
|
+
},
|
|
875
|
+
metrics: this.metrics.toJSON(),
|
|
876
|
+
traces: this.tracer.getAllSpans()
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Create a logger
|
|
883
|
+
*/
|
|
884
|
+
function createLogger(options = {}) {
|
|
885
|
+
return new Logger(options);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Create a metrics collector
|
|
890
|
+
*/
|
|
891
|
+
function createMetricsCollector(options = {}) {
|
|
892
|
+
return new MetricsCollector(options);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Create a tracer
|
|
897
|
+
*/
|
|
898
|
+
function createTracer(options = {}) {
|
|
899
|
+
return new Tracer(options);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Create an observability provider
|
|
904
|
+
*/
|
|
905
|
+
function createObservability(options = {}) {
|
|
906
|
+
return new ObservabilityProvider(options);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
module.exports = {
|
|
910
|
+
// Classes
|
|
911
|
+
Logger,
|
|
912
|
+
ConsoleOutput,
|
|
913
|
+
FileOutput,
|
|
914
|
+
MetricsCollector,
|
|
915
|
+
CounterMetric,
|
|
916
|
+
GaugeMetric,
|
|
917
|
+
HistogramMetric,
|
|
918
|
+
Span,
|
|
919
|
+
Tracer,
|
|
920
|
+
ConsoleExporter,
|
|
921
|
+
MemoryExporter,
|
|
922
|
+
CorrelationContext,
|
|
923
|
+
ObservabilityProvider,
|
|
924
|
+
|
|
925
|
+
// Constants
|
|
926
|
+
LogLevel,
|
|
927
|
+
TraceStatus,
|
|
928
|
+
SpanKind,
|
|
929
|
+
|
|
930
|
+
// Factories
|
|
931
|
+
createLogger,
|
|
932
|
+
createMetricsCollector,
|
|
933
|
+
createTracer,
|
|
934
|
+
createObservability,
|
|
935
|
+
|
|
936
|
+
// Utilities
|
|
937
|
+
generateId
|
|
938
|
+
};
|