|
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. |
2 | 27 |
|
3 | | -[](https://github.com/WoLpH/python-statsd/actions/workflows/ci.yml) |
4 | | -[](https://pypi.org/project/python-statsd/) |
5 | | -[](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 | +``` |
12 | 31 |
|
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 |
17 | 34 |
|
18 | | -## Links |
| 35 | +counter = statsd.Counter('app') |
| 36 | +counter += 1 |
19 | 37 |
|
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 | +``` |
27 | 41 |
|
28 | | -## Install |
| 42 | +Two metrics, two UDP packets, nothing blocking. A statsd server that is |
| 43 | +down costs you graphs rather than requests. |
29 | 44 |
|
30 | | -```bash |
31 | | -pip install python-statsd |
32 | | -``` |
| 45 | +## What goes on the wire |
33 | 46 |
|
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: |
35 | 49 |
|
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> |
39 | 55 |
|
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. |
41 | 58 |
|
42 | | -### Basic Usage |
| 59 | +## The metric types |
43 | 60 |
|
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 | |
45 | 68 |
|
46 | 69 | ```python |
47 | 70 | import statsd |
48 | 71 |
|
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 |
54 | 74 |
|
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 |
57 | 77 |
|
58 | | -```python |
59 | | -import statsd |
| 78 | +average = statsd.Average('app') |
| 79 | +average.send('batch', 123) # app.batch:123|a |
60 | 80 |
|
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 |
63 | 83 | ``` |
64 | 84 |
|
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: |
66 | 88 |
|
67 | 89 | ```python |
68 | 90 | import statsd |
69 | 91 |
|
70 | | -timer = statsd.Timer('MyApplication') |
| 92 | +timer = statsd.Timer('app') |
| 93 | + |
| 94 | +with timer.time('render'): |
| 95 | + pass # the work you are measuring |
71 | 96 |
|
72 | 97 |
|
73 | 98 | @timer.decorate |
74 | | -def some_function(): |
75 | | - pass # resulting timer name: MyApplication.some_function |
| 99 | +def render_page(): # sends app.render_page |
| 100 | + pass |
76 | 101 | ``` |
77 | 102 |
|
78 | | -#### Counters |
| 103 | +Names build themselves when you nest clients, which keeps the string |
| 104 | +formatting out of your call sites: |
79 | 105 |
|
80 | 106 | ```python |
81 | 107 | import statsd |
82 | 108 |
|
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) |
92 | 111 |
|
93 | | -gauge = statsd.Gauge('MyApplication') |
94 | | -# do something here |
95 | | -gauge.send('SomeName', 42) |
| 112 | +queries.increment() # app.database.queries:1|c |
96 | 113 | ``` |
97 | 114 |
|
98 | | -#### Raw |
| 115 | +## See it working |
99 | 116 |
|
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: |
104 | 119 |
|
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 |
111 | 123 | ``` |
112 | 124 |
|
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: |
118 | 127 |
|
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> |
121 | 133 |
|
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. |
126 | 139 |
|
127 | | -#### Connection settings |
| 140 | +## Configuration |
128 | 141 |
|
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: |
131 | 143 |
|
132 | 144 | ```python |
133 | 145 | import statsd |
134 | 146 |
|
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) |
138 | 148 | ``` |
139 | 149 |
|
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: |
152 | 151 |
|
153 | 152 | ```python |
154 | 153 | import statsd |
155 | 154 |
|
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 | +``` |
163 | 158 |
|
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. |
166 | 162 |
|
| 163 | +## Documentation |
167 | 164 |
|
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/). |
172 | 167 |
|
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 |
176 | 176 |
|
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. |
179 | 180 |
|
180 | | - # do something |
181 | | - timer.intermediate('intermediate_value') |
| 181 | +## Note on the package name |
182 | 182 |
|
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. |
186 | 187 |
|
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 |
189 | 189 |
|
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. |
192 | 195 |
|
193 | | -statsd_connection = statsd.Connection( |
194 | | - host='server', |
195 | | - port=1234, |
196 | | - sample_rate=0.5, |
197 | | - disabled=True, |
198 | | -) |
199 | | -``` |
| 196 | +## Links |
200 | 197 |
|
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/> |
0 commit comments