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

@@ -0,0 +1,187 @@
.entry {
border-bottom: 1px solid var(--border-subtle);
cursor: pointer;
transition: background 0.1s;
}
.entry:hover {
background: var(--bg-hover);
}
.expanded {
background: var(--bg-surface);
}
.row {
display: flex;
align-items: baseline;
gap: 8px;
padding: 6px 12px;
font-size: 12px;
font-family: var(--font-mono);
min-height: 28px;
}
.timestamp {
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.level {
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
min-width: 40px;
}
.logger {
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
}
.message {
color: var(--text-primary);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chips {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.chip {
font-size: 10px;
padding: 1px 6px;
border-radius: var(--radius-sm);
background: var(--bg-raised);
color: var(--text-secondary);
cursor: pointer;
font-family: var(--font-body);
}
.chip:hover {
background: var(--border);
}
.detail {
padding: 8px 12px 12px 60px;
font-size: 12px;
}
.detailGrid {
display: grid;
grid-template-columns: 70px 1fr;
gap: 2px 8px;
margin-bottom: 8px;
}
.detailLabel {
color: var(--text-muted);
font-size: 11px;
}
.detailValue {
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 11px;
word-break: break-all;
}
.fullMessage {
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
margin-bottom: 8px;
padding: 8px;
background: var(--bg-deep);
border-radius: var(--radius-sm);
}
.stackTrace {
font-family: var(--font-mono);
font-size: 11px;
color: var(--error);
background: var(--bg-deep);
border-radius: var(--radius-sm);
padding: 8px;
margin: 8px 0;
overflow-x: auto;
white-space: pre;
max-height: 300px;
overflow-y: auto;
}
.mdcSection {
margin-top: 8px;
}
.mdcGrid {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.mdcEntry {
display: flex;
gap: 2px;
font-size: 11px;
font-family: var(--font-mono);
background: var(--bg-deep);
border-radius: var(--radius-sm);
padding: 2px 6px;
}
.mdcKey {
color: var(--text-muted);
}
.mdcValue {
color: var(--text-primary);
}
.actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.actionBtn {
background: none;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 4px 10px;
font-size: 11px;
color: var(--text-secondary);
cursor: pointer;
font-family: var(--font-body);
}
.actionBtn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.linkBtn {
background: none;
border: none;
padding: 0;
color: var(--amber);
cursor: pointer;
font-family: var(--font-mono);
font-size: 11px;
text-decoration: underline;
}