Add tests and implement timeline ingestion options with NATS and Redis subscribers

- Introduced `BinaryReachabilityLifterTests` to validate binary lifting functionality.
- Created `PackRunWorkerOptions` for configuring worker paths and execution persistence.
- Added `TimelineIngestionOptions` for configuring NATS and Redis ingestion transports.
- Implemented `NatsTimelineEventSubscriber` for subscribing to NATS events.
- Developed `RedisTimelineEventSubscriber` for reading from Redis Streams.
- Added `TimelineEnvelopeParser` to normalize incoming event envelopes.
- Created unit tests for `TimelineEnvelopeParser` to ensure correct field mapping.
- Implemented `TimelineAuthorizationAuditSink` for logging authorization outcomes.
This commit is contained in:
StellaOps Bot
2025-12-03 09:46:48 +02:00
parent e923880694
commit 35c8f9216f
520 changed files with 4416 additions and 31492 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using StellaOps.TimelineIndexer.Core.Abstractions;
using StellaOps.TimelineIndexer.Infrastructure.DependencyInjection;
using StellaOps.TimelineIndexer.Infrastructure.Options;
using StellaOps.TimelineIndexer.Infrastructure.Subscriptions;
using StellaOps.TimelineIndexer.Worker;
@@ -11,6 +12,12 @@ builder.Configuration.AddJsonFile("appsettings.Development.json", optional: true
builder.Configuration.AddEnvironmentVariables(prefix: "TIMELINE_");
builder.Services.AddTimelineIndexerPostgres(builder.Configuration);
builder.Services.AddOptions<TimelineIngestionOptions>()
.Bind(builder.Configuration.GetSection("Ingestion"));
builder.Services.AddSingleton<TimelineEnvelopeParser>();
builder.Services.AddSingleton<ITimelineEventSubscriber, NatsTimelineEventSubscriber>();
builder.Services.AddSingleton<ITimelineEventSubscriber, RedisTimelineEventSubscriber>();
builder.Services.AddSingleton<ITimelineEventSubscriber, NullTimelineEventSubscriber>();
builder.Services.AddHostedService<TimelineIngestionWorker>();

View File

@@ -12,17 +12,20 @@ namespace StellaOps.TimelineIndexer.Worker;
public sealed class TimelineIngestionWorker(
IEnumerable<ITimelineEventSubscriber> subscribers,
ITimelineIngestionService ingestionService,
ILogger<TimelineIngestionWorker> logger) : BackgroundService
ILogger<TimelineIngestionWorker> logger,
TimeProvider? timeProvider = null) : BackgroundService
{
private static readonly Meter Meter = new("StellaOps.TimelineIndexer", "1.0.0");
private static readonly Counter<long> IngestedCounter = Meter.CreateCounter<long>("timeline.ingested");
private static readonly Counter<long> DuplicateCounter = Meter.CreateCounter<long>("timeline.duplicates");
private static readonly Counter<long> FailedCounter = Meter.CreateCounter<long>("timeline.failed");
private static readonly Histogram<double> LagHistogram = Meter.CreateHistogram<double>("timeline.ingest.lag.seconds");
private readonly IEnumerable<ITimelineEventSubscriber> _subscribers = subscribers;
private readonly ITimelineIngestionService _ingestion = ingestionService;
private readonly ILogger<TimelineIngestionWorker> _logger = logger;
private readonly ConcurrentDictionary<(string tenant, string eventId), byte> _sessionSeen = new();
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
@@ -48,6 +51,7 @@ public sealed class TimelineIngestionWorker(
if (result.Inserted)
{
IngestedCounter.Add(1);
LagHistogram.Record((_timeProvider.GetUtcNow() - envelope.OccurredAt).TotalSeconds);
_logger.LogInformation("Ingested timeline event {EventId} from {Source} (tenant {Tenant})", envelope.EventId, envelope.Source, envelope.TenantId);
}
else

View File

@@ -1,8 +1,34 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Postgres": {
"Timeline": {
"ConnectionString": "Host=localhost;Database=timeline;Username=timeline;Password=timeline;",
"SchemaName": "timeline",
"CommandTimeoutSeconds": 30
}
},
"Ingestion": {
"Nats": {
"Enabled": false,
"Url": "nats://localhost:4222",
"Subject": "orch.event",
"QueueGroup": "timeline-indexer",
"Prefetch": 64
},
"Redis": {
"Enabled": false,
"ConnectionString": "localhost:6379",
"Stream": "timeline.events",
"ConsumerGroup": "timeline-indexer",
"ConsumerName": "timeline-worker",
"ValueField": "data",
"MaxBatchSize": 128,
"PollIntervalMilliseconds": 250
}
}
}

View File

@@ -1,8 +1,34 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Postgres": {
"Timeline": {
"ConnectionString": "Host=localhost;Database=timeline;Username=timeline;Password=timeline;",
"SchemaName": "timeline",
"CommandTimeoutSeconds": 30
}
},
"Ingestion": {
"Nats": {
"Enabled": false,
"Url": "nats://localhost:4222",
"Subject": "orch.event",
"QueueGroup": "timeline-indexer",
"Prefetch": 64
},
"Redis": {
"Enabled": false,
"ConnectionString": "localhost:6379",
"Stream": "timeline.events",
"ConsumerGroup": "timeline-indexer",
"ConsumerName": "timeline-worker",
"ValueField": "data",
"MaxBatchSize": 128,
"PollIntervalMilliseconds": 250
}
}
}