Skip to content

halilsafakkilic/la06

Repository files navigation

LA06 - Real-time JSON log viewer for local files and SSH hosts.

Bun server backend with a React frontend. Watches .log files (newline-delimited JSON) across configurable sources — both local directories and remote SSH hosts — streams new entries to the browser over WebSocket, and provides filtering, querying, and column customization.

LA06 demo

Features

  • 🔴 Live tail.log files (NDJSON) streamed to the browser in real time over WebSocket
  • 🌐 Local & SSH sources — watch local directories or remote hosts via ssh2 + tail -F, mixed freely
  • 🔍 Built-in query languagelevel:error AND meta.status:>=500, full-text search, AND/OR combinators
  • 🧱 Customizable columns — pick fields, reorder, persist layout per-file in localStorage
  • Virtual scrolling — handles tens of thousands of rows without breaking a sweat
  • 🔁 Auto-reconnect — exponential-backoff for both SSH and WebSocket; disconnects replay missed bytes
  • 🩺 Health indicators — per-source connection status and per-file activity dots
  • ♻️ Hot reload — edit sources.json, hit reload, no restart

Requirements

  • Bun ≥ 1.0
  • For SSH sources: GNU tail available on the remote host

Quick Start

bun install
cp sources.example.json sources.json    # edit to match your environment
bun run dev

This starts both the server (default port 3001) and UI dev server concurrently. Open the URL printed by Vite.

Configuration

Sources (sources.json)

Log sources — local and SSH — are declared in ./sources.json at the project root. The file is gitignored; use sources.example.json as a template.

{
  "sources": [
    { "type": "local", "name": "logs", "dirs": ["./logs"] },
    {
      "type": "local",
      "name": "system",
      "files": ["/var/log/syslog", "/var/log/auth.log"],
    },
    {
      "type": "ssh",
      "name": "prod-api",
      "host": "api.example.com",
      "port": 22,
      "user": "deploy",
      "dirs": ["/var/log/app", "/var/log/nginx"],
      "files": ["/var/log/syslog"],
      "auth": { "kind": "key", "path": "~/.ssh/id_rsa", "passphrase": "" },
    },
    {
      "type": "ssh",
      "name": "worker",
      "host": "worker.example.com",
      "user": "ops",
      "dirs": ["/srv/worker/logs"],
      "auth": { "kind": "password", "password": "replace-me" },
    },
  ],
}

Each source can declare any combination of dirs (recursively scanned for .log files) and files (specific paths, any extension). At least one of the two must be non-empty. Legacy dir: string is still accepted as shorthand for dirs: [dir].

name is required and must be unique across sources. Add "isActive": false to keep a source defined in the file but not actually watched (handy for parking environments without deleting their config). Inactive sources are completely hidden from the UI.

SSH connections use tail -F over an exec channel for live tailing and SFTP for discovery and historical reads. Remote hosts must have GNU tail available. Auto-reconnect with exponential backoff (1s → 30s) handles transient disconnects; the UI shows a connection-status dot next to each source in the file picker. When the SSH server's per-connection channel limit (sshd MaxSessions, default 10) is reached, additional auxiliary ssh2.Client connections are spawned automatically and tail channels are distributed across them. Bytes consumed per file are tracked at source level so that on reconnect tail -c +N replays anything written during the disconnect window.

Host key verification — every SSH connection runs through a verifier. On first connection the host's SHA256 fingerprint is stored at <project>/.ssh/known_hosts.json (TOFU). Subsequent connections must present the same key, or the connection is refused and the source goes to error state with a clear mismatch message. To pin a key explicitly (skip TOFU), set "hostKey": "SHA256:..." on the source — get the fingerprint with ssh-keyscan host | ssh-keygen -lf -.

Reloading without a restart

