Skip to content

feat : Add health endpoint#175

Merged
thgO-O merged 6 commits intobtcpayserver:masterfrom
teamssUTXO:master
Mar 25, 2026
Merged

feat : Add health endpoint#175
thgO-O merged 6 commits intobtcpayserver:masterfrom
teamssUTXO:master

Conversation

@teamssUTXO
Copy link
Contributor

@teamssUTXO teamssUTXO commented Mar 21, 2026

Related to #163 .

  • The health endpoint returns 503 when startup is incomplete or a critical dependency is unavailable.
  • Returns 200 when the service is fully operational.

Can be requested as an API (returns JSON) :

health_json

Or as a page (health page styled after BTCPay Server error pages) :

Healthy Unhealthy
healthy_page unhealthy_page

Do you want me to add health endpoint to tests ?

Looking forwards your feedback :)

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'auto_resolve_threads'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3ae647b5-0b9b-4a4b-bdd5-3e747b12d55d

📥 Commits

Reviewing files that changed from the base of the PR and between f100113 and 3605f18.

⛔ Files ignored due to path filters (1)
  • PluginBuilder/wwwroot/img/healthpage/up-uncleok.jpg is excluded by !**/*.jpg
📒 Files selected for processing (3)
  • PluginBuilder/Controllers/HomeController.cs
  • PluginBuilder/Services/HealthService.cs
  • PluginBuilder/Views/Home/HealthPage.cshtml
✅ Files skipped from review due to trivial changes (1)
  • PluginBuilder/Views/Home/HealthPage.cshtml
🚧 Files skipped from review as they are similar to previous changes (2)
  • PluginBuilder/Services/HealthService.cs
  • PluginBuilder/Controllers/HomeController.cs

Walkthrough

Adds a health-check subsystem and a /health endpoint. Introduces HealthService implementing IHealthCheck that checks database, Docker, and Azure storage (and guards on application startup). Registers the health check and HealthService in DI. Adds CheckHealth action to HomeController with content negotiation (JSON or HealthPage view). Hosted startup services (Database, Docker, Azure) now expose/reset static completion and error properties. Adds AzureStorageClient.IsDefaultContainerAccessible, a HealthCheckViewModel, a HealthPage Razor view, and includes health page images in the project file.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: add plugins video #150 — Modifies Docker startup/build behavior in the same DockerStartupHostedService area; likely relevant to startup/build changes.

Suggested reviewers

  • thgO-O
  • rockstardev
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat : Add health endpoint' directly and clearly summarizes the main change—adding a health endpoint to the application.
Description check ✅ Passed The description is fully related to the changeset, providing context about the health endpoint's functionality, HTTP status codes, response formats, and visual examples of both API and UI modes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

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 (4)
PluginBuilder/HostedServices/DockerStartupHostedService.cs (1)

8-27: Consider moving startup state to the hosted service class.

The startup state properties and helpers are placed on DockerStartupException rather than DockerStartupHostedService, unlike the Database and Azure hosted services which keep their state directly on the service class. This creates an inconsistency and reduces cohesion (the exception class shouldn't own startup state).

♻️ Suggested refactor

Move the static properties and methods from DockerStartupException to DockerStartupHostedService:

 public class DockerStartupException : Exception
 {
     public DockerStartupException(string message) : base(message)
     {
     }
-
-    public static bool DockerStartupCompleted { get; private set; }
-    public static Exception? DockerStartupError { get; private set; }
-
-    public static void ResetStartupState()
-    {
-        DockerStartupCompleted = false;
-        DockerStartupError = null;
-    }
-
-    public static void MarkStartupCompleted()
-    {
-        DockerStartupCompleted = true;
-    }
-
-    public static void MarkStartupFailed(Exception ex)
-    {
-        DockerStartupError = ex;
-        DockerStartupCompleted = false;
-    }
 }

 public class DockerStartupHostedService : IHostedService
 {
+    public static bool DockerStartupCompleted { get; private set; }
+    public static Exception? DockerStartupError { get; private set; }
+
     // ... existing code, update method calls to use 'this' class

Then update HealthService.cs to use:

-using static PluginBuilder.HostedServices.DockerStartupException;
+using static PluginBuilder.HostedServices.DockerStartupHostedService;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PluginBuilder/HostedServices/DockerStartupHostedService.cs` around lines 8 -
27, Move the startup state static properties and helper methods
(DockerStartupCompleted, DockerStartupError, ResetStartupState,
MarkStartupCompleted, MarkStartupFailed) out of the DockerStartupException class
and into the DockerStartupHostedService class; update any callers (e.g.,
HealthService) to reference DockerStartupHostedService.DockerStartupCompleted
and DockerStartupHostedService.DockerStartupError instead of
DockerStartupException, and remove the state members from DockerStartupException
so it only represents an exception.
PluginBuilder/Services/AzureStorageClient.cs (1)

65-79: Pass the cancellation token from the health check.

The IsDefaultContainerAccessible method correctly implements a timeout via linked CancellationTokenSource, but the caller in HealthService (line 34) doesn't pass the cancellationToken. If the health check is cancelled, the Azure probe will continue running until its 15-second timeout.

♻️ Proposed fix

In PluginBuilder/Services/HealthService.cs, update line 34:

-var azureTask = AzureStorageClient.IsDefaultContainerAccessible();
+var azureTask = AzureStorageClient.IsDefaultContainerAccessible(cancellationToken);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PluginBuilder/Services/AzureStorageClient.cs` around lines 65 - 79, The
health-check cancellation isn't propagated: update the caller in HealthService
so it passes its CancellationToken into
AzureStorageClient.IsDefaultContainerAccessible(cancellationToken) instead of
calling it without parameters; this ensures the linked CancellationTokenSource
timeout is preempted when the health check is cancelled. Locate the call to
IsDefaultContainerAccessible in HealthService and forward the existing
cancellationToken parameter to that method.
PluginBuilder/Services/HealthService.cs (1)

60-77: Remove unused OutputCapture instances.

The OutputCapture objects capture output that is never read. While the overhead is minimal, removing them simplifies the code.

♻️ Proposed fix
 private async Task<bool> IsDockerHealthy(CancellationToken cancellationToken)
 {
     try
     {
         var code = await ProcessRunner.RunAsync(new ProcessSpec
         {
             Executable = "docker",
-            Arguments = ["info", "--format", "{{ .ServerVersion }}"],
-            OutputCapture = new OutputCapture(),
-            ErrorCapture = new OutputCapture()
+            Arguments = ["info", "--format", "{{ .ServerVersion }}"]
         }, cancellationToken);
         return code == 0;
     }
     catch
     {
         return false;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PluginBuilder/Services/HealthService.cs` around lines 60 - 77, The
IsDockerHealthy method is passing unused OutputCapture instances into
ProcessSpec; remove those unused captures by omitting the OutputCapture and
ErrorCapture properties when constructing the ProcessSpec passed to
ProcessRunner.RunAsync (or set them to null if the type requires it) so
ProcessSpec only includes Executable and Arguments; keep the rest of the logic
(ProcessRunner.RunAsync call, arguments, cancellationToken check and return code
comparison) unchanged.
PluginBuilder/Views/Home/HealthPage.cshtml (1)

55-58: Redundant @ prefix inside Razor code block.

Inside the @if (...) code block, you don't need the @ prefix to reference Model.Description. This works but is non-idiomatic.

✨ Suggested fix
-            `@if` (!string.IsNullOrWhiteSpace(`@Model.Description`))
+            `@if` (!string.IsNullOrWhiteSpace(Model.Description))
             {
                 <p>Error: <code>@Model.Description</code></p>
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PluginBuilder/Views/Home/HealthPage.cshtml` around lines 55 - 58, The Razor
view uses a redundant '@' before Model.Description inside the `@if` (...) code
block; remove the extra '@' so the line reads <p>Error:
<code>@Model.Description</code></p> only using @ once to render the value, i.e.,
change the reference inside the block from `@Model.Description` to
Model.Description while keeping the outer @ on the <code> element; update the
HealthPage.cshtml `@if` block accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@PluginBuilder/Controllers/HomeController.cs`:
- Around line 806-820: The health response maps report.Status to result.status
using HealthStatus.Healthy only, but the HTTP response checks only
HealthStatus.Unhealthy, causing Degraded to yield body status "DOWN" with HTTP
200; update the logic so mapping and HTTP code align: change the status
assignment (result.status) to explicitly handle HealthStatus.Degraded (e.g., map
Degraded to "DOWN" or "UP" per desired policy) and update the response decision
(the conditional that returns StatusCode(...) vs Ok(...)) to treat
HealthStatus.Degraded the same as whichever payload status you choose; refer to
the variables result.status and report.Status and the return that currently
checks HealthStatus.Unhealthy to make the two consistent.

---

Nitpick comments:
In `@PluginBuilder/HostedServices/DockerStartupHostedService.cs`:
- Around line 8-27: Move the startup state static properties and helper methods
(DockerStartupCompleted, DockerStartupError, ResetStartupState,
MarkStartupCompleted, MarkStartupFailed) out of the DockerStartupException class
and into the DockerStartupHostedService class; update any callers (e.g.,
HealthService) to reference DockerStartupHostedService.DockerStartupCompleted
and DockerStartupHostedService.DockerStartupError instead of
DockerStartupException, and remove the state members from DockerStartupException
so it only represents an exception.

In `@PluginBuilder/Services/AzureStorageClient.cs`:
- Around line 65-79: The health-check cancellation isn't propagated: update the
caller in HealthService so it passes its CancellationToken into
AzureStorageClient.IsDefaultContainerAccessible(cancellationToken) instead of
calling it without parameters; this ensures the linked CancellationTokenSource
timeout is preempted when the health check is cancelled. Locate the call to
IsDefaultContainerAccessible in HealthService and forward the existing
cancellationToken parameter to that method.

In `@PluginBuilder/Services/HealthService.cs`:
- Around line 60-77: The IsDockerHealthy method is passing unused OutputCapture
instances into ProcessSpec; remove those unused captures by omitting the
OutputCapture and ErrorCapture properties when constructing the ProcessSpec
passed to ProcessRunner.RunAsync (or set them to null if the type requires it)
so ProcessSpec only includes Executable and Arguments; keep the rest of the
logic (ProcessRunner.RunAsync call, arguments, cancellationToken check and
return code comparison) unchanged.

In `@PluginBuilder/Views/Home/HealthPage.cshtml`:
- Around line 55-58: The Razor view uses a redundant '@' before
Model.Description inside the `@if` (...) code block; remove the extra '@' so the
line reads <p>Error: <code>@Model.Description</code></p> only using @ once to
render the value, i.e., change the reference inside the block from
`@Model.Description` to Model.Description while keeping the outer @ on the <code>
element; update the HealthPage.cshtml `@if` block accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 79042ec3-a5b2-467b-bceb-9a92e1e8a322

📥 Commits

Reviewing files that changed from the base of the PR and between 40a65e7 and 6ad6c0a.

⛔ Files ignored due to path filters (1)
  • PluginBuilder/wwwroot/img/healthpage/up-successkid.jpg is excluded by !**/*.jpg
📒 Files selected for processing (10)
  • PluginBuilder/Controllers/HomeController.cs
  • PluginBuilder/HostedServices/AzureStartupHostedService.cs
  • PluginBuilder/HostedServices/DatabaseStartupHostedService.cs
  • PluginBuilder/HostedServices/DockerStartupHostedService.cs
  • PluginBuilder/PluginBuilder.csproj
  • PluginBuilder/Program.cs
  • PluginBuilder/Services/AzureStorageClient.cs
  • PluginBuilder/Services/HealthService.cs
  • PluginBuilder/ViewModels/Home/HealthCheckViewModel.cs
  • PluginBuilder/Views/Home/HealthPage.cshtml

Copy link
Collaborator

@thgO-O thgO-O left a comment

Choose a reason for hiding this comment

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

Nice job, bro!

@teamssUTXO
Copy link
Contributor Author

Nice job, bro!

Thanks, I fixed what you commented. That is the new health page :

uncle-health-paeg

@thgO-O thgO-O merged commit b6d1e12 into btcpayserver:master Mar 25, 2026
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.

2 participants