using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Messaging; using StellaOps.Messaging.Abstractions; using StellaOps.Scheduler.WebService.Options; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.Scheduler.WebService.GraphJobs.Events; /// /// Transport-agnostic implementation of using StellaOps.Messaging abstractions. /// Works with any configured transport (Valkey, PostgreSQL, InMemory). /// internal sealed class MessagingGraphJobEventPublisher : IGraphJobCompletionPublisher { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly IOptionsMonitor _options; private readonly IEventStream _eventStream; private readonly ILogger _logger; public MessagingGraphJobEventPublisher( IOptionsMonitor options, IEventStreamFactory eventStreamFactory, ILogger logger) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(eventStreamFactory); _options = options; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var eventsOptions = options.CurrentValue?.GraphJobs ?? new GraphJobEventsOptions(); var streamKey = string.IsNullOrWhiteSpace(eventsOptions.Stream) ? "stella.events" : eventsOptions.Stream; var maxStreamLength = eventsOptions.MaxStreamLength > 0 ? eventsOptions.MaxStreamLength : (long?)null; _eventStream = eventStreamFactory.Create(new EventStreamOptions { StreamName = streamKey, MaxLength = maxStreamLength, ApproximateTrimming = true, }); _logger.LogInformation("Initialized messaging graph job event publisher for stream {Stream}.", streamKey); } public async Task PublishAsync(GraphJobCompletionNotification notification, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(notification); var options = _options.CurrentValue?.GraphJobs ?? new GraphJobEventsOptions(); if (!options.Enabled) { _logger.LogDebug("Graph job events disabled; skipping emission for {JobId}.", notification.Job.Id); return; } try { var envelope = GraphJobEventFactory.Create(notification); var publishOptions = new EventPublishOptions { TenantId = envelope.Tenant, MaxStreamLength = options.MaxStreamLength > 0 ? options.MaxStreamLength : null, Headers = new Dictionary { ["kind"] = envelope.Kind, ["occurredAt"] = envelope.Timestamp.ToString("O", CultureInfo.InvariantCulture), ["jobId"] = notification.Job.Id, ["status"] = notification.Status.ToString() } }; var publishTask = _eventStream.PublishAsync(envelope, publishOptions, cancellationToken); if (options.PublishTimeoutSeconds > 0) { var timeout = TimeSpan.FromSeconds(options.PublishTimeoutSeconds); await publishTask.AsTask().WaitAsync(timeout, cancellationToken).ConfigureAwait(false); } else { await publishTask.ConfigureAwait(false); } _logger.LogDebug("Published graph job event {JobId} to stream.", notification.Job.Id); } catch (Exception ex) { _logger.LogError(ex, "Failed to publish graph job completion for {JobId}; logging payload instead.", notification.Job.Id); LogEnvelope(notification); } } private void LogEnvelope(GraphJobCompletionNotification notification) { var envelope = GraphJobEventFactory.Create(notification); var json = JsonSerializer.Serialize(envelope, SerializerOptions); _logger.LogInformation("{EventJson}", json); } }