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.
- nullscope-0.1.0/.github/workflows/release.yml +83 -0
- nullscope-0.1.0/.github/workflows/test.yml +43 -0
- nullscope-0.1.0/.gitignore +7 -0
- nullscope-0.1.0/LICENSE +21 -0
- nullscope-0.1.0/PKG-INFO +190 -0
- nullscope-0.1.0/README.md +160 -0
- nullscope-0.1.0/ROADMAP.md +37 -0
- nullscope-0.1.0/docs/comparison.md +119 -0
- nullscope-0.1.0/docs/design.md +129 -0
- nullscope-0.1.0/docs/examples.md +215 -0
- nullscope-0.1.0/pyproject.toml +77 -0
- nullscope-0.1.0/src/nullscope/__init__.py +327 -0
- nullscope-0.1.0/src/nullscope/adapters/opentelemetry.py +103 -0
- nullscope-0.1.0/src/nullscope/py.typed +0 -0
- nullscope-0.1.0/tests/test_core.py +84 -0
- nullscope-0.1.0/tests/test_otel.py +76 -0
|
@@ -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
|
nullscope-0.1.0/LICENSE
ADDED
|
@@ -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.
|
nullscope-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/nullscope/)
|
|
34
|
+
[](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
|
+
[](https://pypi.org/project/nullscope/)
|
|
4
|
+
[](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()`
|