Loading Historical Logs
When a user opens a past execution (one that has already completed), logs are not available via SignalR — the execution is over and no hub group exists. Instead, the Observer Panel fetches logs from the REST API and loads them into executionStore via the same appendLogs action used during live streaming. The display path is identical.
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
| Scenario | How Historical Logs Are Triggered |
|---|---|
| User opens a completed execution | loadAllHistoricalLogs(executionId) called in the Observer Panel's useEffect on mount |
| Live execution completes while panel is open | SignalR sends ExecutionCompleted — panel calls loadAllHistoricalLogs to fill any gaps from reconnects |
| User reconnects mid-execution | Panel 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__=""
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.