elsa-core/test/unit/Elsa.Diagnostics.StructuredLogs.UnitTests/Sources/StructuredLogSourceRegistryTests.cs
Sipke Schoorstra 507522469b
fix(diagnostics): align Relational structured-log sort and ListSources with InMemory (#8148)
* fix(diagnostics): align Relational structured-log sort and ListSources with InMemory

Include SourceId in the Relational ORDER BY tie-break chain, MarkSeen on the durable write path, and prefer the in-process source registry for ListSources so heartbeat and identity metadata survive Sqlite persistence.

Fixes #8117

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* fix(diagnostics): keep MarkSeen monotonic and ListSources IDs distinct

Do not regress LastSeen when a delayed older flush replays MarkSeen. Use Ordinal keys when merging registry sources so case-distinct IDs stay distinct.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-14 09:05:01 +02:00

166 lines
5.5 KiB
C#

using Elsa.Diagnostics.StructuredLogs.Models;
using Elsa.Diagnostics.StructuredLogs.Options;
using Elsa.Diagnostics.StructuredLogs.Services;
using MicrosoftOptions = Microsoft.Extensions.Options.Options;
namespace Elsa.Diagnostics.StructuredLogs.UnitTests.Sources;
public class StructuredLogSourceRegistryTests : IDisposable
{
private readonly Dictionary<string, string?> _originalEnvironment = new();
private readonly StructuredLogsOptions _options = new()
{
SourceHeartbeatTimeout = TimeSpan.FromSeconds(30)
};
public StructuredLogSourceRegistryTests()
{
CaptureEnvironment("HOSTNAME");
CaptureEnvironment("OTEL_SERVICE_NAME");
CaptureEnvironment("POD_NAMESPACE");
CaptureEnvironment("CONTAINER_NAME");
CaptureEnvironment("NODE_NAME");
}
[Fact]
public void Current_WhenKubernetesMetadataExists_UsesEnvironmentMetadata()
{
SetEnvironment("HOSTNAME", "elsa-pod-7");
SetEnvironment("OTEL_SERVICE_NAME", "elsa-api");
SetEnvironment("POD_NAMESPACE", "workflows");
SetEnvironment("CONTAINER_NAME", "server");
SetEnvironment("NODE_NAME", "node-a");
var registry = CreateRegistry();
Assert.Equal("elsa-pod-7", registry.Current.DisplayName);
Assert.Equal("elsa-api", registry.Current.ServiceName);
Assert.Equal("workflows", registry.Current.Namespace);
Assert.Equal("server", registry.Current.ContainerName);
Assert.Equal("node-a", registry.Current.NodeName);
}
[Fact]
public void MarkSeen_WhenSourceIsUnknown_AddsSourceWithMatchingId()
{
var registry = CreateRegistry();
registry.MarkSeen("pod-b", DateTimeOffset.UtcNow);
var source = Assert.Single(registry.List(), x => x.Id == "pod-b");
Assert.Equal("pod-b", source.DisplayName);
Assert.Equal(StructuredLogSourceStatus.Connected, source.Status);
}
[Fact]
public void MarkSeen_WhenSourceIsUnknown_DoesNotCopyLocalContainerMetadata()
{
SetEnvironment("HOSTNAME", "local-pod");
SetEnvironment("OTEL_SERVICE_NAME", "local-service");
SetEnvironment("POD_NAMESPACE", "local-namespace");
SetEnvironment("CONTAINER_NAME", "local-container");
SetEnvironment("NODE_NAME", "local-node");
var registry = CreateRegistry();
registry.MarkSeen("pod-b", DateTimeOffset.UtcNow);
var source = Assert.Single(registry.List(), x => x.Id == "pod-b");
Assert.Equal("pod-b", source.MachineName);
Assert.Equal(0, source.ProcessId);
Assert.Null(source.ServiceName);
Assert.Null(source.PodName);
Assert.Null(source.Namespace);
Assert.Null(source.ContainerName);
Assert.Null(source.NodeName);
}
[Fact]
public void MarkSeen_WhenSourceIsUnknown_RaisesSourceChanged()
{
var registry = CreateRegistry();
StructuredLogSource? changedSource = null;
registry.SourceChanged += source => changedSource = source;
registry.MarkSeen("pod-b", DateTimeOffset.UtcNow);
Assert.NotNull(changedSource);
Assert.Equal("pod-b", changedSource.Id);
Assert.Equal(StructuredLogSourceStatus.Connected, changedSource.Status);
}
[Fact]
public void MarkSeen_WhenSourceIsKnown_DoesNotRaiseSourceChanged()
{
var registry = CreateRegistry();
registry.MarkSeen("pod-b", DateTimeOffset.UtcNow);
StructuredLogSource? changedSource = null;
registry.SourceChanged += source => changedSource = source;
registry.MarkSeen("pod-b", DateTimeOffset.UtcNow.AddSeconds(1));
Assert.Null(changedSource);
}
[Fact]
public void MarkSeen_WhenTimestampIsOlder_DoesNotRegressLastSeen()
{
var registry = CreateRegistry();
var newer = DateTimeOffset.UtcNow;
var older = newer.AddMinutes(-1);
registry.MarkSeen("pod-b", newer);
registry.MarkSeen("pod-b", older);
var source = Assert.Single(registry.List(), x => x.Id == "pod-b");
Assert.Equal(newer, source.LastSeen);
Assert.Equal(StructuredLogSourceStatus.Connected, source.Status);
}
[Fact]
public void MarkSeen_WhenTimestampIsEqual_KeepsLastSeen()
{
var registry = CreateRegistry();
var timestamp = DateTimeOffset.UtcNow;
registry.MarkSeen("pod-b", timestamp);
registry.MarkSeen("pod-b", timestamp);
var source = Assert.Single(registry.List(), x => x.Id == "pod-b");
Assert.Equal(timestamp, source.LastSeen);
Assert.Equal(StructuredLogSourceStatus.Connected, source.Status);
}
[Fact]
public void List_WhenSourceHasNotBeenSeenRecently_MarksSourceAsStale()
{
_options.SourceHeartbeatTimeout = TimeSpan.FromSeconds(5);
var registry = CreateRegistry();
registry.MarkSeen("pod-b", DateTimeOffset.UtcNow.AddMinutes(-1));
var source = Assert.Single(registry.List(), x => x.Id == "pod-b");
Assert.Equal(StructuredLogSourceStatus.Stale, source.Status);
}
public void Dispose()
{
foreach (var item in _originalEnvironment)
Environment.SetEnvironmentVariable(item.Key, item.Value);
}
private StructuredLogSourceRegistry CreateRegistry()
{
return new(MicrosoftOptions.Create(_options));
}
private void CaptureEnvironment(string name)
{
_originalEnvironment[name] = Environment.GetEnvironmentVariable(name);
}
private static void SetEnvironment(string name, string? value)
{
Environment.SetEnvironmentVariable(name, value);
}
}