Skip to content

Commit d580db1

Browse files
committed
Merge branch feature/docs-rework into develop
2 parents a03277a + 2e70aae commit d580db1

30 files changed

Lines changed: 2081 additions & 827 deletions

‎README.md‎

Lines changed: 136 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,202 +1,201 @@
1-
# Python StatsD Client
1+
<h1 align="center">python-statsd</h1>
2+
3+
<p align="center">
4+
<img src="https://raw.githubusercontent.com/wolph/python-statsd/develop/docs/_static/dataflow.svg"
5+
alt="Metrics leave your application over UDP, statsd aggregates them, Graphite stores and graphs them"
6+
width="880">
7+
</p>
8+
9+
<p align="center">
10+
<a href="https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml"><img src="https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml/badge.svg?branch=master" alt="Test status"></a>
11+
<a href="https://github.com/WoLpH/python-statsd/actions/workflows/codeql.yml"><img src="https://github.com/WoLpH/python-statsd/actions/workflows/codeql.yml/badge.svg?branch=develop" alt="CodeQL status"></a>
12+
<a href="https://python-statsd.readthedocs.io/"><img src="https://img.shields.io/readthedocs/python-statsd?logo=readthedocs&logoColor=white" alt="Documentation"></a>
13+
<a href="https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="Coverage, enforced in CI"></a>
14+
<br>
15+
<a href="https://pypi.org/project/python-statsd/"><img src="https://img.shields.io/pypi/v/python-statsd.svg?logo=pypi&logoColor=white" alt="PyPI version"></a>
16+
<a href="https://pypi.org/project/python-statsd/"><img src="https://img.shields.io/pypi/pyversions/python-statsd.svg?logo=python&logoColor=white" alt="Supported Python versions"></a>
17+
<a href="https://pepy.tech/project/python-statsd"><img src="https://static.pepy.tech/badge/python-statsd" alt="Downloads"></a>
18+
<a href="https://github.com/WoLpH/python-statsd/blob/develop/LICENSE"><img src="https://img.shields.io/pypi/l/python-statsd.svg" alt="BSD-3-Clause licence"></a>
19+
<br>
20+
<a href="https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/types-mypy%20%7C%20basedpyright%20%7C%20pyrefly%20%7C%20ty-8957e5" alt="Type checked by four checkers in CI"></a>
21+
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Linted and formatted with ruff"></a>
22+
</p>
23+
24+
`python-statsd` is a client for Etsy's statsd server, a front end and proxy
25+
for the Graphite stats collection and graphing server. It supports Python
26+
3.10 and newer, and it has no dependencies.
227

