Portal Community

API Endpoint

GET /api/executions/{executionId}/logs?page={page}&pageSize={pageSize}

// Response envelope
{
    "page"       : 1,
    "pageSize"   : 500,
    "totalCount" : 12400,
    "items"      : [ /* LogEntry[] */ ]
}

// LogEntry schema is identical to the SignalR payload
{
    "logId"     : "3f8e1a2c-...",
    "nodeId"    : "node-abc",
    "level"     : "Information",
    "message"   : "HTTP request completed",
    "timestamp" : "2026-05-21T14:33:01.123Z",
    "fields"    : { "executionId": "...", "tenantId": "..." },
    "traceId"   : "4bf92f3577b34da6..."
}

Client — executionLogsApiClient

// executionLogsApiClient.ts
export async function fetchExecutionLogs(
    executionId : string,
    page        : number,
    pageSize    : number = 500
): Promise<PagedResult<LogEntry>> {
    const response = await apiClient.get(
        `/api/executions/${executionId}/logs`,
        { params: { page, pageSize } }
    );
    return response.data;
}

Loading Strategy — Full Fetch with Pagination

For historical views the Observer Panel fetches all pages sequentially and calls appendLogs after each page. This loads the complete log set into executionStore so that in-memory filters (level, nodeId, text search) work over the full history without additional round-trips.

// useHistoricalLogs.ts
export async function loadAllHistoricalLogs(executionId: string) {
    const PAGE_SIZE = 500;
    let page = 1;
    let totalLoaded = 0;

    useExecutionStore.getState().setLoading(true);

    try {
        while (true) {
            const result = await fetchExecutionLogs(executionId, page, PAGE_SIZE);
            useExecutionStore.getState().appendLogs(result.items);
            totalLoaded += result.items.length;

            // appendLogs is idempotent — duplicates from live streaming are silently skipped

            if (totalLoaded >= result.totalCount) break;
            page++;
        }
    } finally {
        useExecutionStore.getState().setLoading(false);
    }
}

Invocation — When Logs Load

ScenarioHow Historical Logs Are Triggered
User opens a completed executionloadAllHistoricalLogs(executionId) called in the Observer Panel's useEffect on mount
Live execution completes while panel is openSignalR sends ExecutionCompleted — panel calls loadAllHistoricalLogs to fill any gaps from reconnects
User reconnects mid-executionPanel calls loadAllHistoricalLogs for logs missed during disconnect, then re-joins SignalR group for live streaming

Deduplication During Mixed Load

When a live execution completes and the panel immediately loads historical logs, some entries may have already been appended via SignalR. The appendLogs action uses a Set-based logId check — duplicate entries are discarded without modifying state, so no re-render is triggered for already-known entries.

// executionStore.ts — appendLogs is always safe to call, even with overlapping data
appendLogs: (entries: LogEntry[]) => set(state => {
    const existingIds = new Set(state.logs.map(l => l.logId));
    const newEntries  = entries.filter(e => !existingIds.has(e.logId));

    if (newEntries.length === 0) return state;  // No change — no re-render

    return { logs: [...state.logs, ...newEntries] };
})

Backend — Log Storage

All log entries are written to Loki via the Serilog pipeline at emission time (see Backend Log Emission). The REST API reads from Loki using the executionId label as the query filter. Loki returns entries in insertion order; the API preserves this ordering in the paginated response.

// Loki label query used by the backend API handler
{executionId="3f8e1a2c-..."}
    | json
    | line_format "{{.message}}"

// Equivalent LogQL query for all entries in an execution
{executionId="3f8e1a2c-..."} | json | __error__=""
Same display path as live streaming. The Logs tab component (ExecutionLogsTabContent) has no knowledge of whether its logs arrived via SignalR or REST API. It reads executionStore.logs, applies the filter memo, and passes the result to VirtualLogList. The source of the data is irrelevant to the rendering layer.
Logs are always durable. Because Serilog routes entries to Loki independently of SignalR, historical logs are available even if no client was connected during the execution. The REST API can retrieve logs for any execution that ran within the Loki retention window.