pylemetry 1.1.4__tar.gz → 1.2.1__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.
- pylemetry-1.2.1/.github/actions/bump-version/action.yaml +26 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/.github/actions/configure-uv/action.yaml +2 -1
- {pylemetry-1.1.4 → pylemetry-1.2.1}/.github/workflows/ci.yaml +4 -8
- pylemetry-1.2.1/.github/workflows/release.yaml +102 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/PKG-INFO +67 -23
- {pylemetry-1.1.4 → pylemetry-1.2.1}/README.md +65 -21
- {pylemetry-1.1.4 → pylemetry-1.2.1}/pyproject.toml +2 -2
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/decorators/__init__.py +0 -1
- pylemetry-1.2.1/src/pylemetry/decorators/count.py +78 -0
- pylemetry-1.2.1/src/pylemetry/decorators/time.py +81 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/counter.py +2 -2
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/gauge.py +2 -2
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/meter.py +10 -1
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/timer.py +15 -9
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/registry.py +37 -25
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/__init__.py +2 -3
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/logging.py +12 -6
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/reporter.py +22 -12
- pylemetry-1.2.1/src/pylemetry/utils/__init__.py +3 -0
- pylemetry-1.2.1/src/pylemetry/utils/timer_units.py +22 -0
- pylemetry-1.2.1/test/test_pylemetry/decorators/test_count.py +134 -0
- pylemetry-1.2.1/test/test_pylemetry/decorators/test_time.py +143 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/meters/test_counter.py +5 -5
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/meters/test_gauge.py +7 -7
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/meters/test_timer.py +21 -8
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/reporting/test_logging.py +25 -25
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/reporting/test_reporter.py +46 -23
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/test_registry.py +40 -40
- pylemetry-1.2.1/test/test_pylemetry/utils/test_timer_units.py +20 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/uv.lock +2 -2
- pylemetry-1.1.4/.github/workflows/release.yaml +0 -89
- pylemetry-1.1.4/src/pylemetry/decorators/count.py +0 -39
- pylemetry-1.1.4/src/pylemetry/decorators/time.py +0 -38
- pylemetry-1.1.4/test/test_pylemetry/decorators/test_count.py +0 -48
- pylemetry-1.1.4/test/test_pylemetry/decorators/test_time.py +0 -51
- {pylemetry-1.1.4 → pylemetry-1.2.1}/.gitignore +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/LICENSE +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/__init__.py +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/__init__.py +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/__init__.py +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/py.typed +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/reporting_type.py +0 -0
- {pylemetry-1.1.4 → pylemetry-1.2.1}/test/conftest.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Bump version
|
|
2
|
+
description: Bump the project version and create a pull request
|
|
3
|
+
runs:
|
|
4
|
+
using: composite
|
|
5
|
+
steps:
|
|
6
|
+
- name: Bump Version
|
|
7
|
+
run: uv version --bump patch --bump alpha
|
|
8
|
+
shell: bash
|
|
9
|
+
|
|
10
|
+
- name: Get New Version Branch Name
|
|
11
|
+
id: bump_version
|
|
12
|
+
run: echo "branch_name=feature/bump_version_$(uv version --short)" >> $GITHUB_OUTPUT
|
|
13
|
+
shell: bash
|
|
14
|
+
|
|
15
|
+
- name: Commit Version Bump
|
|
16
|
+
uses: EndBug/add-and-commit@v11.1.1
|
|
17
|
+
with:
|
|
18
|
+
message: "Bump project version following release"
|
|
19
|
+
default_author: github_actions
|
|
20
|
+
new_branch: ${{ steps.bump_version.outputs.branch_name }}
|
|
21
|
+
|
|
22
|
+
- name: Raise Pull Request
|
|
23
|
+
run: gh pr create -B main -H ${{ steps.bump_version.outputs.branch_name }} --title "Update version to v$(uv version --short)" --body 'Created by GitHub Actions in the Release pipeline'
|
|
24
|
+
shell: bash
|
|
25
|
+
env:
|
|
26
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
name: Configure uv
|
|
2
|
+
description: Configure uv for running within pipelines
|
|
2
3
|
runs:
|
|
3
4
|
using: composite
|
|
4
5
|
steps:
|
|
5
6
|
- name: Install uv
|
|
6
|
-
uses: astral-sh/setup-uv@
|
|
7
|
+
uses: astral-sh/setup-uv@v10.2.0
|
|
7
8
|
|
|
8
9
|
- name: Install dependencies
|
|
9
10
|
run: uv sync --locked --dev
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
name: Test, Build, and Publish
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- develop
|
|
7
4
|
pull_request:
|
|
8
5
|
branches:
|
|
9
6
|
- main
|
|
10
|
-
- develop
|
|
11
7
|
|
|
12
8
|
jobs:
|
|
13
9
|
lint:
|
|
@@ -15,11 +11,11 @@ jobs:
|
|
|
15
11
|
runs-on: ubuntu-latest
|
|
16
12
|
|
|
17
13
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v7.0.1
|
|
19
15
|
|
|
20
16
|
- uses: ./.github/actions/configure-uv
|
|
21
17
|
|
|
22
|
-
- uses: astral-sh/ruff-action@
|
|
18
|
+
- uses: astral-sh/ruff-action@v4.1.0
|
|
23
19
|
|
|
24
20
|
- name: Type Checks
|
|
25
21
|
run: uv run mypy .
|
|
@@ -29,7 +25,7 @@ jobs:
|
|
|
29
25
|
runs-on: ubuntu-latest
|
|
30
26
|
|
|
31
27
|
steps:
|
|
32
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@v7.0.1
|
|
33
29
|
|
|
34
30
|
- uses: ./.github/actions/configure-uv
|
|
35
31
|
|
|
@@ -43,7 +39,7 @@ jobs:
|
|
|
43
39
|
needs: [lint, test]
|
|
44
40
|
|
|
45
41
|
steps:
|
|
46
|
-
- uses: actions/checkout@
|
|
42
|
+
- uses: actions/checkout@v7.0.1
|
|
47
43
|
|
|
48
44
|
- uses: ./.github/actions/configure-uv
|
|
49
45
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
dry-run:
|
|
7
|
+
description: Run as a dry-run
|
|
8
|
+
type: boolean
|
|
9
|
+
bump-to-stable:
|
|
10
|
+
description: Bump the version to the next stable version
|
|
11
|
+
type: boolean
|
|
12
|
+
default: true
|
|
13
|
+
override-version:
|
|
14
|
+
description: Override the current version and set as the release version
|
|
15
|
+
type: string
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
lint:
|
|
19
|
+
name: Lint & Types
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v7.0.1
|
|
24
|
+
|
|
25
|
+
- uses: ./.github/actions/configure-uv
|
|
26
|
+
|
|
27
|
+
- uses: astral-sh/ruff-action@v4.1.0
|
|
28
|
+
|
|
29
|
+
- name: Type Checks
|
|
30
|
+
run: uv run mypy .
|
|
31
|
+
|
|
32
|
+
test:
|
|
33
|
+
name: Unit Tests
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v7.0.1
|
|
38
|
+
|
|
39
|
+
- uses: ./.github/actions/configure-uv
|
|
40
|
+
|
|
41
|
+
- name: Run tests with Tox
|
|
42
|
+
run: tox
|
|
43
|
+
|
|
44
|
+
publish:
|
|
45
|
+
name: Build & Publish
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment: release
|
|
48
|
+
needs: [lint, test]
|
|
49
|
+
permissions:
|
|
50
|
+
contents: write
|
|
51
|
+
pull-requests: write
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v7.0.1
|
|
55
|
+
with:
|
|
56
|
+
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
|
57
|
+
|
|
58
|
+
- uses: ./.github/actions/configure-uv
|
|
59
|
+
|
|
60
|
+
- name: Set release version
|
|
61
|
+
if: ${{ inputs.bump-to-stable || inputs.override-version != '' }}
|
|
62
|
+
run: |
|
|
63
|
+
if [[ $OVERRIDE_VERSION != '' ]]; then
|
|
64
|
+
uv version $OVERRIDE_VERSION;
|
|
65
|
+
elif $BUMP_TO_STABLE; then
|
|
66
|
+
uv version --bump stable
|
|
67
|
+
fi;
|
|
68
|
+
env:
|
|
69
|
+
BUMP_TO_STABLE: ${{ inputs.bump-to-stable }}
|
|
70
|
+
OVERRIDE_VERSION: ${{ inputs.override-version }}
|
|
71
|
+
|
|
72
|
+
- name: Get version from uv
|
|
73
|
+
id: version
|
|
74
|
+
run: echo "version=$(uv version --short)" >> $GITHUB_OUTPUT
|
|
75
|
+
|
|
76
|
+
- name: Build
|
|
77
|
+
run: uv build
|
|
78
|
+
|
|
79
|
+
- name: Publish to PyPI
|
|
80
|
+
env:
|
|
81
|
+
PYPI_TOKEN: ${{ secrets.pypi_token }}
|
|
82
|
+
DRY_RUN: ${{ inputs.dry-run }}
|
|
83
|
+
run: |
|
|
84
|
+
if $DRY_RUN; then
|
|
85
|
+
uv publish --index pypi --dry-run --token $PYPI_TOKEN
|
|
86
|
+
else
|
|
87
|
+
uv publish --index pypi --token $PYPI_TOKEN
|
|
88
|
+
fi;
|
|
89
|
+
|
|
90
|
+
- name: GitHub Release
|
|
91
|
+
if: ${{ !inputs.dry-run }}
|
|
92
|
+
uses: ncipollo/release-action@v1
|
|
93
|
+
env:
|
|
94
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
95
|
+
with:
|
|
96
|
+
tag: ${{ steps.version.outputs.version }}
|
|
97
|
+
name: ${{ steps.version.outputs.version }}
|
|
98
|
+
artifacts: "dist/pylemetry-*.tar.gz,dist/pylemetry-*.whl"
|
|
99
|
+
|
|
100
|
+
- name: Bump Version
|
|
101
|
+
if: ${{ !inputs.dry-run }}
|
|
102
|
+
uses: ./.github/actions/bump-version
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pylemetry
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: A Python metrics library
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/pylemetry/
|
|
6
6
|
Project-URL: Repository, https://github.com/amurphy4/pylemetry
|
|
@@ -44,7 +44,7 @@ from pylemetry.meters import Counter
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def some_method() -> None:
|
|
47
|
-
counter = Counter()
|
|
47
|
+
counter = Counter("example")
|
|
48
48
|
|
|
49
49
|
for _ in range(100):
|
|
50
50
|
counter.add() # counter += 1 is also supported
|
|
@@ -60,13 +60,11 @@ from pylemetry.decorators import count
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@count()
|
|
63
|
-
def some_method() -> None:
|
|
64
|
-
...
|
|
63
|
+
def some_method() -> None: ...
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
@count("named_counter")
|
|
68
|
-
def another_method() -> None:
|
|
69
|
-
...
|
|
67
|
+
def another_method() -> None: ...
|
|
70
68
|
|
|
71
69
|
|
|
72
70
|
def main() -> None:
|
|
@@ -93,9 +91,9 @@ from pylemetry.meters import Gauge
|
|
|
93
91
|
|
|
94
92
|
|
|
95
93
|
def some_method() -> None:
|
|
96
|
-
gauge = Gauge()
|
|
94
|
+
gauge = Gauge("sample_gauge")
|
|
97
95
|
|
|
98
|
-
registry.add_gauge(
|
|
96
|
+
registry.add_gauge(gauge)
|
|
99
97
|
```
|
|
100
98
|
|
|
101
99
|
The `Gauge` supports incrementing, decrementing, and setting a value directly
|
|
@@ -127,7 +125,7 @@ from pylemetry.meters import Timer
|
|
|
127
125
|
|
|
128
126
|
|
|
129
127
|
def some_method() -> None:
|
|
130
|
-
timer = Timer()
|
|
128
|
+
timer = Timer("example")
|
|
131
129
|
|
|
132
130
|
for _ in range(100):
|
|
133
131
|
with timer.time():
|
|
@@ -145,20 +143,18 @@ from pylemetry.decorators import time
|
|
|
145
143
|
|
|
146
144
|
|
|
147
145
|
@time()
|
|
148
|
-
def some_method() -> None:
|
|
149
|
-
...
|
|
146
|
+
def some_method() -> None: ...
|
|
150
147
|
|
|
151
148
|
|
|
152
149
|
@time("named_timer")
|
|
153
|
-
def another_method() -> None:
|
|
154
|
-
...
|
|
150
|
+
def another_method() -> None: ...
|
|
155
151
|
|
|
156
152
|
|
|
157
153
|
def main() -> None:
|
|
158
154
|
for _ in range(100):
|
|
159
155
|
some_method()
|
|
160
156
|
another_method()
|
|
161
|
-
|
|
157
|
+
|
|
162
158
|
timer = registry.get_timer("some_method")
|
|
163
159
|
timer.get_count() # 100
|
|
164
160
|
timer.get_value() # Sum total execution time of the some_method function
|
|
@@ -173,6 +169,53 @@ def main() -> None:
|
|
|
173
169
|
|
|
174
170
|
When using this meter via a decorator, the meter gets added to the global `registry`, with the method name it's decorating as the meter name. Alternatively, you can provide a name for the meter as a parameter to the decorator
|
|
175
171
|
|
|
172
|
+
By default, timer meters will measure time in nanoseconds, this can be changed via the `unit` parameter using the `TimerUnits` enum in the utils module
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import time
|
|
176
|
+
|
|
177
|
+
from pylemetry.meters import Timer
|
|
178
|
+
from pylemetry.utils import TimerUnits
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
timer_s = Timer("example_s", TimerUnits.SECONDS)
|
|
182
|
+
timer_ms = Timer("example_ms", TimerUnits.MILLISECONDS)
|
|
183
|
+
|
|
184
|
+
with timer_s.time():
|
|
185
|
+
time.sleep(1)
|
|
186
|
+
|
|
187
|
+
with timer_ms.time():
|
|
188
|
+
time.sleep(1)
|
|
189
|
+
|
|
190
|
+
timer_s.get_mean_tick_time() # 1
|
|
191
|
+
timer_ms.get_mean_tick_time() # 1000
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Tags
|
|
195
|
+
|
|
196
|
+
When creating a meter you can assign a set of tags to it as key-value pairs. The value must be one of either `str`, `int`, or `float`.
|
|
197
|
+
When using a decorator to create a meter, you can use a custom format for the value to extract values out of the method's args and kwargs
|
|
198
|
+
|
|
199
|
+
In order to allow for multiple meters with the same name and different tags, the name in the registry gets mangled with the tags to produce a unique name,
|
|
200
|
+
as a result when trying to get the meter from the registry you will need to provide both its name and its tags.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from pylemetry import registry
|
|
204
|
+
from pylemetry.decorators import time
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@time("example_timer", tags={"tag_1": "args[0]", "tag_2": "kwargs[param_2]", "tag_3": "some value"})
|
|
208
|
+
def some_method(param_1: int, param_2: int) -> None: ...
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def main() -> None:
|
|
212
|
+
for _ in range(100):
|
|
213
|
+
some_method(1, param_2=2)
|
|
214
|
+
|
|
215
|
+
timer = registry.get_timer("example_timer", {"tag_1": 1, "tag_2": 2, "tag_3": "some value"})
|
|
216
|
+
timer.get_tags() # {"tag_1": 1, "tag_2": 2, "tag_3": "some value"}
|
|
217
|
+
```
|
|
218
|
+
|
|
176
219
|
## The Registry
|
|
177
220
|
|
|
178
221
|
Pylemetry maintains a global registry of meters, allowing you to share a meter across multiple files, or reference metrics from a central location.
|
|
@@ -183,13 +226,13 @@ from pylemetry import registry
|
|
|
183
226
|
from pylemetry.meters import Counter, Gauge, Timer
|
|
184
227
|
|
|
185
228
|
|
|
186
|
-
counter = Counter()
|
|
187
|
-
gauge = Gauge()
|
|
188
|
-
timer = Timer()
|
|
229
|
+
counter = Counter("example")
|
|
230
|
+
gauge = Gauge("example")
|
|
231
|
+
timer = Timer("example")
|
|
189
232
|
|
|
190
|
-
registry.add_counter(
|
|
191
|
-
registry.add_gauge(
|
|
192
|
-
registry.add_timer(
|
|
233
|
+
registry.add_counter(counter)
|
|
234
|
+
registry.add_gauge(gauge)
|
|
235
|
+
registry.add_timer(timer)
|
|
193
236
|
```
|
|
194
237
|
|
|
195
238
|
Each meter type has an `add_meter`, `get_meter` and `remove_meter` method to manage meters in the `registry`, each requiring a unique meter name.
|
|
@@ -200,9 +243,9 @@ from pylemetry import registry
|
|
|
200
243
|
from pylemetry.meters import Counter, MeterType
|
|
201
244
|
|
|
202
245
|
|
|
203
|
-
counter = Counter()
|
|
246
|
+
counter = Counter("example")
|
|
204
247
|
|
|
205
|
-
registry.add_meter(
|
|
248
|
+
registry.add_meter(counter, MeterType.COUNTER)
|
|
206
249
|
registry.get_meter("example", MeterType.COUNTER)
|
|
207
250
|
registry.remove_meter("example", MeterType.COUNTER)
|
|
208
251
|
registry.get_meter("example", MeterType.COUNTER) # None
|
|
@@ -226,6 +269,7 @@ The message format allows for substitutions for metric values with the following
|
|
|
226
269
|
| max | Maximum value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `max_tick_time` for `Timer` meters |
|
|
227
270
|
| avg | Mean average value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `mean_tick_time` for `Timer` meters |
|
|
228
271
|
| type | Type of the meter (`counter`, `gauge`, or `timer`) |
|
|
272
|
+
| tags | The tags associated with the meter |
|
|
229
273
|
|
|
230
274
|
As an example, a `Counter` meter named `sample_counter` with a value of 10
|
|
231
275
|
```python
|
|
@@ -21,7 +21,7 @@ from pylemetry.meters import Counter
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def some_method() -> None:
|
|
24
|
-
counter = Counter()
|
|
24
|
+
counter = Counter("example")
|
|
25
25
|
|
|
26
26
|
for _ in range(100):
|
|
27
27
|
counter.add() # counter += 1 is also supported
|
|
@@ -37,13 +37,11 @@ from pylemetry.decorators import count
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
@count()
|
|
40
|
-
def some_method() -> None:
|
|
41
|
-
...
|
|
40
|
+
def some_method() -> None: ...
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
@count("named_counter")
|
|
45
|
-
def another_method() -> None:
|
|
46
|
-
...
|
|
44
|
+
def another_method() -> None: ...
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
def main() -> None:
|
|
@@ -70,9 +68,9 @@ from pylemetry.meters import Gauge
|
|
|
70
68
|
|
|
71
69
|
|
|
72
70
|
def some_method() -> None:
|
|
73
|
-
gauge = Gauge()
|
|
71
|
+
gauge = Gauge("sample_gauge")
|
|
74
72
|
|
|
75
|
-
registry.add_gauge(
|
|
73
|
+
registry.add_gauge(gauge)
|
|
76
74
|
```
|
|
77
75
|
|
|
78
76
|
The `Gauge` supports incrementing, decrementing, and setting a value directly
|
|
@@ -104,7 +102,7 @@ from pylemetry.meters import Timer
|
|
|
104
102
|
|
|
105
103
|
|
|
106
104
|
def some_method() -> None:
|
|
107
|
-
timer = Timer()
|
|
105
|
+
timer = Timer("example")
|
|
108
106
|
|
|
109
107
|
for _ in range(100):
|
|
110
108
|
with timer.time():
|
|
@@ -122,20 +120,18 @@ from pylemetry.decorators import time
|
|
|
122
120
|
|
|
123
121
|
|
|
124
122
|
@time()
|
|
125
|
-
def some_method() -> None:
|
|
126
|
-
...
|
|
123
|
+
def some_method() -> None: ...
|
|
127
124
|
|
|
128
125
|
|
|
129
126
|
@time("named_timer")
|
|
130
|
-
def another_method() -> None:
|
|
131
|
-
...
|
|
127
|
+
def another_method() -> None: ...
|
|
132
128
|
|
|
133
129
|
|
|
134
130
|
def main() -> None:
|
|
135
131
|
for _ in range(100):
|
|
136
132
|
some_method()
|
|
137
133
|
another_method()
|
|
138
|
-
|
|
134
|
+
|
|
139
135
|
timer = registry.get_timer("some_method")
|
|
140
136
|
timer.get_count() # 100
|
|
141
137
|
timer.get_value() # Sum total execution time of the some_method function
|
|
@@ -150,6 +146,53 @@ def main() -> None:
|
|
|
150
146
|
|
|
151
147
|
When using this meter via a decorator, the meter gets added to the global `registry`, with the method name it's decorating as the meter name. Alternatively, you can provide a name for the meter as a parameter to the decorator
|
|
152
148
|
|
|
149
|
+
By default, timer meters will measure time in nanoseconds, this can be changed via the `unit` parameter using the `TimerUnits` enum in the utils module
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import time
|
|
153
|
+
|
|
154
|
+
from pylemetry.meters import Timer
|
|
155
|
+
from pylemetry.utils import TimerUnits
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
timer_s = Timer("example_s", TimerUnits.SECONDS)
|
|
159
|
+
timer_ms = Timer("example_ms", TimerUnits.MILLISECONDS)
|
|
160
|
+
|
|
161
|
+
with timer_s.time():
|
|
162
|
+
time.sleep(1)
|
|
163
|
+
|
|
164
|
+
with timer_ms.time():
|
|
165
|
+
time.sleep(1)
|
|
166
|
+
|
|
167
|
+
timer_s.get_mean_tick_time() # 1
|
|
168
|
+
timer_ms.get_mean_tick_time() # 1000
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Tags
|
|
172
|
+
|
|
173
|
+
When creating a meter you can assign a set of tags to it as key-value pairs. The value must be one of either `str`, `int`, or `float`.
|
|
174
|
+
When using a decorator to create a meter, you can use a custom format for the value to extract values out of the method's args and kwargs
|
|
175
|
+
|
|
176
|
+
In order to allow for multiple meters with the same name and different tags, the name in the registry gets mangled with the tags to produce a unique name,
|
|
177
|
+
as a result when trying to get the meter from the registry you will need to provide both its name and its tags.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from pylemetry import registry
|
|
181
|
+
from pylemetry.decorators import time
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@time("example_timer", tags={"tag_1": "args[0]", "tag_2": "kwargs[param_2]", "tag_3": "some value"})
|
|
185
|
+
def some_method(param_1: int, param_2: int) -> None: ...
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main() -> None:
|
|
189
|
+
for _ in range(100):
|
|
190
|
+
some_method(1, param_2=2)
|
|
191
|
+
|
|
192
|
+
timer = registry.get_timer("example_timer", {"tag_1": 1, "tag_2": 2, "tag_3": "some value"})
|
|
193
|
+
timer.get_tags() # {"tag_1": 1, "tag_2": 2, "tag_3": "some value"}
|
|
194
|
+
```
|
|
195
|
+
|
|
153
196
|
## The Registry
|
|
154
197
|
|
|
155
198
|
Pylemetry maintains a global registry of meters, allowing you to share a meter across multiple files, or reference metrics from a central location.
|
|
@@ -160,13 +203,13 @@ from pylemetry import registry
|
|
|
160
203
|
from pylemetry.meters import Counter, Gauge, Timer
|
|
161
204
|
|
|
162
205
|
|
|
163
|
-
counter = Counter()
|
|
164
|
-
gauge = Gauge()
|
|
165
|
-
timer = Timer()
|
|
206
|
+
counter = Counter("example")
|
|
207
|
+
gauge = Gauge("example")
|
|
208
|
+
timer = Timer("example")
|
|
166
209
|
|
|
167
|
-
registry.add_counter(
|
|
168
|
-
registry.add_gauge(
|
|
169
|
-
registry.add_timer(
|
|
210
|
+
registry.add_counter(counter)
|
|
211
|
+
registry.add_gauge(gauge)
|
|
212
|
+
registry.add_timer(timer)
|
|
170
213
|
```
|
|
171
214
|
|
|
172
215
|
Each meter type has an `add_meter`, `get_meter` and `remove_meter` method to manage meters in the `registry`, each requiring a unique meter name.
|
|
@@ -177,9 +220,9 @@ from pylemetry import registry
|
|
|
177
220
|
from pylemetry.meters import Counter, MeterType
|
|
178
221
|
|
|
179
222
|
|
|
180
|
-
counter = Counter()
|
|
223
|
+
counter = Counter("example")
|
|
181
224
|
|
|
182
|
-
registry.add_meter(
|
|
225
|
+
registry.add_meter(counter, MeterType.COUNTER)
|
|
183
226
|
registry.get_meter("example", MeterType.COUNTER)
|
|
184
227
|
registry.remove_meter("example", MeterType.COUNTER)
|
|
185
228
|
registry.get_meter("example", MeterType.COUNTER) # None
|
|
@@ -203,6 +246,7 @@ The message format allows for substitutions for metric values with the following
|
|
|
203
246
|
| max | Maximum value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `max_tick_time` for `Timer` meters |
|
|
204
247
|
| avg | Mean average value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `mean_tick_time` for `Timer` meters |
|
|
205
248
|
| type | Type of the meter (`counter`, `gauge`, or `timer`) |
|
|
249
|
+
| tags | The tags associated with the meter |
|
|
206
250
|
|
|
207
251
|
As an example, a `Counter` meter named `sample_counter` with a value of 10
|
|
208
252
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pylemetry"
|
|
3
|
-
version = "1.1
|
|
3
|
+
version = "1.2.1"
|
|
4
4
|
description = "A Python metrics library"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -60,7 +60,7 @@ explicit_package_bases = true
|
|
|
60
60
|
line-length = 120
|
|
61
61
|
|
|
62
62
|
[tool.ruff.lint]
|
|
63
|
-
select = ["E", "F", "
|
|
63
|
+
select = ["ARG", "B", "E", "F", "I", "N", "RUF", "TID", "UP", "W"]
|
|
64
64
|
|
|
65
65
|
[tool.pytest.ini_options]
|
|
66
66
|
addopts = "--cov=pylemetry --cov-report=html"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import ParamSpec, TypeVar
|
|
5
|
+
|
|
6
|
+
from pylemetry import registry
|
|
7
|
+
from pylemetry.meters import Counter
|
|
8
|
+
|
|
9
|
+
P = ParamSpec("P")
|
|
10
|
+
R = TypeVar("R", covariant=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def count(
|
|
14
|
+
name: str | None = None, tags: dict[str, str | int | float] | None = None
|
|
15
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
16
|
+
"""
|
|
17
|
+
Decorator to count the number of invocations of a given callable. Creates a Counter meter in the Registry
|
|
18
|
+
with either the provided name or the fully qualified name of the callable object as the metric name.
|
|
19
|
+
|
|
20
|
+
:param name: Name of the meter to create, if None the function name is used
|
|
21
|
+
:param tags: Optional key-value pairs of tags for this meter
|
|
22
|
+
:return: Result of the wrapped function
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def decorator(f: Callable[P, R]) -> Callable[P, R]:
|
|
26
|
+
@wraps(f)
|
|
27
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
28
|
+
counter_name = f.__qualname__ if name is None else name
|
|
29
|
+
|
|
30
|
+
_tags: dict[str, str | int | float] = {}
|
|
31
|
+
|
|
32
|
+
if tags:
|
|
33
|
+
for key, value in tags.items():
|
|
34
|
+
args_match = re.search(r"^args\[[0-9]+]", str(value))
|
|
35
|
+
kwargs_match = re.search(r"^kwargs\[[a-zA-Z0-9_]+]", str(value))
|
|
36
|
+
|
|
37
|
+
if args_match:
|
|
38
|
+
args_index = int(args_match.group().replace("args[", "").replace("]", ""))
|
|
39
|
+
|
|
40
|
+
if args_index >= len(args):
|
|
41
|
+
raise IndexError(f"args index {args_index} out of range, args has length {len(args)}")
|
|
42
|
+
|
|
43
|
+
if not isinstance(args[args_index], (str, int, float)):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Only args of types (str | int | float) may be added as tags. "
|
|
46
|
+
f"Got {args[args_index].__class__}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_tags[key] = args[args_index] # type: ignore
|
|
50
|
+
elif kwargs_match:
|
|
51
|
+
kwargs_key = kwargs_match.group().replace("kwargs[", "").replace("]", "")
|
|
52
|
+
|
|
53
|
+
if kwargs_key not in kwargs:
|
|
54
|
+
raise KeyError(f"Key '{kwargs_key}' not present in kwargs")
|
|
55
|
+
|
|
56
|
+
if not isinstance(kwargs[kwargs_key], (str, int, float)):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Only kwargs of types (str | int | float) may be added as tags. "
|
|
59
|
+
f"Got {kwargs[kwargs_key].__class__}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_tags[key] = kwargs[kwargs_key] # type: ignore
|
|
63
|
+
else:
|
|
64
|
+
_tags[key] = value
|
|
65
|
+
|
|
66
|
+
counter = registry.get_counter(counter_name, tags=_tags)
|
|
67
|
+
|
|
68
|
+
if not counter:
|
|
69
|
+
counter = Counter(name=counter_name, tags=_tags)
|
|
70
|
+
registry.add_counter(counter)
|
|
71
|
+
|
|
72
|
+
counter += 1
|
|
73
|
+
|
|
74
|
+
return f(*args, **kwargs)
|
|
75
|
+
|
|
76
|
+
return wrapper
|
|
77
|
+
|
|
78
|
+
return decorator
|