{"id":44635241,"url":"https://github.com/seanbrar/nullscope","last_synced_at":"2026-02-17T07:51:03.939Z","repository":{"id":336524980,"uuid":"1150021821","full_name":"seanbrar/nullscope","owner":"seanbrar","description":"Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.","archived":false,"fork":false,"pushed_at":"2026-02-05T06:43:27.000Z","size":21,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-05T08:47:19.602Z","etag":null,"topics":["metrics","nullscope","observability","telemetry","tracing","zero-cost"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/seanbrar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-04T19:42:13.000Z","updated_at":"2026-02-04T21:02:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/seanbrar/nullscope","commit_stats":null,"previous_names":["seanbrar/nullscope"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/seanbrar/nullscope","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanbrar%2Fnullscope","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanbrar%2Fnullscope/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanbrar%2Fnullscope/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanbrar%2Fnullscope/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/seanbrar","download_url":"https://codeload.github.com/seanbrar/nullscope/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanbrar%2Fnullscope/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29450850,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T15:52:44.973Z","status":"ssl_error","status_checked_at":"2026-02-14T15:52:11.208Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["metrics","nullscope","observability","telemetry","tracing","zero-cost"],"created_at":"2026-02-14T17:21:51.945Z","updated_at":"2026-02-14T17:21:52.054Z","avatar_url":"https://github.com/seanbrar.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nullscope\n\n[![PyPI](https://img.shields.io/pypi/v/nullscope)](https://pypi.org/project/nullscope/)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nZero-cost telemetry for Python. No-op when disabled, rich context when enabled.\n\n## Why Nullscope?\n\nMost telemetry libraries have runtime cost even when you don't need them. Nullscope is different:\n\n- **Disabled**: Returns a singleton no-op object. No allocations, no timing calls, no overhead.\n- **Enabled**: Full-featured timing and metrics with automatic scope hierarchy.\n\nThis makes Nullscope ideal for **libraries** (users can enable telemetry if they want) and **applications** where you want zero production overhead but rich debugging capability.\n\n```python\nfrom nullscope import TelemetryContext\n\n# When NULLSCOPE_ENABLED != \"1\", this is literally just returning a cached object\ntelemetry = TelemetryContext()\n\nwith telemetry(\"database.query\"):  # No-op when disabled\n    results = db.execute(query)\n```\n\n## What Nullscope Is Not\n\n- **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.\n\n- **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).\n\n- **Auto-instrumentation.** Nullscope won't patch your HTTP client or database driver. You instrument what you want, explicitly.\n\n- **A logging framework.** Scopes are for timing and metrics, not structured log events. (Though a reporter *could* emit logs.)\n\n## Installation\n\n```bash\npip install nullscope\n```\n\nWith OpenTelemetry support:\n\n```bash\npip install nullscope[otel]\n```\n\n## Quick Start\n\n```python\nimport os\nos.environ[\"NULLSCOPE_ENABLED\"] = \"1\"  # Enable telemetry\n\nfrom nullscope import TelemetryContext, SimpleReporter\n\n# Create a reporter to see output\nreporter = SimpleReporter()\ntelemetry = TelemetryContext(reporter)\n\n# Time operations with automatic hierarchy\nwith telemetry(\"request\"):\n    with telemetry(\"auth\"):\n        validate_token()\n\n    with telemetry(\"handler\"):\n        process_data()\n\n# See what was collected\nreporter.print_report()\n```\n\nOutput:\n\n```text\n=== Nullscope Report ===\n\n--- Timings ---\nrequest:\n  auth                           | Calls: 1    | Avg: 0.0012s | Total: 0.0012s\n  handler                        | Calls: 1    | Avg: 0.0234s | Total: 0.0234s\n```\n\n## Configuration\n\n| Environment Variable  | Description                          |\n| --------------------- | ------------------------------------ |\n| `NULLSCOPE_ENABLED=1` | Enable telemetry (default: disabled) |\n| `NULLSCOPE_STRICT=1`  | Enforce strict dotted scope names    |\n\nNote: environment flags are read at import time. In tests, reload `nullscope` after changing env vars.\n\n## API\n\n### TelemetryContext\n\n```python\nfrom nullscope import TelemetryContext\n\ntelemetry = TelemetryContext()  # Uses default SimpleReporter when enabled\ntelemetry = TelemetryContext(my_reporter)  # Custom reporter\ntelemetry = TelemetryContext(reporter1, reporter2)  # Multiple reporters\n```\n\n### Scopes (Timing)\n\n```python\nwith telemetry(\"operation\"):\n    do_work()\n\n# With metadata\nwith telemetry(\"http.request\", method=\"GET\", path=\"/api/users\"):\n    handle_request()\n```\n\n### Decorators\n\n```python\n@telemetry.timed(\"http.handler\")\ndef handle() -\u003e None:\n    process_request()\n\n@telemetry.timed(\"db.query\", table=\"users\")\nasync def fetch_users() -\u003e list[dict]:\n    return await db.fetch_all()\n```\n\n### Metrics\n\n```python\ntelemetry.count(\"cache.hit\")  # Increment counter\ntelemetry.count(\"items.processed\", 5)  # Increment by N\ntelemetry.gauge(\"queue.depth\", len(queue))  # Point-in-time value\ntelemetry.metric(\"custom\", value, metric_type=\"counter\")  # Generic\n```\n\n### Check Status\n\n```python\nif telemetry.is_enabled:\n    # Do expensive debug logging\n    pass\n```\n\n### Reporter Lifecycle\n\n```python\n# Flush buffered reporters (if they implement flush())\ntelemetry.flush()\n\n# Shutdown reporters cleanly (if they implement shutdown())\ntelemetry.shutdown()\n```\n\n### Async Safety\n\nNullscope uses `contextvars`, so each async task keeps its own scope stack without cross-talk:\n\n```python\nimport asyncio\n\nasync def worker(task_id: int):\n    with telemetry(\"task\", task_id=task_id):\n        await asyncio.sleep(0.1)\n```\n\n## OpenTelemetry Adapter\n\nExport to OpenTelemetry backends:\n\n```python\nfrom nullscope import TelemetryContext\nfrom nullscope.adapters.opentelemetry import OTelReporter\n\n# Configure OTel SDK first (providers, exporters, etc.)\n# Then use Nullscope with OTel reporter\ntelemetry = TelemetryContext(OTelReporter(service_name=\"my-service\"))\n```\n\nThe adapter emits:\n\n- **Timings** → Histogram (seconds) + synthetic Span when wall-clock bounds are present\n- **Counters** → Counter\n- **Gauges** → Histogram (sampled values, since Python OTel sync gauge support is limited)\n\n## Documentation\n\n- [Design](docs/design.md) - Architecture and implementation details\n- [Examples](docs/examples.md) - Real-world usage patterns\n- [Comparison](docs/comparison.md) - When to use Nullscope vs alternatives\n- [Roadmap](ROADMAP.md) - Version milestones and planned features\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseanbrar%2Fnullscope","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseanbrar%2Fnullscope","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseanbrar%2Fnullscope/lists"}