Capture outgoing and incoming HTTP requests, pytest results, Python logs, and unhandled exceptions in a local web dashboard.
Like Mailpit, but for your entire debug output. HTTP traffic in both directions, pytest results, log records, and crash tracebacks all appear in one timeline. Prefix your command with smello run; no code changes are needed.
Why port 5110? Read it as 5-1-1-0 → S-L-L-O → smello.
pip install smello smello-server
smello-serverOr run with Docker:
docker run -p 127.0.0.1:5110:5110 ghcr.io/smelloscope/smellosmello run my_app.py
smello run pytest tests/
smello run uvicorn app:appThat's it. Outgoing HTTP requests, pytest results, unhandled exceptions, and optional log records are captured automatically. Subprocess instrumentation propagates automatically, so smello run gunicorn app:app also captures traffic from worker processes.
Browse captured events at http://localhost:5110.
Tag events with --app and --session to isolate a debugging run without clearing existing data:
smello run --app myapp --session debug-payment python scripts/checkout.pyThen filter the dashboard or API to see only those events:
curl -s 'http://localhost:5110/api/events?app=myapp&session=debug-payment'If you prefer to activate Smello from within your code (e.g., for programmatic configuration or projects with a custom sitecustomize.py):
import smello
smello.init() # activates only when SMELLO_URL is setTo capture incoming requests, add the Smello middleware to your web framework and run with smello run:
FastAPI:
from smello.integrations.fastapi import SmelloMiddleware
from fastapi import FastAPI
app = FastAPI()
app.add_middleware(SmelloMiddleware, ignore_paths=["/health"])smello run uvicorn app:appDjango:
# settings.py
MIDDLEWARE = [
"smello.integrations.django.SmelloMiddleware",
...
]
SMELLO_IGNORE_PATHS = ["/health/", "/admin/"]smello run manage.py runserverEvery request your server handles appears in the dashboard with method, path, status code, duration, route pattern, and client IP. Unhandled exceptions are captured with full tracebacks.
Smello ships with Agent Skills for Claude Code, Cursor, GitHub Copilot, and 20+ other AI coding tools.
npx skills add smelloscope/smello| Skill | Install individually | Description |
|---|---|---|
/smello-setup |
npx skills add smelloscope/smello --skill smello-setup |
Explores your codebase and proposes a plan to integrate Smello (package install, entrypoint placement, Docker Compose, env vars). Does not make changes without approval. |
/smello |
npx skills add smelloscope/smello --skill smello |
Queries the Smello API to inspect captured events — HTTP traffic, log records, and exceptions. Also activates automatically when you ask about debugging. |
For every outgoing HTTP and gRPC call:
- Method, URL, headers, and body
- Response status code, headers, and body
- Duration in milliseconds
- Library used (requests, httpx, aiohttp, grpc, or botocore)
gRPC calls are displayed with a grpc:// URL scheme. Protobuf bodies are automatically serialized to JSON. Sensitive headers (Authorization, X-Api-Key) are redacted by default.
Failed outgoing calls retain exception details. If a library raises after receiving a response, Smello stores both the response status and headers and the exception.
Calls to the OpenAI, Anthropic, and Gemini APIs render as a readable conversation in the dashboard, with the system prompt, tool calls, and token usage broken out. The raw JSON stays one tab away.
With the FastAPI or Django middleware, Smello captures every request your server handles:
- Method, path, full URL, and route pattern
- Request and response headers and bodies
- Status code, duration, and client IP
- Exception type and traceback (if the handler raises)
The bundled plugin supports pytest 7 and later. Install the pytest extra if your project doesn't already include pytest:
pip install "smello[pytest]"Prefix your pytest command with smello run to capture one event for every test function or method invocation. Parametrized cases produce separate events.
- Outcome: passed, failed, error, skipped, xfailed, or xpassed
- Total duration and setup, call, and teardown timings
- Fixture names used by the test
- Failure details for assertion, setup, or teardown errors, including pytest's formatted traceback
- Test node ID, source location, run ID, and xdist worker ID
Each test invocation also carries runtime trace and span IDs plus stable references to its directory, file, class, and unparameterized test case. Outgoing Requests, HTTPX, aiohttp, botocore, and gRPC calls inherit the active test membership and appear as child operations. Runtime parenthood is always visible. The dashboard's Group selector can add one available pytest level or remote-host grouping without changing event deep links. Virtual groups merge only across adjacent siblings, which preserves record order. Timeline filters retain matching records' runtime ancestors, so filtered children remain connected to their runtime context.
Smello stores fixture names as structured data. The traceback generated by pytest may also include repr() output for parameter, fixture, or local variable values.
See Debug pytest tests with Smello for a runnable parametrized failure example and dashboard walkthrough.
When capture_logs=True, Smello hooks into Python's logging module and captures log records at or above the configured level:
- Log level, logger name, and formatted message
- Source file, line number, and function name
- Extra attributes attached to the record
Unhandled exceptions are captured automatically (enabled by default):
- Exception type, message, and module
- Full formatted traceback
- Stack frames with file, line, function, and source context
smello.init(
server_url="http://localhost:5110", # where to send captured data
# HTTP capture
capture_hosts=["api.stripe.com"], # only capture these hosts
capture_all=True, # capture everything (default)
ignore_hosts=["localhost"], # skip these hosts
redact_headers=["Authorization"], # replace header values with [REDACTED]
redact_query_params=["api_key", "token"], # replace query param values with [REDACTED]
# Tests, logs, and exceptions
capture_tests=True, # capture pytest executions (default)
capture_exceptions=True, # capture unhandled exceptions (default)
capture_logs=False, # capture log records (opt-in)
log_level=30, # minimum log level to capture (WARNING)
ignore_loggers=["uvicorn.access"], # suppress noisy framework loggers
# Tagging
app="myapp", # tag events with an application name
session="debug-payment", # tag events with a session ID
)All parameters fall back to SMELLO_* environment variables when not passed explicitly:
| Parameter | Env variable | Default |
|---|---|---|
server_url |
SMELLO_URL |
None (inactive) |
capture_all |
SMELLO_CAPTURE_ALL |
True |
capture_hosts |
SMELLO_CAPTURE_HOSTS |
[] |
ignore_hosts |
SMELLO_IGNORE_HOSTS |
[] |
redact_headers |
SMELLO_REDACT_HEADERS |
["Authorization", "X-Api-Key"] |
redact_query_params |
SMELLO_REDACT_QUERY_PARAMS |
[] |
capture_tests |
SMELLO_CAPTURE_TESTS |
True |
capture_exceptions |
SMELLO_CAPTURE_EXCEPTIONS |
True |
capture_logs |
SMELLO_CAPTURE_LOGS |
False |
log_level |
SMELLO_LOG_LEVEL |
30 (WARNING) |
ignore_loggers |
SMELLO_IGNORE_LOGGERS |
[] |
app |
SMELLO_APP |
"" |
session |
SMELLO_SESSION |
"" |
The server URL is the activation signal — init() does nothing unless server_url is passed or SMELLO_URL is set. Boolean env vars accept true/1/yes and false/0/no (case-insensitive). List env vars are comma-separated.
Smello Server deletes events older than seven days when it starts and every hour after that. Set SMELLO_RETENTION_DAYS on the server process to change the retention period, or set it to 0 to keep events indefinitely. The server rejects negative or malformed values at startup.
Smello provides a JSON API for exploring captured events from the command line.
The smello query command wraps the API's common filters and output formats:
smello query
smello query --session debug-payment --type http --status 500 --ancestors
smello query --session debug-payment --after-seq 1642
smello query --session debug-payment --stats
smello query 5ae54ca2
smello query 'http://localhost:5110/#5ae54ca2-45a7-45a6-a6cd-533569fc8db7'
smello query --type log --format jsonl
smello metaList queries use compact text by default and indent events by their runtime hierarchy,
adding an app/session column when the results span more than one run. Pass an
eight-character ID from text output, a full UUID, or a dashboard URL to print the
complete event as formatted JSON. smello meta lists every app, session, host, event
type, and method on the server, so you can see what is worth filtering for. Use
--server or SMELLO_URL to query a different server.
# All captured events (unified timeline) — returns {epoch, max_seq, events}
curl -s http://localhost:5110/api/events | python -m json.tool
# Filter by event type
curl -s 'http://localhost:5110/api/events?event_type=log'
# Filter by method, host, or status (HTTP events)
curl -s 'http://localhost:5110/api/events?method=POST&host=api.stripe.com'
# Filter by app or session
curl -s 'http://localhost:5110/api/events?app=myapp&session=debug-payment'
# Full-text search across summaries and event data
curl -s 'http://localhost:5110/api/events?search=ValueError'
# Limit results (default: 200, max: 10000)
curl -s 'http://localhost:5110/api/events?limit=10'
# Only what arrived after a previous call's max_seq
curl -s 'http://localhost:5110/api/events?after_seq=1642'Aggregate counts by event type, test status, and HTTP status class — the fast way to learn how a run of thousands of events went.
curl -s 'http://localhost:5110/api/events/stats?session=debug-payment' | python -m json.toolReturns the full event data, including headers and bodies for HTTP, test outcomes and timings, traceback frames for exceptions, and message metadata for logs.
curl -s http://localhost:5110/api/events/{id} | python -m json.toolsmello clear
# Or start one command with an empty timeline
smello run --clear my_app.py| Package | Python |
|---|---|
| smello (client SDK) | >= 3.10 |
| smello-server | >= 3.14 |
- requests — patches
Session.send() - httpx — patches
Client.send()andAsyncClient.send() - aiohttp — injects
TraceConfiglifecycle hooks to capture async HTTP traffic - grpc — patches
insecure_channel()andsecure_channel()to intercept unary-unary calls - botocore — patches
URLLib3Session.send()to capture boto3 / AWS SDK traffic - pytest: loads a plugin that captures each test function or method invocation
boto3 uses botocore, which calls urllib3 directly, bypassing requests and httpx. Smello patches botocore's HTTP session to capture all AWS API calls. Just run your script with smello run:
smello run my_aws_script.pyAWS calls appear at http://localhost:5110 — XML responses show as a collapsible tree, just like JSON.
Many Google Cloud Python libraries use gRPC under the hood. Smello automatically captures these calls with zero additional configuration:
- Google BigQuery (
google-cloud-bigquery) - Google Cloud Firestore (
google-cloud-firestore) - Google Cloud Pub/Sub (
google-cloud-pubsub) - Google Analytics Data API (
google-analytics-data) — GA4 reporting - Google Cloud Vertex AI (
google-cloud-aiplatform) - Google Cloud Speech-to-Text (
google-cloud-speech) - Google Cloud Vision (
google-cloud-vision) - Google Cloud Translation (
google-cloud-translate) - Google Cloud Secret Manager (
google-cloud-secret-manager) - Google Cloud Spanner (
google-cloud-spanner) - Google Cloud Bigtable (
google-cloud-bigtable)
Any library that calls grpc.secure_channel() or grpc.insecure_channel() is automatically captured.
Requires uv, Node.js 22+, and just.
git clone https://github.com/smelloscope/smello.git
cd smello
uv sync
# Terminal 1: API server with auto-reload (http://localhost:5110)
just server
# Terminal 2: Frontend dev server (http://localhost:5111, proxies /api to :5110)
just frontend-install
just frontend-dev
# Terminal 3: Run an example
uv run python examples/python/basic_requests.py
# Or exercise runtime trees with optional remote-host grouping
uv run smello run --capture-logs --log-level INFO -- \
uvicorn examples.python.hierarchy_demo:app --port 8001Run just to see all available recipes.
Worktrunk creates an isolated worktree for each branch. Install its shell integration once, then approve this repository's setup hook after reviewing it:
wt config shell install
wt config approvals addCreate a worktree with wt switch --create feature-name. It is created beside this checkout, copies your .env, and prepares Python and frontend dependencies. Use wt switch feature-name to return to it, wt list to see worktrees, and wt remove feature-name after the branch has been merged.
smello run my_app.py ──→ Smello Server ──→ Web Dashboard
(FastAPI+SQLite) (localhost:5110)
- smello (client SDK): Monkey-patches
requests,httpx,aiohttp,grpc, andbotocoreto capture outgoing traffic. Includes FastAPI and Django middleware for incoming request capture. Hookssys.excepthookfor exceptions andlogging.Logger.callHandlersfor log records. Sends everything to the server in a background thread. - smello-server: FastAPI app with SQLite. Receives captured events and serves a JSON API plus a React web dashboard with a unified timeline.
smello/
├── server/ # smello-server (FastAPI + Tortoise ORM + SQLite)
│ └── tests/
├── frontend/ # React SPA (MUI + TanStack Query + jotai)
├── clients/python/ # smello client SDK
│ └── tests/
├── tests/test_e2e/ # End-to-end tests
└── examples/python/
Questions, feedback, or ideas? Reach out at roman@smello.io.
MIT

