Skip to content

feat(observability): add /readyz readiness endpoint checking DB + RPC connectivity - #542

Closed
MissBlue00 wants to merge 2 commits into
TegoLabs:mainfrom
MissBlue00:feat/readyz-endpoint
Closed

MissBlue00 wants to merge 2 commits into
TegoLabs:mainfrom
MissBlue00:feat/readyz-endpoint

Conversation

@MissBlue00

@MissBlue00 MissBlue00 commented Jul 29, 2026 •

Copy link
Copy Markdown

Summary

Adds a /readyz HTTP readiness endpoint that checks SQLite database connectivity and Stellar RPC connectivity, distinct from a liveness check.

Changes

  • src/observability/server.ts — createReadinessServer() creates an HTTP server with a /readyz endpoint that:
    • Runs SELECT 1 against SQLite for DB connectivity
    • Calls StellarRpcClient.getCurrentLedger() with a short timeout for RPC connectivity
    • Returns 200 with {"status":"ok"} when both succeed
    • Returns 503 with {"status":"not ready","checks":{"db":"...","rpc":"..."}} identifying which dependency failed
    • Caches the RPC check result for 5 seconds to avoid hammering the endpoint under frequent polling

Testing

  • 6 new tests covering: success, RPC failure, DB failure, both failures, RPC result caching, and unknown route 404
  • All 1001 tests pass (78 test files)

closes: #338

… connectivity

- Add src/observability/server.ts with createReadinessServer() that starts an HTTP server with a /readyz endpoint
- /readyz runs a trivial SELECT 1 query against SQLite and calls getCurrentLedger() via StellarRpcClient
- Returns 200 when both checks pass, 503 with identifying which check failed otherwise
- Caches RPC check result for 5 seconds to avoid hammering the RPC endpoint under frequent polling
- Add 6 tests covering success, RPC failure, DB failure, both failures, RPC caching, and 404 for unknown routes
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a readiness endpoint that reports database and RPC service health.
    • Returns clear JSON status details, including error information when checks fail.
    • Supports temporary caching of RPC health results for faster repeated checks.
    • Unknown routes now return a standard not-found response.
  • Tests

    • Added coverage for successful checks, individual and combined failures, caching, and invalid routes.

Walkthrough

The change adds a /readyz HTTP endpoint that checks SQLite and Stellar RPC health. RPC results remain cached for five seconds. The server returns readiness status, error details, unknown-route responses, and its bound port.

Changes

Readiness endpoint

Layer / File(s) Summary
Database and RPC health checks
src/observability/server.ts
The server probes SQLite with SELECT 1 and Stellar RPC with getLatestLedger. RPC success results use a five-second cache. Errors are normalized, and resetRpcCache() clears the cache.
Readiness HTTP server and validation
src/observability/server.ts, tests/observability/server.test.ts
GET /readyz returns JSON with status 200 when both checks pass and 503 with error details otherwise. Other routes return 404. Tests cover success, failures, caching, and server cleanup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HTTPClient
  participant ReadinessServer
  participant SQLiteDatabase
  participant StellarRpcClient
  HTTPClient->>ReadinessServer: GET /readyz
  ReadinessServer->>SQLiteDatabase: Execute SELECT 1
  ReadinessServer->>StellarRpcClient: Request getLatestLedger
  ReadinessServer-->>HTTPClient: Return JSON with 200 or 503
Loading

Suggested reviewers: abdulmalikalayande

Poem

I’m a rabbit checking paths,
SQLite hops, RPC chats.
Five-second carrots, neatly stored,
Ready greens or errors poured.
/readyz thumps its little heart.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR meets the main requirements in #338, but it uses getCurrentLedger instead of the specified getLatestLedger method. Use the existing getLatestLedger method from StellarRpcClient, or update issue #338 if getCurrentLedger is the approved API.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new /readyz endpoint and its database and RPC connectivity checks.
Description check ✅ Passed The description directly explains the readiness endpoint, dependency checks, caching behavior, and tests.
Out of Scope Changes check ✅ Passed The changes are limited to the readiness server and directly related tests; no unrelated production or configuration changes appear.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@MissBlue00 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@gitguardian

