Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ const { CircuitBreaker } = require('./resilience/circuit-breaker');
const { EventBus } = require('./events/event-bus');
const { TaskScheduler } = require('./orchestration/scheduler');

const {
TelemetryExporter,
PrometheusExporter,
OpenTelemetryExporter,
TelemetryManager
} = require('./telemetry');

module.exports = {
TelemetryExporter,
PrometheusExporter,
OpenTelemetryExporter,
TelemetryManager,
Engine,
CircuitBreaker,
EventBus,
Expand Down
141 changes: 141 additions & 0 deletions src/telemetry/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
class TelemetryExporter {
constructor(options = {}) {
this.name = options.name || 'base-exporter';
this.enabled = options.enabled !== false;
}

async export(metrics) {
throw new Error('export() must be implemented by subclasses');
}
}

Documentation = 'Prometheus Exporter';

class PrometheusExporter extends TelemetryExporter {
constructor(options = {}) {
super({ name: 'prometheus', ...options });
this.prefix = options.prefix || 'brocode';
}

format(metrics) {
const prefix = this.prefix;
const lines = [];

lines.push('# HELP ' + prefix + '_engine_status Current operational status of the runtime engine');
lines.push('# TYPE' + ' ' + prefix + '_engine_status gauge');
const statusVal = metrics.status === 'RUNNING' ? 1 : 0;
lines.push(prefix + '_engine_status{engine="' + metrics.engine + '",version="' + metrics.version + '"} ' + statusVal);

lines.push('# HELP ' + prefix + '_uptime_seconds Engine uptime duration in seconds');
lines.push('# TYPE' + ' ' + prefix + '_uptime_seconds counter');
lines.push(prefix + '_uptime_seconds{engine="' + metrics.engine + '"} ' + (metrics.uptimeSeconds || 0));

if (metrics.memory) {
lines.push('# HELP ' + prefix + '_memory_heap_used_mb Heap memory used in MB');
lines.push('# TYPE' + ' ' + prefix + '_memory_heap_used_mb gauge');
lines.push(prefix + '_memory_heap_used_mb{engine="' + metrics.engine + '"} ' + (metrics.memory.heapUsedMB || 0));

lines.push('# HELP ' + prefix + '_memory_rss_mb Resident Set Size memory in MB');
lines.push('# TYPE' + ' ' + prefix + '_memory_rss_mb gauge');
lines.push(prefix + '_memory_rss_mb{engine="' + metrics.engine + '"} ' + (metrics.memory.rssMB || 0));
}

return lines.join('\n') + '\n';
}

async export(metrics) {
return this.format(metrics);
}
}

class OpenTelemetryExporter extends TelemetryExporter {
constructor(options = {}) {
super({ name: 'opentelemetry', ...options });
this.serviceName = options.serviceName || 'brocode-service';
}

format(metrics) {
const timestampNano = (Date.now() * 1000000).toString();
return {
resourceMetrics: [
{
resource: {
attributes: [
{ key: 'service.name', value: { stringValue: this.serviceName } },
{ key: 'engine.name', value: { stringValue: metrics.engine } },
{ key: 'engine.version', value: { stringValue: metrics.version } }
]
},
scopeMetrics: [
{
scope: { name: 'brocode.runtime.metrics', vesion: '0.1.0' },
metrics: [
{
name: 'runtime.uptime',
description: 'Uptime duration in seconds',
unit: 's',
gauge: {
dataPoints: [{ timeUnixNano: timestampNano, asInt: metrics.uptimeSeconds || 0 }]
}
},
{
name: 'runtime.memory.heap_used',
description: 'Memory heap used in MB',
unit: 'MB',
gauge: {
dataPoints: [{ timeUnixNano: timestampNano, asDouble: metrics.memory ? metrics.memory.heapUsedMB : 0 }]
}
}
]
}
]
}
]
};
}

async export(metrics) {
return this.format(metrics);
}
}

class TelemetryManager {
constructor() {
this.exporters = new Map();
}

register(exporter) {
if (!exporter || !exporter.name) {
throw new Error('Exporter must have a name');
}
this.exporters.set(exporter.name, exporter);
return this;
}


unregister(name) {
return this.exporters.delete(name);
}

get(name) {
return this.exporters.get(name);
}


async collect(metrics) {
const results = {};
for (const [name, exporter] of this.exporters.entries()) {
if (exporter.enabled) {
results[name] = await exporter.export(metrics);
}
}
return results;
}
}

module.exports = {
TelemetryExporter,
PrometheusExporter,
OpenTelemetryExporter,
TelemetryManager
};
74 changes: 74 additions & 0 deletions tests/telemetry.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const test = require('node:test');
const assert = require('node:assert');
const {
TelemetryExporter,
PrometheusExporter,
OpenTelemetryExporter,
TelemetryManager
} = require('../src/telemetry');

test('PrometheusExporter formats metrics into Prometheus exposition format', async () => {
const exporter = new PrometheusExporter({ prefix: 'test_brocode' });
const metrics = {
status: 'RUNNING',
engine: 'engine-1',
version: '0.1.0',
uptimeSeconds: 120,
memory: {
heapUsedMB: 45.5,
heapTotalMB: 60.0,
rssMB: 85.2
}
};

const output = await exporter.export(metrics);
assert.ok(output.includes('# HELP test_brocode_engine_status'));
assert.ok(output.includes('test_brocode_engine_status{engine="engine-1",version="0.1.0"} 1'));
assert.ok(output.includes('test_brocode_uptime_seconds{engine="engine-1"} 120'));
assert.ok(output.includes('test_brocode_memory_heap_used_mb{engine="engine-1"} 45.5'));
});

test('OpenTelemetryExporter produces compliant OTel ResourceMetrics payload', async () => {
const exporter = new OpenTelemetryExporter({ serviceName: 'custom-service' });
const metrics = {
status: 'RUNNING',
engine: 'engine-1',
version: '0.1.0',
uptimeSeconds: 60,
memory: { heapUsedMB: 30 }
};

const output = await exporter.export(metrics);
assert.ok(output.resourceMetrics);
const resource = output.resourceMetrics[0].resource;
const serviceAttr = resource.attributes.find(a => a.key === 'service.name');
assert.strictEqual(serviceAttr.value.stringValue, 'custom-service');

const metricList = output.resourceMetrics[0].scopeMetrics[0].metrics;
const uptimeMetric = metricList.find(m => m.name === 'runtime.uptime');
assert.strictEqual(uptimeMetric.gauge.dataPoints[0].asInt, 60);
});

test('TelemetryManager registers, retrieves, and collects across multiple exporters', async () => {
const manager = new TelemetryManager();
const prom = new PrometheusExporter();
const otel = new OpenTelemetryExporter();

manager.register(prom);
manager.register(otel);

assert.strictEqual(manager.get('prometheus'), prom);
assert.strictEqual(manager.get('opentelemetry'), otel);

const metrics = {
status: 'STOPPED',
engine: 'kernel',
version: '0.1.0',
uptimeSeconds: 0
};

const collected = await manager.collect(metrics);
assert.ok(collected.prometheus);
assert.ok(collected.opentelemetry);
assert.ok(collected.prometheus.includes('brocode_engine_status{engine="kernel",version="0.1.0"} 0'));
});
Loading