feat: add Logs tab with cursor-paginated search, level filters, and live tail
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 1m11s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 49s

- Extend GET /api/v1/logs with cursor pagination, multi-level filtering,
  optional application scoping, and level count aggregation
- Add exchangeId, instanceId, application, mdc fields to log responses
- Refactor ClickHouseLogStore with keyset pagination (N+1 pattern)
- Add LogSearchRequest/LogSearchResponse core domain records
- Create LogSearchPageResponse wrapper DTO
- Add Logs as 4th content tab (Exchanges | Dashboard | Runtime | Logs)
- Implement LogSearch component with debounced search, level filter bar,
  expandable log entries, cursor pagination, and live tail mode
- Add cross-navigation: exchange header → logs, log tab → logs tab
- Update ClickHouseLogStoreIT with cursor, multi-level, cross-app tests

Closes: #104

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-02 08:47:16 +02:00
parent a52751da1b
commit b73f5e6dd4
22 changed files with 1405 additions and 119 deletions

View File

@@ -7,6 +7,7 @@ const TABS = [
{ label: 'Exchanges', value: 'exchanges' },
{ label: 'Dashboard', value: 'dashboard' },
{ label: 'Runtime', value: 'runtime' },
{ label: 'Logs', value: 'logs' },
];
interface ContentTabsProps {

View File

@@ -1,4 +1,5 @@
import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router';
import { useApplicationLogs } from '../../../api/queries/logs';
import type { LogEntryResponse } from '../../../api/queries/logs';
import styles from '../ExecutionDiagram.module.css';
@@ -30,6 +31,7 @@ function formatTime(iso: string): string {
export function LogTab({ applicationId, exchangeId, processorId }: LogTabProps) {
const [filter, setFilter] = useState('');
const navigate = useNavigate();
const { data: logs, isLoading } = useApplicationLogs(
applicationId,
@@ -93,23 +95,35 @@ export function LogTab({ applicationId, exchangeId, processorId }: LogTabProps)
{processorId ? 'No logs for this processor' : 'No logs available'}
</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tbody>
{entries.map((entry, i) => (
<tr key={i} style={{ borderBottom: '1px solid var(--border-subtle)' }}>
<td style={{ padding: '3px 6px', whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>
{formatTime(entry.timestamp)}
</td>
<td style={{ padding: '3px 4px', whiteSpace: 'nowrap', fontWeight: 600, color: levelColor(entry.level), width: '40px' }}>
{entry.level}
</td>
<td style={{ padding: '3px 6px', color: 'var(--text-primary)', wordBreak: 'break-word' }}>
{entry.message}
</td>
</tr>
))}
</tbody>
</table>
<>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tbody>
{entries.map((entry, i) => (
<tr key={i} style={{ borderBottom: '1px solid var(--border-subtle)' }}>
<td style={{ padding: '3px 6px', whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>
{formatTime(entry.timestamp)}
</td>
<td style={{ padding: '3px 4px', whiteSpace: 'nowrap', fontWeight: 600, color: levelColor(entry.level), width: '40px' }}>
{entry.level}
</td>
<td style={{ padding: '3px 6px', color: 'var(--text-primary)', wordBreak: 'break-word' }}>
{entry.message}
</td>
</tr>
))}
</tbody>
</table>
{exchangeId && (
<div style={{ padding: '6px 10px', borderTop: '1px solid var(--border-subtle)', fontSize: '11px', textAlign: 'center' }}>
<button
onClick={() => navigate(`/logs/${applicationId}?exchangeId=${exchangeId}`)}
style={{ background: 'none', border: 'none', color: 'var(--amber)', cursor: 'pointer', fontSize: '11px', fontFamily: 'var(--font-body)' }}
>
Open in Logs tab {'\u2192'}
</button>
</div>
)}
</>
)}
</div>
</div>