A log ingestion and observability system that stores high-volume structured log data and surfaces real-time insights through a dashboard.
┌──────────────────┐ HTTP/REST ┌──────────────┐ asyncpg ┌─────────────┐
│ Svelte 5 + TS │ ◄────────────────► │ FastAPI API │ ◄──────────────► │ PostgreSQL │
│ (Vite SPA) │ │ (Python 3.12)│ │ (16-alpine) │
└──────────────────┘ └──────────────┘ └─────────────┘
:3000 :8000 :5432
| Layer | Tech | Role |
|---|---|---|
| Frontend | Svelte 5 + TypeScript + Vite + Chart.js | Dashboard SPA with charts, filters, log table |
| Backend | FastAPI + asyncpg | REST API for ingest and query |
| Database | PostgreSQL 16 | Persistent log storage with optimized indexes |
| Infra | Docker Compose + K8s | Local dev and cluster deployment |
CREATE TABLE logs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
level VARCHAR(10) NOT NULL, -- DEBUG INFO WARN ERROR FATAL
service VARCHAR(100) NOT NULL,
message TEXT NOT NULL,
host VARCHAR(255),
environment VARCHAR(20) NOT NULL DEFAULT 'production',
trace_id VARCHAR(64),
span_id VARCHAR(32),
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Indexes (all targeting < 100ms at 100K rows):
| Index | Type | Why |
|---|---|---|
(timestamp DESC) |
BTree | Primary time-range scans |
(level, timestamp DESC) |
BTree | Level + time filter combos |
(service, timestamp DESC) |
BTree | Service + time filter combos |
(trace_id) WHERE NOT NULL |
BTree | Trace correlation lookups |
to_tsvector(message) |
GIN | Full-text search on log messages |
metadata |
GIN | Arbitrary JSON metadata filtering |
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/logs |
Ingest a single log entry |
POST |
/api/v1/logs/batch |
Batch ingest up to 1,000 logs |
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/logs |
Paginated log query with filters |
GET |
/api/v1/logs/{id} |
Fetch single log by UUID |
Query params: level[], service[], start, end, q (full-text), limit, cursor
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/stats/overview |
Total, error count, error rate, unique services |
GET |
/api/v1/stats/by-level |
Log count per level |
GET |
/api/v1/stats/by-service |
Top N services by volume + errors |
GET |
/api/v1/stats/timeline |
Time-bucketed log volume (all levels) |
GET |
/api/v1/stats/services |
Distinct service names |
All stat endpoints accept a ?hours=24 window parameter.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
docker compose up --build
docker compose --profile seed run --rm seed
Note: The seed script distributes logs realistically — 70% within the last 24 hours and 30% spread randomly across the last 30 days. The default dashboard window is 24 hours, so switching to the 7-day view will show ~77K logs and the full 100K only appears at the 30-day window.
docker compose down
K8s is supported as an alternative to Docker Compose using kind (Kubernetes inside Docker). Docker Compose and the kind cluster cannot run at the same time as they share ports — stop one before starting the other.
brew install kind kubectl
bash k8s/setup-kind.sh
Builds both images, loads them into the cluster, applies all manifests, and waits for readiness.
kubectl run seed --image=logpulse-backend:latest --image-pull-policy=IfNotPresent --restart=Never \
--overrides='{"spec":{"hostNetwork":true}}' \
--env=DATABASE_URL=postgresql://logpulse:logpulse@localhost:5432/logpulse \
-- python scripts/seed.py
bash k8s/redeploy.sh
kind delete cluster --name logpulse
The K8s manifests are tuned for kind and would need the following changes before a production deployment:
hostNetwork: true — used to bypass iptables latency in kind. Remove it in production (real clusters have fast CNI) and revert DATABASE_URL to postgres:5432.secret-local.yaml contains plaintext dev credentials. Production should use a secrets manager (AWS Secrets Manager, Vault, Sealed Secrets).strategy: Recreate + replicas: 1 — required in kind because hostNetwork causes port conflicts on a single node. Production should use RollingUpdate with multiple replicas and PodDisruptionBudgets.Ingress resource with TLS termination (nginx-ingress, ALB, etc.).StatefulSet with backup configuration.OFFSET N degrades linearly; cursor-based pagination stays constant-time at any depth.metadata JSONB — keeps arbitrary metadata filterable without schema changes, at the cost of slightly slower writes.$state, $effect, $derived) make reactivity explicit and easy to follow; Chart.js bound directly to canvas elements keeps the charting layer thin.alert_rules table.pg_partman table partitioning by month + automatic partition drop for cost control.metadata.request_id = "abc").