Skip to content

Implement MetricsService with time-series storage, Prometheus exposit…#126

Merged
respp merged 7 commits into
PayStell:mainfrom
Cedarich:feat/metrics-prometheus-alerting-dashboard
Oct 6, 2025
Merged

Implement MetricsService with time-series storage, Prometheus exposit…#126
respp merged 7 commits into
PayStell:mainfrom
Cedarich:feat/metrics-prometheus-alerting-dashboard

Conversation

@Cedarich

@Cedarich Cedarich commented Sep 29, 2025

Copy link
Copy Markdown
Contributor

Summary

  • Implement a full monitoring stack with:
    • Time-series storage
    • Prometheus-formatted metrics
    • Threshold-based alerting
    • Lightweight dashboard UI
  • Fix TypeScript issues in metrics and configuration integrations to ensure clean builds.

Key Changes

Added

  • Metrics service for collection, storage, export, and alerts:
    • MetricsService.ts
  • Metrics routes and dashboard:
    • metrics.routes.ts
  • Metrics middleware and integration:
    • app.ts
  • Extended health checks:
    • health.routes.ts

Updated

  • Startup sequence compatibility:
    • index.ts

TypeScript Fixes

  • Fixed nested template literal parsing in metrics.routes.ts causing server-side parsing issues.
  • Adjusted Express route handler signatures to return void per Express v5 types.
  • Updated configuration reads:
    • Pass string defaults to getConfig.
    • Convert to numbers locally in MetricsService.ts.
    • Resolves: “Argument of type 'number' is not assignable to parameter of type 'string'”.

Exposed Endpoints

Endpoint Description
/metrics/summary JSON snapshot with system, external, and request stats
/metrics/historical Historical metrics with query params metric, windowMinutes, aggregation
/metrics/prometheus Prometheus exposition format
/metrics/dashboard Minimal monitoring dashboard UI
/health/system Extended system health check
/health/dependencies Check external service health
/health/db Database health check

Alerting Configuration

Env Variable Default Purpose
METRICS_API_LATENCY_THRESHOLD_MS 1000 API latency threshold (ms)
METRICS_DB_LATENCY_THRESHOLD_MS 500 DB latency threshold (ms)
METRICS_CPU_THRESHOLD 85 CPU usage threshold (%)
METRICS_MEMORY_THRESHOLD 85 Memory usage threshold (%)
METRICS_ALERT_EMAILS - Comma-separated alert recipients

Dependencies


How to Test

  1. Install dependencies and start the dev server.
  2. Visit /metrics/dashboard to view live charts.
  3. Verify:
    • /metrics/summary
    • /metrics/historical
    • /metrics/prometheus
  4. Adjust thresholds in .env to trigger alerts and confirm email notifications are sent.
  5. Validate /health/system and /health/dependencies return correct metrics and latency data.

Risks & Considerations

  • Optional disk metrics:
    • Relies on dynamic import of check-disk-space.
    • Failure handled gracefully.
  • Alert rate limiting prevents flooding but may delay repeated notifications.

Checklist

  • Dependencies installed
  • Metrics endpoints functional
  • Dashboard loads and charts render
  • Alert emails configured correctly
  • CI passes

Summary by CodeRabbit

  • New Features

    • App-wide metrics and alerts with request-level stats, per-route throughput and periodic system/external collection.
    • New /metrics endpoints: summary, historical queries, Prometheus output, and a lightweight dashboard.
    • Health endpoints enhanced: per-dependency latency details and a new /system resource endpoint.
  • Chores

    • Added runtime deps for disk checks and Prometheus support.
    • Updated Prettier line-ending setting.

…ion, and threshold-based alerting with email notifications and rate limiting.
@coderabbitai

