elsa-core/src/modules/Elsa.Diagnostics.ConsoleLogs/RealTime/ConsoleLogSubscriptionManager.cs
Sipke Schoorstra 43108c2e48
Add diagnostics console logs (#7462)
* feat: add diagnostics console logs

* test: avoid secret-like redaction fixtures

* fix: address console logs review feedback

* fix: harden console log capture lifecycle

* fix: report console log drop summaries

* fix: address diagnostics review cleanups
2026-05-18 02:16:46 +02:00

143 lines
5.5 KiB
C#

using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace Elsa.Diagnostics.ConsoleLogs.RealTime;
public class ConsoleLogSubscriptionManager : IDisposable
{
private readonly ConcurrentDictionary<string, ConsoleLogSubscription> _subscriptions = new(StringComparer.Ordinal);
private readonly IConsoleLogProvider _provider;
private readonly IConsoleLogSourceRegistry _sourceRegistry;
private readonly IHubContext<ConsoleLogsHub, IConsoleLogsClient> _hubContext;
private readonly ILogger<ConsoleLogSubscriptionManager> _logger;
public ConsoleLogSubscriptionManager(
IConsoleLogProvider provider,
IConsoleLogSourceRegistry sourceRegistry,
IHubContext<ConsoleLogsHub, IConsoleLogsClient> hubContext,
ILogger<ConsoleLogSubscriptionManager> logger)
{
_provider = provider;
_sourceRegistry = sourceRegistry;
_hubContext = hubContext;
_logger = logger;
_sourceRegistry.SourceChanged += OnSourceChanged;
}
public Task SubscribeAsync(string connectionId, ConsoleLogFilter filter, CancellationToken cancellationToken)
{
Unsubscribe(connectionId);
var subscriptionCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var subscription = new ConsoleLogSubscription(filter, subscriptionCancellation);
_subscriptions[connectionId] = subscription;
_ = StreamAsync(connectionId, filter, subscription, subscriptionCancellation.Token);
return Task.CompletedTask;
}
public Task UpdateFilterAsync(string connectionId, ConsoleLogFilter filter, CancellationToken cancellationToken)
{
return SubscribeAsync(connectionId, filter, cancellationToken);
}
public Task UnsubscribeAsync(string connectionId)
{
Unsubscribe(connectionId);
return Task.CompletedTask;
}
public void Dispose()
{
_sourceRegistry.SourceChanged -= OnSourceChanged;
foreach (var subscription in _subscriptions.Values)
{
subscription.CancellationTokenSource.Cancel();
subscription.CancellationTokenSource.Dispose();
}
_subscriptions.Clear();
}
private async Task StreamAsync(string connectionId, ConsoleLogFilter filter, ConsoleLogSubscription subscription, CancellationToken cancellationToken)
{
try
{
await foreach (var item in _provider.SubscribeAsync(filter, cancellationToken))
{
if (item.Line != null)
await _hubContext.Clients.Client(connectionId).ReceiveConsoleLogLineAsync(item.Line, cancellationToken);
if (item.DroppedLines != null)
await _hubContext.Clients.Client(connectionId).ReceiveDroppedLinesAsync(item.DroppedLines, cancellationToken);
if (item.Source != null)
await _hubContext.Clients.Client(connectionId).ReceiveSourceChangedAsync(item.Source, cancellationToken);
}
}
catch (OperationCanceledException e)
{
_logger.LogDebug(e, "Console log subscription for connection {ConnectionId} was canceled", connectionId);
}
catch (Exception e) when (e is not OperationCanceledException)
{
_logger.LogWarning(e, "Console log subscription for connection {ConnectionId} stopped unexpectedly", connectionId);
}
finally
{
Remove(connectionId, subscription);
}
}
private void Unsubscribe(string connectionId)
{
if (!_subscriptions.TryRemove(connectionId, out var subscription))
return;
subscription.CancellationTokenSource.Cancel();
subscription.CancellationTokenSource.Dispose();
}
private void Remove(string connectionId, ConsoleLogSubscription subscription)
{
var entry = new KeyValuePair<string, ConsoleLogSubscription>(connectionId, subscription);
if (((ICollection<KeyValuePair<string, ConsoleLogSubscription>>)_subscriptions).Remove(entry))
subscription.CancellationTokenSource.Dispose();
}
private void OnSourceChanged(ConsoleLogSource source)
{
_ = BroadcastSourceChangedAsync(source, _subscriptions.ToArray());
}
private async Task BroadcastSourceChangedAsync(ConsoleLogSource source, IReadOnlyCollection<KeyValuePair<string, ConsoleLogSubscription>> subscriptions)
{
try
{
foreach (var (connectionId, subscription) in subscriptions)
{
if (!MatchesSource(source, subscription.Filter))
continue;
await _hubContext.Clients.Client(connectionId).ReceiveSourceChangedAsync(source, subscription.CancellationTokenSource.Token);
}
}
catch (OperationCanceledException e)
{
_logger.LogDebug(e, "Console log source change broadcast for source {SourceId} was canceled", source.Id);
}
catch (Exception e) when (e is not OperationCanceledException)
{
_logger.LogDebug(e, "Failed to broadcast console log source change for source {SourceId}", source.Id);
}
}
private static bool MatchesSource(ConsoleLogSource source, ConsoleLogFilter filter)
{
return string.IsNullOrWhiteSpace(filter.SourceId) || string.Equals(source.Id, filter.SourceId, StringComparison.OrdinalIgnoreCase);
}
private sealed record ConsoleLogSubscription(ConsoleLogFilter Filter, CancellationTokenSource CancellationTokenSource);
}