├── .gitattributes ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── README.txt ├── makedoc.sh ├── setup.py ├── statsd.py ├── statsd_test.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py diff=python 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Gaelen Hadlett 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Apache License 16 | Version 2.0, January 2004 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, 24 | and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all 30 | other entities that control, are controlled by, or are under common 31 | control with that entity. For the purposes of this definition, 32 | "control" means (i) the power, direct or indirect, to cause the 33 | direction or management of such entity, whether by contract or 34 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 35 | outstanding shares, or (iii) beneficial ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation 42 | source, and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical 45 | transformation or translation of a Source form, including but 46 | not limited to compiled object code, generated documentation, 47 | and conversions to other media types. 48 | 49 | "Work" shall mean the work of authorship, whether in Source or 50 | Object form, made available under the License, as indicated by a 51 | copyright notice that is included in or attached to the work 52 | (an example is provided in the Appendix below). 53 | 54 | "Derivative Works" shall mean any work, whether in Source or Object 55 | form, that is based on (or derived from) the Work and for which the 56 | editorial revisions, annotations, elaborations, or other modifications 57 | represent, as a whole, an original work of authorship. For the purposes 58 | of this License, Derivative Works shall not include works that remain 59 | separable from, or merely link (or bind by name) to the interfaces of, 60 | the Work and Derivative Works thereof. 61 | 62 | "Contribution" shall mean any work of authorship, including 63 | the original version of the Work and any modifications or additions 64 | to that Work or Derivative Works thereof, that is intentionally 65 | submitted to Licensor for inclusion in the Work by the copyright owner 66 | or by an individual or Legal Entity authorized to submit on behalf of 67 | the copyright owner. For the purposes of this definition, "submitted" 68 | means any form of electronic, verbal, or written communication sent 69 | to the Licensor or its representatives, including but not limited to 70 | communication on electronic mailing lists, source code control systems, 71 | and issue tracking systems that are managed by, or on behalf of, the 72 | Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise 74 | designated in writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity 77 | on behalf of whom a Contribution has been received by Licensor and 78 | subsequently incorporated within the Work. 79 | 80 | 2. Grant of Copyright License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | copyright license to reproduce, prepare Derivative Works of, 84 | publicly display, publicly perform, sublicense, and distribute the 85 | Work and such Derivative Works in Source or Object form. 86 | 87 | 3. Grant of Patent License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | (except as stated in this section) patent license to make, have made, 91 | use, offer to sell, sell, import, and otherwise transfer the Work, 92 | where such license applies only to those patent claims licensable 93 | by such Contributor that are necessarily infringed by their 94 | Contribution(s) alone or by combination of their Contribution(s) 95 | with the Work to which such Contribution(s) was submitted. If You 96 | institute patent litigation against any entity (including a 97 | cross-claim or counterclaim in a lawsuit) alleging that the Work 98 | or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses 100 | granted to You under this License for that Work shall terminate 101 | as of the date such litigation is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the 104 | Work or Derivative Works thereof in any medium, with or without 105 | modifications, and in Source or Object form, provided that You 106 | meet the following conditions: 107 | 108 | (a) You must give any other recipients of the Work or 109 | Derivative Works a copy of this License; and 110 | 111 | (b) You must cause any modified files to carry prominent notices 112 | stating that You changed the files; and 113 | 114 | (c) You must retain, in the Source form of any Derivative Works 115 | that You distribute, all copyright, patent, trademark, and 116 | attribution notices from the Source form of the Work, 117 | excluding those notices that do not pertain to any part of 118 | the Derivative Works; and 119 | 120 | (d) If the Work includes a "NOTICE" text file as part of its 121 | distribution, then any Derivative Works that You distribute must 122 | include a readable copy of the attribution notices contained 123 | within such NOTICE file, excluding those notices that do not 124 | pertain to any part of the Derivative Works, in at least one 125 | of the following places: within a NOTICE text file distributed 126 | as part of the Derivative Works; within the Source form or 127 | documentation, if provided along with the Derivative Works; or, 128 | within a display generated by the Derivative Works, if and 129 | wherever such third-party notices normally appear. The contents 130 | of the NOTICE file are for informational purposes only and 131 | do not modify the License. You may add Your own attribution 132 | notices within Derivative Works that You distribute, alongside 133 | or as an addendum to the NOTICE text from the Work, provided 134 | that such additional attribution notices cannot be construed 135 | as modifying the License. 136 | 137 | You may add Your own copyright statement to Your modifications and 138 | may provide additional or different license terms and conditions 139 | for use, reproduction, or distribution of Your modifications, or 140 | for any such Derivative Works as a whole, provided Your use, 141 | reproduction, and distribution of the Work otherwise complies with 142 | the conditions stated in this License. 143 | 144 | 5. Submission of Contributions. Unless You explicitly state otherwise, 145 | any Contribution intentionally submitted for inclusion in the Work 146 | by You to the Licensor shall be under the terms and conditions of 147 | this License, without any additional terms or conditions. 148 | Notwithstanding the above, nothing herein shall supersede or modify 149 | the terms of any separate license agreement you may have executed 150 | with Licensor regarding such Contributions. 151 | 152 | 6. Trademarks. This License does not grant permission to use the trade 153 | names, trademarks, service marks, or product names of the Licensor, 154 | except as required for reasonable and customary use in describing the 155 | origin of the Work and reproducing the content of the NOTICE file. 156 | 157 | 7. Disclaimer of Warranty. Unless required by applicable law or 158 | agreed to in writing, Licensor provides the Work (and each 159 | Contributor provides its Contributions) on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 161 | implied, including, without limitation, any warranties or conditions 162 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 163 | PARTICULAR PURPOSE. You are solely responsible for determining the 164 | appropriateness of using or redistributing the Work and assume any 165 | risks associated with Your exercise of permissions under this License. 166 | 167 | 8. Limitation of Liability. In no event and under no legal theory, 168 | whether in tort (including negligence), contract, or otherwise, 169 | unless required by applicable law (such as deliberate and grossly 170 | negligent acts) or agreed to in writing, shall any Contributor be 171 | liable to You for damages, including any direct, indirect, special, 172 | incidental, or consequential damages of any character arising as a 173 | result of this License or out of the use or inability to use the 174 | Work (including but not limited to damages for loss of goodwill, 175 | work stoppage, computer failure or malfunction, or any and all 176 | other commercial damages or losses), even if such Contributor 177 | has been advised of the possibility of such damages. 178 | 179 | 9. Accepting Warranty or Additional Liability. While redistributing 180 | the Work or Derivative Works thereof, You may choose to offer, 181 | and charge a fee for, acceptance of support, warranty, indemnity, 182 | or other liability obligations and/or rights consistent with this 183 | License. However, in accepting such obligations, You may act only 184 | on Your own behalf and on Your sole responsibility, not on behalf 185 | of any other Contributor, and only if You agree to indemnify, 186 | defend, and hold each Contributor harmless for any liability 187 | incurred by, or claims asserted against, such Contributor by reason 188 | of your accepting any such warranty or additional liability. 189 | 190 | END OF TERMS AND CONDITIONS 191 | 192 | APPENDIX: How to apply the Apache License to your work. 193 | 194 | To apply the Apache License to your work, attach the following 195 | boilerplate notice, with the fields enclosed by brackets "[]" 196 | replaced with your own identifying information. (Don't include 197 | the brackets!) The text should be enclosed in the appropriate 198 | comment syntax for the file format. We also recommend that a 199 | file or class name and description of purpose be included on the 200 | same "printed page" as the copyright notice for easier 201 | identification within third-party archives. 202 | 203 | Copyright [yyyy] [name of copyright owner] 204 | 205 | Licensed under the Apache License, Version 2.0 (the "License"); 206 | you may not use this file except in compliance with the License. 207 | You may obtain a copy of the License at 208 | 209 | http://www.apache.org/licenses/LICENSE-2.0 210 | 211 | Unless required by applicable law or agreed to in writing, software 212 | distributed under the License is distributed on an "AS IS" BASIS, 213 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 214 | See the License for the specific language governing permissions and 215 | limitations under the License. 216 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Gaelen Hadlett 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python StatsD Client 2 | 3 | [StatsD](https://github.com/etsy/statsd) is a stats server that plays with [Graphite](http://graphite.wikidot.com/). Together, 4 | they collect, aggregate, and show stats. If you don't know what either of those are, well, why are 5 | you still reading this? If you write software or know someone that does, I bet collecting stats will 6 | make your software better, or at the very least give you something to look at and think about. 7 | StatsD makes it really easy to send stats within your code. This client maks it even easier to get 8 | stats out of your python code. 9 | 10 | ## Install 11 | Annoyed with managing external packages? There are plenty of statsd clients that come up 12 | under 'pip search statsd'. Who has time to keep track of tiny dependencies for small projects? Just 13 | copy statsd.py into your project if you're into that kind of thing. No need to depend on some 14 | multi-file package for what should be a simple client. Grab and Go! If you're a stickler for 15 | dependencies, you probably don't need to know how to install this, but here you go any how: 16 | 17 | Clone and install: 18 | 19 | git clone git@github.com:gaelenh/python-statsd-client.git 20 | cd python-statsd-client 21 | python setup.py install 22 | 23 | Install with pip: 24 | 25 | pip install statsd-client 26 | 27 | Or like I said above, just copy statsd.py into your code base. 28 | 29 | ## Usage 30 | 31 | ### Basic 32 | Setup is easy. By default, the client will connect to localhost on the default statsd port (8125). 33 | 34 | import statsd 35 | statsd.incr('processed') # Increment processed bucket by 1 36 | statsd.incr('processed', 5) # This time by 5 37 | statsd.incr('processed', sample_rate=0.9) # Increment with a sample rate of .9 38 | statsd.timing('pipeline', 2468.34) # Pipeline took 2468.34 ms to execute 39 | 40 | Want to connect to a non-local statsd? Use statsd.init_statsd(settings). Settings is a dict with 41 | any of these keys: 42 | 43 | STATSD_HOST (Default 'localhost'): String host name. 44 | STATSD_PORT (Default 8125): Integer port number. 45 | STATSD_SAMPLE_RATE (Default None (same as 1.0)): Integer/Float between 0 and 1. 46 | STATSD_BUCKET_PREFIX (Default None): String prefix added to all buckets. The code will handle dotting them together. 47 | 48 | If you do not want to use init_statsd, you can always pass in your settings when you create the 49 | clients, timers or counters: 50 | 51 | from statsd import StatsdClient 52 | client = StatsdClient(host='127.0.0.1', port=9999, prefix='app', sample_rate=0.9) 53 | 54 | ### Counters 55 | Want to count things? Use StatsdCounter: 56 | 57 | import statsd 58 | statsd.init_statsd({'STATSD_BUCKET_PREFIX': 'photos'}) 59 | counter = statsd.StatsdCounter('processed') 60 | # calls on counter will send updates to bucket named 'photos.processed' 61 | counter += 1 # equivalent to counter.incr() or counter.incr(1) 62 | counter += 5 # equivalent to counter.incr(5) 63 | counter -= 10 # equivalent to counter.decr(10) 64 | 65 | ### Gauge 66 | Want to gauge something? Use statsd.gauge: 67 | 68 | import statsd 69 | statsd.init_statsd({'STATSD_BUCKET_PREFIX': 'photos'}) 70 | statsd.gauge('filesize', 100) # sends out gauge value 100 for bucket 'photos.filesize' 71 | 72 | ### Timing 73 | Interested in timing? Check out all the ways you can time things: 74 | 75 | import statsd 76 | statsd.init_statsd({'STATSD_BUCKET_PREFIX': 'photos'}) 77 | timer = statsd.StatsdTimer('pipeline') 78 | timer.start() 79 | # Do stuff 80 | timer.split('stage1') # Sends timing data for bucket 'photos.pipeline.stage1' 81 | # Do more stuff 82 | timer.split('stage2') # Sends timing data for bucket 'photos.pipeline.stage2' 83 | # Do even more stuff 84 | timer.stop() # Sends timing data for bucket 'photos.pipeline.total' 85 | 86 | Timers can be used as decorators too: 87 | 88 | from statsd import StatsdTimer 89 | @StatsdTimer.wrap('pipeline') 90 | def process(): 91 | pass 92 | process() # Sends timing data for bucket 'pipeline.total' 93 | 94 | Fancy with statement usage! 95 | 96 | from statsd import StatsdTimer 97 | with StatsdTimer('photos'): 98 | pass # Do stuff 99 | 100 | Even fancier: 101 | 102 | from statsd import StatsdTimer 103 | with StatsdTimer('photos') as t: 104 | # Do stuff 105 | t.split('stage1') 106 | # Do more stuff 107 | t.split('stage2') 108 | # Finish up 109 | 110 | Using timers with decorators or the with statement will still sends stats if an exception is raised 111 | in the code block: 112 | 113 | from statsd import StatsdTimer 114 | class Foo(object): 115 | @StatsdTimer('photos') 116 | def proc(self): 117 | # Do stuff 118 | raise ValueError('Whoops') 119 | f = Foo() 120 | f.proc() # Raises exception, but sends timing data for bucket 'photos.total-except' 121 | 122 | ## Misc 123 | 124 | The client integrates great with [Flask](http://flask.pocoo.org/). Just call statsd.init_statsd 125 | when you're initializing all your other framework components. Once that's done, you can use the 126 | timers and counters anywhere in your code. 127 | 128 | ## Contributing 129 | 130 | If you find a bug and want to fix it, fork, branch, and submit a pull request. The master branch 131 | will always have the latest working code. 132 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Python StatsD Client 2 | ==================== 3 | 4 | `StatsD `_ is a stats server that plays 5 | with `Graphite `_. Together, they collect, 6 | aggregate, and show stats. If you don't know what either of those are, 7 | well, why are you still reading this? If you write software or know 8 | someone that does, I bet collecting stats will make your software 9 | better, or at the very least give you something to look at and think 10 | about. StatsD makes it really easy to send stats within your code. This 11 | client maks it even easier to get stats out of your python code. 12 | 13 | Install 14 | ------- 15 | 16 | Annoyed with managing external packages? There are plenty of statsd 17 | clients that come up under 'pip search statsd'. Who has time to keep 18 | track of tiny dependencies for small projects? Just copy statsd.py into 19 | your project if you're into that kind of thing. No need to depend on 20 | some multi-file package for what should be a simple client. Grab and Go! 21 | If you're a stickler for dependencies, you probably don't need to know 22 | how to install this, but here you go any how: 23 | 24 | Clone and install: 25 | 26 | :: 27 | 28 | git clone git@github.com:gaelenh/python-statsd-client.git 29 | cd python-statsd-client 30 | python setup.py install 31 | 32 | Install with pip: 33 | 34 | :: 35 | 36 | pip install statsd-client 37 | 38 | Or like I said above, just copy statsd.py into your code base. 39 | 40 | Usage 41 | ----- 42 | 43 | Basic 44 | ~~~~~ 45 | 46 | Setup is easy. By default, the client will connect to localhost on the 47 | default statsd port (8125). 48 | 49 | :: 50 | 51 | import statsd 52 | statsd.incr('processed') # Increment processed bucket by 1 53 | statsd.incr('processed', 5) # This time by 5 54 | statsd.incr('processed', sample_rate=0.9) # Increment with a sample rate of .9 55 | statsd.timing('pipeline', 2468.34) # Pipeline took 2468.34 ms to execute 56 | 57 | Want to connect to a non-local statsd? Use 58 | statsd.init\_statsd(settings). Settings is a dict with any of these 59 | keys: 60 | 61 | :: 62 | 63 | STATSD_HOST (Default 'localhost'): String host name. 64 | STATSD_PORT (Default 8125): Integer port number. 65 | STATSD_SAMPLE_RATE (Default None (same as 1.0)): Integer/Float between 0 and 1. 66 | STATSD_BUCKET_PREFIX (Default None): String prefix added to all buckets. The code will handle dotting them together. 67 | 68 | If you do not want to use init\_statsd, you can always pass in your 69 | settings when you create the clients, timers or counters: 70 | 71 | :: 72 | 73 | from statsd import StatsdClient 74 | client = StatsdClient(host='127.0.0.1', port=9999, prefix='app', sample_rate=0.9) 75 | 76 | Counters 77 | ~~~~~~~~ 78 | 79 | Want to count things? Use StatsdCounter: 80 | 81 | :: 82 | 83 | import statsd 84 | statsd.init_statsd({'STATSD_BUCKET_PREFIX': 'photos'}) 85 | counter = statsd.StatsdCounter('processed') 86 | # calls on counter will send updates to bucket named 'photos.processed' 87 | counter += 1 # equivalent to counter.incr() or counter.incr(1) 88 | counter += 5 # equivalent to counter.incr(5) 89 | counter -= 10 # equivalent to counter.decr(10) 90 | 91 | Timing 92 | ~~~~~~ 93 | 94 | Interested in timing? Check out all the ways you can time things: 95 | 96 | :: 97 | 98 | import statsd 99 | statsd.init_statsd({'STATSD_BUCKET_PREFIX': 'photos'}) 100 | timer = statsd.StatsdTimer('pipeline') 101 | timer.start() 102 | # Do stuff 103 | timer.split('stage1') # Sends timing data for bucket 'photos.pipeline.stage1' 104 | # Do more stuff 105 | timer.split('stage2') # Sends timing data for bucket 'photos.pipeline.stage2' 106 | # Do even more stuff 107 | timer.stop() # Sends timing data for bucket 'photos.pipeline.total' 108 | 109 | Timers can be used as decorators too: 110 | 111 | :: 112 | 113 | from statsd import StatsdTimer 114 | @StatsdTimer.wrap('pipeline') 115 | def process(): 116 | pass 117 | process() # Sends timing data for bucket 'pipeline.total' 118 | 119 | Fancy with statement usage! 120 | 121 | :: 122 | 123 | from statsd import StatsdTimer 124 | with StatsdTimer('photos'): 125 | pass # Do stuff 126 | 127 | Even fancier: 128 | 129 | :: 130 | 131 | from statsd import StatsdTimer 132 | with StatsdTimer('photos') as t: 133 | # Do stuff 134 | t.split('stage1') 135 | # Do more stuff 136 | t.split('stage2') 137 | # Finish up 138 | 139 | Using timers with decorators or the with statement will still sends 140 | stats if an exception is raised in the code block: 141 | 142 | :: 143 | 144 | from statsd import StatsdTimer 145 | class Foo(object): 146 | @StatsdTimer('photos') 147 | def proc(self): 148 | # Do stuff 149 | raise ValueError('Whoops') 150 | f = Foo() 151 | f.proc() # Raises exception, but sends timing data for bucket 'photos.total-except' 152 | 153 | Misc 154 | ---- 155 | 156 | The client integrates great with `Flask `_. 157 | Just call statsd.init\_statsd when you're initializing all your other 158 | framework components. Once that's done, you can use the timers and 159 | counters anywhere in your code. 160 | 161 | Contributing 162 | ------------ 163 | 164 | If you find a bug and want to fix it, fork, branch, and submit a pull 165 | request. The master branch will always have the latest working code. 166 | -------------------------------------------------------------------------------- /makedoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pandoc -f markdown -t rst -o README.txt README.md 4 | 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This file is part of python-statsd-client released under the Apache 4 | # License, Version 2.0. See the NOTICE for more information. 5 | 6 | 7 | from distutils.core import setup 8 | 9 | from statsd import __version__ 10 | 11 | 12 | def fread(filename): 13 | with open(filename) as f: 14 | return f.read() 15 | 16 | 17 | def main(): 18 | setup(name='statsd-client', 19 | description='StatsD client for Python', 20 | long_description=fread('README.txt'), 21 | version=__version__, 22 | license='Apache 2.0', 23 | author='Gaelen Hadlett', 24 | author_email='gaelenh@gmail.com', 25 | url='https://github.com/gaelenh/python-statsd-client', 26 | py_modules=['statsd'], 27 | keywords=['statsd', 'graphite', 'stats'], 28 | classifiers=['License :: OSI Approved :: Apache Software License', 29 | 'Programming Language :: Python :: 2.6', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Topic :: System :: Logging', 33 | 'Operating System :: MacOS :: MacOS X', 34 | 'Operating System :: POSIX :: Linux', 35 | 'Operating System :: Unix', 36 | 'Intended Audience :: Developers'], 37 | ) 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /statsd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This file is part of python-statsd-client released under the Apache 4 | # License, Version 2.0. See the NOTICE for more information. 5 | 6 | 7 | import random 8 | from functools import wraps 9 | from socket import socket, AF_INET, SOCK_DGRAM 10 | import time 11 | import logging 12 | 13 | __version__ = '1.0.7' 14 | 15 | STATSD_HOST = 'localhost' 16 | STATSD_PORT = 8125 17 | STATSD_SAMPLE_RATE = None 18 | STATSD_BUCKET_PREFIX = None 19 | 20 | 21 | def decrement(bucket, delta=1, sample_rate=None): 22 | _statsd.decr(bucket, delta, sample_rate) 23 | 24 | 25 | def increment(bucket, delta=1, sample_rate=None): 26 | _statsd.incr(bucket, delta, sample_rate) 27 | 28 | 29 | def gauge(bucket, value, sample_rate=None): 30 | _statsd.gauge(bucket, value, sample_rate) 31 | 32 | 33 | def timing(bucket, ms, sample_rate=None): 34 | _statsd.timing(bucket, ms, sample_rate) 35 | 36 | 37 | class StatsdClient(object): 38 | 39 | def __init__(self, host=None, port=None, prefix=None, sample_rate=None): 40 | self._host = host or STATSD_HOST 41 | self._port = port or STATSD_PORT 42 | self._sample_rate = sample_rate or STATSD_SAMPLE_RATE 43 | self._socket = socket(AF_INET, SOCK_DGRAM) 44 | self._prefix = prefix or STATSD_BUCKET_PREFIX 45 | if self._prefix and not isinstance(self._prefix, bytes): 46 | self._prefix = self._prefix.encode('utf8') 47 | 48 | def __del__(self): 49 | self._socket.close() 50 | 51 | def decr(self, bucket, delta=1, sample_rate=None): 52 | """Decrements a counter by delta. 53 | """ 54 | value = str(-1 * delta).encode('utf8') + b'|c' 55 | self._send(bucket, value, sample_rate) 56 | 57 | def incr(self, bucket, delta=1, sample_rate=None): 58 | """Increment a counter by delta. 59 | """ 60 | value = str(delta).encode('utf8') + b'|c' 61 | self._send(bucket, value, sample_rate) 62 | 63 | def gauge(self, bucket, value, sample_rate=None): 64 | """Send a gauge value. 65 | """ 66 | str_value = str(value).encode('utf8') + b'|g' 67 | self._send(bucket, str_value, sample_rate) 68 | 69 | def _send(self, bucket, value, sample_rate=None): 70 | """Format and send data to statsd. 71 | """ 72 | try: 73 | bucket = bucket if isinstance(bucket, bytes) else bucket.encode('utf8') 74 | 75 | sample_rate = sample_rate or self._sample_rate 76 | if sample_rate and sample_rate < 1.0 and sample_rate > 0: 77 | if random.random() <= sample_rate: 78 | value = value + b'|@' + str(sample_rate).encode('utf8') 79 | else: 80 | return 81 | 82 | stat = bucket + b':' + value 83 | if self._prefix: 84 | stat = self._prefix + b'.' + stat 85 | 86 | self._socket.sendto(stat, (self._host, self._port)) 87 | except Exception as e: 88 | _logger.error("Failed to send statsd packet.", exc_info=True) 89 | 90 | def timing(self, bucket, ms, sample_rate=None): 91 | """Creates a timing sample. 92 | """ 93 | value = str(ms).encode('utf8') + b'|ms' 94 | self._send(bucket, value, sample_rate) 95 | 96 | 97 | class StatsdCounter(object): 98 | """Counter for StatsD. 99 | """ 100 | def __init__(self, bucket, host=None, port=None, prefix=None, 101 | sample_rate=None): 102 | self._client = StatsdClient(host=host, port=port, prefix=prefix, 103 | sample_rate=sample_rate) 104 | self._bucket = bucket if isinstance(bucket, bytes) else bucket.encode('utf8') 105 | 106 | def __add__(self, num): 107 | self._client.incr(self._bucket, delta=num) 108 | return self 109 | 110 | def __sub__(self, num): 111 | self._client.decr(self._bucket, delta=num) 112 | return self 113 | 114 | 115 | class StatsdTimer(object): 116 | """Timer for StatsD. 117 | """ 118 | def __init__(self, bucket, host=None, port=None, prefix=None, 119 | sample_rate=None): 120 | self._client = StatsdClient(host=host, port=port, prefix=prefix, 121 | sample_rate=sample_rate) 122 | self._bucket = bucket if isinstance(bucket, bytes) else bucket.encode('utf8') 123 | 124 | def __enter__(self): 125 | self.start() 126 | return self 127 | 128 | def __exit__(self, type, value, traceback): 129 | if type is not None: 130 | self.stop(b'total-except') 131 | else: 132 | self.stop() 133 | 134 | def start(self, bucket_key=b'start'): 135 | """Start the timer. 136 | """ 137 | bucket_key = bucket_key if isinstance(bucket_key, bytes) else bucket_key.encode('utf8') 138 | self._start = time.time() * 1000 139 | self._splits = [(bucket_key, self._start), ] 140 | 141 | def split(self, bucket_key): 142 | """Records time since start() or last call to split() and sends 143 | result to statsd. 144 | """ 145 | bucket_key = bucket_key if isinstance(bucket_key, bytes) else bucket_key.encode('utf8') 146 | self._splits.append((bucket_key, time.time() * 1000)) 147 | self._client.timing(self._bucket + b'.' + bucket_key, 148 | self._splits[-1][1] - self._splits[-2][1]) 149 | 150 | def stop(self, bucket_key=b'total'): 151 | """Stops the timer and sends total time to statsd. 152 | """ 153 | bucket_key = bucket_key if isinstance(bucket_key, bytes) else bucket_key.encode('utf8') 154 | self._stop = time.time() * 1000 155 | self._client.timing(self._bucket + b'.' + bucket_key, 156 | self._stop - self._start) 157 | 158 | @staticmethod 159 | def wrap(bucket): 160 | def wrapper(func): 161 | @wraps(func) 162 | def f(*args, **kw): 163 | with StatsdTimer(bucket): 164 | return func(*args, **kw) 165 | return f 166 | return wrapper 167 | 168 | 169 | def init_statsd(settings=None): 170 | """Initialize global statsd client. 171 | """ 172 | global _statsd 173 | global STATSD_HOST 174 | global STATSD_PORT 175 | global STATSD_SAMPLE_RATE 176 | global STATSD_BUCKET_PREFIX 177 | 178 | if settings: 179 | STATSD_HOST = settings.get('STATSD_HOST', STATSD_HOST) 180 | STATSD_PORT = settings.get('STATSD_PORT', STATSD_PORT) 181 | STATSD_SAMPLE_RATE = settings.get('STATSD_SAMPLE_RATE', 182 | STATSD_SAMPLE_RATE) 183 | STATSD_BUCKET_PREFIX = settings.get('STATSD_BUCKET_PREFIX', 184 | STATSD_BUCKET_PREFIX) 185 | _statsd = StatsdClient(host=STATSD_HOST, port=STATSD_PORT, 186 | sample_rate=STATSD_SAMPLE_RATE, prefix=STATSD_BUCKET_PREFIX) 187 | return _statsd 188 | 189 | _logger = logging.getLogger('statsd') 190 | _statsd = init_statsd() 191 | -------------------------------------------------------------------------------- /statsd_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This file is part of python-statsd-client released under the Apache 4 | # License, Version 2.0. See the NOTICE for more information. 5 | 6 | import unittest 7 | import socket 8 | import time 9 | import statsd 10 | 11 | 12 | class mock_udp_socket(object): 13 | def __init__(self, family, socktype): 14 | assert family == socket.AF_INET 15 | assert socktype == socket.SOCK_DGRAM 16 | 17 | def sendto(self, data, addr): 18 | self.data = data 19 | 20 | 21 | class TestStatsd(unittest.TestCase): 22 | 23 | def setUp(self): 24 | # Moneky patch statsd socket for testing 25 | statsd.socket = mock_udp_socket 26 | statsd.init_statsd() 27 | 28 | def tearDown(self): 29 | statsd.STATSD_HOST = 'localhost' 30 | statsd.STATSD_PORT = 8125 31 | statsd.STATSD_SAMPLE_RATE = None 32 | statsd.STATSD_BUCKET_PREFIX = None 33 | 34 | def test_init_statsd(self): 35 | settings = {'STATSD_HOST': '127.0.0.1', 36 | 'STATSD_PORT': 9999, 37 | 'STATSD_SAMPLE_RATE': 0.99, 38 | 'STATSD_BUCKET_PREFIX': 'testing'} 39 | statsd.init_statsd(settings) 40 | self.assertEqual(statsd.STATSD_HOST, '127.0.0.1') 41 | self.assertEqual(statsd.STATSD_PORT, 9999) 42 | self.assertEqual(statsd.STATSD_SAMPLE_RATE, 0.99) 43 | self.assertEqual(statsd.STATSD_BUCKET_PREFIX, 'testing') 44 | 45 | def test_exception_in_send(self): 46 | def mock_sendto_raise_error(data, addr): 47 | mock_sendto_raise_error.exception_raised = True 48 | raise socket.gaierror 49 | statsd._statsd._socket.sendto = mock_sendto_raise_error 50 | statsd.decrement('counted') 51 | self.assertTrue(mock_sendto_raise_error.exception_raised) 52 | 53 | def test_decrement(self): 54 | statsd.decrement('counted') 55 | self.assertEqual(statsd._statsd._socket.data, b'counted:-1|c') 56 | statsd.decrement('counted', 5) 57 | self.assertEqual(statsd._statsd._socket.data, b'counted:-5|c') 58 | statsd.decrement('counted', 5, 0.99) 59 | self.assertTrue(statsd._statsd._socket.data.startswith(b'counted:-5|c')) 60 | if statsd._statsd._socket.data != b'counted:-5|c': 61 | self.assertTrue(statsd._statsd._socket.data.endswith(b'|@0.99')) 62 | 63 | def test_increment(self): 64 | statsd.increment('counted') 65 | self.assertEqual(statsd._statsd._socket.data, b'counted:1|c') 66 | statsd.increment('counted', 5) 67 | self.assertEqual(statsd._statsd._socket.data, b'counted:5|c') 68 | statsd.increment('counted', 5, 0.99) 69 | self.assertTrue(statsd._statsd._socket.data.startswith(b'counted:5|c')) 70 | if statsd._statsd._socket.data != b'counted:5|c': 71 | self.assertTrue(statsd._statsd._socket.data.endswith(b'|@0.99')) 72 | 73 | def test_gauge(self): 74 | statsd.gauge('gauged', 1) 75 | self.assertEqual(statsd._statsd._socket.data, b'gauged:1|g') 76 | statsd.gauge('gauged', 5) 77 | self.assertEqual(statsd._statsd._socket.data, b'gauged:5|g') 78 | statsd.gauge('gauged', -5, 0.99) 79 | self.assertTrue(statsd._statsd._socket.data.startswith(b'gauged:-5|g')) 80 | if statsd._statsd._socket.data != b'gauged:-5|g': 81 | self.assertTrue(statsd._statsd._socket.data.endswith(b'|@0.99')) 82 | 83 | def test_timing(self): 84 | statsd.timing('timed', 250) 85 | self.assertEqual(statsd._statsd._socket.data, b'timed:250|ms') 86 | statsd.timing('timed', 250, 0.99) 87 | self.assertTrue(statsd._statsd._socket.data.startswith(b'timed:250|ms')) 88 | if statsd._statsd._socket.data != b'timed:250|ms': 89 | self.assertTrue(statsd._statsd._socket.data.endswith(b'|@0.99')) 90 | 91 | 92 | class TestStatsdClient(unittest.TestCase): 93 | 94 | def setUp(self): 95 | # Moneky patch statsd socket for testing 96 | statsd.socket = mock_udp_socket 97 | 98 | def test_prefix(self): 99 | client = statsd.StatsdClient('localhost', 8125, prefix='main.bucket', sample_rate=None) 100 | client._send(b'subname', b'100|c') 101 | self.assertEqual(client._socket.data, b'main.bucket.subname:100|c') 102 | 103 | client = statsd.StatsdClient('localhost', 8125, prefix='main', sample_rate=None) 104 | client._send(b'subname', b'100|c') 105 | self.assertEqual(client._socket.data, b'main.subname:100|c') 106 | client._send(b'subname.subsubname', b'100|c') 107 | self.assertEqual(client._socket.data, b'main.subname.subsubname:100|c') 108 | 109 | def test_decr(self): 110 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=None) 111 | client.decr('buck.counter', 5) 112 | self.assertEqual(client._socket.data, b'buck.counter:-5|c') 113 | 114 | def test_decr_sample_rate(self): 115 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=0.999) 116 | client.decr('buck.counter', 5) 117 | self.assertEqual(client._socket.data, b'buck.counter:-5|c|@0.999') 118 | if client._socket.data != 'buck.counter:-5|c': 119 | self.assertTrue(client._socket.data.endswith(b'|@0.999')) 120 | 121 | def test_incr(self): 122 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=None) 123 | client.incr('buck.counter', 5) 124 | self.assertEqual(client._socket.data, b'buck.counter:5|c') 125 | 126 | def test_incr_sample_rate(self): 127 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=0.999) 128 | client.incr('buck.counter', 5) 129 | self.assertEqual(client._socket.data, b'buck.counter:5|c|@0.999') 130 | if client._socket.data != 'buck.counter:5|c': 131 | self.assertTrue(client._socket.data.endswith(b'|@0.999')) 132 | 133 | def test_send(self): 134 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=None) 135 | client._send(b'buck', b'50|c') 136 | self.assertEqual(client._socket.data, b'buck:50|c') 137 | 138 | def test_send_sample_rate(self): 139 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=0.999) 140 | client._send(b'buck', b'50|c') 141 | self.assertEqual(client._socket.data, b'buck:50|c|@0.999') 142 | if client._socket.data != 'buck:50|c': 143 | self.assertTrue(client._socket.data.endswith(b'|@0.999')) 144 | 145 | def test_timing(self): 146 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=None) 147 | client.timing('buck.timing', 100) 148 | self.assertEqual(client._socket.data, b'buck.timing:100|ms') 149 | 150 | def test_timing_sample_rate(self): 151 | client = statsd.StatsdClient('localhost', 8125, prefix='', sample_rate=0.999) 152 | client.timing('buck.timing', 100) 153 | self.assertEqual(client._socket.data, b'buck.timing:100|ms|@0.999') 154 | if client._socket.data != '': 155 | self.assertTrue(client._socket.data.endswith(b'|@0.999')) 156 | 157 | 158 | class TestStatsdCounter(unittest.TestCase): 159 | 160 | def setUp(self): 161 | # Moneky patch statsd socket for testing 162 | statsd.socket = mock_udp_socket 163 | 164 | def test_add(self): 165 | counter = statsd.StatsdCounter('counted', 'localhost', 8125, prefix='', sample_rate=None) 166 | counter += 1 167 | self.assertEqual(counter._client._socket.data, b'counted:1|c') 168 | counter += 5 169 | self.assertEqual(counter._client._socket.data, b'counted:5|c') 170 | 171 | def test_sub(self): 172 | counter = statsd.StatsdCounter('counted', 'localhost', 8125, prefix='', sample_rate=None) 173 | counter -= 1 174 | self.assertEqual(counter._client._socket.data, b'counted:-1|c') 175 | counter -= 5 176 | self.assertEqual(counter._client._socket.data, b'counted:-5|c') 177 | 178 | 179 | class TestStatsdTimer(unittest.TestCase): 180 | 181 | def setUp(self): 182 | # Moneky patch statsd socket for testing 183 | statsd.socket = mock_udp_socket 184 | 185 | def test_startstop(self): 186 | timer = statsd.StatsdTimer('timeit', 'localhost', 8125, prefix='', sample_rate=None) 187 | timer.start() 188 | time.sleep(0.25) 189 | timer.stop() 190 | self.assertTrue(timer._client._socket.data.startswith(b'timeit.total:2')) 191 | self.assertTrue(timer._client._socket.data.endswith(b'|ms')) 192 | 193 | def test_split(self): 194 | timer = statsd.StatsdTimer('timeit', 'localhost', 8125, prefix='', sample_rate=None) 195 | timer.start() 196 | time.sleep(0.25) 197 | timer.split('lap') 198 | self.assertTrue(timer._client._socket.data.startswith(b'timeit.lap:2')) 199 | self.assertTrue(timer._client._socket.data.endswith(b'|ms')) 200 | time.sleep(0.26) 201 | timer.stop() 202 | self.assertTrue(timer._client._socket.data.startswith(b'timeit.total:5')) 203 | self.assertTrue(timer._client._socket.data.endswith(b'|ms')) 204 | 205 | def test_wrap(self): 206 | class TC(object): 207 | @statsd.StatsdTimer.wrap('timeit') 208 | def do(self): 209 | time.sleep(0.25) 210 | return 1 211 | tc = TC() 212 | result = tc.do() 213 | self.assertEqual(result, 1) 214 | 215 | def test_with(self): 216 | timer = statsd.StatsdTimer('timeit', 'localhost', 8125, prefix='', sample_rate=None) 217 | with timer: 218 | time.sleep(0.25) 219 | self.assertTrue(timer._client._socket.data.startswith(b'timeit.total:2')) 220 | self.assertTrue(timer._client._socket.data.endswith(b'|ms')) 221 | 222 | if __name__ == '__main__': 223 | unittest.main() 224 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py32,py33,py34,pypy 3 | 4 | [testenv] 5 | commands = python statsd_test.py 6 | 7 | [testenv:py27] 8 | basepython = python2.7 9 | 10 | [testenv:py32] 11 | basepython = python3.2 12 | 13 | [testenv:py33] 14 | basepython = python3.3 15 | 16 | [testenv:pypy] 17 | basepython = pypy 18 | --------------------------------------------------------------------------------