// 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); } } }