Files
git.stella-ops.org/src/StellaOps.Scheduler.Storage.Mongo/Services/RunSummaryService.cs
master 799f787de2 Add Policy DSL Validator, Schema Exporter, and Simulation Smoke tools
- Implemented PolicyDslValidator with command-line options for strict mode and JSON output.
- Created PolicySchemaExporter to generate JSON schemas for policy-related models.
- Developed PolicySimulationSmoke tool to validate policy simulations against expected outcomes.
- Added project files and necessary dependencies for each tool.
- Ensured proper error handling and usage instructions across tools.
2025-10-27 08:00:11 +02:00

205 lines
7.0 KiB
C#

using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Documents;
using StellaOps.Scheduler.Storage.Mongo.Projections;
using StellaOps.Scheduler.Storage.Mongo.Repositories;
namespace StellaOps.Scheduler.Storage.Mongo.Services;
internal sealed class RunSummaryService : IRunSummaryService
{
private const int RecentLimit = 20;
private readonly IRunSummaryRepository _repository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<RunSummaryService> _logger;
public RunSummaryService(
IRunSummaryRepository repository,
TimeProvider? timeProvider,
ILogger<RunSummaryService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<RunSummaryProjection> ProjectAsync(
Run run,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(run);
if (string.IsNullOrWhiteSpace(run.ScheduleId))
{
throw new ArgumentException("Run must contain a scheduleId to project summary data.", nameof(run));
}
var document = await _repository
.GetAsync(run.TenantId, run.ScheduleId!, cancellationToken)
.ConfigureAwait(false)
?? new RunSummaryDocument
{
TenantId = run.TenantId,
ScheduleId = run.ScheduleId!,
};
UpdateDocument(document, run);
document.UpdatedAt = _timeProvider.GetUtcNow().UtcDateTime;
await _repository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
_logger.LogDebug(
"Projected run summary for tenant {TenantId} schedule {ScheduleId} using run {RunId}.",
run.TenantId,
run.ScheduleId,
run.Id);
return ToProjection(document);
}
public async Task<RunSummaryProjection?> GetAsync(
string tenantId,
string scheduleId,
CancellationToken cancellationToken = default)
{
var document = await _repository
.GetAsync(tenantId, scheduleId, cancellationToken)
.ConfigureAwait(false);
return document is null ? null : ToProjection(document);
}
public async Task<IReadOnlyList<RunSummaryProjection>> ListAsync(
string tenantId,
CancellationToken cancellationToken = default)
{
var documents = await _repository.ListAsync(tenantId, cancellationToken).ConfigureAwait(false);
return documents.Select(ToProjection).ToArray();
}
private static void UpdateDocument(RunSummaryDocument document, Run run)
{
var entry = document.Recent.FirstOrDefault(item => string.Equals(item.RunId, run.Id, StringComparison.Ordinal));
if (entry is null)
{
entry = new RunSummaryEntryDocument
{
RunId = run.Id,
};
document.Recent.Add(entry);
}
entry.Trigger = run.Trigger;
entry.State = run.State;
entry.CreatedAt = run.CreatedAt.UtcDateTime;
entry.StartedAt = run.StartedAt?.UtcDateTime;
entry.FinishedAt = run.FinishedAt?.UtcDateTime;
entry.Error = run.Error;
entry.Stats = run.Stats;
document.Recent = document.Recent
.OrderByDescending(item => item.CreatedAt)
.ThenByDescending(item => item.RunId, StringComparer.Ordinal)
.Take(RecentLimit)
.ToList();
document.LastRun = document.Recent.FirstOrDefault();
document.Counters = ComputeCounters(document.Recent);
}
private static RunSummaryCountersDocument ComputeCounters(IEnumerable<RunSummaryEntryDocument> entries)
{
var counters = new RunSummaryCountersDocument();
foreach (var entry in entries)
{
counters.Total++;
switch (entry.State)
{
case RunState.Planning:
counters.Planning++;
break;
case RunState.Queued:
counters.Queued++;
break;
case RunState.Running:
counters.Running++;
break;
case RunState.Completed:
counters.Completed++;
break;
case RunState.Error:
counters.Error++;
break;
case RunState.Cancelled:
counters.Cancelled++;
break;
default:
break;
}
counters.TotalDeltas += entry.Stats.Deltas;
counters.TotalNewCriticals += entry.Stats.NewCriticals;
counters.TotalNewHigh += entry.Stats.NewHigh;
counters.TotalNewMedium += entry.Stats.NewMedium;
counters.TotalNewLow += entry.Stats.NewLow;
}
return counters;
}
private static RunSummaryProjection ToProjection(RunSummaryDocument document)
{
var updatedAt = new DateTimeOffset(DateTime.SpecifyKind(document.UpdatedAt, DateTimeKind.Utc));
var lastRun = document.LastRun is null
? null
: ToSnapshot(document.LastRun);
var recent = document.Recent
.Select(ToSnapshot)
.ToImmutableArray();
var counters = new RunSummaryCounters(
document.Counters.Total,
document.Counters.Planning,
document.Counters.Queued,
document.Counters.Running,
document.Counters.Completed,
document.Counters.Error,
document.Counters.Cancelled,
document.Counters.TotalDeltas,
document.Counters.TotalNewCriticals,
document.Counters.TotalNewHigh,
document.Counters.TotalNewMedium,
document.Counters.TotalNewLow);
return new RunSummaryProjection(
document.TenantId,
document.ScheduleId,
updatedAt,
lastRun,
recent,
counters);
}
private static RunSummarySnapshot ToSnapshot(RunSummaryEntryDocument entry)
{
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(entry.CreatedAt, DateTimeKind.Utc));
DateTimeOffset? startedAt = entry.StartedAt is null
? null
: new DateTimeOffset(DateTime.SpecifyKind(entry.StartedAt.Value, DateTimeKind.Utc));
DateTimeOffset? finishedAt = entry.FinishedAt is null
? null
: new DateTimeOffset(DateTime.SpecifyKind(entry.FinishedAt.Value, DateTimeKind.Utc));
return new RunSummarySnapshot(
entry.RunId,
entry.Trigger,
entry.State,
createdAt,
startedAt,
finishedAt,
entry.Stats,
entry.Error);
}
}