coderabbitai Bot commented Sep 29, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a MetricsService singleton, request-observing middleware and periodic collectors, Prometheus exposition and dashboard routes, enhanced health endpoints (/dependencies, /db, /system) with per-dependency latencies, typed request config cache changes, and two runtime deps: check-disk-space and prom-client.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added runtime deps: check-disk-space@^3.4.0, prom-client@^15.1.3; minor prettier script quoting change.
App wiring
src/app.ts
Registered metricsMiddleware and mounted metricsRoutes at /metrics.
Metrics middleware
src/middlewares/metrics.middleware.ts
New middleware metricsMiddleware(req,res,next) records high-res start time and observes requests on finish via metricsService.observeRequest; added collectPeriodicMetrics() to run periodic collectors.
Metrics service
src/services/MetricsService.ts
New singleton MetricsService (exported as metricsService) with types and methods: observeRequest, collectSystemResources, checkExternalStatus, getSnapshot, getPrometheusMetrics, getHistorical; maintains time-series, alerts, Prometheus text export, optional disk checks, and external dependency probes (DB, Redis, Stellar).
Metrics routes
src/routes/metrics.routes.ts
New router exposing GET /metrics/summary, /metrics/historical (validation + aggregation), /metrics/prometheus (Prometheus text format), /metrics/dashboard (static HTML dashboard); includes Swagger-style docs.
Health routes
src/routes/health.routes.ts
Enhanced health endpoints: dependency checks (stellar/redis/db) with per-check latencies and FAIL/OK aggregation returning 503 on failure; /db returns query latency; new /system endpoint reports memory, uptime, CPU, load averages, cores, and timestamp.
Request config typing & cache
src/middlewares/configurationMiddleware.ts
Introduced ConfigGet type, bound req.config.get to configurationService.getConfig, added request-scoped cache with typed cachedGet wrapper that caches results per-request.
Prettier config
.prettierrc
Set endOfLine: "auto" in Prettier config.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant A as Express App
  participant M as metricsMiddleware
  participant R as Route Handler
  participant S as MetricsService

  C->>A: HTTP Request
  A->>M: invoke metricsMiddleware
  Note right of M #cfe8ff: record start HR time
  M->>A: next()
  A->>R: handle route
  R-->>A: send response
  A-->>C: HTTP Response
  Note right of M #e8f7e8: on "finish" -> observeRequest
  M->>S: observeRequest(req,res,start)
  S-->>M: recorded
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant A as Express App
  participant Rt as metrics.routes
  participant S as MetricsService

  C->>A: GET /metrics/prometheus
  A->>Rt: metrics route handler
  Rt->>S: collectSystemResources()
  Rt->>S: checkExternalStatus()
  Rt->>S: getPrometheusMetrics()
  S-->>Rt: metrics text/plain
  Rt-->>C: 200 text/plain (v0.0.4)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • respp
  • MPSxDev

Poem

I hop and tally every beat,
CPUs hum, memory neat,
Latency gleams on charts so bright,
Prometheus pours metrics light,
Carrots counted — observability complete. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title precisely describes the primary addition of a MetricsService with time-series storage and Prometheus exposition, which matches the core changes of the PR without extraneous detail.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c18483 and 242a129.

📒 Files selected for processing (1)
  • src/middlewares/configurationMiddleware.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/middlewares/configurationMiddleware.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (12)
src/routes/health.routes.ts (2)

209-214: Extract duplicated database check logic.

The database connection check logic is duplicated between /db and /dependencies endpoints.

Consider extracting this into a shared utility function:

async function checkDatabaseHealth(): Promise<{ isHealthy: boolean; latencyMs: number }> {
  if (!AppDataSource.isInitialized) {
    await AppDataSource.initialize();
  }
  const start = Date.now();
  await AppDataSource.query("SELECT 1");
  return { isHealthy: true, latencyMs: Date.now() - start };
}

This would reduce duplication and make the code more maintainable.


231-231: Route handler should not be marked as async without await.

The route handler is marked as async but doesn't use await anywhere in the try block.

Remove the async keyword since it's not needed:

-router.get("/system", async (_req, res) => {
+router.get("/system", (_req, res) => {
src/middlewares/metrics.middleware.ts (3)

4-14: Capture aborted requests and avoid double-recording

Only listening to "finish" misses aborted connections; add a "close" handler and guard to prevent double observation.

 export function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
-  const start = process.hrtime();
-  res.on("finish", () => {
-    try {
-      metricsService.observeRequest(req, res, start);
-    } catch (err) {
-      // avoid breaking the request
-    }
-  });
+  const start = process.hrtime();
+  let observed = false;
+  const observe = () => {
+    if (observed) return;
+    observed = true;
+    try {
+      metricsService.observeRequest(req, res, start);
+    } catch {
+      // avoid breaking the request
+    }
+  };
+  res.on("finish", observe);
+  res.on("close", observe); // covers aborted requests
   next();
 }

4-4: Annotate explicit return type for Express v5 handlers

Express v5 types expect route/middleware handlers to return void. Add explicit annotation for clarity.

-export function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
+export function metricsMiddleware(req: Request, res: Response, next: NextFunction): void {

Based on learnings.


17-24: Run collectors concurrently and fix no-unused-vars in catch

  • Run both collectors in parallel to reduce latency.
  • Avoid unused catch param to satisfy ESLint.
-export async function collectPeriodicMetrics() {
-  try {
-    await metricsService.collectSystemResources();
-    await metricsService.checkExternalStatus();
-  } catch (err) {
-    // swallow errors
-  }
-}
+export async function collectPeriodicMetrics(): Promise<void> {
+  try {
+    await Promise.all([
+      metricsService.collectSystemResources(),
+      metricsService.checkExternalStatus(),
+    ]);
+  } catch {
+    // swallow errors
+  }
+}
src/routes/metrics.routes.ts (3)

16-21: Avoid heavy checks on every summary request

/metrics/summary calls collectSystemResources() and checkExternalStatus() on each request. The dashboard polls every 5s, which will hit DB/Redis/Horizon aggressively. Prefer returning metricsService.getSnapshot() populated by a background scheduler (collectPeriodicMetrics) with a short TTL cache, and make “forceRefresh=true” optional.


66-73: Prometheus scrape should not block indefinitely

DB/Redis/Horizon checks may hang; add timeouts and consider serving latest snapshot if checks exceed a deadline.

-router.get("/prometheus", async (_req, res) => {
-  // ensure latest sample collected
-  await metricsService.collectSystemResources();
-  await metricsService.checkExternalStatus();
+router.get("/prometheus", async (_req, res) => {
+  // ensure latest sample collected, but bound the work
+  const withTimeout = <T>(p: Promise<T>, ms: number) =>
+    Promise.race([p, new Promise<T>((_, r) => setTimeout(() => r(new Error("timeout")), ms))]);
+  try {
+    await withTimeout(metricsService.collectSystemResources(), 1500);
+    await withTimeout(metricsService.checkExternalStatus(), 1500);
+  } catch {
+    // fall back to last snapshot
+  }
   const body = metricsService.getPrometheusMetrics();
-  res.set("Content-Type", "text/plain; version=0.0.4");
+  res.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
   res.send(body);
 });

82-167: Minor: dashboard safety and UX nits

  • Use textContent consistently (already used for RPS) and prefer encodeURIComponent for all query parts (already done).
  • Consider caching-busting headers for the HTML to avoid stale reloads.
-  res.setHeader("Content-Type", "text/html");
+  res.setHeader("Content-Type", "text/html; charset=utf-8");
+  res.setHeader("Cache-Control", "no-store");
src/services/MetricsService.ts (4)

125-143: CPU percent is process-average since start; consider windowed sampling

process.cpuUsage() divided by uptime gives lifetime average, not a current load sample. Track deltas across intervals for a more accurate gauge and alerting.


370-372: Avoid unused catch variable

Change to catch { … } or log the error to satisfy ESLint and improve observability.

-    } catch (err) {
-      logger.error("Failed to send alert email", { error: (err as Error)?.message });
-    }
+    } catch (err) {
+      logger.error("Failed to send alert email", { error: (err as Error)?.message });
+    }

If you intentionally want to swallow without logging elsewhere, prefer:

-    } catch (err) {
-      // ...
-    }
+    } catch {
+      // intentionally ignored
+    }

349-381: maybeAlert’s value param is unused

Remove the unused parameter or use it in logs; keeps API minimal.

-  private async maybeAlert(key: string, value: number, predicate: () => Promise<boolean>, message: string) {
+  private async maybeAlert(key: string, predicate: () => Promise<boolean>, message: string) {
@@
-      if (await predicate()) this.triggerAlert(key, message);
+      if (await predicate()) this.triggerAlert(key, message);

And update the only call site accordingly.


118-123: Update maybeAlert call after signature change

Follow-up to previous comment if applied.

-    this.maybeAlert("api_latency", durationMs, async () => {
+    this.maybeAlert("api_latency", async () => {
       const rawLatency = await configurationService.getConfig("METRICS_API_LATENCY_THRESHOLD_MS", "1000");
       const threshold = Number(rawLatency ?? 1000);
       return durationMs > threshold;
     }, `High API latency: ${durationMs.toFixed(2)}ms for ${method} ${route}`);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f63dd29 and c6cc4b5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json (2 hunks)
  • src/app.ts (4 hunks)
  • src/middlewares/metrics.middleware.ts (1 hunks)
  • src/routes/health.routes.ts (3 hunks)
  • src/routes/metrics.routes.ts (1 hunks)
  • src/services/MetricsService.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/middlewares/metrics.middleware.ts (2)
src/utils/subscriptionScheduler.ts (1)
  • start (14-29)
src/services/MetricsService.ts (1)
  • metricsService (384-384)
src/routes/metrics.routes.ts (1)
src/services/MetricsService.ts (1)
  • metricsService (384-384)
src/app.ts (1)
src/middlewares/metrics.middleware.ts (1)
  • metricsMiddleware (4-14)
src/services/MetricsService.ts (3)
src/services/ConfigurationService.ts (1)
  • configurationService (780-780)
src/config/redisConfig.ts (1)
  • redisClient (78-78)
src/utils/mailer.ts (1)
  • sendEmail (11-31)
src/routes/health.routes.ts (1)
src/config/redisConfig.ts (1)
  • redisClient (78-78)
🪛 GitHub Check: build-and-test
src/middlewares/metrics.middleware.ts

[warning] 21-21:
'err' is defined but never used


[warning] 9-9:
'err' is defined but never used

src/routes/metrics.routes.ts

[failure] 54-54:
Unexpected any. Specify a different type

src/services/MetricsService.ts

[warning] 151-151:
'err' is defined but never used


[failure] 148-148:
Unexpected any. Specify a different type


[failure] 146-146:
Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free

🪛 GitHub Actions: CI Pipeline
src/middlewares/metrics.middleware.ts

[warning] 9-9: ESLint: 'err' is defined but never used. (no-unused-vars)


[warning] 21-21: ESLint: 'err' is defined but never used. (no-unused-vars)

src/routes/metrics.routes.ts

[error] 54-54: ESLint: Unexpected any. Specify a different type. (no-explicit-any)

🔇 Additional comments (7)
src/routes/health.routes.ts (3)

3-6: LGTM! Imports are appropriate for the enhanced health checks.

The added imports support the new functionality:

  • Redis and Stellar config for dependency checks
  • Node.js built-ins for system resource monitoring

99-99: Database latency measurement implemented correctly.

Good addition of latency tracking for the database health check. The implementation is clean and follows a simple pattern.

Also applies to: 107-109


237-239: Critical: CPU percentage calculation is incorrect.

The CPU usage calculation has a unit conversion error. process.cpuUsage() returns values in microseconds, but the calculation incorrectly divides by 1000 to convert to milliseconds, then divides by milliseconds again.

Fix the calculation:

-    const cpuPercent =
-      ((cpuUsage.user + cpuUsage.system) / 1000 /* to ms */) /
-      (uptimeSec * 1000 * cores);
+    // cpuUsage is in microseconds, convert to seconds
+    const cpuPercent =
+      ((cpuUsage.user + cpuUsage.system) / 1_000_000) /
+      (uptimeSec * cores);

The correct formula should:

  1. Convert CPU usage from microseconds to seconds (divide by 1,000,000)
  2. Divide by total available CPU time (uptime × cores)

Likely an incorrect or invalid review comment.

src/app.ts (3)

12-12: Metrics imports properly integrated.

The new metrics middleware and routes imports follow the existing code structure and naming conventions.

Also applies to: 30-30


90-90: Metrics middleware correctly positioned in the middleware chain.

Good placement after rate limiting and request logging - this ensures metrics capture only legitimate requests that pass rate limiting.


174-174: Require authentication for /metrics endpoints or confirm mount-level protection.

src/routes/metrics.routes.ts exposes /summary, /historical, /prometheus, and /dashboard and contains no auth imports or router.use middleware; verify src/app.ts (mount at app.use("/metrics", metricsRoutes) — src/app.ts:174) applies authentication/authorization when mounting. If not, protect these endpoints (especially /prometheus) with auth, IP allowlist or mTLS and limit access to internal/prometheus scrapers.

package.json (1)

29-29: Approve dependency additions
The added dependencies align with metrics objectives:

  • check-disk-space ^3.4.0 is fully async and Node.js 16+ compatible.
  • prom-client ^15.1.3 is the latest stable version.

Comment thread src/routes/health.routes.ts Outdated
Comment thread src/routes/metrics.routes.ts
Comment thread src/services/MetricsService.ts
Comment thread src/services/MetricsService.ts
Comment thread src/services/MetricsService.ts
Comment thread src/services/MetricsService.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/services/MetricsService.ts (2)

14-16: Export TimeSeriesPoint for downstream typing
Routes consuming getHistorical may want this type; exporting improves DX without behavior change.

-type TimeSeriesPoint = { ts: number; value: Numeric };
+export type TimeSeriesPoint = { ts: number; value: Numeric };

264-270: Consider ring buffer to avoid O(n) shift() on large series
Current shifting can be costly under high volume. A fixed-size circular buffer would reduce GC/CPU overhead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6cc4b5 and 2be709e.

📒 Files selected for processing (3)
  • src/routes/health.routes.ts (3 hunks)
  • src/routes/metrics.routes.ts (1 hunks)
  • src/services/MetricsService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/routes/metrics.routes.ts
  • src/routes/health.routes.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/services/MetricsService.ts (3)
src/services/ConfigurationService.ts (1)
  • configurationService (780-780)
src/config/redisConfig.ts (1)
  • redisClient (78-78)
src/utils/mailer.ts (1)
  • sendEmail (11-31)
🪛 GitHub Check: build-and-test
src/services/MetricsService.ts

[failure] 309-309:
Unnecessary escape character: "


[warning] 152-152:
'err' is defined but never used

🪛 GitHub Actions: CI Pipeline
src/services/MetricsService.ts

[warning] 152-152: 'err' is defined but never used (no-unused-vars)


[error] 309-309: Unnecessary escape character: " (no-useless-escape)

🔇 Additional comments (2)
src/services/MetricsService.ts (2)

93-101: Use stable route patterns to prevent label cardinality blow‑ups
Using req.originalUrl/req.url can include querystrings and IDs. Prefer route pattern (optionally with baseUrl), then fall back to req.path.

-    const route = req.route?.path || req.originalUrl || req.url || "unknown";
+    // Use stable route pattern (baseUrl + route.path) or fallback to path (no query)
+    const routePattern = ((req.baseUrl ?? "") + (req.route?.path?.toString?.() ?? "")).toString();
+    const route = routePattern || req.path || "unknown";

214-219: No action needed: production Node v24.3.0 includes global fetch support—polyfill isn’t required.

Comment thread src/services/MetricsService.ts
Comment thread src/services/MetricsService.ts Outdated
Comment thread src/services/MetricsService.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (7)
src/services/MetricsService.ts (7)

114-121: Throughput calculation is O(n) per request; make it O(1) with 60 buckets

Maintaining and shifting a timestamp array each request will hurt under load. Track counts in a 60‑slot ring buffer keyed by current second.

Example (fields to add):

+  private throughputBuckets: number[] = Array(60).fill(0);
+  private lastBucketSec = 0;

Update in observeRequest:

-    const now = Date.now();
-    this.lastRequestsTimestamps.push(now);
-    // keep only last 60s
-    const cutoff = now - 60_000;
-    while (this.lastRequestsTimestamps.length && this.lastRequestsTimestamps[0] < cutoff) {
-      this.lastRequestsTimestamps.shift();
-    }
+    const now = Date.now();
+    const sec = Math.floor(now / 1000);
+    if (sec !== this.lastBucketSec) {
+      // zero buckets between last and current second
+      const steps = Math.min(60, sec - this.lastBucketSec);
+      for (let i = 1; i <= steps; i++) this.throughputBuckets[(sec - i) % 60] = 0;
+      this.lastBucketSec = sec;
+    }
+    this.throughputBuckets[sec % 60] += 1;

Then in getSnapshot:

-    const throughputRps = this.lastRequestsTimestamps.filter((t) => t >= cutoff).length / 60;
+    const throughputRps = this.throughputBuckets.reduce((a, b) => a + b, 0) / 60;

131-147: CPU percent computed since process start; switch to interval‑based sampling

Using lifetime CPU time skews percent down over long uptimes. Track deltas between samples for accuracy.

Add fields:

+  private lastCpu = process.cpuUsage();
+  private lastCpuHr = process.hrtime.bigint();

Replace CPU calculation:

-    const cpuUsage = process.cpuUsage();
-    const cores = os.cpus().length || 1;
-    const cpuPercent = (((cpuUsage.user + cpuUsage.system) / 1000) / (uptimeSec * 1000 * cores)) * 100;
+    const diff = process.cpuUsage(this.lastCpu); // microseconds since last sample
+    const nowHr = process.hrtime.bigint();
+    const elapsedUs = Number(nowHr - this.lastCpuHr) / 1000;
+    const cores = os.cpus().length || 1;
+    const cpuPercent = ((diff.user + diff.system) / (elapsedUs * cores)) * 100;
+    this.lastCpu = process.cpuUsage();
+    this.lastCpuHr = nowHr;

167-178: Memory alert uses heap ratio; consider RSS vs system memory

Heap ratio can under‑report memory pressure (native memory excluded). Prefer rss / os.totalmem() for a host‑level signal, or make the basis configurable.

-    const memPercent = (resources.memory.heapUsed / resources.memory.heapTotal) * 100;
+    const memPercent = (resources.memory.rss / os.totalmem()) * 100;

190-224: Timeouts are hard‑coded; derive from config and cancel DB if supported

Reuse existing thresholds or add config keys for DB/Redis/Stellar timeouts. If your DB driver supports per‑query timeout, prefer that over Promise.race to avoid orphan queries.

-    const deadline = (ms: number) => new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));
+    const deadline = (ms: number) => new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));
+    const dbTimeoutMs = Number(await configurationService.getConfig("METRICS_DB_TIMEOUT_MS", "1500"));
+    const redisTimeoutMs = Number(await configurationService.getConfig("METRICS_REDIS_TIMEOUT_MS", "800"));
+    const stellarTimeoutMs = Number(await configurationService.getConfig("METRICS_STELLAR_TIMEOUT_MS", "1500"));
@@
-      await Promise.race([AppDataSource.query("SELECT 1"), deadline(1500)]);
+      // Prefer driver-level timeout if available; fallback to race
+      await Promise.race([AppDataSource.query("SELECT 1"), deadline(dbTimeoutMs)]);
@@
-      const pingResult = await Promise.race([redisClient.ping(), deadline(800)]);
+      const pingResult = await Promise.race([redisClient.ping(), deadline(redisTimeoutMs)]);
@@
-      const t = setTimeout(() => controller.abort(), 1500);
+      const t = setTimeout(() => controller.abort(), stellarTimeoutMs);

332-344: Expose up/down gauges for external deps

Add Prometheus *_up gauges (1/0) so alerting doesn’t rely solely on latency.

     push(`# HELP paystell_db_latency_ms Database query latency in ms`);
     push(`# TYPE paystell_db_latency_ms gauge`);
     push(`paystell_db_latency_ms ${snapshot.external.database.latencyMs}`);
+    push(`# HELP paystell_db_up Database availability (1=up, 0=down)`);
+    push(`# TYPE paystell_db_up gauge`);
+    push(`paystell_db_up ${snapshot.external.database.status === "OK" ? 1 : 0}`);
@@
     push(`# HELP paystell_redis_latency_ms Redis ping latency in ms`);
     push(`# TYPE paystell_redis_latency_ms gauge`);
     push(`paystell_redis_latency_ms ${snapshot.external.redis.latencyMs}`);
+    push(`# HELP paystell_redis_up Redis availability (1=up, 0=down)`);
+    push(`# TYPE paystell_redis_up gauge`);
+    push(`paystell_redis_up ${snapshot.external.redis.status === "OK" ? 1 : 0}`);
@@
     push(`# HELP paystell_stellar_latency_ms Stellar Horizon latency in ms`);
     push(`# TYPE paystell_stellar_latency_ms gauge`);
     push(`paystell_stellar_latency_ms ${snapshot.external.stellar.latencyMs}`);
+    push(`# HELP paystell_stellar_up Stellar availability (1=up, 0=down)`);
+    push(`# TYPE paystell_stellar_up gauge`);
+    push(`paystell_stellar_up ${snapshot.external.stellar.status === "OK" ? 1 : 0}`);

401-406: Await triggerAlert to preserve ordering and error handling

Without await, errors in triggerAlert won’t be caught here and rate limiting may race.

-      if (await predicate()) this.triggerAlert(key, message);
+      if (await predicate()) await this.triggerAlert(key, message);

308-373: Consider using prom-client for exposition to reduce maintenance

You already added prom-client in the PR. Using it to register gauges/counters will avoid manual 0.0.4 text formatting and edge cases long‑term.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2be709e and 8181774.

📒 Files selected for processing (1)
  • src/services/MetricsService.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/services/MetricsService.ts (2)
src/services/ConfigurationService.ts (1)
  • configurationService (780-780)
src/utils/mailer.ts (1)
  • sendEmail (11-31)
🪛 GitHub Actions: CI Pipeline
src/services/MetricsService.ts

[warning] 1-1: Prettier formatting issues detected. Run 'prettier --write' to fix code style in this file.

🔇 Additional comments (2)
src/services/MetricsService.ts (2)

102-106: Avoid unbounded label cardinality for route keys

Still uses req.originalUrl/req.url which can include query strings and IDs. Prefer stable route pattern or req.path (no query), combined with req.baseUrl. This was flagged previously.

Apply:

-    const route = req.route?.path || req.originalUrl || req.url || "unknown";
-    const method = req.method;
-    const status = res.statusCode;
-    const key: RouteKey = `${method} ${route}`;
+    // Prefer stable route patterns to avoid cardinality blowup
+    const routePattern = (req.baseUrl ?? "") + (req.route?.path ?? req.path ?? "");
+    const route = routePattern || "unknown";
+    const method = req.method;
+    const status = res.statusCode;
+    const key: RouteKey = `${method} ${route}`;

1-1: Prettier formatting verified
All files conform to Prettier style; CI Prettier warning is resolved.

Comment thread src/services/MetricsService.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/services/MetricsService.ts (2)

179-193: Windows mount path should be 'C:/' not 'C:'.

Based on learnings: check-disk-space expects a root path with trailing slash on Windows (e.g., C:/). Using C: may cause the library to fail or return incorrect results.

Apply this diff:

-      const mount = process.platform === "win32" ? "C:" : "/";
+      const mount = process.platform === "win32" ? "C:/" : "/";

Additionally, consider making the mount path configurable via environment variable to support different drive letters or mount points in CI/dev environments.


111-129: Route key still includes query strings → unbounded cardinality.

Line 114 uses req.originalUrl || req.url as fallbacks, which can include query parameters and dynamic IDs (e.g., /users/123?filter=active). This creates unbounded label cardinality in Prometheus metrics and bloats memory with per-request route keys.

Apply this diff to prefer stable route patterns:

-    const route = req.route?.path || req.originalUrl || req.url || "unknown";
+    // Prefer stable route patterns to avoid cardinality explosion
+    const route = req.route?.path || req.path || "unknown";

Note: req.path excludes query strings, unlike req.originalUrl and req.url.

🧹 Nitpick comments (3)
src/routes/health.routes.ts (1)

246-249: CPU calculation reports average since process start, not current usage.

The CPU percentage calculation uses cumulative process.cpuUsage() values divided by total uptime, yielding average CPU usage since the process started rather than current instantaneous usage. This may confuse operators expecting real-time CPU load.

Consider documenting this behavior in the API response or Swagger docs, or implement a time-windowed calculation that samples CPU usage over the last N seconds to provide more actionable current usage data. If you prefer the current approach, add a clarifying field like "cpuPercentSinceStart" instead of "cpuPercent".

src/services/MetricsService.ts (1)

158-227: CPU calculation reports average since process start, not current usage.

The CPU percentage calculation (lines 163-165) uses cumulative process.cpuUsage() values divided by total uptime, yielding average CPU usage since process start. This differs from real-time CPU load that operators typically expect for alerting and dashboards.

Consider implementing time-windowed CPU sampling (e.g., measure delta over the last 5-10 seconds) or clarify in metrics naming/docs that this is a lifetime average. If you keep the current approach, rename the metric to cpu_percent_lifetime_avg for clarity.

package.json (1)

29-29: Specify Node.js engines field in package.json
CI enforces Node.js v18 (≥16), satisfying check-disk-space@3.4.0’s requirement; add an "engines": { "node": ">=16" } entry to package.json for clarity and upstream tooling support.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8181774 and 3bc2d57.

📒 Files selected for processing (6)
  • .prettierrc (1 hunks)
  • package.json (3 hunks)
  • src/middlewares/metrics.middleware.ts (1 hunks)
  • src/routes/health.routes.ts (3 hunks)
  • src/routes/metrics.routes.ts (1 hunks)
  • src/services/MetricsService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/routes/metrics.routes.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/middlewares/metrics.middleware.ts (2)
src/utils/subscriptionScheduler.ts (1)
  • start (14-29)
src/services/MetricsService.ts (1)
  • metricsService (524-524)
src/routes/health.routes.ts (1)
src/config/redisConfig.ts (1)
  • redisClient (78-78)
src/services/MetricsService.ts (3)
src/services/ConfigurationService.ts (1)
  • configurationService (780-780)
src/config/redisConfig.ts (1)
  • redisClient (78-78)
src/utils/mailer.ts (1)
  • sendEmail (11-31)
🪛 GitHub Check: build-and-test
src/middlewares/metrics.middleware.ts

[warning] 25-25:
'err' is defined but never used


[warning] 13-13:
'err' is defined but never used

🔇 Additional comments (7)
src/routes/health.routes.ts (2)

95-116: LGTM! Latency measurement added correctly.

The /db endpoint now properly measures and reports query latency, providing valuable observability for database performance monitoring.


175-237: LGTM! Robust per-dependency health checks.

The /dependencies endpoint now implements separate try-catch blocks for each dependency check (Stellar, Redis, Database) with individual latency measurements. This addresses the previously flagged fragile error identification logic and provides granular failure information.

src/middlewares/metrics.middleware.ts (2)

10-16: LGTM! Error swallowing is appropriate here.

The middleware correctly swallows errors from observeRequest to prevent metrics collection failures from impacting the request lifecycle. The unused catch binding flagged by static analysis is intentional and acceptable in this context.


21-28: LGTM! Periodic collector implementation is correct.

The periodic collector safely invokes system and external status checks with error swallowing, ensuring that failures in metrics collection don't crash the scheduler.

src/services/MetricsService.ts (3)

229-305: LGTM! External checks now have proper timeouts.

The implementation correctly wraps DB, Redis, and Stellar checks with bounded timeouts using Promise.race and AbortController, preventing the endpoints from hanging under failure scenarios. Latency measurements and error handling are appropriate.


393-473: LGTM! Prometheus exposition format is correct.

The Prometheus metrics exposition properly escapes label values (lines 398-403) and follows the 0.0.4 format specification with HELP and TYPE comments. The label value escaping handles backslashes, quotes, newlines, and carriage returns correctly.


476-521: LGTM! Alert rate limiting and email delivery are sound.

The alert system implements proper rate limiting (10-minute minimum interval per alert key) and gracefully handles email failures without crashing. The use of configurable recipient lists and structured error logging is appropriate.

@respp respp merged commit e12c7bf into PayStell:main Oct 6, 2025
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants