Skip to main content

visionary-sdk (Python)

The Python SDK for Visionary. Add one middleware and every request your Django, Flask or FastAPI service handles shows up in the dashboard, with its route, status, time, user, errors, database queries and outbound calls, plus CPU, memory and GC samples of the process.

The SDK writes straight into a database (PostgreSQL, MySQL, SQLite or Cloudflare D1) that the Visionary server reads. All slow work runs on background threads, so requests are never blocked or slowed.

Quick start

About 10 minutes. You need a running Visionary server (see Get started) and its database URL.

1. Install. Python 3.9 or later. Pick the extra for your database:

pip install "visionary-sdk[postgres]"   # PostgreSQL (psycopg 3)
pip install "visionary-sdk[mysql]"      # MySQL (PyMySQL)
pip install visionary-sdk               # SQLite or Cloudflare D1: no extra needed

Add system (psutil) for full host metrics on macOS and Windows, or all for everything: pip install "visionary-sdk[postgres,system]".

2. Point it at the database. Use the same database as the Visionary server:

VISIONARY_DATABASE_URL=postgres://user:pass@host:5432/visionary
VISIONARY_SERVICE=my-api          # any name; tells services apart

3. Add the middleware. Django shown; Flask and FastAPI are below.

# settings.py
MIDDLEWARE = [
    "visionary.integrations.django.VisionaryMiddleware",  # first, so it sees the final response
    # ...
]

The middleware calls visionary.init() the first time it loads. Nothing else is needed.

4. Shutdown. init() registers an atexit handler that flushes queued rows (waiting at most 10 s), so a normal exit or SIGTERM handled by your server loses nothing. Forking servers (gunicorn --preload) are handled: the background threads restart in each worker.

5. Check it works. The SDK logs to the visionary logger. With INFO logging on (logging.basicConfig(level=logging.INFO)), it prints at start:

visionary: logging requests (path_prefix='', bodies=True, retention_days=0)
visionary: recording system metrics every 15.0s

Send a request. It shows up on API overview and Request logs within a few seconds, and the process shows up on System. If the log says visionary: no database configured (set VISIONARY_DATABASE_URL or D1_*); collection is off, step 2 did not reach the process.

Optional next steps, each a line or two: the signed-in user, outbound HTTP, database queries outside Django, Redis and other calls.

Frameworks

Every integration names the request by its route pattern, so /users/42 and /users/43 count as one route. Exceptions the framework turns into a 500 are recorded as errors; 4xx exceptions (Http404, HTTPException, abort(404)) are not.

Django

Shown in the quick start. Works under WSGI and ASGI, with sync and async views.

  • Route: request.resolver_match.route with a leading / (/users/<int:id>/).
  • User: when you don't call set_user, an authenticated request.user is used (pk as the id, get_username() as the handle).
  • Database queries: recorded automatically for every database alias (the middleware adds a wrapper to execute_wrappers).
  • Pool stats: with Django 5.1+ PostgreSQL pooling ("OPTIONS": {"pool": ...}) the db_* columns on System are filled automatically.

Flask

from flask import Flask
from visionary.integrations.flask import Visionary

app = Flask(__name__)
Visionary(app)            # or: ext = Visionary(); ext.init_app(app)

The route is request.url_rule.rule (/users/<int:user_id>). send_file keeps its sendfile path; only its Content-Length is recorded.

FastAPI, Starlette and any ASGI app

from fastapi import FastAPI
from visionary.integrations.asgi import VisionaryASGIMiddleware

app = FastAPI()
app.add_middleware(VisionaryASGIMiddleware)
# any other ASGI app: app = VisionaryASGIMiddleware(app)

The route is the matched route's path (/users/{user_id}). Streaming responses keep streaming; only the first VISIONARY_LOG_MAX_BODY_BYTES of a body are copied. Spans from sync endpoints (run in a thread pool) and asyncio tasks land on the right request.

Other frameworks and scripts

There is no generic WSGI middleware. For a worker, script or another framework, start the agent yourself; spans, errors and system metrics are still recorded:

import visionary

agent = visionary.init()   # reads the environment, starts, flushes at exit

init() never raises. With no database configured it logs one warning and returns an agent that does nothing, and the integrations pass requests straight through.

For explicit control (tests, several agents, config in code):

cfg = visionary.from_env()          # or visionary.Config(...)
cfg.path_prefix = "/api"
agent = visionary.Agent(cfg).start()
app.add_middleware(VisionaryASGIMiddleware, agent=agent)   # Flask: Visionary(app, agent=agent)
...
agent.close(timeout=10)             # drains the queue and flushes samples

Agent is also a context manager (with visionary.Agent(cfg) as agent:).

The signed-in user

Call set_user from auth middleware or a view once the user is known. Only id is required:

visionary.set_user(user.id, handle=user.email, role="admin")

It is contextvar-based, so it works from sync views, thread pools and asyncio tasks. Outside a request handled by an integration, it does nothing. visionary.set_route("/custom/{pattern}") sets the route the same way, for routers the integration cannot see.

To fill in handles and roles for ids logged without them:

def lookup_user(user_id):
    u = USER_CACHE.get(user_id)          # runs on the writer thread; keep it cheap
    return (u.email, u.role) if u else None

cfg = visionary.from_env()
cfg.lookup_user = lookup_user
visionary.init(cfg)

Outbound HTTP

Off until you turn it on. Call this once at startup (or set VISIONARY_INSTRUMENT_HTTP=true):

visionary.instrument_http()   # patches requests, httpx (sync and async) and urllib3

That also covers boto3 and other SDKs built on urllib3, requests or httpx. Each call is a span named METHOD host; the detail is the URL without credentials, query or fragment. It fails on a transport error or a 5xx answer. A call through requests makes one span, not a second one for urllib3.

Database queries

Django's ORM is automatic. For anything else, add one line where the engine or connection is created:

SQLAlchemy (sync or async engine)

engine = visionary.instrument_sqlalchemy(create_engine(DSN))

psycopg 3 (sync or async), psycopg2, PyMySQL, sqlite3

conn = visionary.instrument_connection(psycopg.connect(DSN))   # or instrument_cursor(cur)

Both take an optional target to rename the database.

What is recorded: each statement as a span named after the first keyword and main table (SELECT users, INSERT bookings, BEGIN), with the target system/database (postgresql/app, sqlite/app.db). The statement is sanitized: literals become ?, IN (?, ?, ...) becomes IN (?), whitespace is collapsed. Parameter values are never stored. Until a database is instrumented, a request's database totals stay empty ("cannot see"), not zero.

Redis, queues and your own code

There is no Redis or queue hook. Wrap the call in a span (also async with):

with visionary.span("cache", "GET session"):
    session = redis.get(key)

with visionary.span("queue", "publish order.created", target="rabbitmq") as s:
    channel.basic_publish(...)
    s.set_row_count(1)       # optional; s.set_error(...) marks it failed

Kinds are db, http, cache, queue, custom, graphql and background; anything else becomes custom. An exception marks the span failed and propagates unchanged. Time no span covers shows as Untracked in the request's breakdown; see What the middleware sees.

Work after the response

Work a handler starts and does not wait for (an email, an image resize) stays linked to its request. Start it with one of two helpers:

# a new thread (not a daemon, so the process waits for it at exit)
visionary.background("send welcome email", send_welcome, user.id)

# work you schedule yourself: an executor, asyncio.create_task, ...
ctx = contextvars.copy_context()
def resize():
    with visionary.background_span("resize avatar"):   # also `async with`
        make_thumbnails(path)
executor.submit(ctx.run, resize)

Each records one background span covering the whole job, marked error if the job raises (the exception still propagates). Its database and HTTP calls are their own spans as usual. In the request's breakdown they appear after the end of the request and are left out of its totals and untracked time. Outside a request the helpers still record the span when it takes at least VISIONARY_SPAN_THRESHOLD_MS.

Errors

The integrations record every exception the framework turns into a 500 (handled = 0) and leave the exception and the response unchanged. To record one you handled:

try:
    charge(card)
except PaymentError:
    visionary.capture_exception()      # or capture_exception(exc)

Inside a request the row carries its request id, method, route, path and user. Errors are grouped into issues on Errors by a fingerprint: the first 16 hex characters of sha1(type + "\n" + frames), over the 5 innermost frames of your own code (not the standard library, site-packages or this SDK), each function@path relative to its sys.path entry. If no frame is yours, the 5 innermost frames are used; with no frames, the message with digit runs replaced by 0. At most 20 rows per fingerprint per minute are kept.

Query plans

Not supported in the Python SDK yet. Explain on Queries is answered only by a service that registered an explainer for that database (the Go and Node SDKs can). For a database only Python services use, the request stays pending.

CPU profiles

Off until someone turns them on in the dashboard (Settings → Profiling). No redeploy either way: the SDK reads the switch at start and every 30 seconds, and logs visionary: CPU profiling on (10000 ms every 300 s) or visionary: CPU profiling off. The dashboard can limit profiling to some services (matched against VISIONARY_SERVICE) and set how often a profile is taken (default every 300 s) and how long it samples (default 10 s). Profile now on CPU profiles takes one within about 30 seconds, even while profiling is off.

VISIONARY_PROFILE=false makes a service ignore the dashboard and never profile.

How it works: a daemon thread reads every thread's Python stack about every 10 ms while a profile runs. It costs a few percent of one CPU while sampling and nothing in between, and is skipped when no request arrived since the last profile. Only threads using the CPU are counted. On Linux that uses per-thread CPU clocks. On Windows and macOS it is a heuristic: a thread whose innermost frame is in a known blocking call (time.sleep, lock waits, queue.get, select, socket accept/recv, asyncio's idle select) is left out. Time inside C extensions shows on the Python frame that called them.

The SDK never creates the profiles or visionary_settings tables (the Visionary server does). Until they exist profiling stays off and the log says CPU profiling settings unavailable.

System metrics

On by default, every 15 seconds: host CPU, load, memory, disk and network; process CPU, RSS, threads and open files; Python threads (in the goroutines column), GC collections and pause time, the tracemalloc heap when tracing is on; HTTP throughput seen by the integrations; and node-exporter style columns (CPU breakdown, swap, disk I/O, TCP, processes, context switches).

On Linux everything is read from /proc, with or without psutil. On macOS and Windows install the system extra, or most host and process columns stay empty. iowait, steal, disk busy, TCP and process counts are Linux only.

To fill the database pool columns, give it your pool's stats (counters are cumulative; missing keys are stored empty):

def db_stats():
    s = pool.get_stats()                     # e.g. psycopg_pool
    return {
        "total": s["pool_size"], "idle": s["pool_available"],
        "in_use": s["pool_size"] - s["pool_available"], "max": s["pool_max"],
        "acquires": s.get("requests_num", 0), "empty_acquires": s.get("requests_queued", 0),
        "acquire_wait_ms": s.get("requests_wait_ms", 0),
    }

cfg = visionary.from_env()
cfg.db_stats = db_stats
visionary.init(cfg)

Service and release

VISIONARY_SERVICE names the service, so several can share one database. The release is VISIONARY_RELEASE, else the first of RAILWAY_GIT_COMMIT_SHA, RENDER_GIT_COMMIT, VERCEL_GIT_COMMIT_SHA, HEROKU_SLUG_COMMIT, SOURCE_VERSION, GIT_COMMIT, GITHUB_SHA, K_REVISION; commit hashes are cut to 12 characters. When it is known, a releases row is written at start and the dashboards mark the deploy.

Choosing the database

The app and the Visionary server must use the same database. The SDK takes the first of these that is set:

  1. Your own connection factory, when the app already has a driver:

    cfg = visionary.from_env()
    cfg.connection_factory = lambda: psycopg.connect(DSN)   # returns a new DB-API connection
    cfg.dialect = "postgres"                               # "postgres", "mysql" or "sqlite"
    visionary.init(cfg)
    

    The SDK keeps that connection for itself (one shared connection for PostgreSQL and MySQL, one per worker thread for SQLite), reconnects through the factory after an error and closes it at shutdown. Each batch is one transaction; an autocommit connection makes batches non-atomic.

  2. VISIONARY_DATABASE_URL.

  3. D1_ACCOUNT_ID, D1_DATABASE_ID and D1_API_TOKEN.

Database VISIONARY_DATABASE_URL Install
PostgreSQL postgres://user:pass@host:5432/db (or postgresql://) visionary-sdk[postgres] (psycopg 3)
MySQL 8 / MariaDB mysql://user:pass@host:3306/db visionary-sdk[mysql] (PyMySQL)
SQLite sqlite:///var/lib/app/visionary.db, sqlite:///C:/data/visionary.db on Windows, sqlite://relative.db nothing (stdlib sqlite3)
Cloudflare D1 leave it unset, or d1://, and set D1_ACCOUNT_ID, D1_DATABASE_ID, D1_API_TOKEN nothing

The D1 token needs D1 edit access; D1_API_BASE overrides the API URL. SQLite works only when the app and the server run on the same host.

Tables are created at startup from the schema files in the package (visionary/schema/<dialect>/), on the background threads, so a slow database never delays boot. If the database is down, creation is retried before each write. Older tables gain missing columns in place.

Configuration

Every variable has a matching visionary.Config attribute. Set it on the result of visionary.from_env() and pass that to init().

