Skip to content

lanbugs/chrislog

Repository files navigation

ChrisLOG

A self-contained, single-container log aggregation system for debugging and observability. Receives logs from multiple sources, stores them efficiently in SQLite, and displays them in a real-time Web UI powered by WebSockets.

No Redis. No Elasticsearch. No Kafka. One container, one binary, one file.

docker run -d -p 8080:8080 lanbugsde/chrislog

Features

  • Multi-source ingestion — Syslog (UDP + TCP, RFC 3164 & 5424), GELF (UDP), Docker socket, HTTP JSON
  • Real-time Web UI — Vue 3 frontend with WebSocket live stream; new logs appear instantly without polling
  • Full-text search — SQLite FTS5 with porter stemming; automatic regex detection
  • GROK enrichment — Logstash-compatible pattern set embedded in the binary; parses raw log lines into structured fields
  • Log retention — configurable by age (days) and database size (bytes)
  • HTTP Basic Auth — optional; enabled by setting WEB_PASSWORD
  • Tiny footprint — ~13 MB Go binary, ~30 MB Docker image (Alpine), single SQLite file for all data
  • Zero external dependencies — no sidecar services required

Quick Start

Docker Hub

docker run -d \
  --name chrislog \
  --restart unless-stopped \
  -p 8080:8080 \
  -p 514:514/udp \
  -p 514:514/tcp \
  -p 12201:12201/udp \
  -v chrislog_data:/data \
  lanbugsde/chrislog

Open http://localhost:8080 in your browser.

Docker Compose

services:
  chrislog:
    image: lanbugsde/chrislog
    container_name: chrislog
    restart: unless-stopped
    ports:
      - "8080:8080"         # Web UI + HTTP ingest + WebSocket
      - "514:514/udp"       # Syslog UDP
      - "514:514/tcp"       # Syslog TCP
      - "12201:12201/udp"   # GELF
    volumes:
      - chrislog_data:/data
      - /var/run/docker.sock:/var/run/docker.sock:ro   # optional: Docker log collector
    environment:
      RETENTION_DAYS: "30"
      # WEB_PASSWORD: "secret"

volumes:
  chrislog_data:

Log Sources

Source Protocol Port Format
Syslog UDP 514 RFC 3164, RFC 5424
Syslog TCP 514 RFC 3164, RFC 5424
GELF UDP 12201 Graylog Extended Log Format
Docker Unix socket Via /var/run/docker.sock
HTTP TCP 8080 POST /api/ingest with JSON body

Sending logs

Syslog (logger)

logger -n 127.0.0.1 -P 514 --udp "Hello from syslog"

GELF (curl)

echo '{"version":"1.1","host":"myapp","short_message":"Something happened","level":3}' \
  | nc -u 127.0.0.1 12201

HTTP JSON

curl -X POST http://localhost:8080/api/ingest \
  -H "Content-Type: application/json" \
  -d '{"host":"myserver","level":"error","message":"Disk almost full","tag":"diskmon"}'

HTTP JSON field reference

Field Aliases Description
message msg, short_message Log message text (required)
host hostname Source hostname
level severity Log level string or integer (syslog severity 0–7)
tag app, application Application or component name
facility Syslog facility string
timestamp Unix timestamp (seconds or milliseconds)

Docker — mount the socket and ChrisLOG automatically streams logs from all running containers and any container that starts afterwards:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro

Web UI

ChrisLOG Web UI

  • Live stream — WebSocket connection; the indicator in the header turns teal when connected. Click to pause/resume.
  • Search — plain text uses SQLite FTS5; any query containing regex metacharacters (. * + ? [ ] { } ( ) ^ $ | \ -) is automatically treated as a regex.
  • Filters — filter by level, source, tag, and time range. All filters combine with AND.
  • Sort — click any column header to sort; click again to toggle direction.
  • Column resize — drag the handle on any column edge. Double-click to reset to automatic width.
  • Row expand — click any row to see the raw log entry; JSON payloads are rendered as a collapsible tree.
  • Pagination — 200 logs per page by default (up to 1000 via limit query param).

Log levels

Level Color
emergency, alert red (bright)
critical, error red
warning yellow
notice blue
info green
debug gray

GROK Enrichment

ChrisLOG ships with the complete logstash-patterns-core pattern set embedded in the binary (18 pattern files covering Apache, Nginx, syslog, Java, HAProxy, PostgreSQL, Redis, firewalls, and more). GROK patterns parse raw log messages into structured fields (hostname, level, tag, facility, message).

Set GROK_PATTERNS to a comma-separated list of pattern names. Patterns are tried in order; the first match wins. Unmatched entries pass through unchanged.

Built-in patterns