Edited sources.json and want it to take effect? Use the Reload sources button in the settings popup, or POST /api/reload. The server validates the new file first; if it's invalid, current sources keep running and the response returns the validation error. On success every stream is torn down and re-built (SSH reconnects, file watchers re-attach), the UI clears its log buffer and re-fetches.

Environment variables (.env)

PORT=3001                          # backend server port
UI_PORT=5173                       # Vite dev server port (dev mode only)
TIMESTAMP_FIELD=ts                 # JSON field used for timestamps
MAX_DIR_DEPTH=3                    # how deep to recurse into source dirs (default 3)
LOG_DIR=./logs/_system             # backend internal JSONL logs (empty => disabled)
LOG_LEVEL=info                     # debug|info|warn|error
LOG_TO_STDOUT=false                # mirror internal logs to stdout
LOG_HTTP_REQUEST=false             # HTTP access logging

Scripts

Command Description
bun run dev Start server + UI dev server concurrently
bun run dev:server Server only
bun run dev:ui UI only (Vite with HMR)
bun run build Production build of UI (ui/dist/)
bun run start Production server (serves API + static UI)
bun run lint ESLint
bun run lint:fix ESLint with auto-fix
bun run format Prettier write
bun run format:check Prettier check
bun run knip Dead code / unused dependency detection
bun run test Run all tests (vitest)
bun run test:watch Run tests in watch mode

Architecture

Monorepo with Bun workspaces — three packages:

├── shared/       @la06/shared — types, constants, utils shared between server and UI
├── server/       Bun-native HTTP + WebSocket server (no framework)
└── ui/           React 19 + Vite + Tailwind CSS v4

Server

  • Pluggable LogSource abstraction (server/sources/) with two implementations:
    • LocalBun.Glob discovery, fs.watch tail with byte-offset + remainder buffer, recursive directory watcher with poll fallback
    • SSHssh2 client with persistent connection, SFTP for discovery and historical reads, tail -F exec channel for live tailing, exponential-backoff auto-reconnect
  • NDJSON parsing with _file / _source / _line system fields injected into every entry; non-JSON lines fall back to a raw entry with a synthetic timestamp
  • Streams new entries to connected clients over WebSocket with batching (100ms)
  • Broadcasts source connection-status changes to all clients in real time
  • HTTP API: GET /api/config, GET /api/files, GET /api/logs, GET /api/metrics, POST /api/reload
  • Per-file health: tail/watch failures surface as sticky errors broadcast to clients (MSG_FILE_STATUS); UI shows a colored dot per file (active / idle / stale / error) with a tooltip

UI

  • Initial data load via REST, real-time updates via WebSocket with auto-reconnect
  • Per-source connection status indicator in the file picker (green / yellow pulsing / red with error tooltip)
  • Virtual scrolling via @tanstack/react-virtual
  • Column layout persisted to localStorage
  • Dark/light theme support

Query Language

Filter logs with a built-in query language:

level:error                          # exact match
meta.status:>400                     # numeric comparison
level:error OR level:warn            # OR combinator
"connection refused"                 # full-text search
meta.method:POST AND meta.path:/api  # AND combinator

Supported operators: : (equals), :> :>= :< :<= (comparison).

Log File Format

Each .log file should contain newline-delimited JSON (NDJSON):

{"ts":"2026-04-14T09:31:41.358Z","level":"info","message":"Server started","meta":{"port":3001}}
{"ts":"2026-04-14T09:31:42.100Z","level":"error","message":"Connection refused","meta":{"host":"db"}}

Timestamps must be ISO 8601.

Tech Stack

  • Runtime: Bun
  • Frontend: React 19, Vite, Tailwind CSS v4
  • Language: TypeScript
  • Testing: Vitest
  • Git Hooks: Husky + lint-staged (ESLint + Prettier on commit)

License

This project is licensed under the MIT License.

About

Real-time JSON log viewer for local files and SSH hosts. Bun + React. Streams NDJSON over WebSocket with filtering, querying, and column customization.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages