using System.Text.Json; using Confluent.Kafka; using Microsoft.Extensions.Options; public class TrendAnalyzerService : BackgroundService { private readonly IServiceProvider _services; private readonly KafkaOptions _kafkaOptions; private readonly ILogger _logger; public TrendAnalyzerService( IServiceProvider services, IOptions kafkaOptions, ILogger logger) { _services = services; _kafkaOptions = kafkaOptions.Value; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var config = new ConsumerConfig { BootstrapServers = _kafkaOptions.BootstrapServers, GroupId = "trend-analyzer", AutoOffsetReset = AutoOffsetReset.Earliest, EnableAutoCommit = false }; using var consumer = new ConsumerBuilder(config).Build(); consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded); _logger.LogInformation("TrendAnalyzerService started — consumer group: trend-analyzer"); var guard = new PoisonPillGuard("trend-analyzer", _kafkaOptions.MaxPoisonRetries, _logger); try { while (!stoppingToken.IsCancellationRequested) { ConsumeResult? result = null; try { result = consumer.Consume(stoppingToken); var evt = JsonSerializer.Deserialize( result.Message.Value, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; using var scope = _services.CreateScope(); var detector = scope.ServiceProvider.GetRequiredService(); var outcome = await detector.ProcessObservationAsync( evt.EncounterId, evt.PatientId, evt.ObservationCode, evt.Value, evt.RecordedAt, stoppingToken); if (outcome.Outcome == TrendOutcome.EncounterNotFound) _logger.LogWarning( "Skipping stale observation.recorded event — encounter={Id} code={Code} offset={Offset}", evt.EncounterId, outcome.ObservationCode, result.Offset.Value); else if (outcome.Outcome == TrendOutcome.RapidDeterioration) _logger.LogInformation( "RAPID_DETERIORATION alert via consumer — encounter={Id} code={Code} rate={Rate}/min", evt.EncounterId, outcome.ObservationCode, outcome.RatePerMinute); consumer.Commit(result); guard.OnSuccess(); } catch (OperationCanceledException) { break; } catch (Exception ex) { if (result is not null && guard.ShouldSkip(result, ex)) { consumer.Commit(result); continue; } _logger.LogError(ex, "TrendAnalyzerService failed on topic={Topic} offset={Offset} — will retry", result?.Topic, result?.Offset.Value); await Task.Delay(2000, stoppingToken); } } } finally { consumer.Close(); } } }