Skip to main content

Focused, opt-in runtime call tracing for a single execution context.

Project description

tracepatch

See exactly what your code did — every call, argument, return value, and timing — in one readable tree.

tracepatch records function calls for a single execution scope (a request, a CLI command, a background task) and produces a call tree you can actually read. It's not OpenTelemetry. It's not cProfile. It's a scalpel: point it at the code path that's misbehaving and it tells you exactly what happened.

Zero runtime dependencies. Zero overhead when inactive. Leave it installed in production.

When to Use / When NOT to Use

Use tracepatch when you need to:

  • Debug why a specific request is slow or returning wrong results
  • Understand the call flow through unfamiliar code
  • Compare execution patterns before and after a refactor
  • Trace exactly what happens inside a failing test

Don't use tracepatch for:

  • Distributed tracing across services → use OpenTelemetry
  • Statistical profiling of hot paths → use cProfile or py-spy
  • Audit logging for compliance → use structured logging

Quick Start

from tracepatch import trace

def load_user(uid):
    return {"id": uid, "name": "Alice"}

def validate(user):
    return user["name"] != ""

def handle_request(uid):
    user = load_user(uid)
    validate(user)
    return user

with trace(label="debug-request") as t:
    handle_request(42)

print(t.tree())
Trace: debug-request
────────────────────────────────────────────────────────────────────────
Recorded:     3 calls across 1 modules    | Duration: 0.05ms
Max depth:    1 levels                    | Unique functions: 3
Slowest:      __main__.handle_request [0.05ms]| Most called: __main__.load_user [×1]
────────────────────────────────────────────────────────────────────────

└── __main__.handle_request(uid=42) -> {'id': 42, 'name': 'Alice'}  [0.05ms]
    ├── __main__.load_user(uid=42) -> {'id': 42, 'name': 'Alice'}  [0.01ms]
    └── __main__.validate(user={'id': 42, ...}) -> True  [0.01ms]

Features

  • Zero overhead when inactivesys.settrace hook only installed inside trace() blocks
  • Pure Python — no runtime dependencies (optional tomli on Python <3.11)
  • Async + sync — works with async/await, generators, and async generators via contextvars
  • Thread-safe — each trace is isolated with a unique ID
  • Built-in safetymax_depth, max_calls, max_time auto-disable before damage
  • Call folding — repeated calls in loops fold into [×500] with min/avg/max timing
  • Self-time — see where time is actually spent vs. passed through to children
  • Statistics — summary header with call count, slowest function, module breakdown
  • Multiple outputs — ASCII tree, Unicode tree, coloured ANSI, JSON, HTML, CSV
  • Source locations — optional file:line annotation for each call
  • Trace comparisontph diff before.json after.json
  • CLI (tph) — logs, tree, stats, diff, export, config, init, run, clean
  • TOML configtracepatch.toml or pyproject.toml [tool.tracepatch]
  • Environment overridesTRACEPATCH_ENABLED=0 to disable globally

Installation

pip install tracepatch

From source:

pip install .

Quick Start

Context Manager (Sync)

from tracepatch import trace

def fetch_user(user_id):
    return {"id": user_id, "name": "Alice"}

def handle_request():
    user = fetch_user(42)
    return user

with trace() as t:
    handle_request()

print(t.tree())

Output:

└── __main__.handle_request()  [0.03ms]
    └── __main__.fetch_user(user_id=42) -> {'id': 42, 'name': 'Alice'}  [0.01ms]

Context Manager (Async)

import asyncio
from tracepatch import trace

async def fetch_user(user_id):
    return {"id": user_id, "name": "Alice"}

async def handle_request():
    user = await fetch_user(42)
    return user

async def main():
    async with trace() as t:
        await handle_request()
    print(t.tree())

asyncio.run(main())

Decorator Usage

from tracepatch import trace

# Simple decorator
@trace(label="api-handler")
def handle_api_request():
    process_data()
    return {"status": "OK"}

