Skip to content

Commit b9ea645

Browse files
author
Alex J Lennon
committed
Add health check endpoint
- Add HealthCheck class to track application health metrics: - MQTT connection status - Last update time - Beacon count - Total nodes processed - Uptime - Version information - Add HealthCheckServer using HttpListener: - Lightweight HTTP server for /health endpoint - Returns JSON health status (200 for healthy, 503 for degraded) - Configurable port (default: 8080) - Add IsConnected() method to MQTTControl for health checks - Update UWBManager to track health metrics on each update - Add HealthCheckPort to ApplicationConfig (default: 8080) - Health check endpoint available at http://localhost:8080/health Features: - Returns status: 'healthy' or 'degraded' - MQTT connection status - Time since last update - Beacon and node counts - Application uptime - Version information Useful for: - Kubernetes/Docker health checks - Monitoring and alerting - Operational visibility
1 parent 0d58270 commit b9ea645

File tree

7 files changed

+274
-1
lines changed

7 files changed

+274
-1
lines changed

src/AppConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class ApplicationConfig
5656
{
5757
public int UpdateIntervalMs { get; set; } = 10;
5858
public string LogLevel { get; set; } = "Information";
59+
public int HealthCheckPort { get; set; } = 8080;
5960
}
6061

6162
public class AlgorithmConfig

src/HealthCheck.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text.Json;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace InstDotNet;
8+
9+
/// <summary>
10+
/// Health check service that tracks application health status
11+
/// </summary>
12+
public static class HealthCheck
13+
{
14+
private static ILogger? _logger;
15+
private static DateTime _lastUpdateTime = DateTime.MinValue;
16+
private static int _beaconCount = 0;
17+
private static int _totalNodesProcessed = 0;
18+
private static DateTime _startTime = DateTime.UtcNow;
19+
20+
/// <summary>
21+
/// Initialize the health check service
22+
/// </summary>
23+
public static void Initialize(ILogger? logger = null)
24+
{
25+
_logger = logger;
26+
_startTime = DateTime.UtcNow;
27+
_logger?.LogDebug("Health check service initialized");
28+
}
29+
30+
/// <summary>
31+
/// Update the last processing time
32+
/// </summary>
33+
public static void UpdateLastProcessTime()
34+
{
35+
_lastUpdateTime = DateTime.UtcNow;
36+
}
37+
38+
/// <summary>
39+
/// Update beacon count
40+
/// </summary>
41+
public static void UpdateBeaconCount(int count)
42+
{
43+
_beaconCount = count;
44+
}
45+
46+
/// <summary>
47+
/// Increment total nodes processed
48+
/// </summary>
49+
public static void IncrementNodesProcessed(int count = 1)
50+
{
51+
_totalNodesProcessed += count;
52+
}
53+
54+
/// <summary>
55+
/// Get current health status
56+
/// </summary>
57+
public static HealthStatus GetStatus()
58+
{
59+
var mqttConnected = MQTTControl.IsConnected();
60+
var uptime = DateTime.UtcNow - _startTime;
61+
var timeSinceLastUpdate = _lastUpdateTime == DateTime.MinValue
62+
? TimeSpan.Zero
63+
: DateTime.UtcNow - _lastUpdateTime;
64+
65+
return new HealthStatus
66+
{
67+
Status = mqttConnected && timeSinceLastUpdate.TotalSeconds < 60 ? "healthy" : "degraded",
68+
MqttConnected = mqttConnected,
69+
LastUpdateTime = _lastUpdateTime == DateTime.MinValue ? null : _lastUpdateTime,
70+
TimeSinceLastUpdate = timeSinceLastUpdate.TotalSeconds,
71+
BeaconCount = _beaconCount,
72+
TotalNodesProcessed = _totalNodesProcessed,
73+
UptimeSeconds = uptime.TotalSeconds,
74+
Version = VersionInfo.FullVersion
75+
};
76+
}
77+
78+
/// <summary>
79+
/// Get health status as JSON string
80+
/// </summary>
81+
public static string GetStatusJson()
82+
{
83+
var status = GetStatus();
84+
var options = new JsonSerializerOptions
85+
{
86+
WriteIndented = true,
87+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
88+
};
89+
return JsonSerializer.Serialize(status, options);
90+
}
91+
}
92+
93+
/// <summary>
94+
/// Health status model
95+
/// </summary>
96+
public class HealthStatus
97+
{
98+
public string Status { get; set; } = "unknown";
99+
public bool MqttConnected { get; set; }
100+
public DateTime? LastUpdateTime { get; set; }
101+
public double TimeSinceLastUpdate { get; set; }
102+
public int BeaconCount { get; set; }
103+
public int TotalNodesProcessed { get; set; }
104+
public double UptimeSeconds { get; set; }
105+
public string Version { get; set; } = string.Empty;
106+
}

