@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -1
  3. package/config/crucible-ts/taxonomy/metrics.yaml +1 -1
  4. package/dist/appidentity/index.d.ts +15 -5
  5. package/dist/appidentity/index.js +9 -2204
  6. package/dist/appidentity/index.js.map +1 -1
  7. package/dist/assets/index.d.ts +179 -0
  8. package/dist/assets/index.js +4 -0
  9. package/dist/assets/index.js.map +1 -0
  10. package/dist/bin/prometheus-cli.js +15 -9134
  11. package/dist/bin/prometheus-cli.js.map +1 -1
  12. package/dist/bin/schema-cli.js +4 -6098
  13. package/dist/bin/schema-cli.js.map +1 -1
  14. package/dist/bin/signals-cli.js +15 -1910
  15. package/dist/bin/signals-cli.js.map +1 -1
  16. package/dist/chunk-3HISR2XI.js +36 -0
  17. package/dist/chunk-3HISR2XI.js.map +1 -0
  18. package/dist/chunk-5HZEJGZ5.js +725 -0
  19. package/dist/chunk-5HZEJGZ5.js.map +1 -0
  20. package/dist/chunk-7SVYOPJ3.js +9412 -0
  21. package/dist/chunk-7SVYOPJ3.js.map +1 -0
  22. package/dist/chunk-BCO3FUUO.js +682 -0
  23. package/dist/chunk-BCO3FUUO.js.map +1 -0
  24. package/dist/chunk-BJDL6JEX.js +208 -0
  25. package/dist/chunk-BJDL6JEX.js.map +1 -0
  26. package/dist/chunk-BTUS6PZZ.js +302 -0
  27. package/dist/chunk-BTUS6PZZ.js.map +1 -0
  28. package/dist/chunk-CW4CSNVF.js +410 -0
  29. package/dist/chunk-CW4CSNVF.js.map +1 -0
  30. package/dist/chunk-HIBZ2AY7.js +194 -0
  31. package/dist/chunk-HIBZ2AY7.js.map +1 -0
  32. package/dist/chunk-HS4F3BRR.js +985 -0
  33. package/dist/chunk-HS4F3BRR.js.map +1 -0
  34. package/dist/chunk-HY6JQOFN.js +32 -0
  35. package/dist/chunk-HY6JQOFN.js.map +1 -0
  36. package/dist/chunk-K2FYT43R.js +837 -0
  37. package/dist/chunk-K2FYT43R.js.map +1 -0
  38. package/dist/chunk-K7BMFGQP.js +43 -0
  39. package/dist/chunk-K7BMFGQP.js.map +1 -0
  40. package/dist/chunk-LWAH2UG7.js +103 -0
  41. package/dist/chunk-LWAH2UG7.js.map +1 -0
  42. package/dist/chunk-OHE2AOH4.js +273 -0
  43. package/dist/chunk-OHE2AOH4.js.map +1 -0
  44. package/dist/chunk-P3DNOZOG.js +1488 -0
  45. package/dist/chunk-P3DNOZOG.js.map +1 -0
  46. package/dist/chunk-PQ6TKPUN.js +27 -0
  47. package/dist/chunk-PQ6TKPUN.js.map +1 -0
  48. package/dist/chunk-QCCN6POA.js +211 -0
  49. package/dist/chunk-QCCN6POA.js.map +1 -0
  50. package/dist/chunk-RSLFR4TO.js +631 -0
  51. package/dist/chunk-RSLFR4TO.js.map +1 -0
  52. package/dist/chunk-UP2VWCW5.js +33 -0
  53. package/dist/chunk-UP2VWCW5.js.map +1 -0
  54. package/dist/chunk-W6TQFTTY.js +130 -0
  55. package/dist/chunk-W6TQFTTY.js.map +1 -0
  56. package/dist/chunk-WSMVLWLB.js +269 -0
  57. package/dist/chunk-WSMVLWLB.js.map +1 -0
  58. package/dist/config/index.d.ts +69 -11
  59. package/dist/config/index.js +24 -1227
  60. package/dist/config/index.js.map +1 -1
  61. package/dist/crucible/fulpack/index.js +2 -25
  62. package/dist/crucible/fulpack/index.js.map +1 -1
  63. package/dist/crucible/index.js +13 -1129
  64. package/dist/crucible/index.js.map +1 -1
  65. package/dist/docscribe/index.js +2 -680
  66. package/dist/docscribe/index.js.map +1 -1
  67. package/dist/errors/index.js +5 -1831
  68. package/dist/errors/index.js.map +1 -1
  69. package/dist/errors-D7KXV3QW.js +4 -0
  70. package/dist/errors-D7KXV3QW.js.map +1 -0
  71. package/dist/errors-RBJX6ETJ.js +8 -0
  72. package/dist/errors-RBJX6ETJ.js.map +1 -0
  73. package/dist/export-BWRETGYS.js +6 -0
  74. package/dist/export-BWRETGYS.js.map +1 -0
  75. package/dist/foundry/index.d.ts +1 -1
  76. package/dist/foundry/index.js +340 -3949
  77. package/dist/foundry/index.js.map +1 -1
  78. package/dist/foundry/similarity/index.js +3 -134
  79. package/dist/foundry/similarity/index.js.map +1 -1
  80. package/dist/fulencode/index.js +3 -319
  81. package/dist/fulencode/index.js.map +1 -1
  82. package/dist/fulhash/index.js +3 -611
  83. package/dist/fulhash/index.js.map +1 -1
  84. package/dist/fulpack/index.js +3 -1515
  85. package/dist/fulpack/index.js.map +1 -1
  86. package/dist/hash-YOJYVOWH.js +4 -0
  87. package/dist/hash-YOJYVOWH.js.map +1 -0
  88. package/dist/index.d.ts +2 -2
  89. package/dist/index.js +14 -4390
  90. package/dist/index.js.map +1 -1
  91. package/dist/loader-H5K7HSO5.js +9 -0
  92. package/dist/loader-H5K7HSO5.js.map +1 -0
  93. package/dist/logging/index.js +2 -723
  94. package/dist/logging/index.js.map +1 -1
  95. package/dist/pathfinder/index.js +18 -2341
  96. package/dist/pathfinder/index.js.map +1 -1
  97. package/dist/prom-client-PNEBGD3G.js +4801 -0
  98. package/dist/prom-client-PNEBGD3G.js.map +1 -0
  99. package/dist/reports/license-inventory.csv +1 -1
  100. package/dist/schema/index.d.ts +19 -6
  101. package/dist/schema/index.js +4 -6148
  102. package/dist/schema/index.js.map +1 -1
  103. package/dist/signals/index.d.ts +1 -1
  104. package/dist/signals/index.js +8 -2767
  105. package/dist/signals/index.js.map +1 -1
  106. package/dist/similarity/index.js +3 -134
  107. package/dist/similarity/index.js.map +1 -1
  108. package/dist/telemetry/http/index.js +9 -930
  109. package/dist/telemetry/http/index.js.map +1 -1
  110. package/dist/telemetry/index.js +4 -1561
  111. package/dist/telemetry/index.js.map +1 -1
  112. package/dist/telemetry/prometheus/index.js +7 -8417
  113. package/dist/telemetry/prometheus/index.js.map +1 -1
  114. package/dist/{types-Dv5TERCM.d.ts → types-CHDvDRCf.d.ts} +17 -1
  115. package/dist/validator-U4CKD2Y3.js +6 -0
  116. package/dist/validator-U4CKD2Y3.js.map +1 -0
  117. package/package.json +5 -1
  118. package/schemas/crucible-ts/library/module-manifest/v1.0.0/module-manifest.schema.json +1 -1
  119. package/schemas/crucible-ts/observability/logging/v1.0.0/definitions.schema.json +1 -1
  120. package/schemas/crucible-ts/observability/logging/v1.0.0/log-event.schema.json +13 -13
  121. package/schemas/crucible-ts/observability/logging/v1.0.0/logger-config.schema.json +2 -2
  122. package/schemas/crucible-ts/observability/logging/v1.0.0/logging-policy.schema.json +1 -1
  123. package/schemas/crucible-ts/observability/logging/v1.0.0/middleware-config.schema.json +2 -2
  124. package/schemas/crucible-ts/observability/logging/v1.0.0/severity-filter.schema.json +3 -3
  125. package/schemas/crucible-ts/upstream/3leaps/crucible/PROVENANCE.md +3 -3
  126. package/schemas/crucible-ts/upstream/3leaps/crucible/docs/catalog/classifiers/README.md +1 -1
  127. package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/classifiers-framework.md +1 -1
  128. 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