feature:
Full SOFA Score: Data Layer + Scoring Engine Glasgow Coma Scale: Data Layer + Scoring Engine
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
using System.Text.Json;
|
||||
using Confluent.Kafka;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class GcsScoringService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly KafkaOptions _kafkaOptions;
|
||||
private readonly ILogger<GcsScoringService> _logger;
|
||||
|
||||
public GcsScoringService(
|
||||
IServiceProvider services,
|
||||
IOptions<KafkaOptions> kafkaOptions,
|
||||
ILogger<GcsScoringService> logger)
|
||||
{
|
||||
_services = services;
|
||||
_kafkaOptions = kafkaOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var config = new ConsumerConfig
|
||||
{
|
||||
BootstrapServers = _kafkaOptions.BootstrapServers,
|
||||
GroupId = "gcs-scoring",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
|
||||
|
||||
_logger.LogInformation("GcsScoringService started — consumer group: gcs-scoring");
|
||||
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
ConsumeResult<string, string>? result = null;
|
||||
try
|
||||
{
|
||||
result = consumer.Consume(stoppingToken);
|
||||
|
||||
var evt = JsonSerializer.Deserialize<News2ObservationEvent>(
|
||||
result.Message.Value,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<GcsDetector>();
|
||||
|
||||
var outcome = await detector.ProcessObservationAsync(
|
||||
evt.EncounterId,
|
||||
evt.PatientId,
|
||||
evt.ObservationCode,
|
||||
evt.Value,
|
||||
stoppingToken);
|
||||
|
||||
if (outcome.Outcome == GcsOutcome.ScoreComputed)
|
||||
_logger.LogInformation(
|
||||
"GCS scored via consumer — encounter={Id} total={Total} class={Class}",
|
||||
evt.EncounterId, outcome.TotalScore, outcome.Classification);
|
||||
|
||||
consumer.Commit(result);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"GcsScoringService failed on topic={Topic} offset={Offset} — not committing",
|
||||
result?.Topic, result?.Offset.Value);
|
||||
await Task.Delay(2000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
consumer.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ public class KafkaTopicProvisioner : IHostedService
|
||||
_options.Topics.AlertAcknowledged,
|
||||
_options.Topics.EncounterStatusChanged,
|
||||
_options.Topics.SepsisBundleCreated,
|
||||
_options.Topics.SepsisBundleUpdated
|
||||
_options.Topics.SepsisBundleUpdated,
|
||||
_options.Topics.GcsScored
|
||||
};
|
||||
|
||||
var specs = topicNames.Select(name => new TopicSpecification
|
||||
@@ -44,7 +45,9 @@ public class KafkaTopicProvisioner : IHostedService
|
||||
}
|
||||
catch (CreateTopicsException ex)
|
||||
{
|
||||
var errors = ex.Results.Where(r => r.Error.Code != ErrorCode.TopicAlreadyExists).ToList();
|
||||
var errors = ex.Results
|
||||
.Where(r => r.Error.Code is not (ErrorCode.NoError or ErrorCode.TopicAlreadyExists))
|
||||
.ToList();
|
||||
if (errors.Count > 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create Kafka topics: {string.Join(", ", errors.Select(e => e.Error.Reason))}");
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Text.Json;
|
||||
using Confluent.Kafka;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class SofaScoringService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly KafkaOptions _kafkaOptions;
|
||||
private readonly ILogger<SofaScoringService> _logger;
|
||||
|
||||
public SofaScoringService(
|
||||
IServiceProvider services,
|
||||
IOptions<KafkaOptions> kafkaOptions,
|
||||
ILogger<SofaScoringService> logger)
|
||||
{
|
||||
_services = services;
|
||||
_kafkaOptions = kafkaOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var config = new ConsumerConfig
|
||||
{
|
||||
BootstrapServers = _kafkaOptions.BootstrapServers,
|
||||
GroupId = "sofa-scoring",
|
||||
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||
EnableAutoCommit = false
|
||||
};
|
||||
|
||||
using var consumer = new ConsumerBuilder<string, string>(config).Build();
|
||||
consumer.Subscribe(new[]
|
||||
{
|
||||
_kafkaOptions.Topics.ObservationRecorded,
|
||||
_kafkaOptions.Topics.GcsScored
|
||||
});
|
||||
|
||||
_logger.LogInformation("SofaScoringService started — consumer group: sofa-scoring");
|
||||
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
ConsumeResult<string, string>? result = null;
|
||||
try
|
||||
{
|
||||
result = consumer.Consume(stoppingToken);
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<SofaDetector>();
|
||||
|
||||
if (result.Topic == _kafkaOptions.Topics.GcsScored)
|
||||
{
|
||||
var gcsEvt = JsonSerializer.Deserialize<GcsScoredEvent>(
|
||||
result.Message.Value,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
|
||||
var outcome = await detector.ProcessGcsScoredAsync(
|
||||
gcsEvt.EncounterId, gcsEvt.PatientId, stoppingToken);
|
||||
|
||||
if (outcome.Outcome == SofaOutcome.EncounterNotFound)
|
||||
_logger.LogWarning(
|
||||
"Skipping stale gcs.scored event — encounter={Id} offset={Offset}",
|
||||
gcsEvt.EncounterId, result.Offset.Value);
|
||||
else if (outcome.Outcome == SofaOutcome.ScoreComputed)
|
||||
_logger.LogInformation(
|
||||
"SOFA re-scored via gcs.scored — encounter={Id} total={Total}",
|
||||
gcsEvt.EncounterId, outcome.Score!.Total);
|
||||
}
|
||||
else
|
||||
{
|
||||
var evt = JsonSerializer.Deserialize<News2ObservationEvent>(
|
||||
result.Message.Value,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
|
||||
var outcome = await detector.ProcessObservationAsync(
|
||||
evt.EncounterId,
|
||||
evt.PatientId,
|
||||
evt.ObservationCode,
|
||||
evt.Value,
|
||||
DateTimeOffset.UtcNow,
|
||||
stoppingToken);
|
||||
|
||||
if (outcome.Outcome == SofaOutcome.EncounterNotFound)
|
||||
_logger.LogWarning(
|
||||
"Skipping stale observation.recorded event — encounter={Id} code={Code} offset={Offset}",
|
||||
evt.EncounterId, evt.ObservationCode, result.Offset.Value);
|
||||
else if (outcome.Outcome == SofaOutcome.ScoreComputed)
|
||||
_logger.LogInformation(
|
||||
"SOFA scored via consumer — encounter={Id} total={Total}",
|
||||
evt.EncounterId, outcome.Score!.Total);
|
||||
}
|
||||
|
||||
consumer.Commit(result);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"SofaScoringService failed on topic={Topic} offset={Offset} — not committing",
|
||||
result?.Topic, result?.Offset.Value);
|
||||
await Task.Delay(2000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
consumer.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record GcsScoredEvent(Guid EncounterId, Guid PatientId, int TotalScore);
|
||||
Reference in New Issue
Block a user