Glasshouse
루팅된 LG webOS TV용 대시보드·프라이버시 제어·Home Assistant 연동
LLM 호출별 소요 시간과 자체 호스팅 트레이스·비용·평가·프롬프트를 확인하고 DNS·연결·TLS·첫 바이트 대기 시간을 측정
See where every LLM call spends its time. Self-hosted traces, costs, evals and prompts, plus a gateway that times DNS, connect, TLS and the wait for the first byte.
개발 도구TypeScript · GoGitHub @Veeky-kumar온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Self-hostable LLM observability, evaluation, and prompt management.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗See where every LLM call spends its time.
Self-hosted traces, evals and prompts, plus a gateway that times every call: DNS, connect, TLS, the wait for the first byte, and the transfer.
Website · Architecture · Python SDK
Install · First trace · Gateway · Run it in public · Backups · Limits
curl -fsSL https://raw.githubusercontent.com/Veeky-kumar/SpanRail/main/deploy/install.sh | sh
From the landing page to a first trace. The dashboards in the clip show demo data.
Your LLM call took 3 seconds. Was it the DNS lookup? The TLS handshake? The provider thinking? The answer crawling back?
Most tools give you one number. Spanrail's gateway splits each call into its phases and draws them to scale, next to the traces, costs and prompts you already need. Point your client at the gateway, change nothing else, and every call shows up with its own time budget.
It runs on your machine. Spanrail itself never phones home.
production from your app, edit in the dashboard.A working MVP, not a hardened product. It has basic rate limits and a switch to close signups, but it does not have password reset, roles with different powers, or an end-to-end test suite. The known limitations list is the honest version.
You need Docker with Compose v2. This pulls the published images, so there is nothing to clone or build:
curl -fsSL https://raw.githubusercontent.com/Veeky-kumar/SpanRail/main/deploy/install.sh | sh
It makes a spanrail/ folder, writes a .env with fresh secrets, starts Postgres, Redis, the web app and the worker (migrations run first), and prints the address: http://localhost:3000. Create an account, make a project, make an API key, then send a trace.
Prefer to read before you run? deploy/install.sh is short. The stack is deploy/docker-compose.yml plus a .env: copy deploy/.env.example and fill in the three secrets with openssl rand -hex 32. Add the gateway with docker compose --profile gateway up -d once your key pair is in .env.
Two things to know:
127.0.0.1 only. Put a reverse proxy with TLS in front before you open it up, and set WEB_BIND=0.0.0.0 only behind one. Then read Run it in public.SPANRAIL_VERSION=0.1.0 in .env (the default is latest). Back up .env. If you lose API_KEY_SALT, every API key stops working.Python (spanrail on PyPI, no dependencies, Python 3.9+):
pip install spanrail
from spanrail import Spanrail
sr = Spanrail("pk-...", "sk-...", base_url="http://localhost:3000")
trace = sr.trace("support-chat", user_id="u_123")
with trace.generation("answer", model="gpt-4o-mini", input="Where is my order?") as gen:
gen.end(output="It shipped yesterday.", usage={"input": 12, "output": 7})
Events are sent from a background thread and flushed when your program exits. The SDK never raises into your app. More in sdk-python/README.md.
TypeScript (not on npm yet, so import the local source):
import { Spanrail } from "./sdk"; // local source for now
const spanrail = new Spanrail({
publicKey: process.env.SPANRAIL_PUBLIC_KEY!,
secretKey: process.env.SPANRAIL_SECRET_KEY!,
baseUrl: "http://localhost:3000",
});
const trace = spanrail.trace({ name: "my-agent-run", userId: "user_123" });
const span = trace.span({ name: "tool:search", input: { query: "..." } });
span.end({ output: { results: [] } });
const generation = trace.generation({ name: "llm:answer", model: "gpt-4o-mini" });
generation.end({ output: "...", usageDetails: { input: 120, output: 40 }, costDetails: { input: 0.00006, output: 0.00004 } });
await spanrail.shutdown(); // flushes queued events before the process exits
Fuller examples you can run:
examples/simple-agent.ts is a multi-span agent with a deliberate tool failure, so you can see errors and nesting.examples/run-dataset-eval.ts runs the whole loop: dataset, run, scorers.SPANRAIL_PUBLIC_KEY=pk-tw-... SPANRAIL_SECRET_KEY=sk-tw-... npx tsx examples/simple-agent.ts
If you would rather not touch your code, use the gateway. gateway/ is a small reverse proxy in Go (standard library plus prometheus/client_golang) that sits in front of any OpenAI-compatible API. It forwards every request unchanged, streaming included, and reports each call to Spanrail with its DNS, connect, TLS and first-byte timings. It talks to Spanrail only through the public ingestion API, so the web app and worker do not depend on it.
your app --> spanrail-gateway (Go) --> LLM provider
|
+--(async, batched)--> Spanrail POST /api/public/ingestion
A prebuilt image (linux/amd64 and linux/arm64, about 5 MB) is on GitHub Container Registry:
docker run -p 127.0.0.1:8080:8080 \
-e UPSTREAM_URL=https://api.openai.com \
-e SPANRAIL_BASE_URL=http://host.docker.internal:3000 \
-e SPANRAIL_PUBLIC_KEY=pk-... -e SPANRAIL_SECRET_KEY=sk-... \
ghcr.io/veeky-kumar/spanrail-gateway:latest
Then set your client's base URL to http://localhost:8080/v1. The provider key stays in your client.
Notes:
host.docker.internal reaches a Spanrail running on your machine from Docker Desktop. On Linux, add --add-host=host.docker.internal:host-gateway, or use your server's address.
The gateway does not authenticate its callers, so keep it on localhost or a private network.
To build it yourself you need Go 1.27.3+ (the Docker image does it for you). From the repo root, with the key pair that npm run db:seed printed:
export SPANRAIL_PUBLIC_KEY=pk-... SPANRAIL_SECRET_KEY=sk-...
docker compose --profile gateway up -d --build gateway # listens on 127.0.0.1:8080
Configuration, the Go test and CI commands, and the measured results are in gateway/README.md and gateway/RESULTS.md. The gateway has its own CI (gateway.yml): gofmt, vet, race-detector tests, build, govulncheck and a Docker image build.
Create a prompt and fetch it by label over HTTP:
curl -X POST http://localhost:3000/api/public/prompts \
-H "authorization: Basic $(echo -n "$SPANRAIL_PUBLIC_KEY:$SPANRAIL_SECRET_KEY" | base64)" \
-H "content-type: application/json" \
-d '{"name":"support-agent","prompt":"You are a support agent. {{tone}}","labels":["production"]}'
curl "http://localhost:3000/api/public/prompts/support-agent?label=production" \
-H "authorization: Basic $(echo -n "$SPANRAIL_PUBLIC_KEY:$SPANRAIL_SECRET_KEY" | base64)"
The dashboard's Prompts tab uses the same code path.
If your instance is reachable from the internet:
SIGNUPS_DISABLED=true. Existing users still sign in, and the sign-up page says registration is closed.SITE_URL to your public address (for example https://spanrail.example.com). The landing page then gets a canonical link, a link-preview card, a sitemap and structured data. Leave it unset on a private install: the site then asks search engines not to index it. To verify the site in Google Search Console with the HTML tag method, also set GOOGLE_SITE_VERIFICATION to the token it gives you.GET /api/health answers {"status":"ok"}, or 503 when the database is unreachable. Point an uptime monitor at it..github/workflows/backup.yml dumps the database every night, encrypts the dump and keeps it for 14 days as a workflow artifact. It does nothing until you add two repository secrets:
BACKUP_DATABASE_URL: the database's external connection string.BACKUP_PASSPHRASE: a long random passphrase. Keep a copy somewhere safe, because without it the backups cannot be read.The dump is encrypted because artifacts of a public repository can be downloaded by any signed-in GitHub user.
To restore, download the artifact, then:
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in spanrail-YYYY-MM-DD.dump.enc -out spanrail.dump
pg_restore --no-owner --no-privileges --clean --if-exists -d "$DATABASE_URL" spanrail.dump
Try a restore into an empty database once, before you need it. Dumps are made with Postgres 18 tools, so restore them with a pg_restore of version 18 or newer; an older one refuses the file. Restoring into an older server can print warnings about settings it does not know, so read them before you trust the result.
A self-hosted install never reports anything to anyone. If you run a public instance with a landing page, two optional features show you who arrives:
UMAMI_SCRIPT_URL (for example https://cloud.umami.is/script.js) and UMAMI_WEBSITE_ID. The tracker loads only on the landing, sign-in and sign-up pages, never inside the signed-in app, and it respects Do Not Track. If either variable is unset, nothing loads./admin/growth, for the addresses in ADMIN_EMAILS (comma separated; everyone else gets a 404). It counts accounts per day, how far people get (project, API key, trace, gateway call) and where they came from. At sign-up the app records the referring site, any utm_source, utm_medium and utm_campaign tags, and a two-letter country when your host's edge network sends one (for example CF-IPCountry). It never stores a path, a query string or an IP address, and the sign-up form says so. A first-party cookie (sr_src, 30 days) carries the source from the first page to sign-up, and it is skipped when the browser sends Do Not Track.You need Node 24+, Docker, and a Postgres and Redis pair (the included Compose file gives you both).
npm install
cp .env.example .env # edit if you changed the Compose ports below
docker compose up -d # starts Postgres (5455) and Redis (6399)
npm run db:migrate # applies the schema
npm run db:seed # creates a demo user, project and API key. Read the output.
The seed prints a demo login ([email protected] and a password) and an SDK key pair (SPANRAIL_PUBLIC_KEY, SPANRAIL_SECRET_KEY). Save the secret key now: it is hashed at rest and cannot be recovered.
Then, in two terminals:
npm run dev # dashboard and public API at http://localhost:3000
npm run worker # background ingestion. Traces will not appear unless this is running.
Sign in at http://localhost:3000/login with the seeded account.
Tests
npm run typecheck # strict TypeScript across the project
npm test # unit tests: trace trees, scorers, hashing, formatting, charts
The unit tests cover logic that needs no database. The path from ingestion to worker to dashboard, auth and the eval pipeline are exercised by the examples/*.ts scripts and by hand. There is no automated end-to-end suite for the Node app yet.
Containers for a real deployment
cp .env.production.example .env.production # set real secrets; the file explains each one
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.production up -d --build
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec web npx prisma migrate deploy
This builds web and worker as containers next to Postgres and Redis. See docker-compose.prod.yml and the Dockerfiles in docker/ for exactly what ships.
The full list is in ARCHITECTURE.md. In short:
exact_match, contains, json_valid) are built and tested. Model-based judging has a reserved name (llm_judge:*) but is not connected to any provider.OWNER and MEMBER exist in the schema, but both have the same permissions today.gateway/scripts/e2e.sh, run by hand), and its unit tests, vet and vulnerability check run in CI.MIT. See LICENSE and THIRD_PARTY_NOTICES.md.