src/HealthCheckServer.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#nullable enable
2+
using System;
3+
using System.IO;
4+
using System.Net;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace InstDotNet;
11+
12+
/// <summary>
13+
/// HTTP server for health check endpoint
14+
/// </summary>
15+
public static class HealthCheckServer
16+
{
17+
private static HttpListener? _listener;
18+
private static Task? _serverTask;
19+
private static CancellationTokenSource? _cts;
20+
private static ILogger? _logger;
21+
private static int _port = 8080;
22+
23+
/// <summary>
24+
/// Start the health check HTTP server
25+
/// </summary>
26+
public static void Start(int port = 8080, ILogger? logger = null)
27+
{
28+
_port = port;
29+
_logger = logger;
30+
_cts = new CancellationTokenSource();
31+
32+
_listener = new HttpListener();
33+
_listener.Prefixes.Add($"http://+:{port}/");
34+
35+
try
36+
{
37+
_listener.Start();
38+
_logger?.LogInformation("Health check server started on port {Port}", port);
39+
40+
_serverTask = Task.Run(async () => await ListenAsync(_cts.Token), _cts.Token);
41+
}
42+
catch (Exception ex)
43+
{
44+
_logger?.LogError(ex, "Failed to start health check server on port {Port}", port);
45+
throw;
46+
}
47+
}
48+
49+
/// <summary>
50+
/// Stop the health check server
51+
/// </summary>
52+
public static void Stop()
53+
{
54+
_cts?.Cancel();
55+
_listener?.Stop();
56+
_listener?.Close();
57+
_logger?.LogInformation("Health check server stopped");
58+
}
59+
60+
private static async Task ListenAsync(CancellationToken cancellationToken)
61+
{
62+
while (!cancellationToken.IsCancellationRequested && _listener != null && _listener.IsListening)
63+
{
64+
try
65+
{
66+
var context = await _listener.GetContextAsync().ConfigureAwait(false);
67+
_ = Task.Run(() => HandleRequest(context), cancellationToken);
68+
}
69+
catch (ObjectDisposedException)
70+
{
71+
// Listener was closed, exit gracefully
72+
break;
73+
}
74+
catch (HttpListenerException ex)
75+
{
76+
_logger?.LogWarning(ex, "Health check server error");
77+
if (!_listener.IsListening)
78+
break;
79+
}
80+
catch (Exception ex)
81+
{
82+
_logger?.LogError(ex, "Unexpected error in health check server");
83+
}
84+
}
85+
}
86+
87+
private static void HandleRequest(HttpListenerContext context)
88+
{
89+
try
90+
{
91+
var request = context.Request;
92+
var response = context.Response;
93+
94+
// Only handle GET requests to /health
95+
if (request.HttpMethod == "GET" && request.Url?.AbsolutePath == "/health")
96+
{
97+
var status = HealthCheck.GetStatus();
98+
var json = HealthCheck.GetStatusJson();
99+
100+
// Set status code based on health
101+
response.StatusCode = status.Status == "healthy" ? 200 : 503;
102+
response.ContentType = "application/json";
103+
response.ContentEncoding = Encoding.UTF8;
104+
105+
var buffer = Encoding.UTF8.GetBytes(json);
106+
response.ContentLength64 = buffer.Length;
107+
response.OutputStream.Write(buffer, 0, buffer.Length);
108+
response.OutputStream.Close();
109+
110+
_logger?.LogDebug("Health check request: {Status}", status.Status);
111+
}
112+
else
113+
{
114+
// 404 for other paths
115+
response.StatusCode = 404;
116+
response.Close();
117+
}
118+
}
119+
catch (Exception ex)
120+
{
121+
_logger?.LogError(ex, "Error handling health check request");
122+
try
123+
{
124+
context.Response.StatusCode = 500;
125+
context.Response.Close();
126+
}
127+
catch
128+
{
129+
// Ignore errors when closing response
130+
}
131+
}
132+
}
133+
}
134+

