Log Viewer
Indexes Docker container logs with full-text search and real-time SSE streaming. Reads log files directly from the host filesystem – no agents or log shippers required.
| Port: 5011 | Binary: log-viewer | Database: logs.db |
How it works
- A background thread scans Docker container config files to find monitored containers
- Reads Docker JSON log files (
/var/lib/docker/containers/{id}/{id}-json.log) - Parses JSON log entries, reassembles multiline messages (Python tracebacks, etc.)
- Auto-detects log levels from message content
- Stores entries in SQLite with FTS5 indexing
- Tracks file cursors to avoid re-reading on restart
Log rotation is handled automatically via inode tracking. When a log file is rotated, the viewer detects the inode change and reads from the beginning of the new file.
Wildcard container patterns
CONTAINERS accepts both exact names and glob patterns containing * (e.g. myapp_* matches myapp_web, myapp_worker, myapp_db, etc.). Patterns are expanded into the matching set of real container names at startup, and the log_sources directory is re-scanned every 30 seconds so newly-started containers matching a pattern are picked up automatically. Each container’s logs are stored under its real name, not the pattern, so the Container filter dropdown shows one entry per concrete container.
Configuration
| Variable | Required | Default | Description |
|---|---|---|---|
API_KEY | Yes | – | Authentication key |
DATABASE_PATH | No | ./data/logs.db | Path to SQLite database |
CONTAINERS | No | (empty = all) | Comma-separated container names or glob patterns to monitor (e.g. myapp_*,prod_db) |
LOG_SOURCES | No | /var/lib/docker/containers | Docker containers directory path |
MAX_ENTRIES | No | 100000 | Maximum log entries to keep (ring buffer) |
POLL_INTERVAL | No | 2 | Seconds between log file polls |
TAIL_BUFFER | No | 65536 | Bytes to read from end of file on first run |
LOG_LEVEL | No | info | error, warn, info, debug |
API
GET /api/logs
Query indexed logs with filtering and full-text search.
Headers: X-API-Key: <api-key>
Query parameters:
| Param | Description |
|---|---|
container | Filter by container name |
level | Filter by level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
search (or q) | Full-text search query (FTS5 MATCH syntax) |
since | ISO 8601 start time |
until | ISO 8601 end time |
limit | Max results (default 100, max 500) |
offset | Pagination offset |
FTS5 search syntax examples:
error– entries containing “error”"connection refused"– exact phraseerror OR timeout– either termerror NOT debug– exclude term
Response:
{
"logs": [
{
"id": 1,
"timestamp": "2026-01-01T12:00:00Z",
"container": "my_app",
"stream": "stderr",
"level": "ERROR",
"message": "Connection refused"
}
],
"total": 42,
"limit": 100,
"offset": 0
}
GET /api/logs/tail
Real-time log streaming via Server-Sent Events (SSE).
Query parameters:
| Param | Description |
|---|---|
container | Filter by container name |
level | Filter by level |
SSE events:
| Event | Data | Description |
|---|---|---|
log | JSON log entry | New log entry |
heartbeat | – | Keep-alive (every 15s) |
close | – | Server closing connection |
Limits: Max 5 concurrent SSE connections. Connections close after 30 minutes.
GET /api/logs/containers
List monitored containers with log counts.
Response: {"containers":[{"name":"my_app","log_count":1234}]}
GET /api/logs/stats
Aggregated statistics.
Response:
{
"total_logs": 50000,
"oldest_log": "2026-01-01T00:00:00Z",
"newest_log": "2026-01-02T00:00:00Z",
"by_level": {"ERROR": 100, "INFO": 49900},
"by_container": {"my_app": 30000, "worker": 20000}
}
GET /health
Response: {"status":"ok"}
Web UI
The log viewer includes a built-in web interface at /. The UI requires an API key for authentication – on first visit, a prompt asks for the key, which is stored in the browser’s localStorage.
Log level detection
Levels are auto-detected from log messages using these patterns (in priority order):
- JSON field:
"level":"ERROR"or"severity":"warning" - Bracket format:
[ERROR],[WARN] - Key-value:
level=error - Colon prefix:
ERROR: something failed - Default:
stderrstream maps toERROR,stdouttoINFO
Multiline reassembly
The ingestion engine reassembles multiline log entries by detecting continuation lines:
- Lines starting with whitespace
- Python traceback patterns (
Traceback,File "...", exception lines) - Stack trace frames
These are appended to the previous log entry rather than creating new entries.
Database schema
log_entries – indexed log storage:
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
timestamp | DATETIME | Log timestamp |
container | VARCHAR(200) | Container name |
stream | VARCHAR(10) | stdout or stderr |
level | VARCHAR(10) | Detected log level |
message | TEXT | Log message |
raw | TEXT | Original raw line |
log_entries_fts – FTS5 virtual table on the message column.
cursors – tracks ingestion position per container:
| Column | Type | Description |
|---|---|---|
container_id | TEXT | Container ID (unique) |
file_path | TEXT | Log file path |
position | INTEGER | Byte offset in file |
inode | INTEGER | File inode for rotation detection |
updated_at | DATETIME | Last update time |
Background tasks
- Log ingestion: Polls Docker log files every
POLL_INTERVALseconds. - Ring buffer cleanup: After each poll cycle, trims entries exceeding
MAX_ENTRIES.
Rate limits
- 60 requests/minute per IP
- 64KB max request body