Skip to main content

Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.

Project description

Nullscope

PyPI License

Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.

Why Nullscope?

Most telemetry libraries have runtime cost even when you don't need them. Nullscope is different:

  • Disabled: Returns a singleton no-op object. No allocations, no timing calls, no overhead.
  • Enabled: Full-featured timing and metrics with automatic scope hierarchy.

This makes Nullscope ideal for libraries (users can enable telemetry if they want) and applications where you want zero production overhead but rich debugging capability.

from nullscope import TelemetryContext

# When NULLSCOPE_ENABLED != "1", this is literally just returning a cached object
telemetry = TelemetryContext()

with telemetry("database.query"):  # No-op when disabled
    results = db.execute(query)

What Nullscope Is Not

  • A distributed tracing system. No trace propagation, no span IDs, no context injection for cross-service correlation. If you need that, use OpenTelemetry directly. Nullscope can feed OTel, but it doesn't replace it.

  • A metrics aggregation layer. Nullscope reports raw events to reporters. It doesn't compute percentiles, histograms, or roll up data. That's the reporter's job (or the backend's).

  • Auto-instrumentation. Nullscope won't patch your HTTP client or database driver. You instrument what you want, explicitly.

  • A logging framework. Scopes are for timing and metrics, not structured log events. (Though a reporter could emit logs.)

Installation

pip install nullscope

With OpenTelemetry support:

pip install nullscope[otel]

Quick Start

import os
os.environ["NULLSCOPE_ENABLED"] = "1"  # Enable telemetry

from nullscope import TelemetryContext, SimpleReporter

# Create a reporter to see output
reporter = SimpleReporter()
telemetry = TelemetryContext(reporter)

# Time operations with automatic hierarchy
with telemetry("request"):
    with telemetry("auth"):
        validate_token()

    with telemetry("handler"):
        process_data()

# See what was collected
reporter.print_report()

Output:

=== Nullscope Report ===

--- Timings ---
request                        | Calls: 1    | Avg: 0.0250s | Total: 0.0250s
  auth                         | Calls: 1    | Avg: 0.0012s | Total: 0.0012s
  handler                      | Calls: 1    | Avg: 0.0234s | Total: 0.0234s

Configuration

Environment Variable Description
NULLSCOPE_ENABLED=1 Enable telemetry (default: disabled)
NULLSCOPE_STRICT=1 Enforce strict dotted scope names

Note: environment flags are read at import time. In tests, reload nullscope after changing env vars.

API

TelemetryContext

from nullscope import TelemetryContext

telemetry = TelemetryContext()  # Uses default SimpleReporter when enabled
telemetry = TelemetryContext(my_reporter)  # Custom reporter
telemetry = TelemetryContext(reporter1, reporter2)  # Multiple reporters

Scopes (Timing)

with telemetry("operation"):
    do_work()

# With metadata
with telemetry("http.request", method="GET", path="/api/users"):
    handle_request()

Decorators

@telemetry.timed("http.handler")
def handle() -> None:
    process_request()

@telemetry.timed("db.query", table="users")
async def fetch_users() -> list[dict]:
    return await db.fetch_all()

Metrics

telemetry.count("cache.hit")  # Increment counter
telemetry.count("items.processed", 5)  # Increment by N
telemetry.gauge("queue.depth", len(queue))  # Point-in-time value
telemetry.metric("custom", value, metric_type="counter")  # Generic

Check Status

if telemetry.is_enabled:
    # Do expensive debug logging
    pass

Reporter Lifecycle

# Flush buffered reporters (if they implement flush())
telemetry.flush()

# Shutdown reporters cleanly (if they implement shutdown())
telemetry.shutdown()

Async Safety

Nullscope uses contextvars, so each async task keeps its own scope stack without cross-talk:

import asyncio

async def worker(task_id: int):
    with telemetry("task", task_id=task_id):
        await asyncio.sleep(0.1)

OpenTelemetry Adapter

Export to OpenTelemetry backends:

from nullscope import TelemetryContext
from nullscope.adapters.opentelemetry import OTelReporter

# Configure OTel SDK first (providers, exporters, etc.)
# Then use Nullscope with OTel reporter
telemetry = TelemetryContext(OTelReporter(service_name="my-service"))

The adapter emits:

  • Timings → Histogram (seconds) + synthetic Span when wall-clock bounds are present
  • Counters → Counter
  • Gauges → Histogram (sampled values, since Python OTel sync gauge support is limited)

Documentation

  • Design - Architecture and implementation details
  • Examples - Real-world usage patterns
  • Comparison - When to use Nullscope vs alternatives
  • Roadmap - Version milestones and planned features

License

MIT

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

nullscope-0.3.0.tar.gz (51.7 kB view details)

Uploaded Source

Built Distribution

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

nullscope-0.3.0-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

File details

Details for the file nullscope-0.3.0.tar.gz.

File metadata

  • Download URL: nullscope-0.3.0.tar.gz
  • Upload date:
  • Size: 51.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","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

Hashes for nullscope-0.3.0.tar.gz
Algorithm Hash digest
SHA256 c1681b769dc732512725cf6f1729b5be9195153f5fe7df96c1a9538a2df2403a
MD5 5ed7c2dd7a4afa36b7c9862b9b1bac8f
BLAKE2b-256 5f844d9725b04a076dae684ece050101618d783bed809db6b82f08ab6bb657cd

See more details on using hashes here.

File details

Details for the file nullscope-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: nullscope-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 11.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","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

Hashes for nullscope-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 23fbb335555f00c61780f934c0efba1936540cd8e979cf7cc9c476decebc3cd0
MD5 1879e7bbd238b1813a1dc7a5e5a00a69
BLAKE2b-256 f4796aae7714de60fc85d569472f0fd557e921924042932c00b1889da7aa0ec5

See more details on using hashes here.

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