
Logwell is a self-hosted logging platform with real-time streaming, full-text search, and OTLP-compatible ingestion. Deploy in minutes, own your data.
Alpha Software — Expect breaking changes. Not recommended for production workloads.
Features • Quick Start • Usage • Deploy • Contributing • License
Logwell is a lightweight, self-hosted log aggregation platform for developers who want structured logging without the complexity of ELK or the costs of cloud services.
Use Logwell when you need:
Logwell is NOT for:
Real-time log streaming demo
Real-time log viewer with level filtering
|
Quick Start with pre-filled API key
|
API key management and code snippets
|
Log level distribution analytics
|
| vs | Logwell advantage |
|---|---|
| Loki/Grafana | Built-in UI, no LogQL to learn, just PostgreSQL |
| ELK | Lightweight PostgreSQL backend, not Elasticsearch |
| Datadog/etc | Self-hosted, no per-GB pricing, own your data |
| Layer | Technology |
|---|---|
| Framework | SvelteKit |
| Database | PostgreSQL |
| ORM | Drizzle |
| Auth | better-auth |
| UI | shadcn-svelte + Tailwind CSS v4 |
| Real-time | Server-Sent Events |
| Runtime | Bun |
# Clone the repository
git clone https://github.com/divkix/logwell.git
cd logwell
# Install dependencies
bun install
# Set up environment
cp .env.example .env
# Edit .env with your values (see Environment Variables below)
# Start PostgreSQL
docker compose up -d
# Run database migrations
bun run db:migrate
# Create admin user
bun run db:seed
# Start development server
bun run dev
Open http://localhost:5173 and sign in with:
admin (or your ADMIN_USERNAME from .env)ADMIN_PASSWORD from .envNote: Development runs on port 5173 (Vite). Production builds run on port 3000.
Create a .env file with the following:
# Database connection
DATABASE_URL="postgres://root:mysecretpassword@localhost:5432/local"
# Authentication secret (minimum 32 characters)
BETTER_AUTH_SECRET="your-32-character-secret-key-here"
# Admin user password (minimum 8 characters)
ADMIN_PASSWORD="your-admin-password"
# Admin username (optional, defaults to "admin")
# ADMIN_USERNAME="admin"
# Production URL (required for auth behind reverse proxies)
ORIGIN="https://your-domain.com"
# Log retention (optional, defaults shown)
# LOG_RETENTION_DAYS="30" # 0 = never auto-delete
# LOG_CLEANUP_INTERVAL_MS="3600000" # Cleanup job interval (1 hour)
# INCIDENT_AUTO_RESOLVE_MINUTES="30" # Incident resolves after N quiet minutes
# Rate limiting (optional, defaults shown)
# RATE_LIMIT_INGEST_RPM="600" # Max ingest requests per minute per API key
# RATE_LIMIT_LOGIN_RPM="10" # Max login attempts per minute per IP
Generate a secure secret:
openssl rand -base64 32
lw_...)Logwell provides two ingestion APIs:
| API | Endpoint | Best For |
|---|---|---|
| Simple API | POST /v1/ingest |
Quick integration, any HTTP client |
| OTLP API | POST /v1/logs |
OpenTelemetry SDKs, rich metadata |
The simple API accepts flat JSON with minimal boilerplate:
curl -X POST http://localhost:5173/v1/ingest \
-H "Authorization: Bearer lw_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"level": "info", "message": "User signed in"}'
Batch multiple logs:
curl -X POST http://localhost:5173/v1/ingest \
-H "Authorization: Bearer lw_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{"level": "info", "message": "Request started"},
{"level": "error", "message": "Database timeout", "metadata": {"query": "SELECT..."}}
]'
Available fields:
| Field | Required | Type | Description |
|---|---|---|---|
level |
Yes | debug | info | warn | error | fatal |
Log severity |
message |
Yes | string | Log message |
timestamp |
No | ISO8601 string | Defaults to current time |
service |
No | string | Service name for filtering |
metadata |
No | object | Additional structured data |
Node.js (no SDK needed)
await fetch("http://localhost:5173/v1/ingest", {
method: "POST",
headers: {
Authorization: "Bearer lw_YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ level: "info", message: "Hello from Node.js" }),
});
Python (no SDK needed)
import requests
requests.post('http://localhost:5173/v1/ingest',
headers={'Authorization': 'Bearer lw_YOUR_API_KEY'},
json={'level': 'info', 'message': 'Hello from Python'})
Go (no SDK needed)
body := []byte(`{"level": "info", "message": "Hello from Go"}`)
req, _ := http.NewRequest("POST", "http://localhost:5173/v1/ingest", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer lw_YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
For Node.js, browsers, and edge runtimes (Cloudflare Workers, etc.):
npm install logwell
import { Logwell } from "logwell";
const logger = new Logwell({
apiKey: "lw_YOUR_API_KEY",
endpoint: "http://localhost:5173",
});
// Log at different levels
logger.info("User signed in", { userId: "123" });
logger.error("Database failed", { host: "db.local" });
// Flush before shutdown
await logger.shutdown();
Features: Zero dependencies, automatic batching, retry with backoff, TypeScript-first, opt-in source location capture.
Deno users: Install from JSR with
deno add jsr:@divkix/logwelland import from@divkix/logwell
For Python applications with automatic batching and retries:
pip install logwell
import asyncio
from logwell import Logwell
client = Logwell({
'api_key': 'lw_YOUR_API_KEY',
'endpoint': 'http://localhost:5173',
'service': 'my-app',
})
# Log at different levels
client.info('User signed in', {'user_id': '123'})
client.error('Database failed', {'host': 'db.local'})
# Flush before shutdown
asyncio.run(client.shutdown())
Features: Async batching, automatic retries with backoff, child loggers, typed errors, source location capture.
For Go applications with zero external dependencies:
go get github.com/Divkix/Logwell/sdks/go
package main
import (
"context"
"log"
"github.com/Divkix/Logwell/sdks/go/logwell"
)
func main() {
client, err := logwell.New(
"http://localhost:5173",
"lw_YOUR_API_KEY",
logwell.WithService("my-app"),
)
if err != nil {
log.Fatal(err)
}
// Log at different levels
client.Info("User signed in", logwell.M{"userId": "123"})
client.Error("Database failed", logwell.M{"host": "db.local"})
// Flush before shutdown
if err := client.Shutdown(context.Background()); err != nil {
log.Printf("Shutdown error: %v", err)
}
}
Features: Zero dependencies, automatic batching, retry with backoff, child loggers, source location capture.
For applications already using OpenTelemetry, point your OTLP log exporter to POST /v1/logs with your API key in the Authorization header.
Node.js / TypeScript
npm install @opentelemetry/exporter-logs-otlp-http @opentelemetry/sdk-logs
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { LoggerProvider, BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
const exporter = new OTLPLogExporter({
url: "http://localhost:5173/v1/logs",
headers: { Authorization: "Bearer lw_YOUR_API_KEY" },
});
const loggerProvider = new LoggerProvider();
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));
Python
pip install opentelemetry-exporter-otlp-proto-http
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
exporter = OTLPLogExporter(
endpoint="http://localhost:5173/v1/logs",
headers={"Authorization": "Bearer lw_YOUR_API_KEY"},
)
logger_provider = LoggerProvider()
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
Go
go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
```go import ( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "go.opentelemetry.io/otel/sdk/log" )
exporter, _ := otlploghttp.New(ctx, otlploghttp.WithEndpointURL("http://localhost:5173/v1/logs"), otlploghttp.WithHeaders(map[string]string{ "Authorization": "Bearer lw_YOUR_API_KEY",
$ claude mcp add Logwell \
-- python -m otcore.mcp_server <graph>