Variable Config attribute Default What it does
VISIONARY_DATABASE_URL database_url Where rows go; see above.
D1_ACCOUNT_ID, D1_DATABASE_ID, D1_API_TOKEN, D1_API_BASE d1_account_id, d1_database_id, d1_api_token, d1_api_base Cloudflare D1.
connection_factory, dialect Your own connection; see above.
VISIONARY_SERVICE service empty Service name on every row.
VISIONARY_RELEASE version platform commit Release on every row and the deploy marker.
VISIONARY_INSTANCE instance hostname Labels this process on System.
VISIONARY_LOG_REQUESTS log_requests true Request logging on or off.
VISIONARY_LOG_PATH_PREFIX path_prefix empty (all) Only log paths starting with this, e.g. /api.
VISIONARY_LOG_BODIES capture_bodies true Store request and response bodies (redacted).
VISIONARY_LOG_MAX_BODY_BYTES max_body_bytes 8192 Body bytes kept per body.
VISIONARY_LOG_REDACT_HEADERS redact_headers Comma list of headers added to the redacted defaults.
VISIONARY_LOG_REDACT_FIELDS redact_fields JSON/form keys added to the redacted defaults.
VISIONARY_LOG_REDACT_REQUEST_FIELDS redact_request_fields Keys redacted in request bodies only.
VISIONARY_LOG_ERROR_CODE_FIELD error_code_field error.code Where error responses keep their code (dotted JSON path).
VISIONARY_LOG_ERROR_KEY_FIELD error_key_field error.key The same, for the error key.
VISIONARY_LOG_RETENTION_DAYS log_retention_days 0 (off) Only for a database no Visionary server reads: the SDK then deletes its own api_logs, spans, errors and profiles older than this.
VISIONARY_SYSTEM_METRICS system_metrics true System samples on or off.
VISIONARY_SYSTEM_INTERVAL system_interval (seconds) 15s Time between samples: 15, 500ms, 1m.
VISIONARY_SYSTEM_RETENTION_DAYS system_retention_days 0 (off) The same as the log retention, for system_metrics.
VISIONARY_SPANS spans true Database and outbound call spans and totals.
VISIONARY_SPAN_THRESHOLD_MS span_threshold_ms 100 Keep calls made outside a request at least this slow.
VISIONARY_SPAN_MAX span_max 100 Spans kept per request; later calls still count in the totals.
VISIONARY_ERRORS errors true Error capture on or off.
VISIONARY_INSTRUMENT_HTTP instrument_http false Call instrument_http() at start.
VISIONARY_PROFILE profile true (follow the dashboard) false never profiles.
queue_size 5000 Request queue entries; when full, new entries are dropped and logged.
batch_size 50 Rows per write.
flush_interval 3.0 Seconds between writes.
db_stats Pool stats for the db_* columns.
disk_path working directory Filesystem reported in the disk columns.
lookup_user (id) -> (handle, role), see the signed-in user.

Booleans accept true/false/1/0/yes/no. Old rows are normally deleted by the Visionary server, following Settings → Data in the dashboard.

What is captured

Each request (api_logs): the request id (incoming X-Request-ID up to 128 characters, else a random one, echoed on the response), start time and duration, method, path, route and query string, final status (500 when the handler raised), client IP (first X-Forwarded-For hop) and user agent, the user, request headers, bodies up to the cap with truncation flags and full sizes, the error code and key from JSON error bodies, and the request's database and HTTP call counts and time.