result = handle_api_request()

# Works with async functions
@trace(label="async-task")
async def background_job():
    await fetch_data()
    await process_data()

await background_job()

# Works with generators
@trace()
def data_generator():
    for i in range(10):
        yield process_item(i)

# Works with async generators
@trace()
async def async_stream():
    for i in range(10):
        yield await fetch_item(i)

Note: For @staticmethod or @classmethod, apply @trace after the decorator:

class MyClass:
    @staticmethod
    @trace()
    def my_static_method():
        pass

Production Safety

from tracepatch import trace

# Auto-stop after 5 seconds (prevent runaway traces)
with trace(max_time=5.0) as t:
    long_running_operation()

# Limit depth and calls
with trace(max_depth=10, max_calls=1000) as t:
    recursive_function()
    
print(f"Limited: {t.was_limited}") # True if stopped early

Filtering and Focus

from tracepatch import trace

# Ignore noisy modules
with trace(ignore_modules=["logging", "urllib"]) as t:
    make_api_call()  # Won't see internal logging/urllib calls

# Environment variable override (set once, affects all traces)
# export TRACEPATCH_ENABLED=0  # Disable all tracing globally
# export TRACEPATCH_MAX_DEPTH=5

Configuration

Create tracepatch.toml or add [tool.tracepatch] to pyproject.toml:

# Tracing behavior
ignore_modules = ["unittest.mock", "logging"]  # Module prefixes to exclude
# include_modules = ["myapp"]  # Allowlist: only trace these modules
max_depth = 30          # Maximum call nesting depth
max_calls = 10000       # Stop after this many calls
max_repr = 120          # Max length for repr() of args/returns
max_time = 60.0         # Stop after this many seconds

# Cache settings
cache = true            # Auto-save traces
# cache_dir = ".custom_cache"
auto_save = true

# Display settings
show_args = true
show_return = true
tree_style = "ascii"    # "ascii" or "unicode"

# Test setup (for `tph setup`)
[[test.files]]
path = "myapp/core.py"
functions = ["process", "validate"]

[test.custom]
enabled = false         # Use custom test script
script = ""

Generate starter config:

tph init

CLI Usage

tph logs                           # list saved traces
tph tree trace.json --color        # coloured call tree
tph tree trace.json --self-time    # show self-time
tph tree trace.json --style unicode --no-args   # compact view
tph tree trace.json --filter 'myapp.*'          # only myapp calls
tph tree trace.json --filter '!logging'         # exclude logging
tph tree trace.json --depth 3                   # limit depth
tph stats trace.json               # detailed statistics
tph diff before.json after.json    # compare two traces
tph export trace.json --format csv -o trace.csv
tph config --validate              # validate config (CI)
tph init                           # create tracepatch.toml
tph clean --older-than 7d          # remove old traces

Advanced Usage

Circular Reference Handling

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

# Create circular reference
a = Node(1)
b = Node(2)
a.next = b
b.next = a  # Circular!

with trace() as t:
    process(a)  # Won't crash - shows "<circular reference>"

Error Handling

Tracing machinery catches and logs exceptions without affecting traced code:

class BadRepr:
    def __repr__(self):
        raise RuntimeError("broken repr")

with trace() as t:
    use_object(BadRepr())  # Shows "<unprintable>", doesn't crash

# Exception info logged to stderr:
# [tracepatch] repr failed: RuntimeError

Thread Safety

Each trace gets a unique ID for correlation:

import threading

def worker(task_id):
    with trace(label=f"worker-{task_id}") as t:
        do_work(task_id)
        # Each trace isolated - no interference

threads = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

JSON Structure

with trace() as t:
    do_something()

t.to_json("trace.json")

trace.json:

{
  "tracepatch_version": "0.3.2",
  "timestamp": "2026-02-12T14:30:22.123456",
  "label": "my-trace",
  "call_count": 42,
  "was_limited": false,
  "config": {
    "max_depth": 30,
    "max_calls": 10000,
    "max_repr": 120,
    "ignore_modules": ["unittest.mock"]
  },
  "trace": [
    {
      "name": "do_something",
      "module": "__main__",
      "args": "x=42, y='hello'",
      "return_value": "{'result': 'OK'}",
      "start": 1234567890.123,
      "end": 1234567890.456,
      "elapsed_ms": 333.0,
      "children": [...]
    }
  ]
}

HTML Output

tph tree trace.json --format html -o report.html

Creates interactive HTML with:

  • Collapsible tree structure
  • Color-coded timing (green/yellow/red)
  • Syntax highlighting
  • Dark theme

Common Use Cases

Debug API Handler

from flask import Flask
from tracepatch import trace

app = Flask(__name__)

@app.route('/api/user/<user_id>')
@trace(label="api-user")
def get_user(user_id):
    user = fetch_from_db(user_id)
    enrich_user_data(user)
    return jsonify(user)

Trace Specific Request

from fastapi import FastAPI, Request
from tracepatch import trace

app = FastAPI()

@app.middleware("http")
async def trace_requests(request: Request, call_next):
    # Only trace requests with special header
    if "X-Debug-Trace" in request.headers:
        async with trace(label=f"request-{request.url.path}"):
            response = await call_next(request)
    else:
        response = await call_next(request)
    return response

Debug Background Task

from celery import Celery
from tracepatch import trace

app = Celery('tasks')

@app.task
@trace(label="celery-process")
def process_user_data(user_id):
    user = fetch_user(user_id)
    validate_user(user)
    save_results(user)

Test-Driven Development

import pytest
from tracepatch import trace

def test_complex_workflow():
    """Trace test execution to debug failures."""
    with trace(label="test-workflow") as t:
        result = complex_workflow(input_data)
    
    if not result:
        # Save trace for debugging
        t.to_json(f"/tmp/failed-test-{t._trace_id}.json")
        
    assert result

Performance

  • Overhead when inactive: ~0ns (no hooks installed)
  • Overhead when active: ~5–10μs per function call
  • Memory: ~1KB per captured call (includes args/return repr)
  • Recommended limits:
    • max_calls=10000 for typical requests (~10ms overhead)
    • max_depth=30 to avoid deep recursion overhead
    • max_time=60 to auto-stop runaway traces

Documentation

Contributing

Contributions welcome! Please see CONTRIBUTING.md.

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tracepatch-1.0.0.tar.gz (104.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tracepatch-1.0.0-py3-none-any.whl (68.4 kB view details)

Uploaded Python 3

File details

Details for the file tracepatch-1.0.0.tar.gz.

File metadata

  • Download URL: tracepatch-1.0.0.tar.gz
  • Upload date:
  • Size: 104.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for tracepatch-1.0.0.tar.gz
Algorithm Hash digest
SHA256 898751086c4244d348a01506baa30032526803844a3f6c09cf1c68b02c92e5e2
MD5 4dffbe3f34ccd262fdc81578a60f5b63
BLAKE2b-256 5d1e9b5fab49b7cd23dec5091dfa4d93610925624bd5acdbf2de92055b54549b

See more details on using hashes here.

Provenance

The following attestation bundles were made for tracepatch-1.0.0.tar.gz:

Publisher: release.yml on levinismynameirl/tracepatch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tracepatch-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: tracepatch-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 68.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for tracepatch-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a937e2493e65688d5c4635240a244d58d19f045d2ebc1db401e50c15444e1b57
MD5 ffc7bf7e0a697780fae85b20c80bdd01
BLAKE2b-256 9845054364f67c846ba8f788df4258ff649d4011040c60968d19d94325e20500

See more details on using hashes here.

Provenance

The following attestation bundles were made for tracepatch-1.0.0-py3-none-any.whl:

Publisher: release.yml on levinismynameirl/tracepatch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page