Portal Community

SignalRLogSink

// SignalRLogSink.cs — INodeLogger implementation that routes to SignalR
public class SignalRLogSink : INodeLogger
{
    private readonly ILogBuffer          _buffer;
    private readonly NodeExecutionContext _ctx;

    public void LogInformation(string message, params object[] args)
    {
        var entry = new LogEntry
        {
            LogId     = Guid.NewGuid().ToString(),  // Unique ID for deduplication
            NodeId    = _ctx.NodeId,
            Level     = "Information",
            Message   = string.Format(message, args),
            Timestamp = DateTimeOffset.UtcNow.ToString("o"),
            Fields    = ExtractStructuredFields(message, args),
            TraceId   = Activity.Current?.TraceId.ToString()
        };

        // Add pre-seeded context fields
        entry.Fields["executionId"] = _ctx.ExecutionId;
        entry.Fields["tenantId"]    = _ctx.TenantId;
        entry.Fields["processId"]   = _ctx.ProcessId;
        entry.Fields["nodeType"]    = _ctx.NodeType;

        _buffer.Add(_ctx.ExecutionId, entry);
    }
}

LogEntry Schema (Backend)

public class LogEntry
{
    public string LogId     { get; init; }  // GUID — for client-side deduplication
    public string NodeId    { get; init; }
    public string Level     { get; init; }  // Trace|Debug|Information|Warning|Error
    public string Message   { get; init; }
    public string Timestamp { get; init; }  // ISO 8601
    public Dictionary<string, string> Fields { get; init; }
    public string? TraceId  { get; init; }
}

Dual Routing

Log entries go to two destinations simultaneously:

// 1. Buffer → SignalR → Client (Observer Panel, real-time)
_buffer.Add(_ctx.ExecutionId, entry);

// 2. Serilog pipeline → Loki (long-term storage)
// This happens via the normal Serilog sink — the INodeLogger also delegates to Serilog
_serilogLogger.Information(message, args);
Logs are stored durably in Loki regardless of whether any client is connected via SignalR. If no Observer Panel is open when a workflow runs, logs are still captured — they are available for historical viewing via the REST API later.