// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Audit.Emission;
///
/// Sends audit events to the Timeline service's POST /api/v1/audit/ingest endpoint.
/// Failures are logged but never propagated -- audit emission must not block the calling service.
///
public sealed class HttpAuditEventEmitter : IAuditEventEmitter
{
///
/// Named HTTP client identifier used for DI registration.
///
public const string HttpClientName = "StellaOps.AuditEmission";
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
private readonly IHttpClientFactory _httpClientFactory;
private readonly IOptions _options;
private readonly ILogger _logger;
public HttpAuditEventEmitter(
IHttpClientFactory httpClientFactory,
IOptions options,
ILogger logger)
{
_httpClientFactory = httpClientFactory;
_options = options;
_logger = logger;
}
public async Task EmitAsync(AuditEventPayload auditEvent, CancellationToken cancellationToken)
{
var options = _options.Value;
if (!options.Enabled)
{
_logger.LogDebug("Audit emission is disabled; skipping event {EventId}", auditEvent.Id);
return;
}
try
{
var client = _httpClientFactory.CreateClient(HttpClientName);
var uri = new Uri(new Uri(options.TimelineBaseUrl), "/api/v1/audit/ingest");
using var response = await client.PostAsJsonAsync(
uri,
auditEvent,
SerializerOptions,
cancellationToken)
.ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Audit ingest returned HTTP {StatusCode} for event {EventId}",
(int)response.StatusCode,
auditEvent.Id);
}
else
{
_logger.LogDebug(
"Audit event {EventId} emitted successfully ({Module}.{Action})",
auditEvent.Id,
auditEvent.Module,
auditEvent.Action);
}
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
_logger.LogWarning("Audit emission timed out for event {EventId}", auditEvent.Id);
}
catch (Exception ex) when (ex is HttpRequestException or JsonException)
{
_logger.LogWarning(ex, "Audit emission failed for event {EventId}", auditEvent.Id);
}
}
}