diff --git a/HealthMonitoring.Client/HealthMonitoring.Client.csproj b/HealthMonitoring.Client/HealthMonitoring.Client.csproj
index b8ca7c0..2479244 100644
--- a/HealthMonitoring.Client/HealthMonitoring.Client.csproj
+++ b/HealthMonitoring.Client/HealthMonitoring.Client.csproj
@@ -11,6 +11,7 @@
+
diff --git a/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs b/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs
index 94c796f..bcc9bf9 100644
--- a/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs
+++ b/HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs
@@ -1,6 +1,7 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace HealthMonitoring.Client;
@@ -38,7 +39,9 @@ public interface IHealthMonitoringRegistrationClient
Task RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable mappings, CancellationToken cancellationToken = default);
}
-public sealed class HealthMonitoringRegistrationClient(HttpClient httpClient) : IHealthMonitoringRegistrationClient
+public sealed class HealthMonitoringRegistrationClient(
+ HttpClient httpClient,
+ ILogger? logger = null) : IHealthMonitoringRegistrationClient
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
@@ -47,7 +50,7 @@ public async Task RegisterAsync(ServiceRegistrationOp
ArgumentNullException.ThrowIfNull(options);
ValidateServiceOptions(options);
- using var response = await httpClient.PostAsJsonAsync("api/services/register", new ServiceRegistrationPayload(
+ var payload = new ServiceRegistrationPayload(
options.ServiceId,
options.Name,
options.HealthUrl.ToString(),
@@ -61,7 +64,19 @@ public async Task RegisterAsync(ServiceRegistrationOp
options.Group,
options.Description,
options.Version,
- options.Host), JsonOptions, cancellationToken);
+ options.Host);
+
+ var loggedPayload = payload with
+ {
+ ConnectionString = string.IsNullOrWhiteSpace(payload.ConnectionString)
+ ? payload.ConnectionString
+ : "[REDACTED]"
+ };
+ logger?.LogInformation(
+ "POST /api/services/register body: {RequestBody}",
+ JsonSerializer.Serialize(loggedPayload, JsonOptions));
+
+ using var response = await httpClient.PostAsJsonAsync("api/services/register", payload, JsonOptions, cancellationToken);
await EnsureSuccessAsync(response, "register service", cancellationToken);
var result = await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken)
diff --git a/HealthMonitoring.Tests/Api/ServiceRegistrationLoggingTests.cs b/HealthMonitoring.Tests/Api/ServiceRegistrationLoggingTests.cs
new file mode 100644
index 0000000..c0d1a77
--- /dev/null
+++ b/HealthMonitoring.Tests/Api/ServiceRegistrationLoggingTests.cs
@@ -0,0 +1,23 @@
+using HealthMonitoring.Api;
+using HealthMonitoring.Domain;
+
+namespace HealthMonitoring.Tests.Api;
+
+public sealed class ServiceRegistrationLoggingTests
+{
+ [Fact]
+ public void Registration_body_for_logging_redacts_connection_strings()
+ {
+ var request = new ServiceRegistrationRequest("orders-db", "Orders DB", "https://orders/health")
+ {
+ MonitorType = MonitorType.SqlServer,
+ ConnectionString = "Server=orders;Database=Orders;Authentication=Integrated Security"
+ };
+
+ var body = ServiceRegistrationLogFormatter.Serialize(request);
+
+ Assert.Contains("orders-db", body);
+ Assert.Contains("[REDACTED]", body);
+ Assert.DoesNotContain("Integrated Security", body);
+ }
+}
diff --git a/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs b/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs
index 167cb6f..26feae4 100644
--- a/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs
+++ b/HealthMonitoring.Tests/Client/HealthMonitoringRegistrationClientTests.cs
@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using HealthMonitoring.Client;
+using Microsoft.Extensions.Logging;
namespace HealthMonitoring.Tests.Client;
@@ -47,6 +48,37 @@ public async Task RegisterAsync_posts_service_configuration_and_returns_acknowle
Assert.True(payload.IgnoreCertificateErrors);
}
+ [Fact]
+ public async Task RegisterAsync_logs_the_post_body_without_exposing_connection_strings()
+ {
+ var handler = new RecordingHandler(_ => new HttpResponseMessage(HttpStatusCode.Created)
+ {
+ Content = JsonContent.Create(new
+ {
+ serviceId = PaymentsApiServiceId,
+ action = "created",
+ service = new { id = Guid.Parse("11111111-1111-1111-1111-111111111111") }
+ })
+ });
+ var logger = new RecordingLogger();
+ var client = new HealthMonitoringRegistrationClient(
+ new HttpClient(handler) { BaseAddress = new Uri(MonitoringBaseUrl) },
+ logger);
+
+ await client.RegisterAsync(new ServiceRegistrationOptions
+ {
+ ServiceId = PaymentsApiServiceId,
+ Name = "Payments API",
+ HealthUrl = new Uri("https://payments-api/health"),
+ ConnectionString = "Server=secret;Password=super-secret"
+ });
+
+ Assert.Contains("payments-api", logger.Messages.Single());
+ Assert.Contains("/api/services/register", logger.Messages.Single());
+ Assert.Contains("REDACTED", logger.Messages.Single());
+ Assert.DoesNotContain("super-secret", logger.Messages.Single());
+ }
+
[Fact]
public async Task SetDependencyMappingsAsync_resolves_target_service_and_upserts_mapping()
{
@@ -133,6 +165,26 @@ protected override Task SendAsync(HttpRequestMessage reques
}
}
+ private sealed class RecordingLogger : ILogger
+ {
+ public List Messages { get; } = [];
+
+ public IDisposable? BeginScope(TState state) where TState : notnull
+ {
+ _ = state;
+ return null;
+ }
+
+ public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ _ = logLevel;
+ _ = eventId;
+ Messages.Add(formatter(state, exception));
+ }
+ }
+
private static HttpResponseMessage JsonResponse(HttpStatusCode statusCode, object value) => new(statusCode)
{
Content = JsonContent.Create(value)
diff --git a/HealthMonitoring/Api/ServiceRegistrationEndpoints.cs b/HealthMonitoring/Api/ServiceRegistrationEndpoints.cs
index 6ae9ff5..ad7c532 100644
--- a/HealthMonitoring/Api/ServiceRegistrationEndpoints.cs
+++ b/HealthMonitoring/Api/ServiceRegistrationEndpoints.cs
@@ -1,5 +1,7 @@
using HealthMonitoring.Domain;
using HealthMonitoring.Persistence;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
namespace HealthMonitoring.Api;
@@ -17,8 +19,12 @@ public static IEndpointRouteBuilder MapServiceRegistrationEndpoints(this IEndpoi
}
private static void MapRegistrationEndpoint(IEndpointRouteBuilder endpoints, string dashboardEnvironment) =>
- endpoints.MapPost("/api/services/register", async (ServiceRegistrationRequest request, IHealthMonitoringRepository repository, CancellationToken cancellationToken) =>
+ endpoints.MapPost("/api/services/register", async (ServiceRegistrationRequest request, IHealthMonitoringRepository repository, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
+ loggerFactory.CreateLogger("HealthMonitoring.Api.ServiceRegistrationEndpoints").LogInformation(
+ "POST /api/services/register body: {RequestBody}",
+ ServiceRegistrationLogFormatter.Serialize(request));
+
var errors = ServiceConfigurationValidator.Validate(request);
if (errors.Count > 0) return Results.ValidationProblem(errors.ToDictionary(error => error, error => new[] { error }));
@@ -72,3 +78,29 @@ private static void MapArchiveEndpoint(IEndpointRouteBuilder endpoints) =>
return Results.NoContent();
});
}
+
+public static class ServiceRegistrationLogFormatter
+{
+ private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
+
+ public static string Serialize(ServiceRegistrationRequest request) =>
+ JsonSerializer.Serialize(new
+ {
+ request.ServiceId,
+ request.Name,
+ request.HealthUrl,
+ request.MonitorType,
+ ConnectionString = string.IsNullOrWhiteSpace(request.ConnectionString)
+ ? request.ConnectionString
+ : "[REDACTED]",
+ request.IgnoreCertificateErrors,
+ request.PollingIntervalSeconds,
+ request.RequestTimeoutSeconds,
+ request.RetentionDays,
+ request.StatusPolicy,
+ request.Group,
+ request.Description,
+ request.Version,
+ request.Host
+ }, JsonOptions);
+}
diff --git a/HealthMonitoring/appsettings.json b/HealthMonitoring/appsettings.json
index 827c5d3..8a4c2cd 100644
--- a/HealthMonitoring/appsettings.json
+++ b/HealthMonitoring/appsettings.json
@@ -5,7 +5,8 @@
"Logging": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning"
}
},
"Initialization": {