gitguardian Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/observability/server.ts`:
- Line 13: Scope the readiness cache per RPC client instead of using the shared
rpcCache variable: update createReadinessServer to read and write a WeakMap
keyed by rpcClient (or an equivalent instance-local cache), adjust resetRpcCache
to clear the corresponding cache state, and add coverage proving two distinct
clients are probed independently.
- Around line 36-43: Update checkRpc to race rpcClient.getCurrentLedger()
against a configured bounded timeout; when the deadline expires, return and
cache the failed RPC result using the existing cached-failure fallback instead
of leaving /readyz pending. Preserve successful and handled-error behavior, and
add coverage proving a never-settling getCurrentLedger call causes /readyz to
respond within the configured deadline.

In `@tests/observability/server.test.ts`:
- Around line 76-84: Update the “/readyz caches RPC result for a few seconds”
test to control Date.now(), perform the first request, then advance time by
exactly 5,000 ms before the second request and assert getCurrentLedger is called
twice, while retaining the immediate cache-reuse assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8de54a90-98c2-4a1e-9d0d-78c260a80b8a

📥 Commits

Reviewing files that changed from the base of the PR and between ffc36fc and c777bbd.

📒 Files selected for processing (2)
  • src/observability/server.ts
  • tests/observability/server.test.ts
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.45.0)
src/observability/server.ts

[warning] 60-84: Use https protocol over http
Context: http.createServer(async (req, res) => {
res.setHeader("Content-Type", "application/json");

    if (req.url === "/readyz" && req.method === "GET") {
        const dbResult = checkDb(db);
        const rpcResult = await checkRpc(rpcClient);

        const allOk = dbResult.ok && rpcResult.ok;

        res.statusCode = allOk ? 200 : 503;
        res.end(
            JSON.stringify({
                status: allOk ? "ok" : "not ready",
                checks: {
                    db: dbResult.ok ? "ok" : dbResult.error,
                    rpc: rpcResult.ok ? "ok" : rpcResult.error,
                },
            }),
        );
        return;
    }

    res.statusCode = 404;
    res.end(JSON.stringify({ error: "not found" }));
})

Note: [CWE-319] Cleartext Transmission of Sensitive Information. Security best practice.

(https-protocol-missing-typescript)

timestamp: number;
}

let rpcCache: RpcCacheEntry | null = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope the RPC cache to the RPC client.

rpcCache is shared by all createReadinessServer instances. If one server caches a healthy result for client A, a server that uses client B can return that result without probing client B. This can report readiness for an unhealthy dependency.

Use a WeakMap keyed by rpcClient, or create the cache inside createReadinessServer. Update resetRpcCache and add a test with two distinct RPC clients.

Also applies to: 31-33

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/observability/server.ts` at line 13, Scope the readiness cache per RPC
client instead of using the shared rpcCache variable: update
createReadinessServer to read and write a WeakMap keyed by rpcClient (or an
equivalent instance-local cache), adjust resetRpcCache to clear the
corresponding cache state, and add coverage proving two distinct clients are
probed independently.

