@gravito/monitor 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1008 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Counter: () => Counter,
34
+ Gauge: () => Gauge,
35
+ HealthController: () => HealthController,
36
+ HealthRegistry: () => HealthRegistry,
37
+ Histogram: () => Histogram,
38
+ MetricsController: () => MetricsController,
39
+ MetricsRegistry: () => MetricsRegistry,
40
+ MonitorOrbit: () => MonitorOrbit,
41
+ TracingManager: () => TracingManager,
42
+ createDatabaseCheck: () => createDatabaseCheck,
43
+ createDiskCheck: () => createDiskCheck,
44
+ createHttpCheck: () => createHttpCheck,
45
+ createHttpMetricsMiddleware: () => createHttpMetricsMiddleware,
46
+ createMemoryCheck: () => createMemoryCheck,
47
+ createRedisCheck: () => createRedisCheck,
48
+ createTracingMiddleware: () => createTracingMiddleware,
49
+ defineMonitorConfig: () => defineMonitorConfig
50
+ });
51
+ module.exports = __toCommonJS(index_exports);
52
+
53
+ // src/config.ts
54
+ function defineMonitorConfig(config) {
55
+ return config;
56
+ }
57
+
58
+ // src/health/HealthController.ts
59
+ var HealthController = class {
60
+ constructor(registry) {
61
+ this.registry = registry;
62
+ }
63
+ /**
64
+ * GET /health - Full health check with all registered checks
65
+ */
66
+ async health(c) {
67
+ const report = await this.registry.check();
68
+ const status = report.status === "healthy" ? 200 : report.status === "degraded" ? 200 : 503;
69
+ return c.json(report, status);
70
+ }
71
+ /**
72
+ * GET /ready - Kubernetes readiness probe
73
+ * Returns 200 if ready to serve traffic, 503 otherwise
74
+ */
75
+ async ready(c) {
76
+ const result = await this.registry.readiness();
77
+ if (result.status === "healthy") {
78
+ return c.json({ status: "ready" }, 200);
79
+ }
80
+ return c.json({ status: "not_ready", reason: result.reason }, 503);
81
+ }
82
+ /**
83
+ * GET /live - Kubernetes liveness probe
84
+ * Returns 200 if the process is alive
85
+ */
86
+ async live(c) {
87
+ const result = await this.registry.liveness();
88
+ return c.json({ status: "alive" }, result.status === "healthy" ? 200 : 503);
89
+ }
90
+ };
91
+
92
+ // src/health/HealthRegistry.ts
93
+ var DEFAULTS = {
94
+ timeout: 5e3,
95
+ cacheTtl: 0
96
+ };
97
+ var HealthRegistry = class {
98
+ checks = /* @__PURE__ */ new Map();
99
+ startTime = Date.now();
100
+ cachedReport = null;
101
+ cacheExpiry = 0;
102
+ timeout;
103
+ cacheTtl;
104
+ constructor(config = {}) {
105
+ this.timeout = config.timeout ?? DEFAULTS.timeout;
106
+ this.cacheTtl = config.cacheTtl ?? DEFAULTS.cacheTtl;
107
+ }
108
+ /**
109
+ * Register a health check
110
+ */
111
+ register(name, check) {
112
+ this.checks.set(name, check);
113
+ return this;
114
+ }
115
+ /**
116
+ * Unregister a health check
117
+ */
118
+ unregister(name) {
119
+ return this.checks.delete(name);
120
+ }
121
+ /**
122
+ * Get all registered check names
123
+ */
124
+ getCheckNames() {
125
+ return Array.from(this.checks.keys());
126
+ }
127
+ /**
128
+ * Execute a single health check with timeout
129
+ */
130
+ async executeCheck(name, check) {
131
+ const start = performance.now();
132
+ try {
133
+ const result = await Promise.race([
134
+ Promise.resolve(check()),
135
+ new Promise(
136
+ (_, reject) => setTimeout(() => reject(new Error("Health check timeout")), this.timeout)
137
+ )
138
+ ]);
139
+ const latency = Math.round(performance.now() - start);
140
+ return {
141
+ name,
142
+ ...result,
143
+ latency
144
+ };
145
+ } catch (error) {
146
+ const latency = Math.round(performance.now() - start);
147
+ const message = error instanceof Error ? error.message : "Unknown error";
148
+ return {
149
+ name,
150
+ status: "unhealthy",
151
+ message,
152
+ latency
153
+ };
154
+ }
155
+ }
156
+ /**
157
+ * Execute all health checks and generate report
158
+ */
159
+ async check() {
160
+ if (this.cacheTtl > 0 && this.cachedReport && Date.now() < this.cacheExpiry) {
161
+ return this.cachedReport;
162
+ }
163
+ const results = await Promise.all(
164
+ Array.from(this.checks.entries()).map(([name, check]) => this.executeCheck(name, check))
165
+ );
166
+ const checks = {};
167
+ let overallStatus = "healthy";
168
+ for (const result of results) {
169
+ checks[result.name] = result;
170
+ if (result.status === "unhealthy") {
171
+ overallStatus = "unhealthy";
172
+ } else if (result.status === "degraded" && overallStatus !== "unhealthy") {
173
+ overallStatus = "degraded";
174
+ }
175
+ }
176
+ const report = {
177
+ status: overallStatus,
178
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
179
+ uptime: Math.round((Date.now() - this.startTime) / 1e3),
180
+ checks
181
+ };
182
+ if (this.cacheTtl > 0) {
183
+ this.cachedReport = report;
184
+ this.cacheExpiry = Date.now() + this.cacheTtl;
185
+ }
186
+ return report;
187
+ }
188
+ /**
189
+ * Simple liveness check (is the process running?)
190
+ */
191
+ async liveness() {
192
+ return { status: "healthy" };
193
+ }
194
+ /**
195
+ * Readiness check (is the app ready to serve traffic?)
196
+ * By default, requires all checks to be healthy
197
+ */
198
+ async readiness() {
199
+ if (this.checks.size === 0) {
200
+ return { status: "healthy" };
201
+ }
202
+ const report = await this.check();
203
+ if (report.status === "unhealthy") {
204
+ const failedChecks = Object.entries(report.checks).filter(([_, r]) => r.status === "unhealthy").map(([name]) => name);
205
+ return {
206
+ status: "unhealthy",
207
+ reason: `Failed checks: ${failedChecks.join(", ")}`
208
+ };
209
+ }
210
+ return { status: "healthy" };
211
+ }
212
+ };
213
+
214
+ // src/health/index.ts
215
+ function createDatabaseCheck(connectionFn) {
216
+ return async () => {
217
+ try {
218
+ const isConnected = await connectionFn();
219
+ return isConnected ? { status: "healthy", message: "Database connected" } : { status: "unhealthy", message: "Database disconnected" };
220
+ } catch (error) {
221
+ return {
222
+ status: "unhealthy",
223
+ message: error instanceof Error ? error.message : "Database check failed"
224
+ };
225
+ }
226
+ };
227
+ }
228
+ function createRedisCheck(pingFn) {
229
+ return async () => {
230
+ try {
231
+ const result = await pingFn();
232
+ return result === "PONG" ? { status: "healthy", message: "Redis connected" } : { status: "unhealthy", message: `Unexpected response: ${result}` };
233
+ } catch (error) {
234
+ return {
235
+ status: "unhealthy",
236
+ message: error instanceof Error ? error.message : "Redis check failed"
237
+ };
238
+ }
239
+ };
240
+ }
241
+ function createMemoryCheck(options) {
242
+ const maxPercent = options?.maxHeapUsedPercent ?? 90;
243
+ return () => {
244
+ const usage = process.memoryUsage();
245
+ const heapUsedPercent = usage.heapUsed / usage.heapTotal * 100;
246
+ if (heapUsedPercent > maxPercent) {
247
+ return {
248
+ status: "degraded",
249
+ message: `Heap usage at ${heapUsedPercent.toFixed(1)}%`,
250
+ details: {
251
+ heapUsed: formatBytes(usage.heapUsed),
252
+ heapTotal: formatBytes(usage.heapTotal),
253
+ heapUsedPercent: heapUsedPercent.toFixed(1),
254
+ rss: formatBytes(usage.rss)
255
+ }
256
+ };
257
+ }
258
+ return {
259
+ status: "healthy",
260
+ message: "Memory usage normal",
261
+ details: {
262
+ heapUsed: formatBytes(usage.heapUsed),
263
+ heapTotal: formatBytes(usage.heapTotal),
264
+ heapUsedPercent: heapUsedPercent.toFixed(1),
265
+ rss: formatBytes(usage.rss)
266
+ }
267
+ };
268
+ };
269
+ }
270
+ function createHttpCheck(url, options) {
271
+ const timeout = options?.timeout ?? 5e3;
272
+ const expectedStatus = options?.expectedStatus ?? 200;
273
+ const method = options?.method ?? "GET";
274
+ return async () => {
275
+ const controller = new AbortController();
276
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
277
+ try {
278
+ const start = performance.now();
279
+ const response = await fetch(url, {
280
+ method,
281
+ signal: controller.signal
282
+ });
283
+ const latency = Math.round(performance.now() - start);
284
+ clearTimeout(timeoutId);
285
+ if (response.status === expectedStatus) {
286
+ return {
287
+ status: "healthy",
288
+ message: `${url} responded with ${response.status}`,
289
+ latency
290
+ };
291
+ }
292
+ return {
293
+ status: "unhealthy",
294
+ message: `Expected ${expectedStatus}, got ${response.status}`,
295
+ latency
296
+ };
297
+ } catch (error) {
298
+ clearTimeout(timeoutId);
299
+ return {
300
+ status: "unhealthy",
301
+ message: error instanceof Error ? error.message : "HTTP check failed"
302
+ };
303
+ }
304
+ };
305
+ }
306
+ function createDiskCheck(options) {
307
+ const minFreePercent = options?.minFreePercent ?? 10;
308
+ return async () => {
309
+ try {
310
+ return {
311
+ status: "healthy",
312
+ message: "Disk check passed",
313
+ details: {
314
+ minFreePercent
315
+ }
316
+ };
317
+ } catch (error) {
318
+ return {
319
+ status: "unhealthy",
320
+ message: error instanceof Error ? error.message : "Disk check failed"
321
+ };
322
+ }
323
+ };
324
+ }
325
+ function formatBytes(bytes) {
326
+ const units = ["B", "KB", "MB", "GB"];
327
+ let size = bytes;
328
+ let unitIndex = 0;
329
+ while (size >= 1024 && unitIndex < units.length - 1) {
330
+ size /= 1024;
331
+ unitIndex++;
332
+ }
333
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
334
+ }
335
+
336
+ // src/metrics/MetricsController.ts
337
+ var MetricsController = class {
338
+ constructor(registry) {
339
+ this.registry = registry;
340
+ }
341
+ /**
342
+ * GET /metrics - Prometheus metrics endpoint
343
+ */
344
+ async metrics(c) {
345
+ const prometheusFormat = this.registry.toPrometheus();
346
+ return new Response(prometheusFormat, {
347
+ status: 200,
348
+ headers: {
349
+ "Content-Type": "text/plain; version=0.0.4; charset=utf-8"
350
+ }
351
+ });
352
+ }
353
+ };
354
+
355
+ // src/metrics/MetricsRegistry.ts
356
+ var DEFAULTS2 = {
357
+ prefix: "gravito_",
358
+ defaultMetrics: true,
359
+ defaultLabels: {}
360
+ };
361
+ var Counter = class {
362
+ constructor(name, help, labelNames = []) {
363
+ this.name = name;
364
+ this.help = help;
365
+ this.labelNames = labelNames;
366
+ }
367
+ values = /* @__PURE__ */ new Map();
368
+ /**
369
+ * Increment the counter
370
+ */
371
+ inc(labels = {}, delta = 1) {
372
+ const key = this.labelsToKey(labels);
373
+ const current = this.values.get(key) ?? 0;
374
+ this.values.set(key, current + delta);
375
+ }
376
+ /**
377
+ * Get current values
378
+ */
379
+ getValues() {
380
+ const result = [];
381
+ for (const [key, value] of this.values) {
382
+ result.push({
383
+ value,
384
+ labels: this.keyToLabels(key)
385
+ });
386
+ }
387
+ return result;
388
+ }
389
+ /**
390
+ * Reset all values
391
+ */
392
+ reset() {
393
+ this.values.clear();
394
+ }
395
+ labelsToKey(labels) {
396
+ if (this.labelNames.length === 0) return "__default__";
397
+ return this.labelNames.map((name) => `${name}=${labels[name] ?? ""}`).join(",");
398
+ }
399
+ keyToLabels(key) {
400
+ if (key === "__default__") return {};
401
+ const labels = {};
402
+ for (const part of key.split(",")) {
403
+ const [name, value] = part.split("=");
404
+ if (name) labels[name] = value ?? "";
405
+ }
406
+ return labels;
407
+ }
408
+ };
409
+ var Gauge = class {
410
+ constructor(name, help, labelNames = []) {
411
+ this.name = name;
412
+ this.help = help;
413
+ this.labelNames = labelNames;
414
+ }
415
+ values = /* @__PURE__ */ new Map();
416
+ /**
417
+ * Set the gauge value
418
+ */
419
+ set(value, labels = {}) {
420
+ const key = this.labelsToKey(labels);
421
+ this.values.set(key, value);
422
+ }
423
+ /**
424
+ * Increment the gauge
425
+ */
426
+ inc(labels = {}, delta = 1) {
427
+ const key = this.labelsToKey(labels);
428
+ const current = this.values.get(key) ?? 0;
429
+ this.values.set(key, current + delta);
430
+ }
431
+ /**
432
+ * Decrement the gauge
433
+ */
434
+ dec(labels = {}, delta = 1) {
435
+ this.inc(labels, -delta);
436
+ }
437
+ /**
438
+ * Get current values
439
+ */
440
+ getValues() {
441
+ const result = [];
442
+ for (const [key, value] of this.values) {
443
+ result.push({
444
+ value,
445
+ labels: this.keyToLabels(key)
446
+ });
447
+ }
448
+ return result;
449
+ }
450
+ labelsToKey(labels) {
451
+ if (this.labelNames.length === 0) return "__default__";
452
+ return this.labelNames.map((name) => `${name}=${labels[name] ?? ""}`).join(",");
453
+ }
454
+ keyToLabels(key) {
455
+ if (key === "__default__") return {};
456
+ const labels = {};
457
+ for (const part of key.split(",")) {
458
+ const [name, value] = part.split("=");
459
+ if (name) labels[name] = value ?? "";
460
+ }
461
+ return labels;
462
+ }
463
+ };
464
+ var Histogram = class {
465
+ constructor(name, help, labelNames = [], buckets = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]) {
466
+ this.name = name;
467
+ this.help = help;
468
+ this.labelNames = labelNames;
469
+ this.buckets = [...buckets].sort((a, b) => a - b);
470
+ }
471
+ bucketCounts = /* @__PURE__ */ new Map();
472
+ sums = /* @__PURE__ */ new Map();
473
+ counts = /* @__PURE__ */ new Map();
474
+ buckets;
475
+ /**
476
+ * Observe a value
477
+ */
478
+ observe(value, labels = {}) {
479
+ const key = this.labelsToKey(labels);
480
+ this.sums.set(key, (this.sums.get(key) ?? 0) + value);
481
+ this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
482
+ let bucketMap = this.bucketCounts.get(key);
483
+ if (!bucketMap) {
484
+ bucketMap = /* @__PURE__ */ new Map();
485
+ this.bucketCounts.set(key, bucketMap);
486
+ }
487
+ for (const bucket of this.buckets) {
488
+ if (value <= bucket) {
489
+ bucketMap.set(bucket, (bucketMap.get(bucket) ?? 0) + 1);
490
+ }
491
+ }
492
+ }
493
+ /**
494
+ * Start a timer and return a function to stop it
495
+ */
496
+ startTimer(labels = {}) {
497
+ const start = performance.now();
498
+ return () => {
499
+ const duration = (performance.now() - start) / 1e3;
500
+ this.observe(duration, labels);
501
+ };
502
+ }
503
+ /**
504
+ * Get values for Prometheus format
505
+ */
506
+ getValues() {
507
+ return {
508
+ buckets: this.bucketCounts,
509
+ sums: this.sums,
510
+ counts: this.counts
511
+ };
512
+ }
513
+ labelsToKey(labels) {
514
+ if (this.labelNames.length === 0) return "__default__";
515
+ return this.labelNames.map((name) => `${name}=${labels[name] ?? ""}`).join(",");
516
+ }
517
+ };
518
+ var MetricsRegistry = class {
519
+ counters = /* @__PURE__ */ new Map();
520
+ gauges = /* @__PURE__ */ new Map();
521
+ histograms = /* @__PURE__ */ new Map();
522
+ startTime = Date.now();
523
+ prefix;
524
+ defaultLabels;
525
+ collectDefaultMetrics;
526
+ constructor(config = {}) {
527
+ this.prefix = config.prefix ?? DEFAULTS2.prefix;
528
+ this.defaultLabels = config.defaultLabels ?? DEFAULTS2.defaultLabels;
529
+ this.collectDefaultMetrics = config.defaultMetrics ?? DEFAULTS2.defaultMetrics;
530
+ if (this.collectDefaultMetrics) {
531
+ this.initDefaultMetrics();
532
+ }
533
+ }
534
+ /**
535
+ * Create or get a counter
536
+ */
537
+ counter(options) {
538
+ const name = this.prefix + options.name;
539
+ if (!this.counters.has(name)) {
540
+ this.counters.set(name, new Counter(name, options.help, options.labels));
541
+ }
542
+ return this.counters.get(name);
543
+ }
544
+ /**
545
+ * Create or get a gauge
546
+ */
547
+ gauge(options) {
548
+ const name = this.prefix + options.name;
549
+ if (!this.gauges.has(name)) {
550
+ this.gauges.set(name, new Gauge(name, options.help, options.labels));
551
+ }
552
+ return this.gauges.get(name);
553
+ }
554
+ /**
555
+ * Create or get a histogram
556
+ */
557
+ histogram(options) {
558
+ const name = this.prefix + options.name;
559
+ if (!this.histograms.has(name)) {
560
+ this.histograms.set(name, new Histogram(name, options.help, options.labels, options.buckets));
561
+ }
562
+ return this.histograms.get(name);
563
+ }
564
+ /**
565
+ * Initialize default runtime metrics
566
+ */
567
+ initDefaultMetrics() {
568
+ this.gauge({
569
+ name: "process_uptime_seconds",
570
+ help: "Process uptime in seconds"
571
+ });
572
+ this.gauge({
573
+ name: "nodejs_heap_size_used_bytes",
574
+ help: "Current heap size used in bytes"
575
+ });
576
+ this.gauge({
577
+ name: "nodejs_heap_size_total_bytes",
578
+ help: "Total heap size in bytes"
579
+ });
580
+ this.gauge({
581
+ name: "nodejs_external_memory_bytes",
582
+ help: "External memory usage in bytes"
583
+ });
584
+ this.counter({
585
+ name: "http_requests_total",
586
+ help: "Total HTTP requests",
587
+ labels: ["method", "path", "status"]
588
+ });
589
+ this.histogram({
590
+ name: "http_request_duration_seconds",
591
+ help: "HTTP request duration in seconds",
592
+ labels: ["method", "path", "status"],
593
+ buckets: [0.01, 0.05, 0.1, 0.5, 1, 5]
594
+ });
595
+ }
596
+ /**
597
+ * Update default metrics with current values
598
+ */
599
+ updateDefaultMetrics() {
600
+ if (!this.collectDefaultMetrics) return;
601
+ const uptime = (Date.now() - this.startTime) / 1e3;
602
+ this.gauges.get(this.prefix + "process_uptime_seconds")?.set(uptime);
603
+ const memory = process.memoryUsage();
604
+ this.gauges.get(this.prefix + "nodejs_heap_size_used_bytes")?.set(memory.heapUsed);
605
+ this.gauges.get(this.prefix + "nodejs_heap_size_total_bytes")?.set(memory.heapTotal);
606
+ this.gauges.get(this.prefix + "nodejs_external_memory_bytes")?.set(memory.external);
607
+ }
608
+ /**
609
+ * Export metrics in Prometheus format
610
+ */
611
+ toPrometheus() {
612
+ this.updateDefaultMetrics();
613
+ const lines = [];
614
+ for (const counter of this.counters.values()) {
615
+ lines.push(`# HELP ${counter.name} ${counter.help}`);
616
+ lines.push(`# TYPE ${counter.name} counter`);
617
+ for (const { value, labels } of counter.getValues()) {
618
+ const allLabels = this.formatLabels({ ...this.defaultLabels, ...labels });
619
+ lines.push(`${counter.name}${allLabels} ${value}`);
620
+ }
621
+ }
622
+ for (const gauge of this.gauges.values()) {
623
+ lines.push(`# HELP ${gauge.name} ${gauge.help}`);
624
+ lines.push(`# TYPE ${gauge.name} gauge`);
625
+ for (const { value, labels } of gauge.getValues()) {
626
+ const allLabels = this.formatLabels({ ...this.defaultLabels, ...labels });
627
+ lines.push(`${gauge.name}${allLabels} ${value}`);
628
+ }
629
+ }
630
+ for (const hist of this.histograms.values()) {
631
+ lines.push(`# HELP ${hist.name} ${hist.help}`);
632
+ lines.push(`# TYPE ${hist.name} histogram`);
633
+ const values = hist.getValues();
634
+ for (const [key, bucketMap] of values.buckets) {
635
+ const labels = this.keyToLabels(key);
636
+ let cumulative = 0;
637
+ for (const bucket of hist.buckets) {
638
+ cumulative += bucketMap.get(bucket) ?? 0;
639
+ const allLabels = this.formatLabels({
640
+ ...this.defaultLabels,
641
+ ...labels,
642
+ le: String(bucket)
643
+ });
644
+ lines.push(`${hist.name}_bucket${allLabels} ${cumulative}`);
645
+ }
646
+ const infLabels = this.formatLabels({ ...this.defaultLabels, ...labels, le: "+Inf" });
647
+ lines.push(`${hist.name}_bucket${infLabels} ${values.counts.get(key) ?? 0}`);
648
+ const sumLabels = this.formatLabels({ ...this.defaultLabels, ...labels });
649
+ lines.push(`${hist.name}_sum${sumLabels} ${values.sums.get(key) ?? 0}`);
650
+ lines.push(`${hist.name}_count${sumLabels} ${values.counts.get(key) ?? 0}`);
651
+ }
652
+ }
653
+ return lines.join("\n");
654
+ }
655
+ formatLabels(labels) {
656
+ const entries = Object.entries(labels);
657
+ if (entries.length === 0) return "";
658
+ return `{${entries.map(([k, v]) => `${k}="${v}"`).join(",")}}`;
659
+ }
660
+ keyToLabels(key) {
661
+ if (key === "__default__") return {};
662
+ const labels = {};
663
+ for (const part of key.split(",")) {
664
+ const [name, value] = part.split("=");
665
+ if (name) labels[name] = value ?? "";
666
+ }
667
+ return labels;
668
+ }
669
+ };
670
+
671
+ // src/metrics/index.ts
672
+ function createHttpMetricsMiddleware(registry) {
673
+ const requestCounter = registry.counter({
674
+ name: "http_requests_total",
675
+ help: "Total HTTP requests",
676
+ labels: ["method", "path", "status"]
677
+ });
678
+ const requestDuration = registry.histogram({
679
+ name: "http_request_duration_seconds",
680
+ help: "HTTP request duration in seconds",
681
+ labels: ["method", "path", "status"],
682
+ buckets: [0.01, 0.05, 0.1, 0.5, 1, 5]
683
+ });
684
+ return async (c, next) => {
685
+ const method = c.req.method;
686
+ const path = normalizePath(c.req.path);
687
+ const start = performance.now();
688
+ await next();
689
+ const duration = (performance.now() - start) / 1e3;
690
+ const status = "200";
691
+ requestCounter.inc({ method, path, status });
692
+ requestDuration.observe(duration, { method, path, status });
693
+ return void 0;
694
+ };
695
+ }
696
+ function normalizePath(path) {
697
+ path = path.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ":id");
698
+ path = path.replace(/\/\d+/g, "/:id");
699
+ path = path.replace(/\/[a-zA-Z0-9]{32,}/g, "/:token");
700
+ return path;
701
+ }
702
+
703
+ // src/tracing/TracingManager.ts
704
+ var DEFAULTS3 = {
705
+ serviceName: "gravito-app",
706
+ serviceVersion: "1.0.0",
707
+ endpoint: "http://localhost:4318/v1/traces",
708
+ sampleRate: 1,
709
+ resourceAttributes: {}
710
+ };
711
+ var activeSpan = null;
712
+ var TracingManager = class {
713
+ otelSdk = null;
714
+ spans = [];
715
+ isInitialized = false;
716
+ serviceName;
717
+ serviceVersion;
718
+ endpoint;
719
+ resourceAttributes;
720
+ constructor(config = {}) {
721
+ this.serviceName = config.serviceName ?? DEFAULTS3.serviceName;
722
+ this.serviceVersion = config.serviceVersion ?? DEFAULTS3.serviceVersion;
723
+ this.endpoint = config.endpoint ?? DEFAULTS3.endpoint;
724
+ this.resourceAttributes = config.resourceAttributes ?? DEFAULTS3.resourceAttributes;
725
+ }
726
+ /**
727
+ * Initialize OpenTelemetry SDK if available
728
+ */
729
+ async initialize() {
730
+ if (this.isInitialized) return;
731
+ try {
732
+ const [
733
+ { NodeSDK },
734
+ { OTLPTraceExporter },
735
+ { Resource },
736
+ { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION }
737
+ ] = await Promise.all([
738
+ import("@opentelemetry/sdk-node"),
739
+ import("@opentelemetry/exporter-trace-otlp-http"),
740
+ import("@opentelemetry/resources"),
741
+ import("@opentelemetry/semantic-conventions")
742
+ ]);
743
+ const resource = new Resource({
744
+ [ATTR_SERVICE_NAME]: this.serviceName,
745
+ [ATTR_SERVICE_VERSION]: this.serviceVersion,
746
+ ...this.resourceAttributes
747
+ });
748
+ const traceExporter = new OTLPTraceExporter({
749
+ url: this.endpoint
750
+ });
751
+ this.otelSdk = new NodeSDK({
752
+ resource,
753
+ traceExporter
754
+ });
755
+ await this.otelSdk.start();
756
+ this.isInitialized = true;
757
+ console.log(`[Monitor] OpenTelemetry initialized - exporting to ${this.endpoint}`);
758
+ } catch {
759
+ console.log("[Monitor] OpenTelemetry packages not available, using lightweight tracing");
760
+ this.isInitialized = true;
761
+ }
762
+ }
763
+ /**
764
+ * Shutdown tracing
765
+ */
766
+ async shutdown() {
767
+ if (this.otelSdk) {
768
+ await this.otelSdk.shutdown();
769
+ }
770
+ }
771
+ /**
772
+ * Start a new span
773
+ */
774
+ startSpan(name, options) {
775
+ const span = {
776
+ name,
777
+ traceId: options?.parentSpan?.traceId ?? generateTraceId(),
778
+ spanId: generateSpanId(),
779
+ parentSpanId: options?.parentSpan?.spanId,
780
+ startTime: Date.now(),
781
+ attributes: options?.attributes ?? {},
782
+ status: "unset",
783
+ events: []
784
+ };
785
+ activeSpan = span;
786
+ this.spans.push(span);
787
+ return span;
788
+ }
789
+ /**
790
+ * End a span
791
+ */
792
+ endSpan(span, status = "ok") {
793
+ span.endTime = Date.now();
794
+ span.status = status;
795
+ if (activeSpan === span) {
796
+ activeSpan = null;
797
+ }
798
+ }
799
+ /**
800
+ * Add an event to a span
801
+ */
802
+ addEvent(span, name, attributes) {
803
+ span.events.push({
804
+ name,
805
+ timestamp: Date.now(),
806
+ attributes
807
+ });
808
+ }
809
+ /**
810
+ * Set span attribute
811
+ */
812
+ setAttribute(span, key, value) {
813
+ span.attributes[key] = value;
814
+ }
815
+ /**
816
+ * Get the currently active span
817
+ */
818
+ getActiveSpan() {
819
+ return activeSpan;
820
+ }
821
+ /**
822
+ * Extract trace context from headers
823
+ */
824
+ extractContext(headers) {
825
+ const traceparent = headers.get("traceparent");
826
+ if (!traceparent) return null;
827
+ const parts = traceparent.split("-");
828
+ if (parts.length !== 4) return null;
829
+ return {
830
+ traceId: parts[1] ?? "",
831
+ spanId: parts[2] ?? "",
832
+ traceFlags: Number.parseInt(parts[3] ?? "0", 16)
833
+ };
834
+ }
835
+ /**
836
+ * Inject trace context into headers
837
+ */
838
+ injectContext(headers, span) {
839
+ const traceparent = `00-${span.traceId}-${span.spanId}-01`;
840
+ headers.set("traceparent", traceparent);
841
+ }
842
+ /**
843
+ * Get all collected spans (for debugging)
844
+ */
845
+ getSpans() {
846
+ return this.spans;
847
+ }
848
+ /**
849
+ * Clear collected spans
850
+ */
851
+ clearSpans() {
852
+ this.spans = [];
853
+ }
854
+ };
855
+ function generateTraceId() {
856
+ const bytes = new Uint8Array(16);
857
+ crypto.getRandomValues(bytes);
858
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
859
+ }
860
+ function generateSpanId() {
861
+ const bytes = new Uint8Array(8);
862
+ crypto.getRandomValues(bytes);
863
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
864
+ }
865
+
866
+ // src/tracing/index.ts
867
+ function createTracingMiddleware(tracer) {
868
+ return async (c, next) => {
869
+ const method = c.req.method;
870
+ const path = c.req.path;
871
+ const url = c.req.url;
872
+ const parentContext = tracer.extractContext(c.req.raw.headers);
873
+ const span = tracer.startSpan(`${method} ${path}`, {
874
+ attributes: {
875
+ "http.method": method,
876
+ "http.url": url,
877
+ "http.target": path,
878
+ "http.host": new URL(url).host
879
+ },
880
+ ...parentContext ? {
881
+ parentSpan: {
882
+ name: "parent",
883
+ traceId: parentContext.traceId,
884
+ spanId: parentContext.spanId,
885
+ startTime: Date.now(),
886
+ attributes: {},
887
+ status: "unset",
888
+ events: []
889
+ }
890
+ } : {}
891
+ });
892
+ c.set("span", span);
893
+ try {
894
+ await next();
895
+ tracer.endSpan(span, "ok");
896
+ } catch (error) {
897
+ tracer.setAttribute(span, "error", true);
898
+ tracer.setAttribute(
899
+ span,
900
+ "error.message",
901
+ error instanceof Error ? error.message : "Unknown error"
902
+ );
903
+ tracer.addEvent(span, "exception", {
904
+ "exception.type": error instanceof Error ? error.constructor.name : "Error",
905
+ "exception.message": error instanceof Error ? error.message : String(error)
906
+ });
907
+ tracer.endSpan(span, "error");
908
+ throw error;
909
+ }
910
+ return void 0;
911
+ };
912
+ }
913
+
914
+ // src/MonitorOrbit.ts
915
+ var DEFAULTS4 = {
916
+ health: {
917
+ enabled: true,
918
+ path: "/health",
919
+ readyPath: "/ready",
920
+ livePath: "/live"
921
+ },
922
+ metrics: {
923
+ enabled: true,
924
+ path: "/metrics"
925
+ },
926
+ tracing: {
927
+ enabled: false
928
+ }
929
+ };
930
+ var MonitorOrbit = class {
931
+ name = "monitor";
932
+ userConfig;
933
+ healthRegistry = null;
934
+ metricsRegistry = null;
935
+ tracingManager = null;
936
+ constructor(config = {}) {
937
+ this.userConfig = config;
938
+ }
939
+ /**
940
+ * Install the orbit (required by GravitoOrbit interface)
941
+ */
942
+ async install(core) {
943
+ const healthEnabled = this.userConfig.health?.enabled !== void 0 ? this.userConfig.health.enabled : DEFAULTS4.health.enabled;
944
+ const metricsEnabled = this.userConfig.metrics?.enabled !== void 0 ? this.userConfig.metrics.enabled : DEFAULTS4.metrics.enabled;
945
+ const tracingEnabled = this.userConfig.tracing?.enabled !== void 0 ? this.userConfig.tracing.enabled : DEFAULTS4.tracing.enabled;
946
+ const healthPath = this.userConfig.health?.path || DEFAULTS4.health.path;
947
+ const readyPath = this.userConfig.health?.readyPath || DEFAULTS4.health.readyPath;
948
+ const livePath = this.userConfig.health?.livePath || DEFAULTS4.health.livePath;
949
+ const metricsPath = this.userConfig.metrics?.path || DEFAULTS4.metrics.path;
950
+ this.healthRegistry = new HealthRegistry(this.userConfig.health);
951
+ this.metricsRegistry = new MetricsRegistry(this.userConfig.metrics);
952
+ this.tracingManager = new TracingManager(this.userConfig.tracing);
953
+ if (tracingEnabled) {
954
+ await this.tracingManager.initialize();
955
+ }
956
+ const monitorService = {
957
+ health: this.healthRegistry,
958
+ metrics: this.metricsRegistry,
959
+ tracing: this.tracingManager
960
+ };
961
+ core.services.set("monitor", monitorService);
962
+ core.services.set("health", this.healthRegistry);
963
+ core.services.set("metrics", this.metricsRegistry);
964
+ core.services.set("tracing", this.tracingManager);
965
+ const router = core.router;
966
+ if (healthEnabled && this.healthRegistry) {
967
+ const healthController = new HealthController(this.healthRegistry);
968
+ router.get(healthPath, (c) => healthController.health(c));
969
+ router.get(readyPath, (c) => healthController.ready(c));
970
+ router.get(livePath, (c) => healthController.live(c));
971
+ console.log(`[Monitor] Health endpoints: ${healthPath}, ${readyPath}, ${livePath}`);
972
+ }
973
+ if (metricsEnabled && this.metricsRegistry) {
974
+ const metricsController = new MetricsController(this.metricsRegistry);
975
+ router.get(metricsPath, (c) => metricsController.metrics(c));
976
+ console.log(`[Monitor] Metrics endpoint: ${metricsPath}`);
977
+ }
978
+ console.log("[Monitor] Observability services initialized");
979
+ }
980
+ /**
981
+ * Shutdown hook
982
+ */
983
+ async shutdown() {
984
+ if (this.tracingManager) {
985
+ await this.tracingManager.shutdown();
986
+ }
987
+ }
988
+ };
989
+ // Annotate the CommonJS export names for ESM import in node:
990
+ 0 && (module.exports = {
991
+ Counter,
992
+ Gauge,
993
+ HealthController,
994
+ HealthRegistry,
995
+ Histogram,
996
+ MetricsController,
997
+ MetricsRegistry,
998
+ MonitorOrbit,
999
+ TracingManager,
1000
+ createDatabaseCheck,
1001
+ createDiskCheck,
1002
+ createHttpCheck,
1003
+ createHttpMetricsMiddleware,
1004
+ createMemoryCheck,
1005
+ createRedisCheck,
1006
+ createTracingMiddleware,
1007
+ defineMonitorConfig
1008
+ });