Skip to content

Getting Started

Install

Install the client SDK and the server:

pip install smello smello-server

Start the server:

smello-server

Or run with Docker:

docker run -p 127.0.0.1:5110:5110 ghcr.io/smelloscope/smello

The server listens at http://localhost:5110. The 127.0.0.1: prefix keeps it accessible only from your machine. Omit it if you need LAN access.

Why port 5110?

Read it as 5-1-1-0S-L-L-Osmello.

Run your code with Smello

Prefix any Python command with smello run:

smello run my_app.py
smello run pytest tests/
smello run uvicorn app:app

That's it. Smello activates before your code runs and captures outgoing traffic, pytest results, and unhandled exceptions. No code changes needed.

Subprocess instrumentation propagates automatically, so smello run gunicorn app:app also captures traffic from worker processes.

Use -- to disambiguate when smello flags conflict with the wrapped command's flags:

smello run --capture-logs --log-level INFO -- python -m my_module --debug

CLI flags map 1:1 to the environment variables:

Flag Env var
--server SMELLO_URL
--debug / --no-debug SMELLO_DEBUG
--capture-host SMELLO_CAPTURE_HOSTS
--ignore-host SMELLO_IGNORE_HOSTS
--capture-all / --no-capture-all SMELLO_CAPTURE_ALL
--redact-header SMELLO_REDACT_HEADERS
--redact-query-param SMELLO_REDACT_QUERY_PARAMS
--capture-tests / --no-capture-tests SMELLO_CAPTURE_TESTS
--capture-logs / --no-capture-logs SMELLO_CAPTURE_LOGS
--log-level SMELLO_LOG_LEVEL
--ignore-logger SMELLO_IGNORE_LOGGERS
--app SMELLO_APP
--session SMELLO_SESSION

Query captured events

Use smello query to inspect the same events as the dashboard from a terminal. Lists default to hierarchy-aware text, and a displayed event ID opens its complete JSON data:

smello query
smello query --session debug-payment --type http --status 500 --ancestors
smello query 5ae54ca2
smello meta

smello meta lists every app, session, host, event type, and method on the server, which is the quickest way to find out what you can filter for.

See Query captured events for filters, output formats, event and dashboard URL lookups, hierarchy behavior, and server selection.

Using smello.init() instead

If you prefer to activate Smello from within your code, call smello.init():

import smello
smello.init()

Smello only activates when a server URL is provided, either via the server_url parameter or the SMELLO_URL environment variable. Without a URL, init() is a safe no-op: no monkey-patching, no background threads, no side effects.

# Activate in development
export SMELLO_URL=http://localhost:5110

Like Sentry's SENTRY_DSN, this keeps instrumentation in place with zero production overhead. smello.init() is also the right choice for projects with a custom sitecustomize.py, where smello run can't be used.

FastAPI middleware

To capture incoming HTTP requests in a FastAPI app, add the Smello middleware:

from smello.integrations.fastapi import SmelloMiddleware
from fastapi import FastAPI

app = FastAPI()
app.add_middleware(SmelloMiddleware)

Then run your server with smello run:

smello run uvicorn app:app

Every request your server handles appears in the dashboard with method, path, status code, duration, route pattern, and client IP. If a route handler raises an unhandled exception, the middleware captures the traceback before re-raising.

By default, all paths are captured. Use ignore_paths to skip noisy endpoints like health checks and OpenAPI schema routes. Matching is prefix-based:

app.add_middleware(SmelloMiddleware, ignore_paths=["/health", "/openapi.json", "/docs"])