Comment on lines +36 to +43
try {
await rpcClient.getCurrentLedger();
rpcCache = { ok: true, timestamp: Date.now() };
return { ok: true };
} catch (e) {
const error = e instanceof Error ? e.message : String(e);
rpcCache = { ok: false, error, timestamp: Date.now() };
return { ok: false, error };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect whether the RPC client already exposes a timeout or cancellation mechanism.
ast-grep outline src/rpc/client.ts --items all --match 'StellarRpcClient'
rg -n -C 3 'getCurrentLedger|AbortSignal|timeout|setTimeout|rpc\.Server' src/rpc/client.ts

Repository: AbdulmalikAlayande/sorokeep

Length of output: 5710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the health endpoint and configuration around readyz and RPC client construction.
wc -l src/observability/server.ts src/rpc/client.ts
sed -n '1,120p' src/observability/server.ts
sed -n '260,330p' src/rpc/client.ts

# Find readiness route and any timeout/env configuration definitions.
rg -n -C 3 '/readyz|readyz|rpcClient|StellarRpcClient|RPC_TIMEOUT|timeout|RACHE|ready' src package.json tsconfig.json .github 2>/dev/null || true

# Search package manifests for Stellar SDK versions to understand available RPC APIs.
rg -n '"(`@stellar/stellar-sdk-stable`|`@stellar/stellar-sdk`|stellar-sdk)"|node version|typescript' package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || true

Repository: AbdulmalikAlayande/sorokeep

Length of output: 44699


Add a bounded RPC health-check timeout.

checkRpc() awaits getCurrentLedger() without a deadline and caches only retryable HTTP errors. A hung getLatestLedger() or getHealth() call can keep /readyz pending past any RPC timeout requirement. Add a bounded timeout that falls back to the cached failed RPC result, and add a test where getCurrentLedger() never settles while /readyz responds within the configured deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/observability/server.ts` around lines 36 - 43, Update checkRpc to race
rpcClient.getCurrentLedger() against a configured bounded timeout; when the
deadline expires, return and cache the failed RPC result using the existing
cached-failure fallback instead of leaving /readyz pending. Preserve successful
and handled-error behavior, and add coverage proving a never-settling
getCurrentLedger call causes /readyz to respond within the configured deadline.

Comment on lines +76 to +84
it("/readyz caches RPC result for a few seconds", async () => {
mockRpcClient.getCurrentLedger.mockResolvedValue(500000);

await fetchUrl(`http://localhost:${port}/readyz`);
expect(mockRpcClient.getCurrentLedger).toHaveBeenCalledTimes(1);

await fetchUrl(`http://localhost:${port}/readyz`);
expect(mockRpcClient.getCurrentLedger).toHaveBeenCalledTimes(1);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test the five-second cache expiration boundary.

This test only verifies immediate cache reuse. A cache that never expires also passes. Advance or mock Date.now() to exactly 5,000 ms after the first request, then verify that the second request calls getCurrentLedger() again.

Proposed test addition
+    it("/readyz refreshes the RPC result after five seconds", async () => {
+        mockRpcClient.getCurrentLedger.mockResolvedValue(500000);
+        const now = vi.spyOn(Date, "now").mockReturnValue(0);
+
+        await fetchUrl(`http://localhost:${port}/readyz`);
+        now.mockReturnValue(5_000);
+        await fetchUrl(`http://localhost:${port}/readyz`);
+
+        expect(mockRpcClient.getCurrentLedger).toHaveBeenCalledTimes(2);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/observability/server.test.ts` around lines 76 - 84, Update the “/readyz
caches RPC result for a few seconds” test to control Date.now(), perform the
first request, then advance time by exactly 5,000 ms before the second request
and assert getCurrentLedger is called twice, while retaining the immediate
cache-reuse assertion.

AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
… connectivity (#542, #338)

PR #542's server.ts (despite a commit claiming to merge current main
in) still fully replaced the real Hono-based server with a separate
createReadinessServer(db, rpcClient, port) running its own plain-http
server — losing /metrics, the bearer-token auth from #573, and the
daemon integration (createMetricsServer/stopMetricsServer) entirely.

Ported the readiness-check logic (DB SELECT 1, cached RPC
getCurrentLedger() check, 503 + per-dependency error detail on
failure) as a /readyz route on the existing shared Hono app instead,
so it automatically gets the bearer-token auth middleware for free:

- server.ts: /readyz route, 5s RPC-check cache (reset whenever
  createMetricsServer is called, matching the activeDb reset pattern),
  createMetricsServer/stopMetricsServer extended with an optional
  rpcClient parameter.
- daemon/loop.ts: constructs a StellarRpcClient from the daemon's own
  network/rpcUrl when starting the metrics server, so /readyz has
  something real to check — no other daemon logic touched.
- Added 4 tests to the existing server.test.ts (success, RPC failure,
  no RPC client configured, caching behavior) using a mocked RPC
  client, matching the PR's own test scenarios.

Verified: tsc clean, full suite 1296/1296, npm audit clean, build
succeeds, and manually smoke-tested both the RPC-configured and
RPC-unconfigured cases end-to-end against the compiled server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@AbdulmalikAlayande

Copy link
Copy Markdown
Collaborator

Thanks — the readiness-check logic itself (DB SELECT 1, cached getCurrentLedger() RPC check, 503 with per-dependency error detail) was correctly designed and matched the issue exactly.

Closing manually rather than merging: despite a commit merging current main in, your server.ts still ended up as a fully separate createReadinessServer(db, rpcClient, port) running its own plain-http server — which would have replaced the real Hono-based server that /metrics, the bearer-token auth from #573, and the daemon's createMetricsServer/stopMetricsServer integration all depend on.

Ported your /readyz logic onto the existing shared Hono app instead (commit 77e037f) — it now gets the bearer-token auth applied automatically, no extra work needed. daemon/loop.ts constructs a StellarRpcClient from its own network/rpcUrl when starting the metrics server so /readyz has something real to check.

Verified: tsc clean, full suite 1296/1296, npm audit clean, build succeeds, manually smoke-tested both the RPC-configured and unconfigured cases end-to-end. Good design work — thanks!

AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
… the HTTP server (#540, #339)

PR #540's registry.ts diff was, again, a complete standalone
reimplementation (computeMetrics/formatPrometheus hand-rolling its own
SQL queries and Prometheus serializer, bypassing prom-client and every
already-registered metric entirely) — the exact same collision pattern
as #543/#566/#591/#542. The issue's own step 1 ("extract the
metric-computation logic... reusable by both the HTTP route and this
command") was actually already satisfied by the real registry.ts —
collectAllMetrics(db) + register.metrics() is exactly that shared
logic, already used by the /metrics route.

Wrote src/commands/metrics.ts fresh against the real registry:
collectAllMetrics(db) then register.metrics() for the default
Prometheus-text output, or register.getMetricsAsJSON() (prom-client's
own structured format) for --json — no new serialization logic
needed. Registered in src/cli/program.ts (the current entry point;
src/index.ts is now just a thin wrapper around createProgram()).

Added tests asserting the command's default output matches what
collectAllMetrics + register.metrics() produce directly for the same
DB state (the acceptance criteria's actual claim), that --json
produces valid parseable JSON with the expected metric/label shape,
and the empty-database case.

Verified: tsc clean, full suite 1300/1300, npm audit clean, build
succeeds, and manually smoke-tested both `sorokeep metrics` and
`sorokeep metrics --json` against the compiled CLI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
… connectivity (#542, #338)

PR #542's server.ts (despite a commit claiming to merge current main
in) still fully replaced the real Hono-based server with a separate
createReadinessServer(db, rpcClient, port) running its own plain-http
server — losing /metrics, the bearer-token auth from #573, and the
daemon integration (createMetricsServer/stopMetricsServer) entirely.

Ported the readiness-check logic (DB SELECT 1, cached RPC
getCurrentLedger() check, 503 + per-dependency error detail on
failure) as a /readyz route on the existing shared Hono app instead,
so it automatically gets the bearer-token auth middleware for free:

- server.ts: /readyz route, 5s RPC-check cache (reset whenever
  createMetricsServer is called, matching the activeDb reset pattern),
  createMetricsServer/stopMetricsServer extended with an optional
  rpcClient parameter.
- daemon/loop.ts: constructs a StellarRpcClient from the daemon's own
  network/rpcUrl when starting the metrics server, so /readyz has
  something real to check — no other daemon logic touched.
- Added 4 tests to the existing server.test.ts (success, RPC failure,
  no RPC client configured, caching behavior) using a mocked RPC
  client, matching the PR's own test scenarios.

Verified: tsc clean, full suite 1296/1296, npm audit clean, build
succeeds, and manually smoke-tested both the RPC-configured and
RPC-unconfigured cases end-to-end against the compiled server.
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
… the HTTP server (#540, #339)

PR #540's registry.ts diff was, again, a complete standalone
reimplementation (computeMetrics/formatPrometheus hand-rolling its own
SQL queries and Prometheus serializer, bypassing prom-client and every
already-registered metric entirely) — the exact same collision pattern
as #543/#566/#591/#542. The issue's own step 1 ("extract the
metric-computation logic... reusable by both the HTTP route and this
command") was actually already satisfied by the real registry.ts —
collectAllMetrics(db) + register.metrics() is exactly that shared
logic, already used by the /metrics route.

Wrote src/commands/metrics.ts fresh against the real registry:
collectAllMetrics(db) then register.metrics() for the default
Prometheus-text output, or register.getMetricsAsJSON() (prom-client's
own structured format) for --json — no new serialization logic
needed. Registered in src/cli/program.ts (the current entry point;
src/index.ts is now just a thin wrapper around createProgram()).

Added tests asserting the command's default output matches what
collectAllMetrics + register.metrics() produce directly for the same
DB state (the acceptance criteria's actual claim), that --json
produces valid parseable JSON with the expected metric/label shape,
and the empty-database case.

Verified: tsc clean, full suite 1300/1300, npm audit clean, build
succeeds, and manually smoke-tested both `sorokeep metrics` and
`sorokeep metrics --json` against the compiled CLI.
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
…le phases (#572, #341)

PR #572's daemon/loop.ts and core/monitor.ts diffs were based on a
stale snapshot of both files, predating everything merged into them
this wave — taking them as-is would have deleted the metrics-port
wiring (#569), daemonCycleDuration/daemonCyclesSkipped instrumentation
(#566), the /readyz RPC client construction (#542), and — critically —
reintroduced a real bug: resetting cycleInFlight inside stopDaemon()
in loop.ts, the exact re-entrance-guard race the current code
deliberately avoids. In monitor.ts, it would have deleted the fan-out
delivery feature (#541) and the per-config enabled check (#580).

Its package.json diff also downgraded @stellar/stellar-sdk and
dropped hono/nodemailer entirely.

Ported the actual tracing work — a self-contained tracing.ts module
(tracer provider setup, OTLP/in-memory exporter config via env vars,
defensive span-error/end helpers) needed no changes and was taken as
written, since the issue's own scope kept it isolated from
registry.ts/metrics. Manually re-applied the span instrumentation
against the current, unmodified control flow of both files:

- daemon/loop.ts: a DaemonCycle parent span wrapping executeCycle,
  with Monitor/Deliver/CostAggregation child spans (renamed from the
  issue's "Auto-Extend" — the third phase actually wraps
  aggregateDailyCostSnapshots; real auto-extension happens inside
  runMonitorCycle's own "Monitor" phase, so labeling it Auto-Extend
  would have been actively misleading). No changes to cycleInFlight,
  stopDaemon, or scheduledTick's skip logic — span calls only.
- core/monitor.ts: a process-contract span per contract inside the
  existing loop, tagged with contract.id. No changes to processContract
  itself (fan-out, enabled check untouched).
- Fixed a real gap in the ported code: initTracing() was never called
  before getTracer() in the original diff, meaning OTLP/in-memory
  exporter configuration would never actually take effect in
  production — getTracer()'s lazy fallback would silently lock in an
  uninstrumented provider on first use. Added the missing call.
- Added tracing tests to the existing loop.test.ts and monitor.test.ts
  suites (not a separate stale-mocked file) verifying the acceptance
  criteria directly: a parent span with Monitor/Deliver/CostAggregation
  children, error status recorded on a failed Monitor span, no
  measurable behavior change when tracing is off, and one
  process-contract span per contract.

Verified: tsc clean, full suite 1304/1304, npm audit clean, build
succeeds, and manually smoke-tested real span creation/parent-child
linking/in-memory export end-to-end against the compiled module.
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.

feat(observability): add /readyz readiness endpoint checking DB + RPC connectivity

2 participants