@fulmenhq/tsfulmen 0.3.2 → 0.4.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/CHANGELOG.md +65 -1
- package/README.md +30 -1
- package/config/crucible-ts/taxonomy/metrics.yaml +1 -1
- package/dist/appidentity/index.d.ts +15 -5
- package/dist/appidentity/index.js +9 -2204
- package/dist/appidentity/index.js.map +1 -1
- package/dist/assets/index.d.ts +179 -0
- package/dist/assets/index.js +4 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/bin/prometheus-cli.js +15 -9134
- package/dist/bin/prometheus-cli.js.map +1 -1
- package/dist/bin/schema-cli.js +4 -6098
- package/dist/bin/schema-cli.js.map +1 -1
- package/dist/bin/signals-cli.js +15 -1910
- package/dist/bin/signals-cli.js.map +1 -1
- package/dist/chunk-3HISR2XI.js +36 -0
- package/dist/chunk-3HISR2XI.js.map +1 -0
- package/dist/chunk-5HZEJGZ5.js +725 -0
- package/dist/chunk-5HZEJGZ5.js.map +1 -0
- package/dist/chunk-7SVYOPJ3.js +9412 -0
- package/dist/chunk-7SVYOPJ3.js.map +1 -0
- package/dist/chunk-BCO3FUUO.js +682 -0
- package/dist/chunk-BCO3FUUO.js.map +1 -0
- package/dist/chunk-BJDL6JEX.js +208 -0
- package/dist/chunk-BJDL6JEX.js.map +1 -0
- package/dist/chunk-BTUS6PZZ.js +302 -0
- package/dist/chunk-BTUS6PZZ.js.map +1 -0
- package/dist/chunk-CW4CSNVF.js +410 -0
- package/dist/chunk-CW4CSNVF.js.map +1 -0
- package/dist/chunk-HIBZ2AY7.js +194 -0
- package/dist/chunk-HIBZ2AY7.js.map +1 -0
- package/dist/chunk-HS4F3BRR.js +985 -0
- package/dist/chunk-HS4F3BRR.js.map +1 -0
- package/dist/chunk-HY6JQOFN.js +32 -0
- package/dist/chunk-HY6JQOFN.js.map +1 -0
- package/dist/chunk-K2FYT43R.js +837 -0
- package/dist/chunk-K2FYT43R.js.map +1 -0
- package/dist/chunk-K7BMFGQP.js +43 -0
- package/dist/chunk-K7BMFGQP.js.map +1 -0
- package/dist/chunk-LWAH2UG7.js +103 -0
- package/dist/chunk-LWAH2UG7.js.map +1 -0
- package/dist/chunk-OHE2AOH4.js +273 -0
- package/dist/chunk-OHE2AOH4.js.map +1 -0
- package/dist/chunk-P3DNOZOG.js +1488 -0
- package/dist/chunk-P3DNOZOG.js.map +1 -0
- package/dist/chunk-PQ6TKPUN.js +27 -0
- package/dist/chunk-PQ6TKPUN.js.map +1 -0
- package/dist/chunk-QCCN6POA.js +211 -0
- package/dist/chunk-QCCN6POA.js.map +1 -0
- package/dist/chunk-RSLFR4TO.js +631 -0
- package/dist/chunk-RSLFR4TO.js.map +1 -0
- package/dist/chunk-UP2VWCW5.js +33 -0
- package/dist/chunk-UP2VWCW5.js.map +1 -0
- package/dist/chunk-W6TQFTTY.js +130 -0
- package/dist/chunk-W6TQFTTY.js.map +1 -0
- package/dist/chunk-WSMVLWLB.js +269 -0
- package/dist/chunk-WSMVLWLB.js.map +1 -0
- package/dist/config/index.d.ts +69 -11
- package/dist/config/index.js +24 -1227
- package/dist/config/index.js.map +1 -1
- package/dist/crucible/fulpack/index.js +2 -25
- package/dist/crucible/fulpack/index.js.map +1 -1
- package/dist/crucible/index.js +13 -1129
- package/dist/crucible/index.js.map +1 -1
- package/dist/docscribe/index.js +2 -680
- package/dist/docscribe/index.js.map +1 -1
- package/dist/errors/index.js +5 -1831
- package/dist/errors/index.js.map +1 -1
- package/dist/errors-D7KXV3QW.js +4 -0
- package/dist/errors-D7KXV3QW.js.map +1 -0
- package/dist/errors-RBJX6ETJ.js +8 -0
- package/dist/errors-RBJX6ETJ.js.map +1 -0
- package/dist/export-BWRETGYS.js +6 -0
- package/dist/export-BWRETGYS.js.map +1 -0
- package/dist/foundry/index.d.ts +1 -1
- package/dist/foundry/index.js +340 -3949
- package/dist/foundry/index.js.map +1 -1
- package/dist/foundry/similarity/index.js +3 -134
- package/dist/foundry/similarity/index.js.map +1 -1
- package/dist/fulencode/index.js +3 -319
- package/dist/fulencode/index.js.map +1 -1
- package/dist/fulhash/index.js +3 -611
- package/dist/fulhash/index.js.map +1 -1
- package/dist/fulpack/index.js +3 -1515
- package/dist/fulpack/index.js.map +1 -1
- package/dist/hash-YOJYVOWH.js +4 -0
- package/dist/hash-YOJYVOWH.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -4390
- package/dist/index.js.map +1 -1
- package/dist/loader-H5K7HSO5.js +9 -0
- package/dist/loader-H5K7HSO5.js.map +1 -0
- package/dist/logging/index.js +2 -723
- package/dist/logging/index.js.map +1 -1
- package/dist/pathfinder/index.js +18 -2341
- package/dist/pathfinder/index.js.map +1 -1
- package/dist/prom-client-PNEBGD3G.js +4801 -0
- package/dist/prom-client-PNEBGD3G.js.map +1 -0
- package/dist/reports/license-inventory.csv +1 -1
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema/index.js +4 -6148
- package/dist/schema/index.js.map +1 -1
- package/dist/signals/index.d.ts +1 -1
- package/dist/signals/index.js +8 -2767
- package/dist/signals/index.js.map +1 -1
- package/dist/similarity/index.js +3 -134
- package/dist/similarity/index.js.map +1 -1
- package/dist/telemetry/http/index.js +9 -930
- package/dist/telemetry/http/index.js.map +1 -1
- package/dist/telemetry/index.js +4 -1561
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/prometheus/index.js +7 -8417
- package/dist/telemetry/prometheus/index.js.map +1 -1
- package/dist/{types-Dv5TERCM.d.ts → types-CHDvDRCf.d.ts} +17 -1
- package/dist/validator-U4CKD2Y3.js +6 -0
- package/dist/validator-U4CKD2Y3.js.map +1 -0
- package/package.json +5 -1
- package/schemas/crucible-ts/library/module-manifest/v1.0.0/module-manifest.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/definitions.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/log-event.schema.json +13 -13
- package/schemas/crucible-ts/observability/logging/v1.0.0/logger-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/logging-policy.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/middleware-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/severity-filter.schema.json +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/PROVENANCE.md +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/catalog/classifiers/README.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/classifiers-framework.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/data-sensitivity-classification.md +1 -1
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { createLogger } from './chunk-5HZEJGZ5.js';
|
|
2
|
+
import { FulmenError } from './chunk-CW4CSNVF.js';
|
|
3
|
+
import { metrics, isHistogramSummary } from './chunk-7SVYOPJ3.js';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
|
|
6
|
+
// src/telemetry/prometheus/constants.ts
|
|
7
|
+
var ADR0007_BUCKETS_MS = [1, 5, 10, 50, 100, 500, 1e3, 5e3, 1e4];
|
|
8
|
+
var ADR0007_BUCKETS_SECONDS = [
|
|
9
|
+
1e-3,
|
|
10
|
+
5e-3,
|
|
11
|
+
0.01,
|
|
12
|
+
0.05,
|
|
13
|
+
0.1,
|
|
14
|
+
0.5,
|
|
15
|
+
1,
|
|
16
|
+
5,
|
|
17
|
+
10
|
|
18
|
+
];
|
|
19
|
+
function msToSeconds(ms) {
|
|
20
|
+
return ms / 1e3;
|
|
21
|
+
}
|
|
22
|
+
var PROMETHEUS_EXPORTER_METRICS = {
|
|
23
|
+
// Refresh metrics
|
|
24
|
+
REFRESH_DURATION_SECONDS: "prometheus_exporter_refresh_duration_seconds",
|
|
25
|
+
REFRESH_TOTAL: "prometheus_exporter_refresh_total",
|
|
26
|
+
REFRESH_ERRORS_TOTAL: "prometheus_exporter_refresh_errors_total",
|
|
27
|
+
REFRESH_INFLIGHT: "prometheus_exporter_refresh_inflight",
|
|
28
|
+
// HTTP metrics
|
|
29
|
+
HTTP_REQUESTS_TOTAL: "prometheus_exporter_http_requests_total",
|
|
30
|
+
HTTP_ERRORS_TOTAL: "prometheus_exporter_http_errors_total",
|
|
31
|
+
// Restart metrics
|
|
32
|
+
RESTARTS_TOTAL: "prometheus_exporter_restarts_total"
|
|
33
|
+
};
|
|
34
|
+
var FOUNDRY_METRICS = {
|
|
35
|
+
// MIME detection counters
|
|
36
|
+
MIME_DETECTIONS_JSON: "foundry_mime_detections_total_json",
|
|
37
|
+
MIME_DETECTIONS_XML: "foundry_mime_detections_total_xml",
|
|
38
|
+
MIME_DETECTIONS_YAML: "foundry_mime_detections_total_yaml",
|
|
39
|
+
MIME_DETECTIONS_CSV: "foundry_mime_detections_total_csv",
|
|
40
|
+
MIME_DETECTIONS_PLAIN_TEXT: "foundry_mime_detections_total_plain_text",
|
|
41
|
+
MIME_DETECTIONS_UNKNOWN: "foundry_mime_detections_total_unknown",
|
|
42
|
+
// MIME detection timing histograms
|
|
43
|
+
MIME_DETECTION_MS_JSON: "foundry_mime_detection_ms_json",
|
|
44
|
+
MIME_DETECTION_MS_XML: "foundry_mime_detection_ms_xml",
|
|
45
|
+
MIME_DETECTION_MS_YAML: "foundry_mime_detection_ms_yaml",
|
|
46
|
+
MIME_DETECTION_MS_CSV: "foundry_mime_detection_ms_csv",
|
|
47
|
+
MIME_DETECTION_MS_PLAIN_TEXT: "foundry_mime_detection_ms_plain_text",
|
|
48
|
+
MIME_DETECTION_MS_UNKNOWN: "foundry_mime_detection_ms_unknown",
|
|
49
|
+
// General lookup counter
|
|
50
|
+
LOOKUP_COUNT: "foundry_lookup_count"
|
|
51
|
+
};
|
|
52
|
+
var ERROR_HANDLING_METRICS = {
|
|
53
|
+
WRAPS_TOTAL: "error_handling_wraps_total",
|
|
54
|
+
WRAP_MS: "error_handling_wrap_ms"
|
|
55
|
+
};
|
|
56
|
+
var FULHASH_METRICS = {
|
|
57
|
+
// Algorithm-specific counters
|
|
58
|
+
OPERATIONS_XXH3_128: "fulhash_operations_total_xxh3_128",
|
|
59
|
+
OPERATIONS_SHA256: "fulhash_operations_total_sha256",
|
|
60
|
+
// General counters
|
|
61
|
+
HASH_STRING_TOTAL: "fulhash_hash_string_total",
|
|
62
|
+
BYTES_HASHED_TOTAL: "fulhash_bytes_hashed_total",
|
|
63
|
+
// Timing histogram
|
|
64
|
+
OPERATION_MS: "fulhash_operation_ms"
|
|
65
|
+
};
|
|
66
|
+
var EXPORTER_LABELS = {
|
|
67
|
+
// Refresh phases
|
|
68
|
+
PHASE_COLLECT: "collect",
|
|
69
|
+
PHASE_CONVERT: "convert",
|
|
70
|
+
PHASE_EXPORT: "export",
|
|
71
|
+
// Results
|
|
72
|
+
RESULT_SUCCESS: "success",
|
|
73
|
+
RESULT_ERROR: "error",
|
|
74
|
+
// Error types
|
|
75
|
+
ERROR_VALIDATION: "validation",
|
|
76
|
+
ERROR_IO: "io",
|
|
77
|
+
ERROR_TIMEOUT: "timeout",
|
|
78
|
+
ERROR_OTHER: "other",
|
|
79
|
+
// Restart reasons
|
|
80
|
+
REASON_CONFIG_CHANGE: "config_change",
|
|
81
|
+
REASON_ERROR: "error",
|
|
82
|
+
REASON_MANUAL: "manual",
|
|
83
|
+
REASON_OTHER: "other"
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/telemetry/prometheus/errors.ts
|
|
87
|
+
var PrometheusExporterError = class extends FulmenError {
|
|
88
|
+
constructor(message, options) {
|
|
89
|
+
super({
|
|
90
|
+
code: options?.code || "PROMETHEUS_EXPORTER_ERROR",
|
|
91
|
+
message,
|
|
92
|
+
severity: "medium",
|
|
93
|
+
context: options?.context
|
|
94
|
+
});
|
|
95
|
+
if (options?.cause) {
|
|
96
|
+
this.cause = options.cause;
|
|
97
|
+
}
|
|
98
|
+
this.name = "PrometheusExporterError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var PromClientNotFoundError = class extends PrometheusExporterError {
|
|
102
|
+
constructor(cause) {
|
|
103
|
+
super(
|
|
104
|
+
"prom-client peer dependency not found. Install with: bun add prom-client\nFor npm: npm install prom-client\nFor more information, see: https://github.com/siimon/prom-client",
|
|
105
|
+
{
|
|
106
|
+
code: "PROM_CLIENT_NOT_FOUND",
|
|
107
|
+
cause,
|
|
108
|
+
context: {
|
|
109
|
+
suggestion: 'Run "bun add prom-client" to install the required peer dependency'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
this.name = "PromClientNotFoundError";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var InvalidMetricNameError = class extends PrometheusExporterError {
|
|
117
|
+
constructor(metricName, reason) {
|
|
118
|
+
super(`Invalid Prometheus metric name: "${metricName}". ${reason}`, {
|
|
119
|
+
code: "INVALID_METRIC_NAME",
|
|
120
|
+
context: {
|
|
121
|
+
metricName,
|
|
122
|
+
reason,
|
|
123
|
+
suggestion: "Prometheus metric names must match: [a-zA-Z_:][a-zA-Z0-9_:]*\nSee: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels"
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
this.name = "InvalidMetricNameError";
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var InvalidLabelNameError = class extends PrometheusExporterError {
|
|
130
|
+
constructor(labelName, reason) {
|
|
131
|
+
super(`Invalid Prometheus label name: "${labelName}". ${reason}`, {
|
|
132
|
+
code: "INVALID_LABEL_NAME",
|
|
133
|
+
context: {
|
|
134
|
+
labelName,
|
|
135
|
+
reason,
|
|
136
|
+
suggestion: "Prometheus label names must match: [a-zA-Z_][a-zA-Z0-9_]*\nSee: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels"
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.name = "InvalidLabelNameError";
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var MetricRegistrationError = class extends PrometheusExporterError {
|
|
143
|
+
constructor(metricName, cause) {
|
|
144
|
+
super(`Failed to register Prometheus metric: "${metricName}"`, {
|
|
145
|
+
code: "METRIC_REGISTRATION_FAILED",
|
|
146
|
+
cause,
|
|
147
|
+
context: {
|
|
148
|
+
metricName,
|
|
149
|
+
suggestion: "Check for duplicate metric names or incompatible collector types"
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
this.name = "MetricRegistrationError";
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var RefreshError = class extends PrometheusExporterError {
|
|
156
|
+
constructor(cause, context) {
|
|
157
|
+
super("Failed to refresh Prometheus metrics from TelemetryRegistry", {
|
|
158
|
+
code: "REFRESH_FAILED",
|
|
159
|
+
cause,
|
|
160
|
+
context
|
|
161
|
+
});
|
|
162
|
+
this.name = "RefreshError";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/telemetry/prometheus/exporter.ts
|
|
167
|
+
var PrometheusExporter = class {
|
|
168
|
+
telemetryRegistry;
|
|
169
|
+
namespace;
|
|
170
|
+
subsystem;
|
|
171
|
+
defaultLabels;
|
|
172
|
+
helpText;
|
|
173
|
+
metricsEnabled;
|
|
174
|
+
recordClientLabel;
|
|
175
|
+
moduleMetricsEnabled;
|
|
176
|
+
// prom-client types unavailable at compile time (optional peer dependency)
|
|
177
|
+
// Using 'any' with runtime validation - see file header comment for rationale
|
|
178
|
+
// biome-ignore lint/suspicious/noExplicitAny: prom-client is optional peer dependency
|
|
179
|
+
promClient = null;
|
|
180
|
+
// biome-ignore lint/suspicious/noExplicitAny: prom-client Registry type unavailable at compile time
|
|
181
|
+
promRegistry = null;
|
|
182
|
+
// biome-ignore lint/suspicious/noExplicitAny: prom-client Counter type unavailable at compile time
|
|
183
|
+
counters = /* @__PURE__ */ new Map();
|
|
184
|
+
// biome-ignore lint/suspicious/noExplicitAny: prom-client Gauge type unavailable at compile time
|
|
185
|
+
gauges = /* @__PURE__ */ new Map();
|
|
186
|
+
// biome-ignore lint/suspicious/noExplicitAny: prom-client Histogram type unavailable at compile time
|
|
187
|
+
histograms = /* @__PURE__ */ new Map();
|
|
188
|
+
// Statistics
|
|
189
|
+
refreshCount = 0;
|
|
190
|
+
errorCount = 0;
|
|
191
|
+
lastRefreshTime = null;
|
|
192
|
+
// Background refresh loop
|
|
193
|
+
refreshInterval = null;
|
|
194
|
+
refreshPromise = null;
|
|
195
|
+
isRefreshing = false;
|
|
196
|
+
/**
|
|
197
|
+
* Create Prometheus exporter
|
|
198
|
+
*
|
|
199
|
+
* @param options - Exporter configuration
|
|
200
|
+
* @throws {InvalidLabelNameError} If default label names are invalid
|
|
201
|
+
*/
|
|
202
|
+
constructor(options = {}) {
|
|
203
|
+
this.telemetryRegistry = options.registry || metrics;
|
|
204
|
+
this.defaultLabels = options.defaultLabels || {};
|
|
205
|
+
this.helpText = options.helpText || {};
|
|
206
|
+
this.metricsEnabled = options.metricsEnabled ?? true;
|
|
207
|
+
this.recordClientLabel = options.recordClientLabel ?? false;
|
|
208
|
+
this.moduleMetricsEnabled = options.moduleMetricsEnabled ?? true;
|
|
209
|
+
for (const labelName of Object.keys(this.defaultLabels)) {
|
|
210
|
+
this.validateLabelName(labelName);
|
|
211
|
+
}
|
|
212
|
+
this.namespace = options.namespace || "tsfulmen";
|
|
213
|
+
this.subsystem = options.subsystem || "app";
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Initialize exporter (async setup)
|
|
217
|
+
*
|
|
218
|
+
* Loads prom-client and optionally loads App Identity for namespace/subsystem.
|
|
219
|
+
* Called automatically on first refresh() or getMetrics() call.
|
|
220
|
+
*
|
|
221
|
+
* @throws {PromClientNotFoundError} If prom-client not installed
|
|
222
|
+
*/
|
|
223
|
+
async init() {
|
|
224
|
+
if (this.promClient) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
this.promClient = await import('./prom-client-PNEBGD3G.js');
|
|
229
|
+
this.promRegistry = new this.promClient.Registry();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
throw new PromClientNotFoundError(err);
|
|
232
|
+
}
|
|
233
|
+
if (this.namespace === "tsfulmen" || this.subsystem === "app") {
|
|
234
|
+
try {
|
|
235
|
+
const { loadIdentity } = await import('./appidentity/index.js');
|
|
236
|
+
const identity = await loadIdentity({ skipValidation: true });
|
|
237
|
+
if (this.namespace === "tsfulmen" && identity.app.vendor) {
|
|
238
|
+
this.namespace = identity.app.vendor;
|
|
239
|
+
}
|
|
240
|
+
if (this.subsystem === "app" && identity.app.binary_name) {
|
|
241
|
+
this.subsystem = identity.app.binary_name;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Safely emit instrumentation metric (silently fails if registry doesn't support it)
|
|
249
|
+
* Used to avoid breaking tests that use mock registries
|
|
250
|
+
*/
|
|
251
|
+
safeInstrument(fn) {
|
|
252
|
+
try {
|
|
253
|
+
fn();
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Refresh metrics from TelemetryRegistry
|
|
259
|
+
*
|
|
260
|
+
* Exports current metrics from TelemetryRegistry and updates Prometheus collectors.
|
|
261
|
+
* Emits instrumentation metrics per Crucible v0.2.7 taxonomy.
|
|
262
|
+
*
|
|
263
|
+
* @throws {RefreshError} If refresh operation fails. Inspect `error.cause` for the
|
|
264
|
+
* underlying error (InvalidMetricNameError, MetricRegistrationError, etc.) to handle
|
|
265
|
+
* specific failure conditions.
|
|
266
|
+
*/
|
|
267
|
+
async refresh() {
|
|
268
|
+
const startTime = performance.now();
|
|
269
|
+
this.safeInstrument(() => {
|
|
270
|
+
this.telemetryRegistry.gauge(PROMETHEUS_EXPORTER_METRICS.REFRESH_INFLIGHT).set(1);
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
await this.init();
|
|
274
|
+
const events = await this.telemetryRegistry.export();
|
|
275
|
+
for (const event of events) {
|
|
276
|
+
this.updateCollector(event);
|
|
277
|
+
}
|
|
278
|
+
this.refreshCount++;
|
|
279
|
+
this.lastRefreshTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
280
|
+
const durationMs = performance.now() - startTime;
|
|
281
|
+
this.safeInstrument(() => {
|
|
282
|
+
this.telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.REFRESH_TOTAL).inc(1, { result: EXPORTER_LABELS.RESULT_SUCCESS });
|
|
283
|
+
this.telemetryRegistry.histogram(PROMETHEUS_EXPORTER_METRICS.REFRESH_DURATION_SECONDS, {
|
|
284
|
+
buckets: [...ADR0007_BUCKETS_SECONDS]
|
|
285
|
+
}).observe(msToSeconds(durationMs), {
|
|
286
|
+
phase: EXPORTER_LABELS.PHASE_EXPORT,
|
|
287
|
+
result: EXPORTER_LABELS.RESULT_SUCCESS
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
this.errorCount++;
|
|
292
|
+
const durationMs = performance.now() - startTime;
|
|
293
|
+
let errorType = EXPORTER_LABELS.ERROR_OTHER;
|
|
294
|
+
if (err instanceof InvalidMetricNameError || err instanceof InvalidLabelNameError) {
|
|
295
|
+
errorType = EXPORTER_LABELS.ERROR_VALIDATION;
|
|
296
|
+
} else if (err instanceof MetricRegistrationError) {
|
|
297
|
+
errorType = EXPORTER_LABELS.ERROR_OTHER;
|
|
298
|
+
}
|
|
299
|
+
this.safeInstrument(() => {
|
|
300
|
+
this.telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.REFRESH_TOTAL).inc(1, { result: EXPORTER_LABELS.RESULT_ERROR });
|
|
301
|
+
this.telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.REFRESH_ERRORS_TOTAL).inc(1, { error_type: errorType });
|
|
302
|
+
this.telemetryRegistry.histogram(PROMETHEUS_EXPORTER_METRICS.REFRESH_DURATION_SECONDS, {
|
|
303
|
+
buckets: [...ADR0007_BUCKETS_SECONDS]
|
|
304
|
+
}).observe(msToSeconds(durationMs), {
|
|
305
|
+
phase: EXPORTER_LABELS.PHASE_EXPORT,
|
|
306
|
+
result: EXPORTER_LABELS.RESULT_ERROR
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
throw new RefreshError(err, {
|
|
310
|
+
refreshCount: this.refreshCount,
|
|
311
|
+
errorCount: this.errorCount
|
|
312
|
+
});
|
|
313
|
+
} finally {
|
|
314
|
+
this.safeInstrument(() => {
|
|
315
|
+
this.telemetryRegistry.gauge(PROMETHEUS_EXPORTER_METRICS.REFRESH_INFLIGHT).set(0);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get Prometheus metrics in text format
|
|
321
|
+
*
|
|
322
|
+
* Returns metrics in Prometheus exposition text format.
|
|
323
|
+
*
|
|
324
|
+
* @returns Prometheus text format string
|
|
325
|
+
*/
|
|
326
|
+
async getMetrics() {
|
|
327
|
+
await this.init();
|
|
328
|
+
if (!this.promRegistry) {
|
|
329
|
+
throw new Error("Prometheus registry not initialized");
|
|
330
|
+
}
|
|
331
|
+
return this.promRegistry.metrics();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Get exporter statistics
|
|
335
|
+
*/
|
|
336
|
+
getStats() {
|
|
337
|
+
return {
|
|
338
|
+
refreshCount: this.refreshCount,
|
|
339
|
+
errorCount: this.errorCount,
|
|
340
|
+
lastRefreshTime: this.lastRefreshTime,
|
|
341
|
+
metricsCount: this.counters.size + this.gauges.size + this.histograms.size,
|
|
342
|
+
isRefreshing: this.isRefreshing
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get current metrics configuration
|
|
347
|
+
*
|
|
348
|
+
* Returns effective configuration being used by exporter,
|
|
349
|
+
* including defaults for any unspecified options.
|
|
350
|
+
*
|
|
351
|
+
* @returns Current metrics configuration
|
|
352
|
+
*/
|
|
353
|
+
getMetricsConfig() {
|
|
354
|
+
return {
|
|
355
|
+
metricsEnabled: this.metricsEnabled,
|
|
356
|
+
recordClientLabel: this.recordClientLabel,
|
|
357
|
+
moduleMetricsEnabled: this.moduleMetricsEnabled,
|
|
358
|
+
namespace: this.namespace,
|
|
359
|
+
subsystem: this.subsystem,
|
|
360
|
+
defaultLabels: { ...this.defaultLabels },
|
|
361
|
+
helpText: { ...this.helpText }
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get underlying prom-client Registry
|
|
366
|
+
*
|
|
367
|
+
* Returns null if not yet initialized.
|
|
368
|
+
* Use for advanced prom-client operations.
|
|
369
|
+
*/
|
|
370
|
+
// biome-ignore lint/suspicious/noExplicitAny: Returns prom-client Registry type (optional peer dependency)
|
|
371
|
+
getRegistry() {
|
|
372
|
+
return this.promRegistry;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get telemetry registry for self-instrumentation
|
|
376
|
+
*
|
|
377
|
+
* Returns the MetricsRegistry instance used for exporter self-instrumentation.
|
|
378
|
+
* Useful for HTTP handler instrumentation.
|
|
379
|
+
*
|
|
380
|
+
* @internal
|
|
381
|
+
*/
|
|
382
|
+
getTelemetryRegistry() {
|
|
383
|
+
return this.telemetryRegistry;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Emit restart metric
|
|
387
|
+
*
|
|
388
|
+
* Records a restart event for tracking exporter or server restarts.
|
|
389
|
+
* Use this when restarting the HTTP metrics server or other exporter components.
|
|
390
|
+
*
|
|
391
|
+
* @param reason - Reason for restart (config_change, error, manual, other)
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```typescript
|
|
395
|
+
* // Restart server after config change
|
|
396
|
+
* await stopMetricsServer(server);
|
|
397
|
+
* exporter.recordRestart('config_change');
|
|
398
|
+
* const newServer = await startMetricsServer(exporter, newConfig);
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
recordRestart(reason) {
|
|
402
|
+
this.safeInstrument(() => {
|
|
403
|
+
this.telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.RESTARTS_TOTAL).inc(1, { reason });
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Reset all metrics
|
|
408
|
+
*/
|
|
409
|
+
reset() {
|
|
410
|
+
if (this.promRegistry) {
|
|
411
|
+
this.promRegistry.clear();
|
|
412
|
+
}
|
|
413
|
+
this.counters.clear();
|
|
414
|
+
this.gauges.clear();
|
|
415
|
+
this.histograms.clear();
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Start background refresh loop
|
|
419
|
+
*
|
|
420
|
+
* Automatically refreshes metrics from TelemetryRegistry at specified interval.
|
|
421
|
+
* Useful for long-running services where metrics should be continuously updated.
|
|
422
|
+
*
|
|
423
|
+
* @param options - Refresh configuration (interval, error handler)
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* const exporter = new PrometheusExporter({ registry: metrics });
|
|
428
|
+
*
|
|
429
|
+
* // Start refreshing every 15 seconds (default)
|
|
430
|
+
* exporter.startRefresh();
|
|
431
|
+
*
|
|
432
|
+
* // Custom interval and error handling
|
|
433
|
+
* exporter.startRefresh({
|
|
434
|
+
* intervalMs: 10000,
|
|
435
|
+
* onError: (err) => logger.error('Refresh failed:', err)
|
|
436
|
+
* });
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
startRefresh(options = {}) {
|
|
440
|
+
const intervalMs = options.intervalMs ?? 15e3;
|
|
441
|
+
!!this.refreshInterval;
|
|
442
|
+
if (this.refreshInterval) {
|
|
443
|
+
this.stopRefresh();
|
|
444
|
+
this.safeInstrument(() => {
|
|
445
|
+
this.telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.RESTARTS_TOTAL).inc(1, {
|
|
446
|
+
reason: options.restartReason || EXPORTER_LABELS.REASON_OTHER
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
this.refreshInterval = setInterval(() => {
|
|
451
|
+
if (this.refreshPromise) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.refreshPromise = this.refresh().catch((err) => {
|
|
455
|
+
if (options.onError) {
|
|
456
|
+
options.onError(err);
|
|
457
|
+
}
|
|
458
|
+
}).finally(() => {
|
|
459
|
+
this.refreshPromise = null;
|
|
460
|
+
});
|
|
461
|
+
}, intervalMs);
|
|
462
|
+
this.isRefreshing = true;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Stop background refresh loop
|
|
466
|
+
*
|
|
467
|
+
* Clears the refresh interval and performs one final refresh to capture
|
|
468
|
+
* the latest metrics state before stopping.
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```typescript
|
|
472
|
+
* // Graceful shutdown
|
|
473
|
+
* await exporter.stopRefresh();
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
async stopRefresh() {
|
|
477
|
+
if (this.refreshInterval) {
|
|
478
|
+
clearInterval(this.refreshInterval);
|
|
479
|
+
this.refreshInterval = null;
|
|
480
|
+
}
|
|
481
|
+
if (this.refreshPromise) {
|
|
482
|
+
await this.refreshPromise;
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
await this.refresh();
|
|
486
|
+
} catch (_err) {
|
|
487
|
+
}
|
|
488
|
+
this.isRefreshing = false;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Update Prometheus collector from MetricsEvent
|
|
492
|
+
*/
|
|
493
|
+
updateCollector(event) {
|
|
494
|
+
const metricName = this.formatMetricName(event.name);
|
|
495
|
+
const help = this.getHelpText(event.name);
|
|
496
|
+
const labelNames = Object.keys(this.defaultLabels);
|
|
497
|
+
if (isHistogramSummary(event.value)) {
|
|
498
|
+
this.updateHistogram(metricName, help, labelNames, event.value);
|
|
499
|
+
} else if (typeof event.value === "number") {
|
|
500
|
+
this.updateGauge(metricName, help, labelNames, event.value);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Update or create Prometheus Gauge
|
|
505
|
+
*/
|
|
506
|
+
updateGauge(name, help, labelNames, value) {
|
|
507
|
+
let gauge = this.gauges.get(name);
|
|
508
|
+
if (!gauge) {
|
|
509
|
+
try {
|
|
510
|
+
gauge = new this.promClient.Gauge({
|
|
511
|
+
name,
|
|
512
|
+
help,
|
|
513
|
+
labelNames,
|
|
514
|
+
registers: [this.promRegistry]
|
|
515
|
+
});
|
|
516
|
+
this.gauges.set(name, gauge);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
throw new MetricRegistrationError(name, err);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
gauge.labels(this.defaultLabels).set(value);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Update or create Prometheus Histogram
|
|
525
|
+
*
|
|
526
|
+
* CRITICAL: Converts OTLP cumulative buckets to Prometheus format.
|
|
527
|
+
* This is an approximation - see Phase 0 investigation for details.
|
|
528
|
+
*
|
|
529
|
+
* LIMITATION: Original observations cannot be perfectly reconstructed from bucket summary.
|
|
530
|
+
* Percentiles will be approximate based on bucket midpoints.
|
|
531
|
+
*/
|
|
532
|
+
// biome-ignore lint/suspicious/noExplicitAny: summary is HistogramSummary from TelemetryRegistry
|
|
533
|
+
updateHistogram(name, help, labelNames, summary) {
|
|
534
|
+
const buckets = summary.buckets.map((b) => b.le);
|
|
535
|
+
let histogram = this.histograms.get(name);
|
|
536
|
+
if (!histogram) {
|
|
537
|
+
try {
|
|
538
|
+
histogram = new this.promClient.Histogram({
|
|
539
|
+
name,
|
|
540
|
+
help,
|
|
541
|
+
labelNames,
|
|
542
|
+
buckets,
|
|
543
|
+
registers: [this.promRegistry]
|
|
544
|
+
});
|
|
545
|
+
this.histograms.set(name, histogram);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
throw new MetricRegistrationError(name, err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const observations = this.reconstructObservations(summary);
|
|
551
|
+
for (const value of observations) {
|
|
552
|
+
histogram.labels(this.defaultLabels).observe(value);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Reconstruct observations from histogram summary
|
|
557
|
+
*
|
|
558
|
+
* Converts OTLP cumulative bucket counts to approximate observation values.
|
|
559
|
+
* Uses bucket midpoints to generate synthetic observations.
|
|
560
|
+
*
|
|
561
|
+
* LIMITATION: This is an approximation. Original observation values cannot be
|
|
562
|
+
* perfectly reconstructed from bucket summaries. Percentiles will be approximate
|
|
563
|
+
* based on bucket boundaries. This is acceptable for v0.1.8 initial release.
|
|
564
|
+
*
|
|
565
|
+
* TODO: Future enhancement - capture raw observations in TelemetryRegistry
|
|
566
|
+
* for lossless Prometheus export. Track in Phase 2+ planning.
|
|
567
|
+
* See: .plans/active/v0.2.x-plans/prometheus-type-safety-enhancement.md
|
|
568
|
+
*
|
|
569
|
+
* Algorithm:
|
|
570
|
+
* 1. Calculate non-cumulative count for each bucket (delta between adjacent buckets)
|
|
571
|
+
* 2. For each bucket, place observations at the midpoint between bucket boundaries
|
|
572
|
+
* 3. Return array of synthetic observation values
|
|
573
|
+
*/
|
|
574
|
+
// biome-ignore lint/suspicious/noExplicitAny: summary is HistogramSummary from TelemetryRegistry
|
|
575
|
+
reconstructObservations(summary) {
|
|
576
|
+
const observations = [];
|
|
577
|
+
const buckets = summary.buckets;
|
|
578
|
+
if (!buckets || buckets.length === 0) {
|
|
579
|
+
return observations;
|
|
580
|
+
}
|
|
581
|
+
let prevCount = 0;
|
|
582
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
583
|
+
const bucket = buckets[i];
|
|
584
|
+
const count = bucket.count - prevCount;
|
|
585
|
+
if (count > 0) {
|
|
586
|
+
let value;
|
|
587
|
+
if (i === 0) {
|
|
588
|
+
value = bucket.le / 2;
|
|
589
|
+
} else {
|
|
590
|
+
const prevLe = buckets[i - 1].le;
|
|
591
|
+
value = (prevLe + bucket.le) / 2;
|
|
592
|
+
}
|
|
593
|
+
for (let j = 0; j < count; j++) {
|
|
594
|
+
observations.push(value);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
prevCount = bucket.count;
|
|
598
|
+
}
|
|
599
|
+
return observations;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Format metric name with namespace and subsystem prefix
|
|
603
|
+
*
|
|
604
|
+
* Format: {namespace}_{subsystem}_{metric}
|
|
605
|
+
* Example: fulmen_myapp_schema_validations
|
|
606
|
+
*/
|
|
607
|
+
formatMetricName(name) {
|
|
608
|
+
this.validateMetricName(name);
|
|
609
|
+
return `${this.namespace}_${this.subsystem}_${name}`;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Get help text for metric
|
|
613
|
+
*
|
|
614
|
+
* Uses custom help text if provided, otherwise generates default.
|
|
615
|
+
*/
|
|
616
|
+
getHelpText(name) {
|
|
617
|
+
if (this.helpText[name]) {
|
|
618
|
+
return this.helpText[name];
|
|
619
|
+
}
|
|
620
|
+
return `TSFulmen metric: ${name}`;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Validate Prometheus metric name
|
|
624
|
+
*
|
|
625
|
+
* Rules: Must start with [a-zA-Z_:] and contain only [a-zA-Z0-9_:]
|
|
626
|
+
*
|
|
627
|
+
* @throws {InvalidMetricNameError} If name is invalid
|
|
628
|
+
*/
|
|
629
|
+
validateMetricName(name) {
|
|
630
|
+
const validPattern = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
|
|
631
|
+
if (!validPattern.test(name)) {
|
|
632
|
+
throw new InvalidMetricNameError(
|
|
633
|
+
name,
|
|
634
|
+
"Metric names must start with [a-zA-Z_:] and contain only [a-zA-Z0-9_:]"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Validate Prometheus label name
|
|
640
|
+
*
|
|
641
|
+
* Rules: Must start with [a-zA-Z_] and contain only [a-zA-Z0-9_]
|
|
642
|
+
*
|
|
643
|
+
* @throws {InvalidLabelNameError} If name is invalid
|
|
644
|
+
*/
|
|
645
|
+
validateLabelName(name) {
|
|
646
|
+
const validPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
647
|
+
if (!validPattern.test(name)) {
|
|
648
|
+
throw new InvalidLabelNameError(
|
|
649
|
+
name,
|
|
650
|
+
"Label names must start with [a-zA-Z_] and contain only [a-zA-Z0-9_]"
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// src/telemetry/prometheus/lifecycle.ts
|
|
657
|
+
async function registerPrometheusShutdown(exporter, manager) {
|
|
658
|
+
const shutdownHandler = async () => {
|
|
659
|
+
await exporter.stopRefresh();
|
|
660
|
+
};
|
|
661
|
+
await manager.register("SIGTERM", shutdownHandler);
|
|
662
|
+
await manager.register("SIGINT", shutdownHandler);
|
|
663
|
+
}
|
|
664
|
+
function safeInstrumentHTTP(registry, fn) {
|
|
665
|
+
if (!registry) return;
|
|
666
|
+
try {
|
|
667
|
+
fn();
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function getExporterLogger(exporter) {
|
|
672
|
+
const exporterAny = exporter;
|
|
673
|
+
const logger = exporterAny.logger;
|
|
674
|
+
return exporterAny.metricsEnabled ? logger : null;
|
|
675
|
+
}
|
|
676
|
+
function extractRequestContext(req) {
|
|
677
|
+
return {
|
|
678
|
+
method: req.method,
|
|
679
|
+
url: req.url,
|
|
680
|
+
headers: req.headers,
|
|
681
|
+
remoteAddress: req.socket.remoteAddress
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function createMetricsHandler(exporter, options = {}) {
|
|
685
|
+
const path = options.path ?? "/metrics";
|
|
686
|
+
const refreshOnScrape = options.refreshOnScrape ?? false;
|
|
687
|
+
const logger = getExporterLogger(exporter);
|
|
688
|
+
return async (req, res) => {
|
|
689
|
+
const telemetryRegistry = exporter.getTelemetryRegistry();
|
|
690
|
+
let _statusCode = 200;
|
|
691
|
+
const startTime = performance.now();
|
|
692
|
+
try {
|
|
693
|
+
const context = extractRequestContext(req);
|
|
694
|
+
if (req.url !== path) {
|
|
695
|
+
_statusCode = 404;
|
|
696
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
697
|
+
res.end("Not Found");
|
|
698
|
+
safeInstrumentHTTP(telemetryRegistry, () => {
|
|
699
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_REQUESTS_TOTAL).inc(1, { status: "404", path: req.url || "unknown" });
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (options.authenticate) {
|
|
704
|
+
const authResult = await options.authenticate(context);
|
|
705
|
+
if (!authResult) {
|
|
706
|
+
_statusCode = 401;
|
|
707
|
+
res.writeHead(401, {
|
|
708
|
+
"Content-Type": "text/plain",
|
|
709
|
+
"WWW-Authenticate": "Bearer"
|
|
710
|
+
});
|
|
711
|
+
res.end("Unauthorized");
|
|
712
|
+
safeInstrumentHTTP(telemetryRegistry, () => {
|
|
713
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_REQUESTS_TOTAL).inc(1, { status: "401", path });
|
|
714
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_ERRORS_TOTAL).inc(1, { status: "401", path });
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (options.rateLimit) {
|
|
720
|
+
const rateLimitResult = await options.rateLimit(context);
|
|
721
|
+
if (!rateLimitResult) {
|
|
722
|
+
_statusCode = 429;
|
|
723
|
+
res.writeHead(429, {
|
|
724
|
+
"Content-Type": "text/plain",
|
|
725
|
+
"Retry-After": "60"
|
|
726
|
+
});
|
|
727
|
+
res.end("Too Many Requests");
|
|
728
|
+
safeInstrumentHTTP(telemetryRegistry, () => {
|
|
729
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_REQUESTS_TOTAL).inc(1, { status: "429", path });
|
|
730
|
+
});
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (refreshOnScrape) {
|
|
735
|
+
await exporter.refresh();
|
|
736
|
+
}
|
|
737
|
+
const metrics2 = await exporter.getMetrics();
|
|
738
|
+
_statusCode = 200;
|
|
739
|
+
res.writeHead(200, {
|
|
740
|
+
"Content-Type": "text/plain; version=0.0.4; charset=utf-8",
|
|
741
|
+
"Content-Length": Buffer.byteLength(metrics2, "utf-8")
|
|
742
|
+
});
|
|
743
|
+
res.end(metrics2);
|
|
744
|
+
safeInstrumentHTTP(telemetryRegistry, () => {
|
|
745
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_REQUESTS_TOTAL).inc(1, { status: "200", path });
|
|
746
|
+
});
|
|
747
|
+
} catch (err) {
|
|
748
|
+
const errorMessage = err instanceof Error ? err.message : "Internal Server Error";
|
|
749
|
+
_statusCode = 500;
|
|
750
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
751
|
+
res.end(`Error: ${errorMessage}`);
|
|
752
|
+
safeInstrumentHTTP(telemetryRegistry, () => {
|
|
753
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_REQUESTS_TOTAL).inc(1, { status: "500", path });
|
|
754
|
+
telemetryRegistry.counter(PROMETHEUS_EXPORTER_METRICS.HTTP_ERRORS_TOTAL).inc(1, { status: "500", path });
|
|
755
|
+
});
|
|
756
|
+
} finally {
|
|
757
|
+
if (logger) {
|
|
758
|
+
const durationMs = performance.now() - startTime;
|
|
759
|
+
const context = extractRequestContext(req);
|
|
760
|
+
logger.info("HTTP request processed", {
|
|
761
|
+
metric_name: PROMETHEUS_EXPORTER_METRICS.HTTP_REQUESTS_TOTAL,
|
|
762
|
+
status: _statusCode.toString(),
|
|
763
|
+
path: req.url || "unknown",
|
|
764
|
+
method: context.method || "GET",
|
|
765
|
+
duration_ms: Math.round(durationMs),
|
|
766
|
+
remote_address: context.remoteAddress
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
async function startMetricsServer(exporter, options = {}) {
|
|
773
|
+
const host = options.host ?? "127.0.0.1";
|
|
774
|
+
const port = options.port ?? 9464;
|
|
775
|
+
const path = options.path ?? "/metrics";
|
|
776
|
+
const handler = createMetricsHandler(exporter, options);
|
|
777
|
+
const server = createServer(handler);
|
|
778
|
+
const exporterLogger = getExporterLogger(exporter);
|
|
779
|
+
const logger = exporterLogger || createLogger({
|
|
780
|
+
service: "prometheus_exporter",
|
|
781
|
+
profile: "structured" /* STRUCTURED */
|
|
782
|
+
});
|
|
783
|
+
return new Promise((resolve, reject) => {
|
|
784
|
+
server.listen(port, host, () => {
|
|
785
|
+
logger.info("Prometheus metrics server started", {
|
|
786
|
+
host,
|
|
787
|
+
port,
|
|
788
|
+
path,
|
|
789
|
+
url: `http://${host}:${port}${path}`
|
|
790
|
+
});
|
|
791
|
+
resolve(server);
|
|
792
|
+
});
|
|
793
|
+
server.on("error", (err) => {
|
|
794
|
+
logger.error("Prometheus metrics server error", err, {
|
|
795
|
+
host,
|
|
796
|
+
port,
|
|
797
|
+
path
|
|
798
|
+
});
|
|
799
|
+
reject(err);
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
async function stopMetricsServer(server, timeoutMs = 5e3, exporter) {
|
|
804
|
+
const exporterLogger = exporter ? getExporterLogger(exporter) : null;
|
|
805
|
+
const logger = exporterLogger || createLogger({
|
|
806
|
+
service: "prometheus_exporter",
|
|
807
|
+
profile: "structured" /* STRUCTURED */
|
|
808
|
+
});
|
|
809
|
+
return new Promise((resolve, reject) => {
|
|
810
|
+
const timeout = setTimeout(() => {
|
|
811
|
+
logger.warn("Prometheus metrics server shutdown timed out, forcing close", {
|
|
812
|
+
timeout_ms: timeoutMs
|
|
813
|
+
});
|
|
814
|
+
server.closeAllConnections?.();
|
|
815
|
+
reject(new Error(`Server shutdown timeout after ${timeoutMs}ms`));
|
|
816
|
+
}, timeoutMs);
|
|
817
|
+
server.close((err) => {
|
|
818
|
+
clearTimeout(timeout);
|
|
819
|
+
if (err) {
|
|
820
|
+
logger.error("Error stopping Prometheus metrics server", err, {
|
|
821
|
+
timeout_ms: timeoutMs
|
|
822
|
+
});
|
|
823
|
+
reject(err);
|
|
824
|
+
} else {
|
|
825
|
+
logger.info("Prometheus metrics server stopped", {
|
|
826
|
+
timeout_ms: timeoutMs
|
|
827
|
+
});
|
|
828
|
+
resolve();
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
server.closeAllConnections?.();
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export { ADR0007_BUCKETS_MS, ADR0007_BUCKETS_SECONDS, ERROR_HANDLING_METRICS, EXPORTER_LABELS, FOUNDRY_METRICS, FULHASH_METRICS, InvalidLabelNameError, InvalidMetricNameError, MetricRegistrationError, PROMETHEUS_EXPORTER_METRICS, PromClientNotFoundError, PrometheusExporter, PrometheusExporterError, RefreshError, createMetricsHandler, msToSeconds, registerPrometheusShutdown, startMetricsServer, stopMetricsServer };
|
|
836
|
+
//# sourceMappingURL=chunk-K2FYT43R.js.map
|
|
837
|
+
//# sourceMappingURL=chunk-K2FYT43R.js.map
|