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