src/MQTTControl.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ public static async Task Initialise(CancellationTokenSource cts, AppConfig? conf
198198
await ConnectWithRetryAsync(options, _cts.Token).ConfigureAwait(false);
199199
}
200200

201+
/// <summary>
202+
/// Check if MQTT client is connected
203+
/// </summary>
204+
public static bool IsConnected()
205+
{
206+
return client != null && client.IsConnected;
207+
}
208+
201209
public static async Task DisconnectAsync()
202210
{
203211
if (client == null)

src/Program.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ static async Task Main()
4646

4747
try
4848
{
49+
// Initialize health check
50+
HealthCheck.Initialize(logger);
51+
52+
// Start health check server
53+
int healthCheckPort = _config.Application.HealthCheckPort;
54+
try
55+
{
56+
HealthCheckServer.Start(healthCheckPort, logger);
57+
logger.LogInformation("Health check endpoint available at http://localhost:{Port}/health", healthCheckPort);
58+
}
59+
catch (Exception ex)
60+
{
61+
logger.LogWarning(ex, "Failed to start health check server on port {Port}, continuing without health endpoint", healthCheckPort);
62+
}
63+
4964
await MQTTControl.Initialise(cts, _config);
5065
UWBManager.Initialise(_config);
5166

@@ -80,6 +95,7 @@ static async Task Main()
8095
try { await Task.Delay(Timeout.Infinite, cts.Token); } catch { }
8196

8297
logger.LogInformation("Shutting down...");
98+
HealthCheckServer.Stop();
8399
MQTTControl.StopReconnect();
84100
await MQTTControl.DisconnectAsync();
85101
AppLogger.Dispose();

src/UWBManager.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#nullable enable
22
using System;
33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Numerics;
56
using System.Text.Json;
67
using System.Threading.Tasks;
@@ -106,6 +107,12 @@ private static void UpdateUwbs()
106107
bool refine = _config?.Algorithm.RefinementEnabled ?? true;
107108
UWB2GPSConverter.ConvertUWBToPositions(network, refine, _config?.Algorithm);
108109

110+
// Update health check metrics
111+
HealthCheck.UpdateLastProcessTime();
112+
var beaconCount = network.uwbs.Count(u => u.positionKnown);
113+
HealthCheck.UpdateBeaconCount(beaconCount);
114+
HealthCheck.IncrementNodesProcessed(network.uwbs.Length);
115+
109116
if (sendUwbsList == null)
110117
{
111118
sendUwbsList = new List<UWB2GPSConverter.UWB>();

src/appsettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
},
2121
"Application": {
2222
"UpdateIntervalMs": 10,
23-
"LogLevel": "Information"
23+
"LogLevel": "Information",
24+
"HealthCheckPort": 8080
2425
},
2526
"Algorithm": {
2627
"MaxIterations": 10,

0 commit comments

Comments
 (0)