This release is a pre-release and may not be stable for production use.
vise
The Sillo development server, with Foreman built in.
pip install sillo-vise
vise serve
One command runs the project's application, replaces uvicorn's logging with something a person can read at a glance, and mounts the Foreman operations dashboard beside it.
▲ vise 0.1.0 sillo 0.2.1 · python 3.12.13
➜ Local http://127.0.0.1:8000
➜ Foreman http://127.0.0.1:8000/__sillo/foreman
➜ App app.main:app
➜ Panels 9 live · 5 waiting on what they watch
reload on · recorder on · .vise
09:14:22 GET /api/v1/documents 200 38ms 12.4 kB
09:14:22 POST /api/v1/documents 201 204ms 840 B
09:14:23 GET /api/v1/insights 503 2.10s 84 B slow
What it shows
Fourteen panels, in four groups, over hooks the framework already has.
| Group | Panels |
|---|---|
| Monitor | Overview, Requests, Queries, Cache, Outgoing |
| Work | Queues, Workers, Schedules |
| Diagnose | Exceptions, Logs, Real-time, Mail |
| Tools | Routes, Config |
Rows open. Click a request and you get its headers both ways, the body it sent, the body that went back, and everything it caused — the queries, cache reads, log lines and mail — each of those clickable in turn. Click a query and you get the statement, its bindings, and the route that ran it. Escape closes.
A panel appears only when it can observe something. An application with no
Redis has no Queues panel — not a Queues panel showing zeroes, and certainly not
one showing sample data. vise panels says which are live and why the rest are
not:
panel group state
overview Monitor live
queries Monitor no database — sillo.record is not set up
queues Work no queue backend — sillo.work is not set up
logs Diagnose live
routes Tools live
Why the logging is different
uvicorn writes this:
INFO: 127.0.0.1:54118 - "GET /api/v1/documents HTTP/1.1" 200 OK
Everything on that line is true and almost none of it is what somebody watching a development server wants. The level is always INFO, the protocol is always HTTP/1.1, the reason phrase restates the code, and the two facts that matter — how long it took and how much came back — are not there at all, because uvicorn's access logger runs inside its protocol implementation where neither is known.
Vise writes the access line from the recorder's own measurement instead. One thing measures a request and the log reads it, so the log and the dashboard can never disagree, and turning the recorder off turns the access log off with it.
uvicorn's logging is switched off rather than reconfigured — log_config=None,
access_log=False, and its loggers silenced explicitly. Passing a custom
log_config would leave uvicorn owning the configuration.
Three styles: vise (aligned and coloured), plain (aligned, no colour, for a
file or CI) and json (one object per line for a collector).
Configuration
A .vise file in the project root. vise init writes one with every setting
present, commented out, at its default.
[app]
target = "app.main:app"
[server]
host = "127.0.0.1"
port = 8000
reload = true
[dashboard]
path = "/__sillo/foreman"
access = "local" # local | token | open
[recorder]
buffer = 2000
slow_request_ms = 500
redact = ["x-tenant-key"]
capture_bodies = true # request and response bodies, capped at 16 kB
[logs]
style = "vise"
level = "info"
Precedence, lowest to highest: built-in defaults, .vise, VISE_* environment
variables, command-line flags. Only flags actually typed override the file.
Commands
| Command | What it does |
|---|---|
vise serve |
Run the application with Foreman alongside it |
vise init |
Write a starter .vise |
vise doctor |
Report what vise can observe here, and what it cannot |
vise panels |
List the panels, and why any are missing |
vise routes |
The route table, with what guards each route |
vise bench |
Measure the per-request overhead, with the recorder on and off |
vise version |
What is installed, and what each optional piece would add |
vise doctor exits non-zero when any declared panel is missing, so it is usable
in a check without parsing its output.
The four rules it ships with
These are from the Foreman specification, and each is expensive to retrofit and cheap to design in.
Bodies are captured by default. That is a deliberate choice for a loopback-only
development server whose Requests panel exists to answer "what did the server
actually send back" — and it is one line to turn off. They are capped at
max_body_bytes and pass through the same redaction as everything else.
1. Redaction happens on capture, never on read. A watcher that stores a
Cookie header and hides it in the interface is a credential store with a filter
on top. The redactor runs inside the recorder's emit(), before an event reaches
the store, and the store has no un-redacted path at all. The tests prove this by
asserting the secret is not in the store — never that the API hides it.
2. Disabled means compiled out. recorder.enabled = false does not leave a
branch on the hot path. Nothing is constructed, no middleware is wrapped around
the application, and no watcher is attached. Off costs one boolean at boot.
3. The cost is a published number. See Overhead.
4. One recorder, many watchers. Fourteen panels with fourteen collection
paths would be fourteen storage decisions and fourteen ways to leak. There is one
Recorder, one bounded Store, and one watcher per concern.
How it is put together
sillo_vise/
config/ .vise discovery, TOML parse, environment overlay
recorder/ Event types, ring store, time series, redaction
watchers/ One per concern; each proves it can observe before it attaches
panels/ Fourteen panel builders over the store
dashboard/ Raw ASGI middleware: assets, JSON API, SSE stream, access gate
server/ uvicorn runner, reload factory, installation
logs/ Banner, aligned formatter, access line
cli/ serve, init, doctor, panels, routes, version
Two ordering decisions carry most of the weight, both because sillo builds its middleware chain inside-out — the last registered runs first:
- The request recorder is registered first, so it runs innermost, closest to the application. What it measures is the application's own time.
- The dashboard is registered last, so it runs outermost. Its own requests are answered before the recorder is reached, which is why the dashboard cannot appear in its own charts.
The dashboard is a middleware rather than a mounted router on purpose: a mounted router in sillo claims its whole prefix subtree and can shadow routes registered later during startup. An observability tool must not do that to the thing it is observing.
Memory is bounded by configuration, not by traffic
A ring per event kind capped at recorder.buffer, plus a per-minute time series
capped at recorder.window_minutes. Percentiles come from a fixed reservoir per
bucket, so they are estimates — said plainly here and in the interface, because
a p95 presented as exact when it is sampled is worse than one presented as
sampled.
Access
access = "local" is the default and admits only loopback peers, read from the
transport's peer address and never from X-Forwarded-For. A header is a claim
made by whoever is talking to you, and this is the check standing between a
stranger and every request the application has served.
token compares a shared secret in constant time. open has a real use behind
somebody else's authentication and is spelled out rather than reachable by
accident. A refusal never says which mode refused.
Overhead
Rule three, kept. Measured with vise bench, which drives the ASGI application
directly — a loopback TCP round trip is tens of microseconds of noise around an
overhead measured in single ones.
2,000 requests to / on a small application, Python 3.12, Apple silicon:
| p50 | p95 | p99 | overhead | |
|---|---|---|---|---|
| sillo alone | 57.0µs | 84.4µs | 153.7µs | — |
| vise, recorder off | 57.1µs | 82.9µs | 145.9µs | +0.1µs |
| vise, recorder on | 121.3µs | 195.2µs | 251.8µs | +64.3µs |
The middle row is the one that matters. +0.1µs is measurement noise, which is
what "disabled means compiled out" has to mean: with the recorder off nothing is
constructed and no middleware is in the chain, so there is no branch to skip.
The +64.3µs when it is on is mostly redaction and event construction, and that
is the design rather than an accident — headers are decoded and redacted on the
way in, because the alternative is a store holding credentials. Vise is a
development server; run uvicorn app.main:app in production and the cost is
zero because vise is not there.
Reproduce with vise bench -n 2000.
What is not built
The Foreman specification lists more than this ships, and the gaps are deliberate rather than pending.
- Request replay and HAR export. Both send requests to somebody's live application, and neither is needed to see what that application is doing.
- Queue actions — retry a job, pause a queue, forget a cache key. The dashboard's only writes act on the recorder itself: pause, resume, clear. A development tool changing application state by accident is a story nobody wants to be in.
- EXPLAIN from the interface. Same reason: it runs SQL the reader did not write against a live database.
- Workers out of process are only partly visible. When the pool runs elsewhere, only what the shared queue backend reports can be shown — and the panel says so rather than reporting zero workers.
CHANGELOG.md carries the full list, and ARCHITECTURE.md explains the three
places vise monkeypatches and why each was the only measurement point available.
The example
example/ is a working application with every subsystem switched on — database,
cache, queues, workers, schedules, websockets, events, outgoing calls and mail —
so all fourteen panels are live in one process with nothing to install and
nothing to start.
cd example
vise serve # then open /__sillo/foreman/
python traffic.py # in another terminal
traffic.py drives the application the way a person would but faster: reads,
writes, searches, queues exports that fail on purpose, opens websockets, and
asks for the route that raises. See example/README.md for what makes each
panel live and what is worth clicking.
Development
uv sync --extra dev
uv run pytest
cd ui && bun install && bun run build # rebuild the dashboard interface
The interface is a Vite + React app under ui/, built into
sillo_vise/dashboard/static/ and committed, so pip install needs no node.
Changing the interface needs bun; using vise does not.
Licence
BSD-3-Clause.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sillo_vise-1.0.0a2.tar.gz.
File metadata
- Download URL: sillo_vise-1.0.0a2.tar.gz
- Upload date:
- Size: 332.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a5bbcdc7c41ce7645851627d95f8f39769b4fd85fefb5ed08b39401e3920c98
|
|
| MD5 |
19598fec4ae7b8a86fbb190c5123f52d
|
|
| BLAKE2b-256 |
f6ad615315258d75195958b02119503f309b41d25a6b00f601063356fa5c6223
|
File details
Details for the file sillo_vise-1.0.0a2-py3-none-any.whl.
File metadata
- Download URL: sillo_vise-1.0.0a2-py3-none-any.whl
- Upload date:
- Size: 231.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef6d2bac983fd6f9a8e3a768ad4ba4db6bc04ca74087edfe3d801006c79592d0
|
|
| MD5 |
b5a606e1e6308159490db78e4e1b5794
|
|
| BLAKE2b-256 |
04039ed91068623ac09cd15adc0125bd35ac7a06d495cd9241b70dd30e174c90
|