Bodies are not captured for multipart/*, image/*, video/*, audio/*, application/octet-stream, application/zip and application/pdf.

Redaction runs before anything is stored, case-insensitive:

  • Headers Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, plus VISIONARY_LOG_REDACT_HEADERS.
  • JSON and form keys password, passwd, secret, client_secret, token, access_token, refresh_token, id_token, api_key, apikey, otp, pin, card_number, cvv, private_key, plus VISIONARY_LOG_REDACT_FIELDS, also in bodies cut at the cap.

Writes: requests, spans, errors and the release row are batched (up to 50 every 3 s, one transaction per flush). A failed batch is retried up to 3 times, then dropped with an error log. System samples are written 4 at a time and kept (up to an hour) while the database is unreachable.

API

Call What it does
init(config=None) Builds, starts and returns the process-wide agent; flushes at exit. Never raises.
get_agent() The agent from init(), if any.
from_env(), Config(...), Agent(config).start(), agent.close(timeout=10) Explicit setup.
VisionaryMiddleware (Django), Visionary (Flask), VisionaryASGIMiddleware Framework integrations, in visionary.integrations.*.
set_user(id, handle=None, role=None), set_route(pattern) Annotate the current request.
capture_exception(exc=None) Record a handled error.
span(kind, name, target=None, detail=None) Custom span (with / async with).
background(name, fn, *args, **kwargs), background_span(name) Work after the response.
instrument_http() Hook requests, httpx and urllib3.
instrument_sqlalchemy(engine), instrument_connection(conn, target=None), instrument_cursor(cur, target=None) Database hooks.

Troubleshooting

Symptom Cause Fix
Nothing shows up; the log says no database configured ... collection is off VISIONARY_DATABASE_URL (or the D1 variables) is not set in the process. Set it where the service runs (check the gunicorn/uvicorn environment, not only your shell).
Nothing shows up; no visionary: lines at all The middleware is not installed, or INFO logs are hidden. Check the middleware is listed first. Turn on INFO for the visionary logger to see its start lines.
Nothing shows up, but the SDK logs fine The app and the server use different databases. Point both at the same database (same host, database name and, for D1, the same database id).
PostgreSQL needs psycopg: pip install 'visionary-sdk[postgres]' (or MySQL / PyMySQL) The driver for the URL scheme is missing. Install the extra.
create ... schema failed (will retry) The database user cannot create tables, or the database is down. Grant CREATE, or check the connection. It retries before each write.
Requests show but no outbound calls HTTP is not hooked by default. Call visionary.instrument_http() at startup or set VISIONARY_INSTRUMENT_HTTP=true.
Requests show but no database queries (Flask, FastAPI) The driver is not instrumented (only Django's ORM is automatic). Add instrument_sqlalchemy(engine) or instrument_connection(conn).
Some requests show large Untracked time Redis, queue clients or CPU work are not wrapped. Wrap them in visionary.span(...).
Explain on Queries stays pending forever The Python SDK has no explainer, so nothing claims the request. Plans are answered only by a Go or Node service registered with an explainer for that database, whose target matches the span target (postgresql/<dbname>).
No CPU profiles Profiling is off in the dashboard, the service is not in the chosen list, VISIONARY_PROFILE=false, or no requests arrived since the last profile. Turn it on in Settings → Profiling and check the service name matches VISIONARY_SERVICE. Look for visionary: CPU profiling on in the log.
request batch write failed ... exceeded D1's free tier daily row read limit Cloudflare D1's free plan allows a fixed number of row reads and writes a day; the SDK's writes and the dashboard's reads count. Move to the Workers Paid plan, or to PostgreSQL or MySQL. Until then, write less: VISIONARY_LOG_PATH_PREFIX=/api, VISIONARY_LOG_BODIES=false, a longer VISIONARY_SYSTEM_INTERVAL.
request queue full; N entries dropped Writes cannot keep up (common on SQLite or D1 under heavy traffic). Use PostgreSQL or MySQL, or raise queue_size / batch_size.
The last requests before a deploy are missing The process was killed before the atexit flush ran (SIGKILL, or a SIGTERM nobody handled). Let the server shut down gracefully, or call visionary.get_agent().close() in your shutdown hook. A request log flush timed out; N pending line means 10 s was not enough.
/users/42 and /users/43 show as separate routes No route pattern for this request. Call visionary.set_route("/users/{id}") in the handler.

Every Visionary SDK follows the same contract: sdks/SPEC.md.

Metadata

Release files for visionary-sdk 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for visionary-sdk 0.8.0
File Size Uploaded Provenance
visionary_sdk-0.8.0.tar.gz 70.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for visionary-sdk 0.8.0
File Interpreter ABI Platform
visionary_sdk-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 158.2 kB

Release files / visionary_sdk-0.8.0.tar.gz

Download URL visionary_sdk-0.8.0.tar.gz
Size 70.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c6e717792c6d24f0030bc96c82e7361373ea59db820b038cb8c5b964bb4f6142
BLAKE2b-256 checksum
How to use checksums
9c3881cf782695fe000a2009d25a68c49c295656e07f12b60c4de70eff5f7b1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / visionary_sdk-0.8.0-py3-none-any.whl

Download URL visionary_sdk-0.8.0-py3-none-any.whl
Size 87.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e0cc47a68e7f4ed8d73ee4a41a8099a4704b4b393c2eb2416813154e2548cf96
BLAKE2b-256 checksum
How to use checksums
cad10396f4069796633fff79a7ff24c25873a5ab6102568446b14546a9c4f4cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for visionary-sdk 0.8.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for visionary_sdk-0.8.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page