feat: add Logs tab with cursor-paginated search, level filters, and live tail
- 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:
@@ -7,6 +7,7 @@ const TABS = [
|
||||
{ label: 'Exchanges', value: 'exchanges' },
|
||||
{ label: 'Dashboard', value: 'dashboard' },
|
||||
{ label: 'Runtime', value: 'runtime' },
|
||||
{ label: 'Logs', value: 'logs' },
|
||||
];
|
||||
|
||||
interface ContentTabsProps {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user