3-
[![Test Status](https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml)
4-
[![PyPI version](https://img.shields.io/pypi/v/python-statsd.svg)](https://pypi.org/project/python-statsd/)
5-
[![Supported Python versions](https://img.shields.io/pypi/pyversions/python-statsd.svg)](https://pypi.org/project/python-statsd/)
6-
7-
`python-statsd` is a client for Etsy's statsd server, a front end/proxy for
8-
the Graphite stats collection and graphing server. It supports Python 3.10+
9-
and has no dependencies.
10-
11-
## Note on the package name
28+
```bash
29+
pip install python-statsd
30+
```
1231

13-
This package installs the `statsd` Python module and is published on PyPI as
14-
`python-statsd`. There is a different project published on PyPI as `statsd`
15-
(jsocol's client) which *also* installs a `statsd` module. Installing both in
16-
one environment will break either or both. Install exactly one of the two.
32+
```python
33+
import statsd
1734

18-
## Links
35+
counter = statsd.Counter('app')
36+
counter += 1
1937

20-
- The source: https://github.com/WoLpH/python-statsd
21-
- Project page: https://pypi.org/project/python-statsd/
22-
- Reporting bugs: https://github.com/WoLpH/python-statsd/issues
23-
- Documentation: https://python-statsd.readthedocs.io/
24-
- My blog: https://wol.ph/
25-
- Statsd: https://github.com/etsy/statsd
26-
- Graphite: https://graphiteapp.org/
38+
with statsd.Timer('app').time('render'):
39+
pass # the work you are measuring
40+
```
2741

28-
## Install
42+
Two metrics, two UDP packets, nothing blocking. A statsd server that is
43+
down costs you graphs rather than requests.
2944

30-
```bash
31-
pip install python-statsd
32-
```
45+
## What goes on the wire
3346

34-
or with uv:
47+
Every line under `udp :8125 <` in this recording is a packet the client
48+
sent, caught by a real listener on the other end:
3549

36-
```bash
37-
uv pip install python-statsd
38-
```
50+
<p align="center">
51+
<img src="https://raw.githubusercontent.com/wolph/python-statsd/develop/docs/_static/demo.gif"
52+
alt="A terminal session sending a counter, a gauge, a timer and a sampled counter, with each UDP payload printed as it arrives"
53+
width="880">
54+
</p>
3955

40-
## Usage
56+
Note the last one. At `sample_rate=0.5` four increments produced a single
57+
packet, and it carries `|@0.5` so the server knows to multiply back up.
4158

42-
### Basic Usage
59+
## The metric types
4360

44-
#### Timers
61+
| Type | A burst of values becomes | Reach for it when |
62+
| --- | --- | --- |
63+
| `Counter` | their sum | you are counting events |
64+
| `Gauge` | the last one | you are reporting a level |
65+
| `Timer` | mean, median, percentiles | you are measuring duration |
66+
| `Average` | their mean | the server should average samples |
67+
| `Raw` | stored as sent | you already did the summarising |
4568

4669
```python
4770
import statsd
4871

49-
timer = statsd.Timer('MyApplication')
50-
timer.start()
51-
# do something here
52-
timer.stop('SomeTimer')
53-
```
72+
counter = statsd.Counter('app')
73+
counter.increment('requests') # app.requests:1|c
5474

55-
Or as a context manager (the metric is also sent when the block raises an
56-
exception):
75+
gauge = statsd.Gauge('app')
76+
gauge.send('queue_depth', 42) # app.queue_depth:42|g
5777

58-
```python
59-
import statsd
78+
average = statsd.Average('app')
79+
average.send('batch', 123) # app.batch:123|a
6080

61-
with statsd.Timer('MyApplication').time('SomeTimer'):
62-
pass # do something here
81+
raw = statsd.Raw('app')
82+
raw.send('summary', 42, timestamp=1234567890) # app.summary:42|r|1234567890
6383
```
6484

65-
Or as a decorator:
85+
Timers come in three forms, and the context manager is the one to reach
86+
for, because it reports the block that raised as well as the block that
87+
did not:
6688

6789
```python
6890
import statsd
6991

70-
timer = statsd.Timer('MyApplication')
92+
timer = statsd.Timer('app')
93+
94+
with timer.time('render'):
95+
pass # the work you are measuring
7196

7297

7398
@timer.decorate
74-
def some_function():
75-
pass # resulting timer name: MyApplication.some_function
99+
def render_page(): # sends app.render_page
100+
pass
76101
```
77102

78-
#### Counters
103+
Names build themselves when you nest clients, which keeps the string
104+
formatting out of your call sites:
79105

80106
```python
81107
import statsd
82108

83-
counter = statsd.Counter('MyApplication')
84-
# do something here
85-
counter += 1
86-
```
87-
88-
#### Gauge
89-
90-
```python
91-
import statsd
109+
app = statsd.Client('app')
110+
queries = app.get_client('database').get_client('queries', statsd.Counter)
92111

93-
gauge = statsd.Gauge('MyApplication')
94-
# do something here
95-
gauge.send('SomeName', 42)
112+
queries.increment() # app.database.queries:1|c
96113
```
97114

98-
#### Raw
115+
## See it working
99116

100-
Raw values should be e.g. pre-summarised data or other data that will get
101-
passed directly to carbon. This can be used as a time and bandwidth-saving
102-
mechanism: sending a lot of samples could use a lot of bandwidth (more b/w is
103-
used in udp headers than data for a gauge, for instance).
117+
The repository ships a compose file with statsd, Graphite and Grafana, so
118+
you can watch a metric arrive instead of taking anyone's word for it:
104119

105-
```python
106-
import statsd
107-
108-
raw = statsd.Raw('MyApplication')
109-
# do something here
110-
raw.send('SomeName', 42, timestamp=1234567890)
120+
```bash
121+
docker compose up -d
122+
uv run python examples/send_metrics.py --seconds 120
111123
```
112124

113-
The raw type wants a timestamp in seconds since the epoch (the standard unix
114-
timestamp, e.g. the output of `date +%s`). If you leave it out or pass `None`
115-
the current time is used.
116-
117-
#### Average
125+
Then open <http://localhost:3000>. Grafana comes up with the datasource
126+
configured and this dashboard loaded, no login in the way:
118127

119-
```python
120-
import statsd
128+
<p align="center">
129+
<img src="https://raw.githubusercontent.com/wolph/python-statsd/develop/docs/_static/grafana.png"
130+
alt="A Grafana dashboard showing request rate, render time percentiles, queue depth and packets received"
131+
width="880">
132+
</p>
121133

122-
average = statsd.Average('MyApplication')
123-
# do something here
124-
average.send('SomeName', 123)
125-
```
134+
That screenshot is the stack in this repository, fed by
135+
`examples/send_metrics.py` through this client. The
136+
[local stack guide](https://python-statsd.readthedocs.io/en/latest/local-stack.html)
137+
covers how statsd renames your metrics on the way through, and what to
138+
check when nothing shows up.
126139

127-
#### Connection settings
140+
## Configuration
128141

129-
If you need some settings other than the defaults for your `Connection`, you
130-
can use `Connection.set_defaults()`:
142+
Set the defaults once at startup and every client built afterwards follows:
131143

132144
```python
133145
import statsd
134146

135-
statsd.Connection.set_defaults(
136-
host='localhost', port=8125, sample_rate=1, disabled=False
137-
)
147+
statsd.Connection.set_defaults(host='localhost', port=8125, sample_rate=1)
138148
```
139149

140-
Every interaction with statsd after these are set will use whatever you
141-
specify, unless you explicitly create a different `Connection` to use
142-
(described below).
143-
144-
Defaults:
145-
146-
- `host` = `'localhost'`
147-
- `port` = `8125`
148-
- `sample_rate` = `1`
149-
- `disabled` = `False`
150-
151-
## Advanced Usage
150+
Or build connections yourself when one destination is not enough:
152151

153152
```python
154153
import statsd
155154

156-
# Open a connection to `server` on port `1234` with a
157-
# `50%` sample rate
158-
statsd_connection = statsd.Connection(
159-
host='server',
160-
port=1234,
161-
sample_rate=0.5,
162-
)
155+
connection = statsd.Connection(host='statsd-1', port=8125, sample_rate=0.1)
156+
statsd.Counter('app.requests', connection).increment()
157+
```
163158

164-
# Create a client for this application
165-
statsd_client = statsd.Client(__name__, statsd_connection)
159+
One trap worth knowing before it costs you an afternoon: a falsy argument
160+
means "use the default", so `Connection(sample_rate=0)` sends everything.
161+
Pass `disabled=True` to send nothing.
166162

163+
## Documentation
167164

168-
class SomeClass:
169-
def __init__(self):
170-
# Create a client specific for this class
171-
self.statsd_client = statsd_client.get_client(type(self).__name__)
165+
Full documentation is at
166+
[python-statsd.readthedocs.io](https://python-statsd.readthedocs.io/).
172167

173-
def do_something(self):
174-
# Create a `timer` client
175-
timer = self.statsd_client.get_client(class_=statsd.Timer)
168+
- [Metrics](https://python-statsd.readthedocs.io/en/latest/metrics.html):
169+
the five types, how to choose, and the exact bytes each one writes
170+
- [Connections](https://python-statsd.readthedocs.io/en/latest/connections.html):
171+
destinations, sampling, disabling, failure behaviour, threads and forks
172+
- [Patterns](https://python-statsd.readthedocs.io/en/latest/patterns.html):
173+
naming, cardinality, client trees, WSGI and Celery integration
174+
- [Local stack](https://python-statsd.readthedocs.io/en/latest/local-stack.html):
175+
docker compose, and how to debug a metric that never arrives
176176

177-
# start the measurement
178-
timer.start()
177+
For Django, use [django-statsd](https://github.com/wolph/django-statsd),
178+
the sister project built on this client. It times views and reports the
179+
queries per request without you writing any of it.
179180

180-
# do something
181-
timer.intermediate('intermediate_value')
181+
## Note on the package name
182182

183-
# do something else
184-
timer.stop('total')
185-
```
183+
This project is published on PyPI as `python-statsd` and installs a module
184+
called `statsd`. A different project, jsocol's client, is published as
185+
`statsd` and installs a module called `statsd` as well. Installing both in
186+
one environment leaves you with whichever was written last, so pick one.
186187

187-
If there is a need to turn *OFF* the service and avoid sending UDP messages,
188-
the `Connection` class can be disabled with the `disabled` argument:
188+
## Contributing
189189

190-
```python
191-
import statsd
190+
Bug reports and patches are welcome, and `CONTRIBUTING.md` covers the
191+
development setup: `uv sync --all-extras`, `uv run pytest`, and
192+
`uv run tox -p auto` to run everything CI runs. Every code sample in this
193+
README and in the documentation is executed by the test suite, so a change
194+
in behaviour tends to tell you which paragraph it just made wrong.
192195

193-
statsd_connection = statsd.Connection(
194-
host='server',
195-
port=1234,
196-
sample_rate=0.5,
197-
disabled=True,
198-
)
199-
```
196+
## Links
200197

201-
If logging's level is set to debug the `Connection` object will inform it is
202-
not sending UDP messages anymore.
198+
- Source: <https://github.com/WoLpH/python-statsd>
199+
- Issues: <https://github.com/WoLpH/python-statsd/issues>
200+
- Statsd: <https://github.com/etsy/statsd>
201+
- Graphite: <https://graphiteapp.org/>

‎docker-compose.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# A local statsd, Graphite and Grafana stack for trying python-statsd out.
2+
#
3+
# docker compose up -d
4+
# uv run python examples/send_metrics.py --seconds 120
5+
#
6+
# Grafana opens on http://localhost:3000 with a provisioned dashboard and
7+
# no login. Graphite's own interface is on http://localhost:8080.
8+
9+
services:
10+
graphite:
11+
image: graphiteapp/graphite-statsd:latest
12+
container_name: python-statsd-graphite
13+
restart: unless-stopped
14+
ports:
15+
# Every port is overridable, so the stack fits next to whatever
16+
# else you are already running: GRAPHITE_HTTP_PORT=8081 docker compose up
17+
- '${GRAPHITE_HTTP_PORT:-8080}:80' # Graphite web and its render API
18+
- '${STATSD_PORT:-8125}:8125/udp' # statsd, where this client sends
19+
- '${STATSD_ADMIN_PORT:-8126}:8126' # statsd admin interface
20+
environment:
21+
# Ten-second flushes so a graph appears while you are still watching.
22+
STATSD_INTERVAL: '10000'
23+
24+
grafana:
25+
image: grafana/grafana:latest
26+
container_name: python-statsd-grafana
27+
restart: unless-stopped
28+
depends_on:
29+
- graphite
30+
ports:
31+
- '${GRAFANA_PORT:-3000}:3000'
32+
environment:
33+
GF_AUTH_ANONYMOUS_ENABLED: 'true'
34+
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
35+
GF_AUTH_DISABLE_LOGIN_FORM: 'true'
36+
GF_USERS_DEFAULT_THEME: dark
37+
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/python-statsd.json
38+
volumes:
39+
- ./examples/grafana/provisioning:/etc/grafana/provisioning:ro
40+
- ./examples/grafana/dashboards:/var/lib/grafana/dashboards:ro

0 commit comments

Comments
 (0)