@frontmcp/observability 1.2.1 → 1.3.0
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/esm/index.mjs +237 -0
- package/esm/package.json +3 -3
- package/index.d.ts +3 -0
- package/index.js +241 -0
- package/package.json +3 -3
- package/process-stats/index.d.ts +1 -0
- package/process-stats/process-stats.collector.d.ts +65 -0
- package/prometheus/index.d.ts +2 -0
- package/prometheus/render.d.ts +58 -0
package/esm/index.mjs
CHANGED
|
@@ -3116,6 +3116,239 @@ init_winston_sink();
|
|
|
3116
3116
|
init_pino_sink();
|
|
3117
3117
|
init_otlp_sink();
|
|
3118
3118
|
|
|
3119
|
+
// libs/observability/src/prometheus/render.ts
|
|
3120
|
+
var METRIC_NAME_REGEX = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
|
|
3121
|
+
var LABEL_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3122
|
+
var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
3123
|
+
function escapeLabelValue(value) {
|
|
3124
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
3125
|
+
}
|
|
3126
|
+
function sortedLabelString(attributes) {
|
|
3127
|
+
const keys = Object.keys(attributes).filter((k) => LABEL_NAME_REGEX.test(k)).sort();
|
|
3128
|
+
if (keys.length === 0) return "";
|
|
3129
|
+
const pairs = keys.map((k) => `${k}="${escapeLabelValue(attributes[k] ?? "")}"`);
|
|
3130
|
+
return `{${pairs.join(",")}}`;
|
|
3131
|
+
}
|
|
3132
|
+
function formatFloat(value) {
|
|
3133
|
+
if (!Number.isFinite(value)) return "";
|
|
3134
|
+
if (Number.isInteger(value)) return String(value);
|
|
3135
|
+
return String(value);
|
|
3136
|
+
}
|
|
3137
|
+
function renderPrometheusExposition(counters, gauges = [], options = {}) {
|
|
3138
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3139
|
+
for (const entry of counters) {
|
|
3140
|
+
if (!METRIC_NAME_REGEX.test(entry.name)) continue;
|
|
3141
|
+
const valueStr = formatFloat(entry.count);
|
|
3142
|
+
if (valueStr === "") continue;
|
|
3143
|
+
const labels = sortedLabelString(entry.attributes ?? {});
|
|
3144
|
+
const line = `${entry.name}${labels} ${valueStr}`;
|
|
3145
|
+
let group = byName.get(entry.name);
|
|
3146
|
+
if (!group) {
|
|
3147
|
+
group = {
|
|
3148
|
+
type: "counter",
|
|
3149
|
+
help: options.counterHelp?.[entry.name],
|
|
3150
|
+
lines: []
|
|
3151
|
+
};
|
|
3152
|
+
byName.set(entry.name, group);
|
|
3153
|
+
}
|
|
3154
|
+
group.lines.push(line);
|
|
3155
|
+
}
|
|
3156
|
+
for (const entry of gauges) {
|
|
3157
|
+
if (!METRIC_NAME_REGEX.test(entry.name)) continue;
|
|
3158
|
+
const valueStr = formatFloat(entry.value);
|
|
3159
|
+
if (valueStr === "") continue;
|
|
3160
|
+
const labels = sortedLabelString(entry.attributes ?? {});
|
|
3161
|
+
const line = `${entry.name}${labels} ${valueStr}`;
|
|
3162
|
+
let group = byName.get(entry.name);
|
|
3163
|
+
if (!group) {
|
|
3164
|
+
group = { type: "gauge", help: entry.help, lines: [] };
|
|
3165
|
+
byName.set(entry.name, group);
|
|
3166
|
+
} else if (entry.help && !group.help) {
|
|
3167
|
+
group.help = entry.help;
|
|
3168
|
+
}
|
|
3169
|
+
group.lines.push(line);
|
|
3170
|
+
}
|
|
3171
|
+
if (byName.size === 0) return "";
|
|
3172
|
+
const sortedNames = Array.from(byName.keys()).sort();
|
|
3173
|
+
const blocks = [];
|
|
3174
|
+
for (const name of sortedNames) {
|
|
3175
|
+
const group = byName.get(name);
|
|
3176
|
+
if (!group) continue;
|
|
3177
|
+
const block = [];
|
|
3178
|
+
if (group.help) {
|
|
3179
|
+
block.push(`# HELP ${name} ${group.help.replace(/\n/g, " ")}`);
|
|
3180
|
+
}
|
|
3181
|
+
block.push(`# TYPE ${name} ${group.type}`);
|
|
3182
|
+
block.push(...group.lines.slice().sort());
|
|
3183
|
+
blocks.push(block.join("\n"));
|
|
3184
|
+
}
|
|
3185
|
+
return blocks.join("\n") + "\n";
|
|
3186
|
+
}
|
|
3187
|
+
function renderJsonExposition(counters, gauges = []) {
|
|
3188
|
+
return {
|
|
3189
|
+
counters: counters.filter((c) => METRIC_NAME_REGEX.test(c.name) && Number.isFinite(c.count)).map((c) => ({ name: c.name, count: c.count, attributes: { ...c.attributes ?? {} } })),
|
|
3190
|
+
gauges: gauges.filter((g) => METRIC_NAME_REGEX.test(g.name) && Number.isFinite(g.value)).map((g) => ({
|
|
3191
|
+
name: g.name,
|
|
3192
|
+
value: g.value,
|
|
3193
|
+
attributes: { ...g.attributes ?? {} },
|
|
3194
|
+
...g.help ? { help: g.help } : {}
|
|
3195
|
+
}))
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// libs/observability/src/process-stats/process-stats.collector.ts
|
|
3200
|
+
import { readdirSync } from "@frontmcp/utils";
|
|
3201
|
+
var NS_PER_SECOND = 1e9;
|
|
3202
|
+
var MICROS_PER_SECOND = 1e6;
|
|
3203
|
+
function defaultMonitorEventLoopDelay() {
|
|
3204
|
+
try {
|
|
3205
|
+
const { monitorEventLoopDelay } = __require("node:perf_hooks");
|
|
3206
|
+
const histogram = monitorEventLoopDelay({ resolution: 10 });
|
|
3207
|
+
histogram.enable();
|
|
3208
|
+
return histogram;
|
|
3209
|
+
} catch {
|
|
3210
|
+
return void 0;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
function defaultGetActiveHandles() {
|
|
3214
|
+
const proc = process;
|
|
3215
|
+
if (typeof proc._getActiveHandles !== "function") return void 0;
|
|
3216
|
+
try {
|
|
3217
|
+
return proc._getActiveHandles();
|
|
3218
|
+
} catch {
|
|
3219
|
+
return void 0;
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
function defaultGetActiveRequests() {
|
|
3223
|
+
const proc = process;
|
|
3224
|
+
if (typeof proc._getActiveRequests !== "function") return void 0;
|
|
3225
|
+
try {
|
|
3226
|
+
return proc._getActiveRequests();
|
|
3227
|
+
} catch {
|
|
3228
|
+
return void 0;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
function defaultReadFdCount() {
|
|
3232
|
+
if (process.platform !== "linux") return void 0;
|
|
3233
|
+
try {
|
|
3234
|
+
return readdirSync("/proc/self/fd").length;
|
|
3235
|
+
} catch {
|
|
3236
|
+
return void 0;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
var ProcessStatsCollector = class {
|
|
3240
|
+
constructor(init = {}) {
|
|
3241
|
+
this.options = init.options ?? {};
|
|
3242
|
+
this.cpuUsage = init.cpuUsage ?? ((prev) => process.cpuUsage(prev));
|
|
3243
|
+
this.memoryUsage = init.memoryUsage ?? (() => process.memoryUsage());
|
|
3244
|
+
this.uptime = init.uptime ?? (() => process.uptime());
|
|
3245
|
+
this.getActiveHandles = init.getActiveHandles ?? defaultGetActiveHandles;
|
|
3246
|
+
this.getActiveRequests = init.getActiveRequests ?? defaultGetActiveRequests;
|
|
3247
|
+
this.readFdCount = init.readFdCount ?? defaultReadFdCount;
|
|
3248
|
+
this.cpuStart = this.cpuUsage();
|
|
3249
|
+
if (this.options.eventLoopLag !== false) {
|
|
3250
|
+
this.histogram = init.monitorEventLoopDelay ? init.monitorEventLoopDelay() : defaultMonitorEventLoopDelay();
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
collect() {
|
|
3254
|
+
const entries = [];
|
|
3255
|
+
const cpu = this.cpuUsage(this.cpuStart);
|
|
3256
|
+
entries.push({
|
|
3257
|
+
name: "frontmcp_process_cpu_seconds_total",
|
|
3258
|
+
value: cpu.user / MICROS_PER_SECOND,
|
|
3259
|
+
attributes: { mode: "user" },
|
|
3260
|
+
help: "CPU time consumed since collector start, by mode (seconds)"
|
|
3261
|
+
});
|
|
3262
|
+
entries.push({
|
|
3263
|
+
name: "frontmcp_process_cpu_seconds_total",
|
|
3264
|
+
value: cpu.system / MICROS_PER_SECOND,
|
|
3265
|
+
attributes: { mode: "system" }
|
|
3266
|
+
});
|
|
3267
|
+
const mem = this.memoryUsage();
|
|
3268
|
+
entries.push({
|
|
3269
|
+
name: "frontmcp_process_resident_memory_bytes",
|
|
3270
|
+
value: mem.rss,
|
|
3271
|
+
help: "Resident memory size in bytes"
|
|
3272
|
+
});
|
|
3273
|
+
entries.push({
|
|
3274
|
+
name: "frontmcp_process_heap_bytes",
|
|
3275
|
+
value: mem.heapTotal,
|
|
3276
|
+
help: "Total V8 heap size in bytes"
|
|
3277
|
+
});
|
|
3278
|
+
entries.push({
|
|
3279
|
+
name: "frontmcp_process_heap_used_bytes",
|
|
3280
|
+
value: mem.heapUsed,
|
|
3281
|
+
help: "Used V8 heap size in bytes"
|
|
3282
|
+
});
|
|
3283
|
+
entries.push({
|
|
3284
|
+
name: "frontmcp_process_external_bytes",
|
|
3285
|
+
value: mem.external,
|
|
3286
|
+
help: "Memory used by C++ objects bound to JS in bytes"
|
|
3287
|
+
});
|
|
3288
|
+
entries.push({
|
|
3289
|
+
name: "frontmcp_process_uptime_seconds",
|
|
3290
|
+
value: this.uptime(),
|
|
3291
|
+
help: "Time since process start in seconds"
|
|
3292
|
+
});
|
|
3293
|
+
if (this.options.eventLoopLag !== false && this.histogram) {
|
|
3294
|
+
const meanSeconds = this.histogram.mean / NS_PER_SECOND;
|
|
3295
|
+
const p99Seconds = this.histogram.percentile(99) / NS_PER_SECOND;
|
|
3296
|
+
if (Number.isFinite(meanSeconds)) {
|
|
3297
|
+
entries.push({
|
|
3298
|
+
name: "frontmcp_nodejs_eventloop_lag_seconds",
|
|
3299
|
+
value: meanSeconds,
|
|
3300
|
+
attributes: { quantile: "mean" },
|
|
3301
|
+
help: "Event-loop lag in seconds, sampled at 10ms resolution since the last scrape"
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
if (Number.isFinite(p99Seconds)) {
|
|
3305
|
+
entries.push({
|
|
3306
|
+
name: "frontmcp_nodejs_eventloop_lag_seconds",
|
|
3307
|
+
value: p99Seconds,
|
|
3308
|
+
attributes: { quantile: "p99" }
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
this.histogram.reset();
|
|
3312
|
+
}
|
|
3313
|
+
if (this.options.activeHandles !== false) {
|
|
3314
|
+
const handles = this.getActiveHandles?.();
|
|
3315
|
+
if (handles !== void 0) {
|
|
3316
|
+
entries.push({
|
|
3317
|
+
name: "frontmcp_nodejs_active_handles",
|
|
3318
|
+
value: handles.length,
|
|
3319
|
+
help: "Currently active libuv handles (sockets, timers, etc.)"
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
const requests = this.getActiveRequests?.();
|
|
3323
|
+
if (requests !== void 0) {
|
|
3324
|
+
entries.push({
|
|
3325
|
+
name: "frontmcp_nodejs_active_requests",
|
|
3326
|
+
value: requests.length,
|
|
3327
|
+
help: "Currently active libuv requests (file I/O, DNS, etc.)"
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
if (this.options.fdCount !== false) {
|
|
3332
|
+
const fdCount = this.readFdCount();
|
|
3333
|
+
if (typeof fdCount === "number") {
|
|
3334
|
+
entries.push({
|
|
3335
|
+
name: "frontmcp_nodejs_open_fds",
|
|
3336
|
+
value: fdCount,
|
|
3337
|
+
help: "Number of open file descriptors (Linux only)"
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
return entries;
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Release the perf_hooks histogram listener. Call when the collector is
|
|
3345
|
+
* no longer used (e.g., when reconfiguring the server).
|
|
3346
|
+
*/
|
|
3347
|
+
close() {
|
|
3348
|
+
this.histogram?.disable?.();
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
|
|
3119
3352
|
// libs/observability/src/testing/index.ts
|
|
3120
3353
|
import {
|
|
3121
3354
|
BasicTracerProvider,
|
|
@@ -3178,8 +3411,10 @@ export {
|
|
|
3178
3411
|
OTEL_TRACER,
|
|
3179
3412
|
ObservabilityPlugin,
|
|
3180
3413
|
OtlpSink,
|
|
3414
|
+
PROMETHEUS_CONTENT_TYPE,
|
|
3181
3415
|
PinoSink,
|
|
3182
3416
|
PrettySpanExporter,
|
|
3417
|
+
ProcessStatsCollector,
|
|
3183
3418
|
REQUEST_LOG_COLLECTOR,
|
|
3184
3419
|
RequestLogCollector,
|
|
3185
3420
|
RpcAttributes,
|
|
@@ -3212,6 +3447,8 @@ export {
|
|
|
3212
3447
|
otelToFrontmcpContext,
|
|
3213
3448
|
recordHookEvent,
|
|
3214
3449
|
redactFields,
|
|
3450
|
+
renderJsonExposition,
|
|
3451
|
+
renderPrometheusExposition,
|
|
3215
3452
|
reportStartup,
|
|
3216
3453
|
resetCounterCacheForTesting,
|
|
3217
3454
|
resetMetricSnapshot,
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/observability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation, structured JSON logging, and request log objects for FrontMCP",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@opentelemetry/api": "^1.9.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@frontmcp/sdk": "1.
|
|
55
|
-
"@frontmcp/utils": "1.
|
|
54
|
+
"@frontmcp/sdk": "1.3.0",
|
|
55
|
+
"@frontmcp/utils": "1.3.0",
|
|
56
56
|
"@opentelemetry/sdk-trace-base": "^1.25.0",
|
|
57
57
|
"@opentelemetry/sdk-node": "^0.52.0",
|
|
58
58
|
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export { StructuredLogTransport, LOG_LEVEL_TO_OTEL_SEVERITY, StdoutSink, Console
|
|
|
6
6
|
export type { StructuredLogEntry, StructuredLogError, StructuredLogTransportOptions, LogSink, SinkConfig, StdoutSinkConfig, ConsoleSinkConfig, WinstonSinkConfig, PinoSinkConfig, CallbackSinkConfig, OtlpSinkConfig, OtlpSinkOptions, WinstonLike, PinoLike, ContextAccessor, ContextSnapshot, } from './logging';
|
|
7
7
|
export { TelemetryAccessor, TelemetrySpan, TelemetryFactory, TELEMETRY_ACCESSOR, TELEMETRY_FACTORY, createCounter, getMetricSnapshot, getCounterTotal, resetMetricSnapshot, resetTelemetrySnapshotForTesting, resetCounterCacheForTesting, normalizeBundleSource, normalizeErrorReason, KNOWN_BUNDLE_SOURCES, KNOWN_ERROR_REASONS, } from './telemetry';
|
|
8
8
|
export type { TelemetryCounter, CounterSnapshotEntry, KnownBundleSource, KnownErrorReason } from './telemetry';
|
|
9
|
+
export { PROMETHEUS_CONTENT_TYPE, renderJsonExposition, renderPrometheusExposition } from './prometheus';
|
|
10
|
+
export type { GaugeSnapshotEntry, RenderOptions } from './prometheus';
|
|
11
|
+
export { ProcessStatsCollector } from './process-stats';
|
|
9
12
|
export { createTestTracer, getFinishedSpans, assertSpanExists, assertSpanAttribute, findSpan, findSpansByAttribute, } from './testing';
|
|
10
13
|
export type { TestTracer } from './testing';
|
|
11
14
|
export { RequestLogCollector, REQUEST_LOG_COLLECTOR } from './request-log';
|
package/index.js
CHANGED
|
@@ -330,8 +330,10 @@ __export(index_exports, {
|
|
|
330
330
|
OTEL_TRACER: () => OTEL_TRACER,
|
|
331
331
|
ObservabilityPlugin: () => ObservabilityPlugin,
|
|
332
332
|
OtlpSink: () => OtlpSink,
|
|
333
|
+
PROMETHEUS_CONTENT_TYPE: () => PROMETHEUS_CONTENT_TYPE,
|
|
333
334
|
PinoSink: () => PinoSink,
|
|
334
335
|
PrettySpanExporter: () => PrettySpanExporter,
|
|
336
|
+
ProcessStatsCollector: () => ProcessStatsCollector,
|
|
335
337
|
REQUEST_LOG_COLLECTOR: () => REQUEST_LOG_COLLECTOR,
|
|
336
338
|
RequestLogCollector: () => RequestLogCollector,
|
|
337
339
|
RpcAttributes: () => RpcAttributes,
|
|
@@ -364,6 +366,8 @@ __export(index_exports, {
|
|
|
364
366
|
otelToFrontmcpContext: () => otelToFrontmcpContext,
|
|
365
367
|
recordHookEvent: () => recordHookEvent,
|
|
366
368
|
redactFields: () => redactFields,
|
|
369
|
+
renderJsonExposition: () => renderJsonExposition,
|
|
370
|
+
renderPrometheusExposition: () => renderPrometheusExposition,
|
|
367
371
|
reportStartup: () => reportStartup,
|
|
368
372
|
resetCounterCacheForTesting: () => resetCounterCacheForTesting,
|
|
369
373
|
resetMetricSnapshot: () => resetMetricSnapshot,
|
|
@@ -3155,6 +3159,239 @@ init_winston_sink();
|
|
|
3155
3159
|
init_pino_sink();
|
|
3156
3160
|
init_otlp_sink();
|
|
3157
3161
|
|
|
3162
|
+
// libs/observability/src/prometheus/render.ts
|
|
3163
|
+
var METRIC_NAME_REGEX = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
|
|
3164
|
+
var LABEL_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3165
|
+
var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
3166
|
+
function escapeLabelValue(value) {
|
|
3167
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
3168
|
+
}
|
|
3169
|
+
function sortedLabelString(attributes) {
|
|
3170
|
+
const keys = Object.keys(attributes).filter((k) => LABEL_NAME_REGEX.test(k)).sort();
|
|
3171
|
+
if (keys.length === 0) return "";
|
|
3172
|
+
const pairs = keys.map((k) => `${k}="${escapeLabelValue(attributes[k] ?? "")}"`);
|
|
3173
|
+
return `{${pairs.join(",")}}`;
|
|
3174
|
+
}
|
|
3175
|
+
function formatFloat(value) {
|
|
3176
|
+
if (!Number.isFinite(value)) return "";
|
|
3177
|
+
if (Number.isInteger(value)) return String(value);
|
|
3178
|
+
return String(value);
|
|
3179
|
+
}
|
|
3180
|
+
function renderPrometheusExposition(counters, gauges = [], options = {}) {
|
|
3181
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3182
|
+
for (const entry of counters) {
|
|
3183
|
+
if (!METRIC_NAME_REGEX.test(entry.name)) continue;
|
|
3184
|
+
const valueStr = formatFloat(entry.count);
|
|
3185
|
+
if (valueStr === "") continue;
|
|
3186
|
+
const labels = sortedLabelString(entry.attributes ?? {});
|
|
3187
|
+
const line = `${entry.name}${labels} ${valueStr}`;
|
|
3188
|
+
let group = byName.get(entry.name);
|
|
3189
|
+
if (!group) {
|
|
3190
|
+
group = {
|
|
3191
|
+
type: "counter",
|
|
3192
|
+
help: options.counterHelp?.[entry.name],
|
|
3193
|
+
lines: []
|
|
3194
|
+
};
|
|
3195
|
+
byName.set(entry.name, group);
|
|
3196
|
+
}
|
|
3197
|
+
group.lines.push(line);
|
|
3198
|
+
}
|
|
3199
|
+
for (const entry of gauges) {
|
|
3200
|
+
if (!METRIC_NAME_REGEX.test(entry.name)) continue;
|
|
3201
|
+
const valueStr = formatFloat(entry.value);
|
|
3202
|
+
if (valueStr === "") continue;
|
|
3203
|
+
const labels = sortedLabelString(entry.attributes ?? {});
|
|
3204
|
+
const line = `${entry.name}${labels} ${valueStr}`;
|
|
3205
|
+
let group = byName.get(entry.name);
|
|
3206
|
+
if (!group) {
|
|
3207
|
+
group = { type: "gauge", help: entry.help, lines: [] };
|
|
3208
|
+
byName.set(entry.name, group);
|
|
3209
|
+
} else if (entry.help && !group.help) {
|
|
3210
|
+
group.help = entry.help;
|
|
3211
|
+
}
|
|
3212
|
+
group.lines.push(line);
|
|
3213
|
+
}
|
|
3214
|
+
if (byName.size === 0) return "";
|
|
3215
|
+
const sortedNames = Array.from(byName.keys()).sort();
|
|
3216
|
+
const blocks = [];
|
|
3217
|
+
for (const name of sortedNames) {
|
|
3218
|
+
const group = byName.get(name);
|
|
3219
|
+
if (!group) continue;
|
|
3220
|
+
const block = [];
|
|
3221
|
+
if (group.help) {
|
|
3222
|
+
block.push(`# HELP ${name} ${group.help.replace(/\n/g, " ")}`);
|
|
3223
|
+
}
|
|
3224
|
+
block.push(`# TYPE ${name} ${group.type}`);
|
|
3225
|
+
block.push(...group.lines.slice().sort());
|
|
3226
|
+
blocks.push(block.join("\n"));
|
|
3227
|
+
}
|
|
3228
|
+
return blocks.join("\n") + "\n";
|
|
3229
|
+
}
|
|
3230
|
+
function renderJsonExposition(counters, gauges = []) {
|
|
3231
|
+
return {
|
|
3232
|
+
counters: counters.filter((c) => METRIC_NAME_REGEX.test(c.name) && Number.isFinite(c.count)).map((c) => ({ name: c.name, count: c.count, attributes: { ...c.attributes ?? {} } })),
|
|
3233
|
+
gauges: gauges.filter((g) => METRIC_NAME_REGEX.test(g.name) && Number.isFinite(g.value)).map((g) => ({
|
|
3234
|
+
name: g.name,
|
|
3235
|
+
value: g.value,
|
|
3236
|
+
attributes: { ...g.attributes ?? {} },
|
|
3237
|
+
...g.help ? { help: g.help } : {}
|
|
3238
|
+
}))
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// libs/observability/src/process-stats/process-stats.collector.ts
|
|
3243
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
3244
|
+
var NS_PER_SECOND = 1e9;
|
|
3245
|
+
var MICROS_PER_SECOND = 1e6;
|
|
3246
|
+
function defaultMonitorEventLoopDelay() {
|
|
3247
|
+
try {
|
|
3248
|
+
const { monitorEventLoopDelay } = require("node:perf_hooks");
|
|
3249
|
+
const histogram = monitorEventLoopDelay({ resolution: 10 });
|
|
3250
|
+
histogram.enable();
|
|
3251
|
+
return histogram;
|
|
3252
|
+
} catch {
|
|
3253
|
+
return void 0;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
function defaultGetActiveHandles() {
|
|
3257
|
+
const proc = process;
|
|
3258
|
+
if (typeof proc._getActiveHandles !== "function") return void 0;
|
|
3259
|
+
try {
|
|
3260
|
+
return proc._getActiveHandles();
|
|
3261
|
+
} catch {
|
|
3262
|
+
return void 0;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
function defaultGetActiveRequests() {
|
|
3266
|
+
const proc = process;
|
|
3267
|
+
if (typeof proc._getActiveRequests !== "function") return void 0;
|
|
3268
|
+
try {
|
|
3269
|
+
return proc._getActiveRequests();
|
|
3270
|
+
} catch {
|
|
3271
|
+
return void 0;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function defaultReadFdCount() {
|
|
3275
|
+
if (process.platform !== "linux") return void 0;
|
|
3276
|
+
try {
|
|
3277
|
+
return (0, import_utils2.readdirSync)("/proc/self/fd").length;
|
|
3278
|
+
} catch {
|
|
3279
|
+
return void 0;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
var ProcessStatsCollector = class {
|
|
3283
|
+
constructor(init = {}) {
|
|
3284
|
+
this.options = init.options ?? {};
|
|
3285
|
+
this.cpuUsage = init.cpuUsage ?? ((prev) => process.cpuUsage(prev));
|
|
3286
|
+
this.memoryUsage = init.memoryUsage ?? (() => process.memoryUsage());
|
|
3287
|
+
this.uptime = init.uptime ?? (() => process.uptime());
|
|
3288
|
+
this.getActiveHandles = init.getActiveHandles ?? defaultGetActiveHandles;
|
|
3289
|
+
this.getActiveRequests = init.getActiveRequests ?? defaultGetActiveRequests;
|
|
3290
|
+
this.readFdCount = init.readFdCount ?? defaultReadFdCount;
|
|
3291
|
+
this.cpuStart = this.cpuUsage();
|
|
3292
|
+
if (this.options.eventLoopLag !== false) {
|
|
3293
|
+
this.histogram = init.monitorEventLoopDelay ? init.monitorEventLoopDelay() : defaultMonitorEventLoopDelay();
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
collect() {
|
|
3297
|
+
const entries = [];
|
|
3298
|
+
const cpu = this.cpuUsage(this.cpuStart);
|
|
3299
|
+
entries.push({
|
|
3300
|
+
name: "frontmcp_process_cpu_seconds_total",
|
|
3301
|
+
value: cpu.user / MICROS_PER_SECOND,
|
|
3302
|
+
attributes: { mode: "user" },
|
|
3303
|
+
help: "CPU time consumed since collector start, by mode (seconds)"
|
|
3304
|
+
});
|
|
3305
|
+
entries.push({
|
|
3306
|
+
name: "frontmcp_process_cpu_seconds_total",
|
|
3307
|
+
value: cpu.system / MICROS_PER_SECOND,
|
|
3308
|
+
attributes: { mode: "system" }
|
|
3309
|
+
});
|
|
3310
|
+
const mem = this.memoryUsage();
|
|
3311
|
+
entries.push({
|
|
3312
|
+
name: "frontmcp_process_resident_memory_bytes",
|
|
3313
|
+
value: mem.rss,
|
|
3314
|
+
help: "Resident memory size in bytes"
|
|
3315
|
+
});
|
|
3316
|
+
entries.push({
|
|
3317
|
+
name: "frontmcp_process_heap_bytes",
|
|
3318
|
+
value: mem.heapTotal,
|
|
3319
|
+
help: "Total V8 heap size in bytes"
|
|
3320
|
+
});
|
|
3321
|
+
entries.push({
|
|
3322
|
+
name: "frontmcp_process_heap_used_bytes",
|
|
3323
|
+
value: mem.heapUsed,
|
|
3324
|
+
help: "Used V8 heap size in bytes"
|
|
3325
|
+
});
|
|
3326
|
+
entries.push({
|
|
3327
|
+
name: "frontmcp_process_external_bytes",
|
|
3328
|
+
value: mem.external,
|
|
3329
|
+
help: "Memory used by C++ objects bound to JS in bytes"
|
|
3330
|
+
});
|
|
3331
|
+
entries.push({
|
|
3332
|
+
name: "frontmcp_process_uptime_seconds",
|
|
3333
|
+
value: this.uptime(),
|
|
3334
|
+
help: "Time since process start in seconds"
|
|
3335
|
+
});
|
|
3336
|
+
if (this.options.eventLoopLag !== false && this.histogram) {
|
|
3337
|
+
const meanSeconds = this.histogram.mean / NS_PER_SECOND;
|
|
3338
|
+
const p99Seconds = this.histogram.percentile(99) / NS_PER_SECOND;
|
|
3339
|
+
if (Number.isFinite(meanSeconds)) {
|
|
3340
|
+
entries.push({
|
|
3341
|
+
name: "frontmcp_nodejs_eventloop_lag_seconds",
|
|
3342
|
+
value: meanSeconds,
|
|
3343
|
+
attributes: { quantile: "mean" },
|
|
3344
|
+
help: "Event-loop lag in seconds, sampled at 10ms resolution since the last scrape"
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
if (Number.isFinite(p99Seconds)) {
|
|
3348
|
+
entries.push({
|
|
3349
|
+
name: "frontmcp_nodejs_eventloop_lag_seconds",
|
|
3350
|
+
value: p99Seconds,
|
|
3351
|
+
attributes: { quantile: "p99" }
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
this.histogram.reset();
|
|
3355
|
+
}
|
|
3356
|
+
if (this.options.activeHandles !== false) {
|
|
3357
|
+
const handles = this.getActiveHandles?.();
|
|
3358
|
+
if (handles !== void 0) {
|
|
3359
|
+
entries.push({
|
|
3360
|
+
name: "frontmcp_nodejs_active_handles",
|
|
3361
|
+
value: handles.length,
|
|
3362
|
+
help: "Currently active libuv handles (sockets, timers, etc.)"
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
const requests = this.getActiveRequests?.();
|
|
3366
|
+
if (requests !== void 0) {
|
|
3367
|
+
entries.push({
|
|
3368
|
+
name: "frontmcp_nodejs_active_requests",
|
|
3369
|
+
value: requests.length,
|
|
3370
|
+
help: "Currently active libuv requests (file I/O, DNS, etc.)"
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (this.options.fdCount !== false) {
|
|
3375
|
+
const fdCount = this.readFdCount();
|
|
3376
|
+
if (typeof fdCount === "number") {
|
|
3377
|
+
entries.push({
|
|
3378
|
+
name: "frontmcp_nodejs_open_fds",
|
|
3379
|
+
value: fdCount,
|
|
3380
|
+
help: "Number of open file descriptors (Linux only)"
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
return entries;
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Release the perf_hooks histogram listener. Call when the collector is
|
|
3388
|
+
* no longer used (e.g., when reconfiguring the server).
|
|
3389
|
+
*/
|
|
3390
|
+
close() {
|
|
3391
|
+
this.histogram?.disable?.();
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
|
|
3158
3395
|
// libs/observability/src/testing/index.ts
|
|
3159
3396
|
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
3160
3397
|
function createTestTracer(name = "test") {
|
|
@@ -3214,8 +3451,10 @@ function findSpansByAttribute(spans, key, value) {
|
|
|
3214
3451
|
OTEL_TRACER,
|
|
3215
3452
|
ObservabilityPlugin,
|
|
3216
3453
|
OtlpSink,
|
|
3454
|
+
PROMETHEUS_CONTENT_TYPE,
|
|
3217
3455
|
PinoSink,
|
|
3218
3456
|
PrettySpanExporter,
|
|
3457
|
+
ProcessStatsCollector,
|
|
3219
3458
|
REQUEST_LOG_COLLECTOR,
|
|
3220
3459
|
RequestLogCollector,
|
|
3221
3460
|
RpcAttributes,
|
|
@@ -3248,6 +3487,8 @@ function findSpansByAttribute(spans, key, value) {
|
|
|
3248
3487
|
otelToFrontmcpContext,
|
|
3249
3488
|
recordHookEvent,
|
|
3250
3489
|
redactFields,
|
|
3490
|
+
renderJsonExposition,
|
|
3491
|
+
renderPrometheusExposition,
|
|
3251
3492
|
reportStartup,
|
|
3252
3493
|
resetCounterCacheForTesting,
|
|
3253
3494
|
resetMetricSnapshot,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/observability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation, structured JSON logging, and request log objects for FrontMCP",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@opentelemetry/api": "^1.9.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@frontmcp/sdk": "1.
|
|
55
|
-
"@frontmcp/utils": "1.
|
|
54
|
+
"@frontmcp/sdk": "1.3.0",
|
|
55
|
+
"@frontmcp/utils": "1.3.0",
|
|
56
56
|
"@opentelemetry/sdk-trace-base": "^1.25.0",
|
|
57
57
|
"@opentelemetry/sdk-node": "^0.52.0",
|
|
58
58
|
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ProcessStatsCollector } from './process-stats.collector';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-stats collector (issue #397).
|
|
3
|
+
*
|
|
4
|
+
* Emits per-scrape Node.js process metrics in `GaugeSnapshotEntry` shape so
|
|
5
|
+
* the Prometheus serializer can pass them straight through. Pure read of
|
|
6
|
+
* `process.*` and `perf_hooks.monitorEventLoopDelay()` — no I/O beyond the
|
|
7
|
+
* single Linux-only `/proc/self/fd` directory probe (wrapped in try/catch
|
|
8
|
+
* so non-Linux platforms silently skip that gauge).
|
|
9
|
+
*
|
|
10
|
+
* Design choices:
|
|
11
|
+
* - CPU is emitted as a monotonic counter-shaped gauge built from
|
|
12
|
+
* `process.cpuUsage()` deltas — operators graph `rate()` of this.
|
|
13
|
+
* - Event-loop lag uses `perf_hooks.monitorEventLoopDelay({ resolution: 10 })`
|
|
14
|
+
* started at construction; `collect()` reads mean + p99 and calls
|
|
15
|
+
* `reset()` so each scrape sees a fresh window.
|
|
16
|
+
* - `process._getActiveHandles()` / `_getActiveRequests()` are
|
|
17
|
+
* undocumented but stable since Node 14; guarded behind feature
|
|
18
|
+
* detection so they no-op on edge runtimes.
|
|
19
|
+
* - All probes are individually toggleable via `MetricsProcessOptions`.
|
|
20
|
+
*/
|
|
21
|
+
import type { GaugeSnapshotEntry } from '../prometheus/render';
|
|
22
|
+
interface MetricsProcessOptions {
|
|
23
|
+
eventLoopLag?: boolean;
|
|
24
|
+
fdCount?: boolean;
|
|
25
|
+
activeHandles?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface ELDHistogram {
|
|
28
|
+
mean: number;
|
|
29
|
+
percentile(p: number): number;
|
|
30
|
+
reset(): void;
|
|
31
|
+
disable?(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Optional dependency injection points used by tests; production callers
|
|
35
|
+
* never pass these.
|
|
36
|
+
*/
|
|
37
|
+
interface ProcessStatsCollectorOptions {
|
|
38
|
+
options?: MetricsProcessOptions;
|
|
39
|
+
cpuUsage?: (prev?: NodeJS.CpuUsage) => NodeJS.CpuUsage;
|
|
40
|
+
memoryUsage?: () => NodeJS.MemoryUsage;
|
|
41
|
+
uptime?: () => number;
|
|
42
|
+
monitorEventLoopDelay?: () => ELDHistogram | undefined;
|
|
43
|
+
getActiveHandles?: () => unknown[] | undefined;
|
|
44
|
+
getActiveRequests?: () => unknown[] | undefined;
|
|
45
|
+
readFdCount?: () => number | undefined;
|
|
46
|
+
}
|
|
47
|
+
export declare class ProcessStatsCollector {
|
|
48
|
+
private readonly options;
|
|
49
|
+
private readonly cpuUsage;
|
|
50
|
+
private readonly memoryUsage;
|
|
51
|
+
private readonly uptime;
|
|
52
|
+
private readonly histogram?;
|
|
53
|
+
private readonly getActiveHandles?;
|
|
54
|
+
private readonly getActiveRequests?;
|
|
55
|
+
private readonly readFdCount;
|
|
56
|
+
private cpuStart;
|
|
57
|
+
constructor(init?: ProcessStatsCollectorOptions);
|
|
58
|
+
collect(): GaugeSnapshotEntry[];
|
|
59
|
+
/**
|
|
60
|
+
* Release the perf_hooks histogram listener. Call when the collector is
|
|
61
|
+
* no longer used (e.g., when reconfiguring the server).
|
|
62
|
+
*/
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prometheus text-exposition serializer (zero-dep).
|
|
3
|
+
*
|
|
4
|
+
* Implements the Prometheus 0.0.4 text format documented at
|
|
5
|
+
* https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
|
|
6
|
+
* without pulling in `prom-client` — keeps `@frontmcp/observability`'s
|
|
7
|
+
* runtime footprint to `@opentelemetry/api` only (issue #397).
|
|
8
|
+
*
|
|
9
|
+
* Counter snapshot entries come from
|
|
10
|
+
* `@frontmcp/observability`'s in-memory store (`getMetricSnapshot()`); gauge
|
|
11
|
+
* entries come from the `ProcessStatsCollector` introduced alongside this
|
|
12
|
+
* serializer.
|
|
13
|
+
*/
|
|
14
|
+
import type { CounterSnapshotEntry } from '../telemetry/telemetry.counters';
|
|
15
|
+
/**
|
|
16
|
+
* A single gauge sample at a point in time. Values are emitted unchanged
|
|
17
|
+
* (no resampling, no rate calculation).
|
|
18
|
+
*/
|
|
19
|
+
export interface GaugeSnapshotEntry {
|
|
20
|
+
/** Metric name (must match `/^[a-zA-Z_:][a-zA-Z0-9_:]*$/`). */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Sample value. NaN / non-finite values are dropped. */
|
|
23
|
+
value: number;
|
|
24
|
+
/** Optional bounded-cardinality labels. */
|
|
25
|
+
attributes?: Record<string, string>;
|
|
26
|
+
/** Optional `# HELP` line. */
|
|
27
|
+
help?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Optional rendering knobs.
|
|
31
|
+
*/
|
|
32
|
+
export interface RenderOptions {
|
|
33
|
+
/** Map of counter-name → `# HELP` text. Counter names not in the map omit the HELP line. */
|
|
34
|
+
counterHelp?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Content-Type emitted alongside the text format (canonical Prometheus
|
|
38
|
+
* 0.0.4 value — pinned so consumers can match exactly).
|
|
39
|
+
*/
|
|
40
|
+
export declare const PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
41
|
+
/**
|
|
42
|
+
* Render counter + gauge snapshots as Prometheus 0.0.4 text exposition.
|
|
43
|
+
*
|
|
44
|
+
* - Groups entries by metric name so `# HELP` / `# TYPE` lines emit once
|
|
45
|
+
* per metric.
|
|
46
|
+
* - Drops entries whose metric name fails Prometheus naming validation.
|
|
47
|
+
* - Sorts metric names alphabetically so output is byte-deterministic.
|
|
48
|
+
* - Empty input → empty string.
|
|
49
|
+
*/
|
|
50
|
+
export declare function renderPrometheusExposition(counters: readonly CounterSnapshotEntry[], gauges?: readonly GaugeSnapshotEntry[], options?: RenderOptions): string;
|
|
51
|
+
/**
|
|
52
|
+
* Render counter + gauge snapshots as a JSON envelope — useful when
|
|
53
|
+
* a consumer prefers JSON over Prometheus text parsing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function renderJsonExposition(counters: readonly CounterSnapshotEntry[], gauges?: readonly GaugeSnapshotEntry[]): {
|
|
56
|
+
counters: CounterSnapshotEntry[];
|
|
57
|
+
gauges: GaugeSnapshotEntry[];
|
|
58
|
+
};
|