The middleware is a raw ASGI middleware (not Starlette's BaseHTTPMiddleware), so it works with streaming responses and background tasks. When Smello is inactive (no server URL configured), the middleware passes requests through without capturing anything.

Django middleware

To capture incoming HTTP requests in a Django app, add the Smello middleware at the top of your MIDDLEWARE list:

# settings.py
MIDDLEWARE = [
    "smello.integrations.django.SmelloMiddleware",  # first — sees the raw request
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    # ...
]

Then run your server with smello run:

smello run manage.py runserver

Every request your server handles appears in the dashboard with method, path, status code, duration, route pattern, and client IP. If a view raises an unhandled exception, the middleware captures the traceback via Django's process_exception hook.

By default, all paths are captured. Use the SMELLO_IGNORE_PATHS setting to skip noisy endpoints. Matching is prefix-based:

SMELLO_IGNORE_PATHS = ["/health/", "/admin/", "/static/"]

When Smello is inactive (no server URL configured), the middleware passes requests through without capturing anything.

Exploring hierarchy views

The local hierarchy demo creates an incoming FastAPI request, logs inside its handler, and successful or failed outgoing HTTP child operations. Start it with log capture:

smello run --capture-logs --log-level INFO -- \
  uvicorn examples.python.hierarchy_demo:app --port 8001

Open http://localhost:8001/docs and call POST /checkout and POST /checkout/provider-down. Smello always places child operations beneath their runtime parents. Choose Remote host to insert destination groups where outgoing calls begin. Virtual groups preserve runtime parenthood, keep chronological sibling order, and count only loaded records. Filtering the timeline keeps the runtime ancestors of matching records visible, so child operations and annotations remain connected to their runtime context.

See hierarchy_demo.py and Hierarchy and data model for the full projection and filtering behavior.

Capturing pytest tests

The bundled plugin supports pytest 7 and later. Install the pytest extra if your project doesn't already include pytest:

pip install "smello[pytest]"

Pytest capture is enabled by default. Run pytest through Smello:

smello run pytest tests/

The plugin creates one event for every test function or method invocation. Parametrized cases appear separately under their full node IDs. Each event includes its outcome, fixture names, source location, timings, and failure details.

The traceback generated by pytest may include repr() output for parameter, fixture, or local variable values. With pytest-xdist, events also include the shared run ID and worker ID.

Smello keeps one runtime operation active across each test's setup, call, and teardown. The event also identifies its directory, file, class, and unparameterized case so API clients can build navigation groups without treating those virtual groups as executed spans. Outgoing Requests, HTTPX, aiohttp, botocore, and gRPC calls made during the test inherit that collection path and record the test operation as their runtime parent. Use the dashboard's Group selector to add one available pytest level or remote-host grouping to the runtime tree. See Hierarchy and data model for the wire format and projection behavior.

See Debug pytest tests with Smello for a runnable parametrized failure example and dashboard walkthrough.

Disable this capture without affecting HTTP, logs, or exceptions:

smello run --no-capture-tests pytest tests/

Capturing logs

Log capture is opt-in. Enable it to see Python log records alongside your HTTP traffic and exceptions in the same timeline:

smello run --capture-logs --log-level INFO my_app.py

Or with smello.init():

import smello
smello.init(capture_logs=True, log_level=20)

Smello's own loggers (smello.*) and urllib3 loggers are always excluded to prevent recursion. You can suppress other noisy loggers with ignore_loggers:

smello run --capture-logs --ignore-logger uvicorn.access --ignore-logger uvicorn.error my_app.py

Matching is hierarchical: "uvicorn" suppresses uvicorn, uvicorn.access, uvicorn.error, etc. See ignore_loggers for details.

Capturing exceptions

Unhandled exceptions are captured by default. No configuration needed. When your program crashes, Smello captures the full traceback with stack frames and source context, then flushes the event before the process exits.

To disable exception capture: smello run --no-capture-exceptions my_app.py or smello.init(capture_exceptions=False).

Debugging sessions

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.py

Then filter the dashboard or API to see only events from that session:

curl -s 'http://localhost:5110/api/events?app=myapp&session=debug-payment'

This is useful when you have multiple services or scripts running at the same time — give each its own --app name and a shared --session to see the full picture. See configuration for more.

Google Cloud libraries

Many Google Cloud Python libraries use gRPC under the hood. Smello captures these calls automatically. No extra setup needed:

smello run my_bigquery_script.py

BigQuery, Firestore, Pub/Sub, Analytics (GA4), Vertex AI, Speech-to-Text, Vision, Translation: anything that calls grpc.secure_channel() or grpc.insecure_channel() is captured.

AWS libraries (boto3)

boto3 uses botocore, which calls urllib3 directly, bypassing requests and httpx. Smello patches botocore's HTTP session to capture all AWS API calls:

smello run my_aws_script.py

AWS calls appear at http://localhost:5110. XML responses show as a collapsible tree, just like JSON.

Troubleshooting

If Smello appears to be running but you don't see events in the dashboard, enable debug mode:

# Via CLI flag
smello run --debug my_app.py

# Via environment variable
SMELLO_DEBUG=1 smello run my_app.py

# In code
smello.init(server_url="http://localhost:5110", debug=True)

Debug mode logs to stderr: the resolved configuration and where each value came from, which libraries were patched, every capture/skip decision, and whether the server is reachable. See configuration for details.

What Smello captures

Outgoing HTTP requests

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)

The dashboard recognizes Unix timestamps in JSON bodies and shows the human-readable date in a tooltip. XML responses (common in AWS S3, STS, EC2) appear as a collapsible tree, just like JSON. Both formats offer Tree and Raw tabs. Tree shows an expandable tree; Raw shows syntax-highlighted source.

gRPC calls are displayed with a grpc:// URL scheme. Protobuf request and response bodies are automatically serialized to JSON.

Incoming HTTP requests

When you add the FastAPI or Django middleware, Smello captures every request your server handles:

  • Method, path, full URL, and route pattern (e.g., /api/users/{id})
  • Request and response headers and bodies
  • Response status code and duration
  • Client IP address
  • Exception type and traceback (if the handler raises)

Request and response bodies are capped at 1 MB, matching the outgoing capture limit.

Pytest tests

For each pytest test function or method invocation, Smello captures:

  • Status: passed, failed, error, skipped, xfailed, or xpassed
  • Total duration and setup, call, and teardown timings
  • Fixture names
  • Test node ID, source file, line number, and function or method name
  • Assertion, setup, and teardown failure details, including pytest's formatted traceback
  • Test run and xdist worker identifiers

Parametrized tests produce one event per parameter set.

Logs

When capture_logs=True, Smello captures Python log records at or above the configured log_level:

  • Level (DEBUG, INFO, WARNING, ERROR, CRITICAL), logger name, and formatted message
  • Source file path, line number, and function name
  • Extra attributes attached to the record via extra={...}

Smello's own loggers (smello.*) and urllib3 loggers are automatically excluded to prevent recursion.

Exceptions

Unhandled exceptions are captured with:

  • Exception type, message, and module
  • Full formatted traceback text
  • Individual stack frames with filename, line number, function name, and source context line

Both sys.excepthook (main thread) and threading.excepthook (worker threads) are hooked.

Smello redacts sensitive headers (Authorization, X-Api-Key) by default and optionally redacts query string parameters (details).

Supported libraries

Library What Smello patches
requests Session.send()
httpx Client.send() and AsyncClient.send()
grpc insecure_channel() and secure_channel() (unary-unary)
aiohttp ClientSession._request() (async HTTP client)
botocore URLLib3Session.send() (all boto3 / AWS SDK calls)
pytest pytest reporting hooks (test functions and methods)

Python version support

Package Python
smello (client SDK) >= 3.10
smello-server >= 3.14