nullscope 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: release-${{ github.ref }}
11
+ cancel-in-progress: false
12
+
13
+ jobs:
14
+ validate:
15
+ name: Validate on Python ${{ matrix.python-version }}
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version:
21
+ - "3.10"
22
+ - "3.11"
23
+ - "3.12"
24
+ - "3.13"
25
+ - "3.14"
26
+ steps:
27
+ - uses: actions/checkout@v6
28
+
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v7
31
+ with:
32
+ enable-cache: true
33
+
34
+ - name: Install Python ${{ matrix.python-version }}
35
+ run: uv python install ${{ matrix.python-version }}
36
+
37
+ - name: Install the project
38
+ run: uv sync --all-extras --dev --python ${{ matrix.python-version }}
39
+
40
+ - name: Run lint
41
+ run: uv run --python ${{ matrix.python-version }} ruff check .
42
+
43
+ - name: Run type checks
44
+ run: uv run --python ${{ matrix.python-version }} mypy src tests
45
+
46
+ - name: Run tests
47
+ run: uv run --python ${{ matrix.python-version }} pytest -q
48
+
49
+ publish:
50
+ name: Publish to PyPI
51
+ runs-on: ubuntu-latest
52
+ needs: validate
53
+ if: startsWith(github.ref, 'refs/tags/v')
54
+ environment:
55
+ name: pypi
56
+ url: https://pypi.org/p/nullscope
57
+ permissions:
58
+ id-token: write
59
+ contents: write
60
+ steps:
61
+ - uses: actions/checkout@v6
62
+
63
+ - name: Install uv
64
+ uses: astral-sh/setup-uv@v7
65
+ with:
66
+ enable-cache: true
67
+
68
+ - name: Install Python for build
69
+ run: uv python install 3.12
70
+
71
+ - name: Build distributions
72
+ run: uv build
73
+
74
+ - name: Create GitHub Release
75
+ uses: softprops/action-gh-release@v2
76
+ with:
77
+ files: |
78
+ dist/*.whl
79
+ dist/*.tar.gz
80
+ generate_release_notes: true
81
+
82
+ - name: Publish to PyPI
83
+ run: uv publish
@@ -0,0 +1,43 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Checks on Python ${{ matrix.python-version }}
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version:
16
+ - "3.10"
17
+ - "3.11"
18
+ - "3.12"
19
+ - "3.13"
20
+ - "3.14"
21
+
22
+ steps:
23
+ - uses: actions/checkout@v6
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v7
27
+ with:
28
+ enable-cache: true
29
+
30
+ - name: Install Python ${{ matrix.python-version }}
31
+ run: uv python install ${{ matrix.python-version }}
32
+
33
+ - name: Install the project
34
+ run: uv sync --all-extras --dev --python ${{ matrix.python-version }}
35
+
36
+ - name: Run lint
37
+ run: uv run --python ${{ matrix.python-version }} ruff check .
38
+
39
+ - name: Run type checks
40
+ run: uv run --python ${{ matrix.python-version }} mypy src tests
41
+
42
+ - name: Run tests
43
+ run: uv run --python ${{ matrix.python-version }} pytest -q
@@ -0,0 +1,7 @@
1
+ __pycache__
2
+ .venv
3
+ .ruff_cache
4
+ .pytest_cache
5
+ .mypy_cache
6
+ .DS_Store
7
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sean Brar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: nullscope
3
+ Version: 0.1.0
4
+ Summary: Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.
5
+ Project-URL: Homepage, https://github.com/seanbrar/nullscope
6
+ Project-URL: Repository, https://github.com/seanbrar/nullscope
7
+ Project-URL: Documentation, https://github.com/seanbrar/nullscope/tree/main/docs
8
+ Project-URL: Issues, https://github.com/seanbrar/nullscope/issues
9
+ Author-email: Sean Brar <hello@seanbrar.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: metrics,nullscope,observability,telemetry,tracing,zero-cost
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: <3.15,>=3.10
27
+ Provides-Extra: otel
28
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == 'otel'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Nullscope
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/nullscope)](https://pypi.org/project/nullscope/)
34
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
35
+
36
+ Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.
37
+
38
+ ## Why Nullscope?
39
+
40
+ Most telemetry libraries have runtime cost even when you don't need them. Nullscope is different:
41
+
42
+ - **Disabled**: Returns a singleton no-op object. No allocations, no timing calls, no overhead.
43
+ - **Enabled**: Full-featured timing and metrics with automatic scope hierarchy.
44
+
45
+ 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.
46
+
47
+ ```python
48
+ from nullscope import TelemetryContext
49
+
50
+ # When NULLSCOPE_ENABLED != "1", this is literally just returning a cached object
51
+ telemetry = TelemetryContext()
52
+
53
+ with telemetry("database.query"): # No-op when disabled
54
+ results = db.execute(query)
55
+ ```
56
+
57
+ ## What Nullscope Is Not
58
+
59
+ - **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.
60
+
61
+ - **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).
62
+
63
+ - **Auto-instrumentation.** Nullscope won't patch your HTTP client or database driver. You instrument what you want, explicitly.
64
+
65
+ - **A logging framework.** Scopes are for timing and metrics, not structured log events. (Though a reporter *could* emit logs.)
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install nullscope
71
+ ```
72
+
73
+ With OpenTelemetry support:
74
+
75
+ ```bash
76
+ pip install nullscope[otel]
77
+ ```
78
+
79
+ ## Quick Start
80
+
81
+ ```python
82
+ import os
83
+ os.environ["NULLSCOPE_ENABLED"] = "1" # Enable telemetry
84
+
85
+ from nullscope import TelemetryContext, SimpleReporter
86
+
87
+ # Create a reporter to see output
88
+ reporter = SimpleReporter()
89
+ telemetry = TelemetryContext(reporter)
90
+
91
+ # Time operations with automatic hierarchy
92
+ with telemetry("request"):
93
+ with telemetry("auth"):
94
+ validate_token()
95
+
96
+ with telemetry("handler"):
97
+ process_data()
98
+
99
+ # See what was collected
100
+ reporter.print_report()
101
+ ```
102
+
103
+ Output:
104
+
105
+ ```text
106
+ === Nullscope Report ===
107
+
108
+ --- Timings ---
109
+ request:
110
+ auth | Calls: 1 | Avg: 0.0012s | Total: 0.0012s
111
+ handler | Calls: 1 | Avg: 0.0234s | Total: 0.0234s
112
+ ```
113
+
114
+ ## Configuration
115
+
116
+ | Environment Variable | Description |
117
+ | --------------------- | ------------------------------------ |
118
+ | `NULLSCOPE_ENABLED=1` | Enable telemetry (default: disabled) |
119
+
120
+ Note: environment flags are read at import time. In tests, reload `nullscope` after changing env vars.
121
+
122
+ ## API
123
+
124
+ ### TelemetryContext
125
+
126
+ ```python
127
+ from nullscope import TelemetryContext
128
+
129
+ telemetry = TelemetryContext() # Uses default SimpleReporter when enabled
130
+ telemetry = TelemetryContext(my_reporter) # Custom reporter
131
+ telemetry = TelemetryContext(reporter1, reporter2) # Multiple reporters
132
+ ```
133
+
134
+ ### Scopes (Timing)
135
+
136
+ ```python
137
+ with telemetry("operation"):
138
+ do_work()
139
+
140
+ # With metadata
141
+ with telemetry("http.request", method="GET", path="/api/users"):
142
+ handle_request()
143
+ ```
144
+
145
+ ### Metrics
146
+
147
+ ```python
148
+ telemetry.count("cache.hit") # Increment counter
149
+ telemetry.count("items.processed", 5) # Increment by N
150
+ telemetry.gauge("queue.depth", len(queue)) # Point-in-time value
151
+ telemetry.metric("custom", value, metric_type="counter") # Generic
152
+ ```
153
+
154
+ ### Check Status
155
+
156
+ ```python
157
+ if telemetry.is_enabled:
158
+ # Do expensive debug logging
159
+ pass
160
+ ```
161
+
162
+ ## OpenTelemetry Adapter
163
+
164
+ Export to OpenTelemetry backends:
165
+
166
+ ```python
167
+ from nullscope import TelemetryContext
168
+ from nullscope.adapters.opentelemetry import OTelReporter
169
+
170
+ # Configure OTel SDK first (providers, exporters, etc.)
171
+ # Then use Nullscope with OTel reporter
172
+ telemetry = TelemetryContext(OTelReporter(service_name="my-service"))
173
+ ```
174
+
175
+ The adapter emits:
176
+
177
+ - **Timings** → Histogram (seconds) + synthetic Span when wall-clock bounds are present
178
+ - **Counters** → Counter
179
+ - **Gauges** → Histogram (sampled values, since Python OTel sync gauge support is limited)
180
+
181
+ ## Documentation
182
+
183
+ - [Design](docs/design.md) - Architecture and implementation details
184
+ - [Examples](docs/examples.md) - Real-world usage patterns
185
+ - [Comparison](docs/comparison.md) - When to use Nullscope vs alternatives
186
+ - [Roadmap](ROADMAP.md) - Version milestones and planned features
187
+
188
+ ## License
189
+
190
+ [MIT](LICENSE)
@@ -0,0 +1,160 @@
1
+ # Nullscope
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/nullscope)](https://pypi.org/project/nullscope/)
4
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.
7
+
8
+ ## Why Nullscope?
9
+
10
+ Most telemetry libraries have runtime cost even when you don't need them. Nullscope is different:
11
+
12
+ - **Disabled**: Returns a singleton no-op object. No allocations, no timing calls, no overhead.
13
+ - **Enabled**: Full-featured timing and metrics with automatic scope hierarchy.
14
+
15
+ 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.
16
+
17
+ ```python
18
+ from nullscope import TelemetryContext
19
+
20
+ # When NULLSCOPE_ENABLED != "1", this is literally just returning a cached object
21
+ telemetry = TelemetryContext()
22
+
23
+ with telemetry("database.query"): # No-op when disabled
24
+ results = db.execute(query)
25
+ ```
26
+
27
+ ## What Nullscope Is Not
28
+
29
+ - **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.
30
+
31
+ - **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).
32
+
33
+ - **Auto-instrumentation.** Nullscope won't patch your HTTP client or database driver. You instrument what you want, explicitly.
34
+
35
+ - **A logging framework.** Scopes are for timing and metrics, not structured log events. (Though a reporter *could* emit logs.)
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install nullscope
41
+ ```
42
+
43
+ With OpenTelemetry support:
44
+
45
+ ```bash
46
+ pip install nullscope[otel]
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ import os
53
+ os.environ["NULLSCOPE_ENABLED"] = "1" # Enable telemetry
54
+
55
+ from nullscope import TelemetryContext, SimpleReporter
56
+
57
+ # Create a reporter to see output
58
+ reporter = SimpleReporter()
59
+ telemetry = TelemetryContext(reporter)
60
+
61
+ # Time operations with automatic hierarchy
62
+ with telemetry("request"):
63
+ with telemetry("auth"):
64
+ validate_token()
65
+
66
+ with telemetry("handler"):
67
+ process_data()
68
+
69
+ # See what was collected
70
+ reporter.print_report()
71
+ ```
72
+
73
+ Output:
74
+
75
+ ```text
76
+ === Nullscope Report ===
77
+
78
+ --- Timings ---
79
+ request:
80
+ auth | Calls: 1 | Avg: 0.0012s | Total: 0.0012s
81
+ handler | Calls: 1 | Avg: 0.0234s | Total: 0.0234s
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ | Environment Variable | Description |
87
+ | --------------------- | ------------------------------------ |
88
+ | `NULLSCOPE_ENABLED=1` | Enable telemetry (default: disabled) |
89
+
90
+ Note: environment flags are read at import time. In tests, reload `nullscope` after changing env vars.
91
+
92
+ ## API
93
+
94
+ ### TelemetryContext
95
+
96
+ ```python
97
+ from nullscope import TelemetryContext
98
+
99
+ telemetry = TelemetryContext() # Uses default SimpleReporter when enabled
100
+ telemetry = TelemetryContext(my_reporter) # Custom reporter
101
+ telemetry = TelemetryContext(reporter1, reporter2) # Multiple reporters
102
+ ```
103
+
104
+ ### Scopes (Timing)
105
+
106
+ ```python
107
+ with telemetry("operation"):
108
+ do_work()
109
+
110
+ # With metadata
111
+ with telemetry("http.request", method="GET", path="/api/users"):
112
+ handle_request()
113
+ ```
114
+
115
+ ### Metrics
116
+
117
+ ```python
118
+ telemetry.count("cache.hit") # Increment counter
119
+ telemetry.count("items.processed", 5) # Increment by N
120
+ telemetry.gauge("queue.depth", len(queue)) # Point-in-time value
121
+ telemetry.metric("custom", value, metric_type="counter") # Generic
122
+ ```
123
+
124
+ ### Check Status
125
+
126
+ ```python
127
+ if telemetry.is_enabled:
128
+ # Do expensive debug logging
129
+ pass
130
+ ```
131
+
132
+ ## OpenTelemetry Adapter
133
+
134
+ Export to OpenTelemetry backends:
135
+
136
+ ```python
137
+ from nullscope import TelemetryContext
138
+ from nullscope.adapters.opentelemetry import OTelReporter
139
+
140
+ # Configure OTel SDK first (providers, exporters, etc.)
141
+ # Then use Nullscope with OTel reporter
142
+ telemetry = TelemetryContext(OTelReporter(service_name="my-service"))
143
+ ```
144
+
145
+ The adapter emits:
146
+
147
+ - **Timings** → Histogram (seconds) + synthetic Span when wall-clock bounds are present
148
+ - **Counters** → Counter
149
+ - **Gauges** → Histogram (sampled values, since Python OTel sync gauge support is limited)
150
+
151
+ ## Documentation
152
+
153
+ - [Design](docs/design.md) - Architecture and implementation details
154
+ - [Examples](docs/examples.md) - Real-world usage patterns
155
+ - [Comparison](docs/comparison.md) - When to use Nullscope vs alternatives
156
+ - [Roadmap](ROADMAP.md) - Version milestones and planned features
157
+
158
+ ## License
159
+
160
+ [MIT](LICENSE)
@@ -0,0 +1,37 @@
1
+ # Nullscope Roadmap
2
+
3
+ ## Version 0.1.0 (Current)
4
+
5
+ - [x] Zero-cost no-op pattern
6
+ - [x] Enabled context with scope hierarchy
7
+ - [x] Context vars for async safety
8
+ - [x] Pluggable reporter protocol
9
+ - [x] SimpleReporter for development
10
+ - [x] OpenTelemetry adapter
11
+ - [x] `py.typed` marker for PEP 561
12
+ - [x] Basic test coverage
13
+
14
+ ## Version 0.2.0 - Ergonomics
15
+
16
+ - [ ] Decorator support (`@nullscope.timed("operation")`)
17
+ - [ ] Reporter lifecycle methods (`flush()`, `shutdown()`)
18
+ - [ ] Async documentation and explicit testing
19
+ - [ ] Improved error messages
20
+
21
+ ## Version 0.3.0 - Observability
22
+
23
+ - [ ] Exception tracking in scopes (record exception info)
24
+ - [ ] Logging-based reporter adapter
25
+ - [ ] Scope tags/labels support
26
+
27
+ ## Version 0.4.0 - Polish
28
+
29
+ - [ ] Real-world usage feedback incorporated
30
+ - [ ] API refinements based on feedback
31
+ - [ ] Performance benchmarks
32
+
33
+ ## Version 1.0.0 - Stable
34
+
35
+ - [ ] API freeze
36
+ - [ ] Comprehensive documentation
37
+ - [ ] Stability commitment
@@ -0,0 +1,119 @@
1
+ # Nullscope vs Alternatives
2
+
3
+ When to use Nullscope versus other observability tools.
4
+
5
+ ## vs OpenTelemetry SDK
6
+
7
+ **OpenTelemetry** is the industry standard for observability. It provides comprehensive tracing, metrics, and logging with a rich ecosystem of exporters and integrations.
8
+
9
+ | Aspect | Nullscope | OpenTelemetry SDK |
10
+ |--------|-----------|-------------------|
11
+ | Zero-cost when disabled | Yes (singleton no-op) | No (still creates spans/contexts) |
12
+ | Setup complexity | Minimal | Moderate to high |
13
+ | Ecosystem | Limited | Extensive |
14
+ | Protocol compliance | None | OTLP standard |
15
+ | Best for | Application code, libraries | Production observability |
16
+
17
+ **Use Nullscope when:**
18
+ - You're writing a library and want optional telemetry
19
+ - You need truly zero overhead in production
20
+ - You want simple, focused timing/metrics without full tracing
21
+ - You'll export to OTel anyway (via the adapter)
22
+
23
+ **Use OpenTelemetry directly when:**
24
+ - You need distributed tracing across services
25
+ - You want baggage propagation, trace context, etc.
26
+ - You're building production infrastructure
27
+ - You need W3C Trace Context compliance
28
+
29
+ **Best of both worlds:** Use Nullscope in your application code with `OTelReporter` to get zero-cost no-op behavior with OTel export when enabled.
30
+
31
+ ## vs structlog
32
+
33
+ **structlog** is a structured logging library that makes logs more useful with context and formatting.
34
+
35
+ | Aspect | Nullscope | structlog |
36
+ |--------|-----------|-----------|
37
+ | Primary purpose | Timing and metrics | Structured logging |
38
+ | Output format | Reporter-defined | Log events |
39
+ | Context tracking | Scope hierarchy | Bound loggers |
40
+ | Zero-cost disable | Yes | No (logs are written) |
41
+
42
+ **Use Nullscope when:**
43
+ - You need timing data, not log events
44
+ - You want to record numeric metrics
45
+ - Zero overhead when disabled is critical
46
+
47
+ **Use structlog when:**
48
+ - You need rich, structured log output
49
+ - You want contextual logging throughout your app
50
+ - You're building audit trails or debug logs
51
+
52
+ **They complement each other:** Use structlog for logging, Nullscope for metrics/timing. They solve different problems.
53
+
54
+ ## vs prometheus_client
55
+
56
+ **prometheus_client** is the official Python client for Prometheus metrics.
57
+
58
+ | Aspect | Nullscope | prometheus_client |
59
+ |--------|-----------|-------------------|
60
+ | Metric types | Counters, gauges, timings | Full Prometheus types |
61
+ | Scope hierarchy | Built-in | Manual labels |
62
+ | Zero-cost disable | Yes | No |
63
+ | Export format | Reporter-defined | Prometheus exposition |
64
+ | Spans/timing | Yes (with wall clock) | Histograms only |
65
+
66
+ **Use Nullscope when:**
67
+ - You want automatic scope hierarchy
68
+ - Zero overhead when disabled matters
69
+ - You're not running Prometheus
70
+
71
+ **Use prometheus_client when:**
72
+ - You have a Prometheus infrastructure
73
+ - You need summaries, histograms with buckets
74
+ - You want direct Prometheus integration
75
+
76
+ ## vs timing decorators / manual timing
77
+
78
+ Many projects use simple timing:
79
+
80
+ ```python
81
+ import time
82
+ start = time.perf_counter()
83
+ do_work()
84
+ duration = time.perf_counter() - start
85
+ ```
86
+
87
+ | Aspect | Nullscope | Manual timing |
88
+ |--------|-----------|---------------|
89
+ | Zero-cost disable | Yes | No (unless you add conditionals) |
90
+ | Scope hierarchy | Automatic | Manual |
91
+ | Multiple reporters | Built-in | DIY |
92
+ | Async safety | Context vars | DIY |
93
+
94
+ **Use Nullscope when:**
95
+ - You want consistent telemetry patterns
96
+ - You need hierarchy tracking
97
+ - You might disable telemetry in production
98
+
99
+ **Use manual timing when:**
100
+ - You have one or two timing points
101
+ - You don't need hierarchy
102
+ - Simplicity is paramount
103
+
104
+ ## Summary: When to Choose Nullscope
105
+
106
+ Choose Nullscope if you value:
107
+
108
+ 1. **Zero overhead** when telemetry is disabled
109
+ 2. **Simple API** for timing and metrics
110
+ 3. **Automatic hierarchy** for nested operations
111
+ 4. **Library-friendly** design (no global state pollution)
112
+ 5. **Async-safe** out of the box
113
+
114
+ Consider alternatives if you need:
115
+
116
+ 1. **Full distributed tracing** → OpenTelemetry
117
+ 2. **Structured logging** → structlog
118
+ 3. **Prometheus-native metrics** → prometheus_client
119
+ 4. **One-off timing** → manual `time.perf_counter()`