Pattern name Matches
COMBINEDAPACHELOG Apache / Nginx combined access log
COMMONAPACHELOG Apache common log format
SYSLOGLINE Syslog line (RFC 3164 date + host + program + message)
SYSLOG5424LINE Syslog RFC 5424
SYSLOGBASE2 Syslog base without message
HAPROXYHTTP HAProxy HTTP log
HAPROXYTCP HAProxy TCP log
POSTGRESQLLOG PostgreSQL log line
RAILSLOG Ruby on Rails log
REDISTMONLOG Redis monitor log
JAVALOGMESSAGE Java log4j / logback message
JAVASTACKTRACEPART Java exception stack frame
NAGIOSLOGLINE Nagios log
MONGODBLOG MongoDB log

Examples

# Apache / Nginx access logs
GROK_PATTERNS: "COMBINEDAPACHELOG"

# Mixed environment: try Apache first, fall back to generic syslog
GROK_PATTERNS: "COMBINEDAPACHELOG,SYSLOGLINE"

# HAProxy + PostgreSQL
GROK_PATTERNS: "HAPROXYHTTP,POSTGRESQLLOG,SYSLOGLINE"

Custom patterns

Mount a directory containing additional .grok pattern files (same format as Logstash):

volumes:
  - ./my-patterns:/patterns:ro
environment:
  GROK_PATTERN_DIR: "/patterns"
  GROK_PATTERNS: "MYAPP_LOG,COMBINEDAPACHELOG"

Pattern file format (/patterns/myapp):

MYAPP_LEVEL (INFO|WARN|ERROR|DEBUG)
MYAPP_LOG \[%{MYAPP_LEVEL:level}\] %{IPORHOST:hostname} %{GREEDYDATA:message}

Field mapping

GROK-captured fields are mapped to log entry fields using this priority order. Protocol-level values (e.g. syslog severity from the PRI byte) always take precedence over GROK-extracted values.

Log field GROK captures tried (left = higher priority)
hostname SYSLOGHOSTlogsourcehostnameclientiphost
level SYSLOGSEVERITYloglevellevelseverity
tag SYSLOGPROGprogramprogapplicationapp
facility SYSLOGFACILITYfacility
message messageMESSAGEmsg

Configuration

All configuration is done via environment variables.

Variable Default Description
WEB_PASSWORD (empty) HTTP Basic Auth password. Authentication is disabled when empty.
WEB_USERNAME admin HTTP Basic Auth username (only used when WEB_PASSWORD is set).
DEFAULT_FILTER_REGEX (empty) Regex pre-filled in the search box on page load.
RETENTION_DAYS 30 Delete logs older than N days (0 = disabled).
RETENTION_BYTES 0 Maximum database size in bytes. Oldest logs are deleted first to stay under the limit (0 = disabled).
DB_PATH /data/chrislog.db Path to the SQLite database file.
STATIC_DIR /app/static Directory containing the compiled frontend assets.
LOG_LEVEL INFO Log level for ChrisLOG's own log output.
GROK_PATTERNS (empty) Comma-separated list of Logstash-compatible GROK pattern names to apply (disabled when empty).
GROK_PATTERN_DIR (empty) Path to a directory of additional custom .grok pattern files.

Authentication

HTTP Basic Auth is enabled by setting WEB_PASSWORD. The browser will prompt for credentials on first access.

environment:
  WEB_USERNAME: "admin"
  WEB_PASSWORD: "changeme"

The /api/ingest endpoint is intentionally not protected by authentication so that external services can ship logs without credentials. All other endpoints (Web UI, /api/logs, /api/stats, etc.) require authentication when WEB_PASSWORD is set.


Retention

Retention runs as a background goroutine every 60 seconds. Both policies can be active simultaneously.

By age — delete all logs older than RETENTION_DAYS days:

RETENTION_DAYS: "7"   # keep 7 days

By database size — when the SQLite file exceeds RETENTION_BYTES, the oldest entries are deleted in batches until the database fits within the limit:

RETENTION_BYTES: "1073741824"   # 1 GB cap

After deletion, VACUUM is run automatically and the FTS5 index is optimized.


API Reference

Method Path Auth Description
GET / Web UI (Vue SPA)
GET /api/logs Query logs
POST /api/ingest Ingest a JSON log entry
DELETE /api/logs Delete all logs
GET /api/stats Database statistics
GET /api/sources Distinct sources, levels, and tags
GET /api/config Frontend configuration (default regex)
GET /api/ws WebSocket live stream

GET /api/logs

Parameter Type Default Description
q string Search query (FTS5 or regex)
regex 1/true false Force regex mode
level string Filter by exact level
source string Filter by source (syslog, gelf, docker, http)
tag string Filter by exact tag
from int Start timestamp (Unix ms)
to int End timestamp (Unix ms)
sort_by string timestamp Sort column
sort_dir asc/desc desc Sort direction
limit int 200 Results per page (max 1000)
offset int 0 Pagination offset

Response

{
  "logs": [
    {
      "id": 42,
      "timestamp": 1715860800000,
      "source": "syslog",
      "hostname": "web01",
      "level": "error",
      "facility": "daemon",
      "tag": "nginx",
      "message": "connect() failed (111: Connection refused)",
      "raw": "<131>May 16 12:00:00 web01 nginx: connect() failed..."
    }
  ],
  "limit": 200,
  "offset": 0
}

POST /api/ingest

curl -X POST http://localhost:8080/api/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "host": "myserver",
    "level": "warning",
    "tag": "myapp",
    "message": "Cache miss rate above threshold",
    "timestamp": 1715860800
  }'

GET /api/ws — WebSocket live stream

Connect with any WebSocket client. Each new log entry is pushed as a JSON frame with the same schema as /api/logs entries.

const ws = new WebSocket('ws://localhost:8080/api/ws')
ws.onmessage = (e) => console.log(JSON.parse(e.data))

Storage

ChrisLOG uses a single SQLite 3 database with WAL journal mode and FTS5 full-text search.

CREATE TABLE logs (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp   INTEGER NOT NULL,   -- Unix milliseconds
    source      TEXT NOT NULL,      -- syslog | gelf | docker | http
    hostname    TEXT,
    level       TEXT,               -- emergency | alert | critical | error | warning | notice | info | debug
    facility    TEXT,
    tag         TEXT,
    message     TEXT NOT NULL,
    raw         TEXT,               -- original raw log string
    size_bytes  INTEGER
);

CREATE VIRTUAL TABLE logs_fts USING fts5(
    hostname, level, facility, tag, message,
    content='logs', content_rowid='id',
    tokenize='porter ascii'
);

Indexes on timestamp, level, and source are created automatically. The FTS5 index is kept in sync via AFTER INSERT and AFTER DELETE triggers.


Building from Source

Requirements: Go 1.23+, Node.js 20+, gcc (for CGo / SQLite)

git clone https://github.com/lanbugs/chrislog.git
cd chrislog

# Build frontend
cd frontend && npm install && npm run build && cd ..

# Copy frontend build output
cp -r frontend/dist/* static/

# Build Go binary (CGo required for SQLite FTS5)
CGO_ENABLED=1 go build -tags fts5 -ldflags="-s -w" -o chrislog .

# Run
./chrislog

Build Docker image

docker build -t lanbugsde/chrislog .

The Dockerfile uses a three-stage build:

  1. node:20-alpine — compiles the Vue 3 frontend with Vite
  2. golang:1.23-alpine — compiles the Go binary with CGo enabled (required for mattn/go-sqlite3 + FTS5)
  3. alpine:3.20 — minimal runtime image (~30 MB)

Frontend development

cd frontend
npm install
npm run dev   # Vite dev server on :5173, proxies /api and /ws to :8080

Start the backend separately:

STATIC_DIR=./static DB_PATH=/tmp/dev.db go run .

Architecture

                     ┌─────────────────────────────────────────┐
                     │              ChrisLOG                   │
                     │                                         │
  Syslog UDP/TCP ───►│  collectors/syslog.go                   │
  GELF UDP       ───►│  collectors/gelf.go      ┌──────────┐   │
  Docker socket  ───►│  collectors/docker.go ──►│ Storage  │──►│ Hub ──► WebSocket clients
  HTTP POST      ───►│  server/server.go         │ (SQLite) │   │
                     │                           │  WAL+FTS5│   │
                     │  GROK enrichment applied  └──────────┘   │
                     │  in commitBatch() before                  │
                     │  insert + broadcast                      │
                     │                                         │
                     │  Retention goroutine (every 60s)        │
                     └─────────────────────────────────────────┘

Concurrency model: all collectors run as goroutines and push entries into a buffered channel (writeQ, capacity 10 000). A single writer goroutine drains the channel in batches of up to 500 entries per SQLite transaction. The WebSocket hub uses a separate channel for broadcasting; each connected client gets its own 256-entry send buffer.


Dependencies

Package Purpose
mattn/go-sqlite3 SQLite 3 driver with FTS5 support (CGo)
gorilla/websocket WebSocket server
vjeantet/grok Logstash-compatible GROK pattern engine

The frontend uses Vue 3, Vite, TailwindCSS, and JetBrains Mono (self-hosted via @fontsource).


License

MIT — see LICENSE.


ChrisLOG — because sometimes you just need one container that catches everything.

About

ChrisLog — a fast, developer-focused log viewer written in Go.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors