├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── Makefile ├── README.rst ├── docs ├── .gitignore ├── Makefile └── source │ ├── conf.py │ └── index.rst ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── counter_test.py ├── ewma_test.py ├── histogram_test.py ├── meter_test.py ├── metrics_test.py ├── reservoir_test.py ├── snapshot_test.py └── timer_test.py ├── tox.ini └── uwsgi_metrics ├── __about__.py ├── __init__.py ├── counter.py ├── ewma.py ├── histogram.py ├── meter.py ├── metrics.py ├── reservoir.py ├── snapshot.py └── timer.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .tox 3 | __pycache__ 4 | uwsgi_metrics.egg-info 5 | .coverage 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | before_install: 4 | - sudo apt-get install -qq cython 5 | 6 | install: 7 | - pip install tox 8 | 9 | script: 10 | - make test 11 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.2.0 (2015-11-24) 2 | * Fix exception handling in metrics.timing 3 | * Py 3 support 4 | 5 | 1.1.5 (2015-03-06) 6 | * Cleanups for open-sourcing 7 | 8 | 1.1.4 (2014-06-24) 9 | * Fix emit() bug 10 | 11 | 1.1.3 (2014-06-24) 12 | * Fix docs build 13 | 14 | 1.1.2 (2014-06-24) 15 | * Fix documentation and initial view() output 16 | 17 | 1.1.1 (2014-06-20) 18 | * Fix meter display bug 19 | 20 | 1.1.0 (2014-06-10) 21 | * Match output of Java Metrics 3.0.0 22 | 23 | 1.0.0 (2014-05-20) 24 | * Match output of Java Metrics 2.2.0 25 | * Add meter function 26 | * Switch over to pytest from testify 27 | 28 | 0.3.4 (2014-04-11) 29 | * Fix pyflakes error 30 | 31 | 0.3.3 (2014-03-24) 32 | * Addition of the counter function to the metrics.py module 33 | 34 | 0.3.2 (2013-12-05) 35 | * More documentation fixes 36 | 37 | 0.3.1 (2013-12-05) 38 | * Fix documentation link 39 | 40 | 0.3.0 (2013-12-04) 41 | * Add 'initialize' function 42 | 43 | 0.2.0 (2013-11-13) 44 | * Rename 'timer' context manager to 'timing' 45 | * Add 'timer' function which takes a timing delta 46 | 47 | 0.1.0 (2013-11-12) 48 | * Initial release 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2010-2015 Coda Hale and Yammer, Inc. 191 | 192 | Copyright 2015 Yelp, Inc. 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs test clean 2 | 3 | test: 4 | tox 5 | 6 | docs: 7 | tox -e docs 8 | 9 | clean: 10 | git clean -Xfd 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/Yelp/uwsgi_metrics.png?branch=master 2 | :target: https://travis-ci.org/Yelp/uwsgi_metrics?branch=master 3 | 4 | .. image:: https://readthedocs.org/projects/uwsgi-metrics/badge/?version=latest 5 | :target: https://readthedocs.org/projects/uwsgi-metrics/?badge=latest 6 | 7 | uwsgi_metrics 8 | ============= 9 | 10 | This project is a port of the Dropwizard Metrics_ package to the uWSGI_ stack. 11 | 12 | Documentation is available at http://uwsgi-metrics.readthedocs.org. 13 | 14 | License 15 | ------- 16 | 17 | Copyright (c) 2010-2015 Coda Hale, Yammer.com 18 | 19 | Copyright (c) 2015 Yelp, Inc. 20 | 21 | Published under Apache Software License 2.0, see LICENSE_. 22 | 23 | .. _Metrics: http://dropwizard.github.io/metrics/ 24 | .. _uWSGI: http://uwsgi-docs.readthedocs.org 25 | .. _LICENSE: https://github.com/Yelp/uwsgi_metrics/blob/master/LICENSE 26 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | EMPTY_DIRS = build source/_templates source/_static 37 | $(EMPTY_DIRS): 38 | mkdir -p $(EMPTY_DIRS) 39 | 40 | clean: $(BUILDDIR) 41 | -rm -rf $(BUILDDIR)/* 42 | 43 | html: $(EMPTY_DIRS) 44 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 45 | @echo 46 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 47 | 48 | dirhtml: $(EMPTY_DIRS) 49 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 50 | @echo 51 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 52 | 53 | singlehtml: $(EMPTY_DIRS) 54 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 55 | @echo 56 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 57 | 58 | pickle: $(EMPTY_DIRS) 59 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 60 | @echo 61 | @echo "Build finished; now you can process the pickle files." 62 | 63 | json: $(EMPTY_DIRS) 64 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 65 | @echo 66 | @echo "Build finished; now you can process the JSON files." 67 | 68 | htmlhelp: $(EMPTY_DIRS) 69 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 70 | @echo 71 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 72 | ".hhp project file in $(BUILDDIR)/htmlhelp." 73 | 74 | qthelp: $(EMPTY_DIRS) 75 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 76 | @echo 77 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 78 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 79 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/uwsgi_metrics.qhcp" 80 | @echo "To view the help file:" 81 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/uwsgi_metrics.qhc" 82 | 83 | devhelp: $(EMPTY_DIRS) 84 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 85 | @echo 86 | @echo "Build finished." 87 | @echo "To view the help file:" 88 | @echo "# mkdir -p $$HOME/.local/share/devhelp/uwsgi_metrics" 89 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/uwsgi_metrics" 90 | @echo "# devhelp" 91 | 92 | epub: $(EMPTY_DIRS) 93 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 94 | @echo 95 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 96 | 97 | latex: $(EMPTY_DIRS) 98 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 99 | @echo 100 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 101 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 102 | "(use \`make latexpdf' here to do that automatically)." 103 | 104 | latexpdf: $(EMPTY_DIRS) 105 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 106 | @echo "Running LaTeX files through pdflatex..." 107 | make -C $(BUILDDIR)/latex all-pdf 108 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 109 | 110 | text: $(EMPTY_DIRS) 111 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 112 | @echo 113 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 114 | 115 | man: $(EMPTY_DIRS) 116 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 117 | @echo 118 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 119 | 120 | changes: $(EMPTY_DIRS) 121 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 122 | @echo 123 | @echo "The overview file is in $(BUILDDIR)/changes." 124 | 125 | linkcheck: $(EMPTY_DIRS) 126 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 127 | @echo 128 | @echo "Link check complete; look for any errors in the above output " \ 129 | "or in $(BUILDDIR)/linkcheck/output.txt." 130 | 131 | doctest: $(EMPTY_DIRS) 132 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 133 | @echo "Testing of doctests in the sources finished, look at the " \ 134 | "results in $(BUILDDIR)/doctest/output.txt." 135 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # uwsgi_metrics documentation build configuration file, created by 4 | # sphinx-quickstart on Fri May 13 14:16:02 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | print os.path.abspath('.') 20 | sys.path.insert(0, os.path.abspath('../..')) 21 | sys.path.insert(0, os.path.abspath('../extensions')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc', 31 | 'sphinx.ext.doctest', 32 | 'sphinx.ext.intersphinx', 33 | 'sphinx.ext.todo', 34 | 'sphinx.ext.coverage', 35 | ] 36 | try: 37 | import sphinx.ext.viewcode 38 | extensions.append('sphinx.ext.viewcode') 39 | except ImportError: 40 | pass 41 | try: 42 | # if it's available, use sphinx_http_domain 43 | import sphinx_http_domain 44 | extensions.append('sphinx_http_domain') 45 | except ImportError: 46 | pass 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix of source filenames. 52 | source_suffix = '.rst' 53 | 54 | # The encoding of source files. 55 | #source_encoding = 'utf-8-sig' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # General information about the project. 61 | project = u'uwsgi_metrics' 62 | copyright = u'Yelp Inc' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | 69 | base_dir = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) 70 | about = {} 71 | with open(os.path.join(base_dir, "uwsgi_metrics", "__about__.py")) as f: 72 | exec(f.read(), about) 73 | 74 | version = release = about["__version__"] 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | #language = None 79 | 80 | # There are two options for replacing |today|: either, you set today to some 81 | # non-false value, then it is used: 82 | #today = '' 83 | # Else, today_fmt is used as the format for a strftime call. 84 | #today_fmt = '%B %d, %Y' 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | exclude_patterns = [] 89 | 90 | # The reST default role (used for this markup: `text`) to use for all documents. 91 | #default_role = None 92 | 93 | # If true, '()' will be appended to :func: etc. cross-reference text. 94 | #add_function_parentheses = True 95 | 96 | # If true, the current module name will be prepended to all description 97 | # unit titles (such as .. function::). 98 | #add_module_names = True 99 | 100 | # If true, sectionauthor and moduleauthor directives will be shown in the 101 | # output. They are ignored by default. 102 | #show_authors = False 103 | 104 | # The name of the Pygments (syntax highlighting) style to use. 105 | pygments_style = 'sphinx' 106 | 107 | # A list of ignored prefixes for module index sorting. 108 | #modindex_common_prefix = [] 109 | 110 | 111 | # -- Options for HTML output --------------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | html_theme = 'sphinxdoc' 116 | 117 | # Theme options are theme-specific and customize the look and feel of a theme 118 | # further. For a list of options available for each theme, see the 119 | # documentation. 120 | #html_theme_options = {} 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | #html_theme_path = [] 124 | 125 | # The name for this set of Sphinx documents. If None, it defaults to 126 | # " v documentation". 127 | #html_title = None 128 | 129 | # A shorter title for the navigation bar. Default is the same as html_title. 130 | #html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the top 133 | # of the sidebar. 134 | #html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon of the 137 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 138 | # pixels large. 139 | #html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) here, 142 | # relative to this directory. They are copied after the builtin static files, 143 | # so a file named "default.css" will overwrite the builtin "default.css". 144 | html_static_path = ['_static'] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = 'uwsgi_metricsdoc' 189 | 190 | 191 | # -- Options for LaTeX output -------------------------------------------------- 192 | 193 | # The paper size ('letter' or 'a4'). 194 | #latex_paper_size = 'letter' 195 | 196 | # The font size ('10pt', '11pt' or '12pt'). 197 | #latex_font_size = '10pt' 198 | 199 | # Grouping the document tree into LaTeX files. List of tuples 200 | # (source start file, target name, title, author, documentclass [howto/manual]). 201 | latex_documents = [ 202 | ('index', 'uwsgi_metrics.tex', u'uwsgi_metrics Documentation', 203 | u'Yelp Inc', 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #latex_preamble = '' 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output -------------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'uwsgi_metrics', u'uwsgi_metrics Documentation', 236 | [u'Yelp Inc'], 1) 237 | ] 238 | 239 | 240 | # Example configuration for intersphinx: refer to the Python standard library. 241 | intersphinx_mapping = {'http://docs.python.org/': None} 242 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. uwsgi_metrics documentation master file, created by 2 | sphinx-quickstart on Fri May 13 14:16:02 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | uwsgi_metrics 7 | ************* 8 | 9 | Overview 10 | ======== 11 | 12 | uwsgi_metrics is a port of the Dropwizard Metrics_ package to the uWSGI_ stack. 13 | It allows you to use functions such as :py:func:`uwsgi_metrics.timing` to 14 | gather application-level metrics:: 15 | 16 | with timing(__name__, 'my_timer'): 17 | do_some_operation() 18 | 19 | You can then invoke the :py:func:`uwsgi_metrics.view` function to get a 20 | dictionary of metrics information:: 21 | 22 | 23 | { 24 | "version": "1.1.1", 25 | "counters": {}, 26 | "gauges": {}, 27 | "histograms": {}, 28 | "meters": {}, 29 | "timers": { 30 | "my_module.my_timer": { 31 | "count": 22, 32 | "p98": 4.8198699951171875, 33 | "m15_rate": 1.0033118138834103, 34 | "p75": 1.9915103912353516, 35 | "p99": 4.8198699951171875, 36 | "min": 1.4159679412841797, 37 | "max": 4.8198699951171875, 38 | "m5_rate": 1.0098078505715211, 39 | "p95": 4.7961950302124023, 40 | "m1_rate": 1.0454161929696191, 41 | "duration_units": "milliseconds", 42 | "stddev": 0.92399302814991413, 43 | "mean_rate": 1.2074971885928811, 44 | "rate_units": "calls/second", 45 | "p999": 4.8198699951171875, 46 | "p50": 1.649022102355957, 47 | "mean": 1.9796761599454014 48 | } 49 | } 50 | } 51 | 52 | You can wire up :py:func:`uwsgi_metrics.view` to an HTTP endpoint so that you can 53 | interactively monitor the performance of your production code. 54 | 55 | Setup 56 | ===== 57 | 58 | There are a couple of steps required before you can use uwsgi_metrics: 59 | 60 | 1. uWSGI must be started with a mule process; this is done by passing the 61 | ``--mule`` option_ to the uWSGI executable. 62 | 2. The :py:func:`uwsgi_metrics.initialize` method must invoked in the master 63 | process prior to forking. 64 | 65 | Performance 66 | =========== 67 | 68 | It takes approximately 30us to log a metric on a 2.3GHz Xeon E5. The 69 | :py:func:`uwsgi_metrics.timing` context manager adds a further 20us, to give 70 | a total of approximately 50us. 71 | 72 | As a very rough guideline, you're probably not going to notice the overhead of 73 | logging 10 metrics (0.5ms) during a service call, but you will start to notice the 74 | overhead of logging 100 metrics (5ms). 75 | 76 | API 77 | === 78 | 79 | .. currentmodule:: uwsgi_metrics 80 | 81 | .. autofunction:: initialize 82 | 83 | .. autofunction:: view 84 | 85 | .. autofunction:: counter 86 | 87 | .. autofunction:: histogram 88 | 89 | .. autofunction:: meter 90 | 91 | .. autofunction:: timer 92 | 93 | .. autofunction:: timing(module, name) 94 | 95 | License 96 | ------- 97 | 98 | Copyright (c) 2010-2015 Coda Hale, Yammer.com 99 | 100 | Copyright (c) 2015, Yelp, Inc. 101 | 102 | Published under Apache Software License 2.0, see LICENSE_. 103 | 104 | .. _Metrics: http://dropwizard.github.io/metrics/ 105 | .. _uWSGI: http://uwsgi-docs.readthedocs.org/en/latest/ 106 | .. _option: http://uwsgi-docs.readthedocs.org/en/latest/Options.html#mule 107 | .. _LICENSE: https://github.com/Yelp/uwsgi_metrics/blob/master/LICENSE 108 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six==1.9.0 2 | treap==1.38 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import os 4 | 5 | from setuptools import find_packages, setup 6 | 7 | base_dir = os.path.dirname(__file__) 8 | 9 | about = {} 10 | with open(os.path.join(base_dir, "uwsgi_metrics", "__about__.py")) as f: 11 | exec(f.read(), about) 12 | 13 | setup( 14 | name=about['__title__'], 15 | version=about['__version__'], 16 | 17 | description=about['__summary__'], 18 | 19 | url=about['__uri__'], 20 | 21 | author=about['__author__'], 22 | author_email=about['__email__'], 23 | packages=find_packages(exclude=["tests", "tests.*"]), 24 | install_requires=[ 25 | 'six', 26 | 'treap', 27 | ], 28 | license=about['__license__'] 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/uwsgi_metrics/534966fd461ff711aecd1e3d4caaafdc23ac33f0/tests/__init__.py -------------------------------------------------------------------------------- /tests/counter_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Translated from CounterTest.java""" 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | import pytest 7 | 8 | from uwsgi_metrics.counter import Counter 9 | 10 | 11 | @pytest.fixture 12 | def counter(): 13 | return Counter() 14 | 15 | 16 | def test_starts_at_zero(counter): 17 | assert counter.get_count() == 0 18 | 19 | 20 | def test_increments_by_one(counter): 21 | counter.inc() 22 | assert counter.get_count() == 1 23 | 24 | 25 | def test_increments_by_an_arbitrary_delta(counter): 26 | counter.inc(12) 27 | assert counter.get_count() == 12 28 | 29 | 30 | def test_decrements_by_one(counter): 31 | counter.dec() 32 | assert counter.get_count() == -1 33 | 34 | 35 | def test_decrements_by_an_arbitrary_delta(counter): 36 | counter.dec(12) 37 | assert counter.get_count() == -12 38 | 39 | 40 | def test_view(counter): 41 | counter.inc(13) 42 | assert counter.view() == {'count': 13} 43 | -------------------------------------------------------------------------------- /tests/ewma_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Translated from EWMATest.java""" 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | from uwsgi_metrics.ewma import EWMA 7 | 8 | 9 | def assert_ewma(ewma, expected_rates): 10 | max_difference = 0.00000001 11 | 12 | def elapse_minute(ewma): 13 | for i in range(12): 14 | ewma.tick() 15 | 16 | ewma.update(3) 17 | ewma.tick() 18 | for rate in expected_rates: 19 | assert abs(ewma.get_rate() - rate) < max_difference 20 | elapse_minute(ewma) 21 | 22 | 23 | def test_one_minute_EWMA(): 24 | expected_rates = [ 25 | 0.6, 26 | 0.22072766, 27 | 0.08120117, 28 | 0.02987224, 29 | 0.01098938, 30 | 0.00404277, 31 | 0.00148725, 32 | 0.00054713, 33 | 0.00020128, 34 | 0.00007405, 35 | 0.00002724, 36 | 0.00001002, 37 | 0.00000369, 38 | 0.00000136, 39 | 0.00000050, 40 | 0.00000018, 41 | ] 42 | 43 | ewma = EWMA.one_minute_EWMA() 44 | assert_ewma(ewma, expected_rates) 45 | 46 | 47 | def test_five_minute_EWMA(): 48 | expected_rates = [ 49 | 0.6, 50 | 0.49123845, 51 | 0.40219203, 52 | 0.32928698, 53 | 0.26959738, 54 | 0.22072766, 55 | 0.18071653, 56 | 0.14795818, 57 | 0.12113791, 58 | 0.09917933, 59 | 0.08120117, 60 | 0.06648190, 61 | 0.05443077, 62 | 0.04456415, 63 | 0.03648604, 64 | 0.02987224 65 | ] 66 | 67 | ewma = EWMA.five_minute_EWMA() 68 | assert_ewma(ewma, expected_rates) 69 | 70 | 71 | def test_fifteen_minute_EWMA(): 72 | expected_rates = [ 73 | 0.6, 74 | 0.56130419, 75 | 0.52510399, 76 | 0.49123845, 77 | 0.45955700, 78 | 0.42991879, 79 | 0.40219203, 80 | 0.37625345, 81 | 0.35198773, 82 | 0.32928698, 83 | 0.30805027, 84 | 0.28818318, 85 | 0.26959738, 86 | 0.25221023, 87 | 0.23594443, 88 | 0.22072766, 89 | ] 90 | 91 | ewma = EWMA.fifteen_minute_EWMA() 92 | assert_ewma(ewma, expected_rates) 93 | -------------------------------------------------------------------------------- /tests/histogram_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Translated from HistogramTest.java""" 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | try: 7 | from unittest import mock 8 | except: 9 | import mock 10 | import pytest 11 | 12 | from uwsgi_metrics.histogram import Histogram 13 | from uwsgi_metrics.reservoir import Reservoir 14 | from uwsgi_metrics.snapshot import Snapshot 15 | 16 | 17 | @pytest.fixture 18 | def histogram(): 19 | reservoir = mock.create_autospec(Reservoir) 20 | histogram = Histogram() 21 | histogram.reservoir = reservoir 22 | return histogram 23 | 24 | 25 | def test_updates_the_count_on_updates(histogram): 26 | assert histogram.get_count() == 0 27 | histogram.update(1) 28 | assert histogram.get_count() == 1 29 | 30 | 31 | def test_returns_the_snapshot_from_the_reservoir(histogram): 32 | histogram.get_snapshot() 33 | histogram.reservoir.get_snapshot.assert_called_once_with() 34 | 35 | 36 | def test_updates_the_reservoir(histogram): 37 | histogram.update(1) 38 | histogram.reservoir.update.assert_called_once_with(1) 39 | 40 | 41 | def test_view(histogram): 42 | snapshot = mock.create_autospec(Snapshot) 43 | snapshot.view.return_value = {'foo': 42} 44 | histogram.reservoir.get_snapshot.return_value = snapshot 45 | expected = { 46 | 'foo': 42, 47 | 'count': 0, 48 | } 49 | assert histogram.view() == expected 50 | -------------------------------------------------------------------------------- /tests/meter_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Translated from MeterTest.java""" 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | try: 7 | from unittest import mock 8 | except: 9 | import mock 10 | import pytest 11 | 12 | from uwsgi_metrics.meter import Meter 13 | 14 | 15 | class Clock(object): 16 | def __init__(self, time): 17 | self._time = time 18 | 19 | def __call__(self): 20 | return self._time 21 | 22 | def add_seconds(self, seconds): 23 | self._time += seconds 24 | 25 | 26 | @pytest.yield_fixture 27 | def meter_and_clock(): 28 | with mock.patch('time.time', Clock(0.0)) as clock: 29 | clock = clock 30 | meter = Meter() 31 | yield (meter, clock) 32 | 33 | 34 | def test_starts_out_with_no_rates_or_count(meter_and_clock): 35 | meter, clock = meter_and_clock[0], meter_and_clock[1] 36 | 37 | clock.add_seconds(10) 38 | assert meter.get_count() == 0 39 | assert meter.get_mean_rate() == 0.0 40 | assert meter.get_one_minute_rate() == 0.0 41 | assert meter.get_five_minute_rate() == 0.0 42 | assert meter.get_fifteen_minute_rate() == 0.0 43 | 44 | 45 | def test_marks_events_and_updates_rates_and_count(meter_and_clock): 46 | def assert_almost_equal(lval, rval): 47 | assert abs(lval - rval) <= 0.001 48 | 49 | meter, clock = meter_and_clock[0], meter_and_clock[1] 50 | 51 | meter.mark() 52 | clock.add_seconds(10) 53 | meter.mark(2) 54 | assert meter.get_mean_rate() == 0.3 55 | assert_almost_equal(meter.get_one_minute_rate(), 0.1840) 56 | assert_almost_equal(meter.get_five_minute_rate(), 0.1966) 57 | assert_almost_equal(meter.get_fifteen_minute_rate(), 0.1988) 58 | 59 | 60 | def test_view(meter_and_clock): 61 | meter = meter_and_clock[0] 62 | expected = { 63 | 'count': 0, 64 | 'm15_rate': 0.0, 65 | 'm1_rate': 0.0, 66 | 'm5_rate': 0.0, 67 | 'mean_rate': 0.0, 68 | 'units': 'events/second', 69 | } 70 | assert meter.view() == expected 71 | -------------------------------------------------------------------------------- /tests/metrics_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | import pytest 10 | 11 | import uwsgi_metrics 12 | from uwsgi_metrics.__about__ import __version__ 13 | from uwsgi_metrics.metrics import emit 14 | 15 | 16 | @pytest.fixture 17 | def setup(): 18 | uwsgi_metrics.metrics.reset() 19 | uwsgi_metrics.initialize() 20 | 21 | 22 | def test_timing(setup): 23 | with mock.patch('time.time', return_value=42.0): 24 | with uwsgi_metrics.timing(__name__, 'my_timer'): 25 | pass 26 | emit(None) 27 | 28 | actual = uwsgi_metrics.view() 29 | expected = { 30 | 'version': __version__, 31 | 'counters': {}, 32 | 'gauges': {}, 33 | 'histograms': {}, 34 | 'meters': {}, 35 | 'timers': { 36 | 'tests.metrics_test.my_timer': { 37 | 'count': 1, 38 | 'max': 0.0, 39 | 'mean': 0.0, 40 | 'min': 0.0, 41 | 'p50': 0.0, 42 | 'p75': 0.0, 43 | 'p95': 0.0, 44 | 'p98': 0.0, 45 | 'p99': 0.0, 46 | 'p999': 0.0, 47 | 'stddev': 0.0, 48 | 'm15_rate': 0.0, 49 | 'm1_rate': 0.0, 50 | 'm5_rate': 0.0, 51 | 'mean_rate': 0.0, 52 | 'duration_units': 'milliseconds', 53 | 'rate_units': 'calls/second', 54 | } 55 | } 56 | } 57 | 58 | assert expected == actual 59 | 60 | 61 | def test_timing_handling(setup): 62 | with mock.patch('time.time', return_value=42.0): 63 | with pytest.raises(Exception) as excinfo: 64 | with uwsgi_metrics.timing(__name__, 'exc_timer'): 65 | raise Exception('testing exception handling') 66 | emit(None) 67 | assert 'testing exception handling' == str(excinfo.value) 68 | 69 | actual = uwsgi_metrics.view() 70 | expected = { 71 | 'version': __version__, 72 | 'counters': {}, 73 | 'gauges': {}, 74 | 'histograms': {}, 75 | 'meters': {}, 76 | 'timers': { 77 | 'tests.metrics_test.exc_timer': { 78 | 'count': 1, 79 | 'max': 0.0, 80 | 'mean': 0.0, 81 | 'min': 0.0, 82 | 'p50': 0.0, 83 | 'p75': 0.0, 84 | 'p95': 0.0, 85 | 'p98': 0.0, 86 | 'p99': 0.0, 87 | 'p999': 0.0, 88 | 'stddev': 0.0, 89 | 'm15_rate': 0.0, 90 | 'm1_rate': 0.0, 91 | 'm5_rate': 0.0, 92 | 'mean_rate': 0.0, 93 | 'duration_units': 'milliseconds', 94 | 'rate_units': 'calls/second', 95 | } 96 | } 97 | } 98 | 99 | assert expected == actual 100 | 101 | 102 | def test_timer(setup): 103 | with mock.patch('time.time', return_value=42.0): 104 | uwsgi_metrics.timer(__name__, 'my_timer', 0.0) 105 | emit(None) 106 | 107 | actual = uwsgi_metrics.view() 108 | 109 | expected = { 110 | 'version': __version__, 111 | 'counters': {}, 112 | 'gauges': {}, 113 | 'histograms': {}, 114 | 'meters': {}, 115 | 'timers': { 116 | 'tests.metrics_test.my_timer': { 117 | 'count': 1, 118 | 'max': 0.0, 119 | 'mean': 0.0, 120 | 'min': 0.0, 121 | 'p50': 0.0, 122 | 'p75': 0.0, 123 | 'p95': 0.0, 124 | 'p98': 0.0, 125 | 'p99': 0.0, 126 | 'p999': 0.0, 127 | 'stddev': 0.0, 128 | 'm15_rate': 0.0, 129 | 'm1_rate': 0.0, 130 | 'm5_rate': 0.0, 131 | 'mean_rate': 0.0, 132 | 'duration_units': 'milliseconds', 133 | 'rate_units': 'calls/second', 134 | } 135 | } 136 | } 137 | 138 | assert expected == actual 139 | 140 | 141 | def test_histogram(setup): 142 | uwsgi_metrics.histogram(__name__, 'my_histogram', 42.0) 143 | emit(None) 144 | 145 | actual = uwsgi_metrics.view() 146 | expected = { 147 | 'version': __version__, 148 | 'counters': {}, 149 | 'gauges': {}, 150 | 'meters': {}, 151 | 'timers': {}, 152 | 'histograms': { 153 | 'tests.metrics_test.my_histogram': { 154 | 'count': 1, 155 | 'max': 42.0, 156 | 'mean': 42.0, 157 | 'min': 42.0, 158 | 'p50': 42.0, 159 | 'p75': 42.0, 160 | 'p95': 42.0, 161 | 'p98': 42.0, 162 | 'p99': 42.0, 163 | 'p999': 42.0, 164 | 'stddev': 0.0, 165 | } 166 | } 167 | } 168 | 169 | assert expected == actual 170 | 171 | 172 | def test_counter(setup): 173 | uwsgi_metrics.counter(__name__, 'my_counter', 17.0) 174 | emit(None) 175 | 176 | actual = uwsgi_metrics.view() 177 | expected = { 178 | 'version': __version__, 179 | 'gauges': {}, 180 | 'histograms': {}, 181 | 'meters': {}, 182 | 'timers': {}, 183 | 'counters': { 184 | 'tests.metrics_test.my_counter': { 185 | 'count': 17.0, 186 | } 187 | } 188 | } 189 | 190 | assert expected == actual 191 | 192 | 193 | def test_meter(setup): 194 | with mock.patch('time.time', return_value=42.0): 195 | uwsgi_metrics.meter(__name__, 'my_meter') 196 | emit(None) 197 | 198 | actual = uwsgi_metrics.view() 199 | expected = { 200 | 'version': __version__, 201 | 'counters': {}, 202 | 'gauges': {}, 203 | 'histograms': {}, 204 | 'timers': {}, 205 | 'meters': { 206 | 'tests.metrics_test.my_meter': { 207 | 'count': 1, 208 | 'm15_rate': 0.0, 209 | 'm1_rate': 0.0, 210 | 'm5_rate': 0.0, 211 | 'mean_rate': 0.0, 212 | 'units': 'events/second', 213 | } 214 | } 215 | } 216 | 217 | assert expected == actual 218 | -------------------------------------------------------------------------------- /tests/reservoir_test.py: -------------------------------------------------------------------------------- 1 | """Translated from ExponentiallyDecayingReservoirTest.java""" 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | try: 7 | from unittest import mock 8 | except: 9 | import mock 10 | 11 | from uwsgi_metrics.reservoir import Reservoir 12 | 13 | 14 | def assert_all_values_between(reservoir, low, high): 15 | for value in reservoir.get_snapshot().values: 16 | assert low <= value < high 17 | 18 | 19 | def test_insert_1000_elements_into_a_reservoir_of_100(): 20 | reservoir = Reservoir('unit', size=100, alpha=0.99) 21 | for i in range(1000): 22 | reservoir.update(i) 23 | 24 | assert reservoir.get_snapshot().size() == 100 25 | assert_all_values_between(reservoir, 0, 1000) 26 | 27 | 28 | def test_insert_10_elements_into_a_reservoir_of_100(): 29 | reservoir = Reservoir('unit', size=100, alpha=0.99) 30 | for i in range(10): 31 | reservoir.update(i) 32 | 33 | assert reservoir.get_snapshot().size() == 10 34 | assert_all_values_between(reservoir, 0, 10) 35 | 36 | 37 | def test_insert_100_elemnts_into_a_heavily_baised_reservoir_of_1000(): 38 | reservoir = Reservoir('unit', size=1000, alpha=0.01) 39 | for i in range(100): 40 | reservoir.update(i) 41 | 42 | assert reservoir.get_snapshot().size() == 100 43 | assert_all_values_between(reservoir, 0, 100) 44 | 45 | 46 | def test_inactivity_should_not_corrupt_sampling_state(): 47 | 48 | class Clock(object): 49 | def __init__(self, time): 50 | self._time = time 51 | 52 | def __call__(self): 53 | return self._time 54 | 55 | def add_millis(self, millis): 56 | self._time += 0.001 * millis 57 | 58 | def add_hours(self, hours): 59 | self._time += 60 * 60 * hours 60 | 61 | with mock.patch(('uwsgi_metrics.reservoir.Reservoir' 62 | '.current_time_in_fractional_seconds'), 63 | Clock(42.0)) as clock: 64 | reservoir = Reservoir('unit', size=10, alpha=0.015) 65 | 66 | # Add 1000 values at a rate of 10 values/second 67 | for i in range(1000): 68 | reservoir.update(1000 + i) 69 | clock.add_millis(100) 70 | 71 | assert reservoir.get_snapshot().size() == 10 72 | assert_all_values_between(reservoir, 1000, 2000) 73 | 74 | # Wait for 15 hours and add another value. This should trigger a 75 | # rescale. Note that the number of samples will be reduced to 2 76 | # because of the very small scaling factor that will make all 77 | # existing priorities equal to zero after rescale. 78 | clock.add_hours(15) 79 | reservoir.update(2000) 80 | reservoir.get_snapshot().size() == 2 81 | assert_all_values_between(reservoir, 1000, 3000) 82 | -------------------------------------------------------------------------------- /tests/snapshot_test.py: -------------------------------------------------------------------------------- 1 | """Translated from SnapshotTest.java""" 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | import random 7 | 8 | import pytest 9 | 10 | from uwsgi_metrics.snapshot import Snapshot 11 | 12 | 13 | @pytest.fixture 14 | def snapshot(): 15 | return Snapshot([5, 1, 2, 3, 4]) 16 | 17 | 18 | def test_small_quantiles_are_the_first_value(snapshot): 19 | assert snapshot.get_value(0.0) == 1 20 | 21 | 22 | def test_big_quantiles_are_the_last_value(snapshot): 23 | assert snapshot.get_value(1.0) == 5 24 | 25 | 26 | def test_has_a_median(snapshot): 27 | assert snapshot.get_median() == 3 28 | 29 | 30 | def test_has_a_75th_percentile(snapshot): 31 | assert snapshot.get_75th_percentile() == 4.5 32 | 33 | 34 | def test_has_a_95th_percentile(snapshot): 35 | assert snapshot.get_95th_percentile() == 5.0 36 | 37 | 38 | def test_has_a_98th_percentile(snapshot): 39 | assert snapshot.get_98th_percentile() == 5.0 40 | 41 | 42 | def test_has_a_99th_percentile(snapshot): 43 | assert snapshot.get_99th_percentile() == 5.0 44 | 45 | 46 | def test_has_a_999th_percentile(snapshot): 47 | assert snapshot.get_999th_percentile() == 5.0 48 | 49 | 50 | def test_has_values(snapshot): 51 | assert snapshot.values == [1, 2, 3, 4, 5] 52 | 53 | 54 | def test_has_a_size(snapshot): 55 | assert snapshot.size() == 5 56 | 57 | 58 | def test_calculates_the_minimum_value(snapshot): 59 | assert snapshot.get_min() == 1 60 | 61 | 62 | def test_calculates_the_maximum_value(snapshot): 63 | assert snapshot.get_max() == 5 64 | 65 | 66 | def test_calculates_the_std_dev(snapshot): 67 | assert abs(snapshot.get_std_dev() - 1.5811) < 0.0001 68 | 69 | 70 | def test_calculates_a_min_of_zero_for_an_empty_snapshot(): 71 | assert Snapshot().get_min() == 0 72 | 73 | 74 | def test_calculates_a_max_of_zero_for_an_empty_snapshot(): 75 | assert Snapshot().get_max() == 0 76 | 77 | 78 | def test_calculates_a_mean_of_zero_for_an_empty_snapshot(): 79 | assert Snapshot().get_mean() == 0 80 | 81 | 82 | def test_calculates_a_std_dev_of_zero_for_an_empty_snapshot(): 83 | assert Snapshot().get_std_dev() == 0 84 | 85 | 86 | def test_calculates_a_std_dev_of_zero_for_a_singelton_snapshot(): 87 | assert Snapshot([1]).get_std_dev() == 0 88 | 89 | 90 | def test_all_percentiles_in_more_detail(): 91 | values = list(range(0, 1000)) 92 | random.shuffle(values) 93 | snapshot = Snapshot(values) 94 | 95 | tolerance = 1 96 | assert snapshot.values == sorted(values) 97 | assert snapshot.size() == 1000 98 | assert snapshot.get_value(0.0) == 0 99 | assert snapshot.get_value(1.0) == 999 100 | assert snapshot.get_median() == 499.5 101 | assert abs(snapshot.get_75th_percentile() - 750) < tolerance 102 | assert abs(snapshot.get_98th_percentile() - 980) < tolerance 103 | assert abs(snapshot.get_99th_percentile() - 990) < tolerance 104 | assert abs(snapshot.get_999th_percentile() - 999) < tolerance 105 | 106 | 107 | def test_view(snapshot): 108 | view = snapshot.view() 109 | assert view['max'] == 5 110 | assert view['mean'] == 3.0 111 | assert view['min'] == 1 112 | assert view['p50'] == 3.0 113 | assert view['p75'] == 4.5 114 | assert view['p98'] == 5 115 | assert view['p99'] == 5 116 | assert view['p999'] == 5 117 | assert abs(view['stddev'] - 1.5811) < 0.0001 118 | -------------------------------------------------------------------------------- /tests/timer_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Translated from TimerTest.java""" 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | try: 7 | from unittest import mock 8 | except: 9 | import mock 10 | import pytest 11 | 12 | from uwsgi_metrics.histogram import Histogram 13 | from uwsgi_metrics.meter import Meter 14 | from uwsgi_metrics.timer import Timer 15 | 16 | 17 | @pytest.fixture 18 | def timer(): 19 | histogram = mock.create_autospec(Histogram) 20 | meter = mock.create_autospec(Meter) 21 | 22 | timer = Timer('seconds') 23 | timer.meter = meter 24 | timer.histogram = histogram 25 | return timer 26 | 27 | 28 | def test_get_count(timer): 29 | timer.histogram.get_count.return_value = mock.sentinel.count 30 | assert timer.get_count() == mock.sentinel.count 31 | 32 | 33 | def test_get_mean_rate(timer): 34 | timer.meter.get_mean_rate.return_value = mock.sentinel.mean_rate 35 | assert timer.get_mean_rate() == mock.sentinel.mean_rate 36 | 37 | 38 | def test_get_one_minute_rate(timer): 39 | timer.meter.get_one_minute_rate.return_value = \ 40 | mock.sentinel.one_minute_rate 41 | assert timer.get_one_minute_rate(), mock.sentinel.one_minute_rate 42 | 43 | 44 | def test_get_five_minute_rate(timer): 45 | timer.meter.get_five_minute_rate.return_value = \ 46 | mock.sentinel.five_minute_rate 47 | assert timer.get_five_minute_rate() == mock.sentinel.five_minute_rate 48 | 49 | 50 | def test_get_fifteen_minute_rate(timer): 51 | timer.meter.get_fifteen_minute_rate.return_value = \ 52 | mock.sentinel.fifteen_minute_rate 53 | assert timer.get_fifteen_minute_rate() == mock.sentinel.fifteen_minute_rate 54 | 55 | 56 | def test_update_modifies_histogram_and_meter(timer): 57 | UPDATE_VALUE = 5 58 | timer.update(UPDATE_VALUE) 59 | timer.histogram.update.assert_called_once_with(UPDATE_VALUE) 60 | timer.meter.mark.assert_called_once_with() 61 | 62 | 63 | def test_snapshot_is_returned(timer): 64 | timer.histogram.get_snapshot.return_value = mock.sentinel.snapshot 65 | assert timer.get_snapshot() == mock.sentinel.snapshot 66 | 67 | 68 | def test_update_ignores_negative_values(timer): 69 | timer.update(-1) 70 | assert not timer.histogram.update.called 71 | assert not timer.meter.mark.called 72 | 73 | 74 | def test_view(timer): 75 | timer.meter.get_count.return_value = mock.sentinel.count 76 | timer.meter.get_fifteen_minute_rate.return_value = mock.sentinel.m15_rate 77 | timer.meter.get_one_minute_rate.return_value = mock.sentinel.m1_rate 78 | timer.meter.get_five_minute_rate.return_value = mock.sentinel.m5_rate 79 | timer.meter.get_mean_rate.return_value = mock.sentinel.mean_rate 80 | 81 | mock_snapshot = mock.Mock() 82 | mock_snapshot.get_max.return_value = mock.sentinel.max 83 | mock_snapshot.get_mean.return_value = mock.sentinel.mean 84 | mock_snapshot.get_min.return_value = mock.sentinel.min 85 | mock_snapshot.get_median.return_value = mock.sentinel.median 86 | mock_snapshot.get_75th_percentile.return_value = mock.sentinel.p75 87 | mock_snapshot.get_95th_percentile.return_value = mock.sentinel.p95 88 | mock_snapshot.get_98th_percentile.return_value = mock.sentinel.p98 89 | mock_snapshot.get_99th_percentile.return_value = mock.sentinel.p99 90 | mock_snapshot.get_999th_percentile.return_value = mock.sentinel.p999 91 | mock_snapshot.get_std_dev.return_value = mock.sentinel.stddev 92 | timer.histogram.get_snapshot.return_value = mock_snapshot 93 | 94 | expected = { 95 | 'count': mock.sentinel.count, 96 | 'max': mock.sentinel.max, 97 | 'mean': mock.sentinel.mean, 98 | 'min': mock.sentinel.min, 99 | 'p50': mock.sentinel.median, 100 | 'p75': mock.sentinel.p75, 101 | 'p95': mock.sentinel.p95, 102 | 'p98': mock.sentinel.p98, 103 | 'p99': mock.sentinel.p99, 104 | 'p999': mock.sentinel.p999, 105 | 'stddev': mock.sentinel.stddev, 106 | 'm15_rate': mock.sentinel.m15_rate, 107 | 'm1_rate': mock.sentinel.m1_rate, 108 | 'm5_rate': mock.sentinel.m5_rate, 109 | 'mean_rate': mock.sentinel.mean_rate, 110 | 'duration_units': 'seconds', 111 | 'rate_units': 'calls/second' 112 | } 113 | 114 | assert timer.view() == expected 115 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py34 3 | 4 | [testenv] 5 | deps = 6 | coverage 7 | flake8 8 | mock 9 | pytest 10 | commands = 11 | coverage erase 12 | coverage run --source=uwsgi_metrics/,tests/ -m pytest --capture=no --strict {posargs} 13 | coverage report -m 14 | flake8 . 15 | 16 | [testenv:docs] 17 | deps = sphinx 18 | changedir = docs 19 | commands = sphinx-build -b html -d build/doctrees source build/html 20 | 21 | [flake8] 22 | exclude = .tox,docs 23 | ignore = E501 24 | -------------------------------------------------------------------------------- /uwsgi_metrics/__about__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 3 | "__email__", "__license__" 4 | ] 5 | 6 | __title__ = "uwsgi_metrics" 7 | __summary__ = "Metrics for uWSGI services" 8 | __uri__ = "https://github.com/Yelp/uwsgi_metrics" 9 | 10 | __version__ = "1.2.0" 11 | 12 | __author__ = "John Billings" 13 | __email__ = "billings@yelp.com" 14 | 15 | __license__ = "Apache Software License 2.0" 16 | -------------------------------------------------------------------------------- /uwsgi_metrics/__init__.py: -------------------------------------------------------------------------------- 1 | from uwsgi_metrics.metrics import counter 2 | from uwsgi_metrics.metrics import histogram 3 | from uwsgi_metrics.metrics import initialize 4 | from uwsgi_metrics.metrics import meter 5 | from uwsgi_metrics.metrics import timer 6 | from uwsgi_metrics.metrics import timing 7 | from uwsgi_metrics.metrics import view 8 | 9 | __all__ = [ 10 | 'counter', 11 | 'histogram', 12 | 'initialize', 13 | 'meter', 14 | 'timer', 15 | 'timing', 16 | 'view', 17 | ] 18 | -------------------------------------------------------------------------------- /uwsgi_metrics/counter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | 6 | class Counter(object): 7 | """An incrementing and decrementing counter metric. 8 | 9 | Translated from Counter.java 10 | """ 11 | 12 | def __init__(self): 13 | self.count = 0 14 | 15 | def inc(self, n=1): 16 | """Increment the counter.""" 17 | self.count += n 18 | 19 | def dec(self, n=1): 20 | """Decrement the counter.""" 21 | self.count -= n 22 | 23 | def get_count(self): 24 | """Get the counter's current value.""" 25 | return self.count 26 | 27 | def view(self): 28 | return { 29 | 'count': self.get_count(), 30 | } 31 | -------------------------------------------------------------------------------- /uwsgi_metrics/ewma.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | from math import exp 6 | 7 | 8 | class EWMA(object): 9 | """An exponentially-weighted moving average. 10 | 11 | See: 12 | * UNIX Load Average Part 1: How It Works 13 | http://www.teamquest.com/pdfs/whitepaper/ldavg1.pdf 14 | * UNIX Load Average Part 2: Not Your Average Average 15 | http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf 16 | * http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 17 | 18 | Translated from EWMA.java 19 | """ 20 | 21 | TICK_INTERVAL_S = 5 22 | SECONDS_PER_MINUTE = 60.0 23 | ONE_MINUTE = 1 24 | FIVE_MINUTES = 5 25 | FIFTEEN_MINUTES = 15 26 | M1_ALPHA = 1 - exp(-TICK_INTERVAL_S / SECONDS_PER_MINUTE / ONE_MINUTE) 27 | M5_ALPHA = 1 - exp(-TICK_INTERVAL_S / SECONDS_PER_MINUTE / FIVE_MINUTES) 28 | M15_ALPHA = 1 - exp( 29 | -TICK_INTERVAL_S / SECONDS_PER_MINUTE / FIFTEEN_MINUTES) 30 | 31 | @classmethod 32 | def one_minute_EWMA(cls): 33 | """Creates a new EWMA which is equivalent to the UNIX one minute load 34 | average and which expects to be ticked every 5 seconds. 35 | """ 36 | 37 | return cls(cls.M1_ALPHA, cls.TICK_INTERVAL_S) 38 | 39 | @classmethod 40 | def five_minute_EWMA(cls): 41 | """Creates a new EWMA which is equivalent to the UNIX five minute load 42 | average and which expects to be ticked every 5 seconds. 43 | """ 44 | 45 | return cls(cls.M5_ALPHA, cls.TICK_INTERVAL_S) 46 | 47 | @classmethod 48 | def fifteen_minute_EWMA(cls): 49 | """Creates a new EWMA which is equivalent to the UNIX fifteen minute 50 | load average and which expects to be ticked every 5 seconds. 51 | """ 52 | 53 | return cls(cls.M15_ALPHA, cls.TICK_INTERVAL_S) 54 | 55 | def __init__(self, alpha, tick_interval_s): 56 | self.alpha = alpha 57 | self.tick_interval_s = tick_interval_s 58 | self.initialized = False 59 | self.rate = 0.0 60 | self.count = 0 61 | 62 | def update(self, n): 63 | """Update the moving average with a new value.""" 64 | self.count += n 65 | 66 | def tick(self): 67 | """Mark the passage of time and decay the current rate accordingly.""" 68 | instant_rate = self.count / float(self.tick_interval_s) 69 | self.count = 0 70 | if self.initialized: 71 | self.rate += (self.alpha * (instant_rate - self.rate)) 72 | else: 73 | self.rate = instant_rate 74 | self.initialized = True 75 | 76 | def get_rate(self): 77 | """Get the per-second rate.""" 78 | return self.rate 79 | -------------------------------------------------------------------------------- /uwsgi_metrics/histogram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | from uwsgi_metrics.reservoir import Reservoir 6 | 7 | 8 | class Histogram(object): 9 | """A metric which calculates the distribution of a value. 10 | 11 | Translated from Histogram.java 12 | """ 13 | 14 | def __init__(self): 15 | self.count = 0 16 | self.reservoir = Reservoir() 17 | 18 | def update(self, value): 19 | self.count += 1 20 | self.reservoir.update(value) 21 | 22 | def get_count(self): 23 | return self.count 24 | 25 | def get_snapshot(self): 26 | return self.reservoir.get_snapshot() 27 | 28 | def view(self): 29 | result = self.get_snapshot().view() 30 | result['count'] = self.get_count() 31 | return result 32 | -------------------------------------------------------------------------------- /uwsgi_metrics/meter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | import sys 6 | import time 7 | 8 | from uwsgi_metrics.ewma import EWMA 9 | 10 | 11 | if sys.version_info[0] == 3: 12 | range_ = range 13 | else: 14 | range_ = xrange # noqa 15 | 16 | 17 | class Meter(object): 18 | """A meter metric which measures mean throughput and one-, five-, and 19 | fifteen-minute exponentially-weighted moving average throughputs. 20 | 21 | Translated from Meter.java 22 | """ 23 | 24 | def __init__(self): 25 | self.m1_rate = EWMA.one_minute_EWMA() 26 | self.m5_rate = EWMA.five_minute_EWMA() 27 | self.m15_rate = EWMA.fifteen_minute_EWMA() 28 | self.count = 0 29 | self.start_time = time.time() 30 | self.last_tick = self.start_time 31 | 32 | def mark(self, n=1): 33 | """Mark the occurrence of a given number of events.""" 34 | self.tick_if_necessary() 35 | self.count += n 36 | self.m1_rate.update(n) 37 | self.m5_rate.update(n) 38 | self.m15_rate.update(n) 39 | 40 | def tick_if_necessary(self): 41 | old_tick, new_tick = self.last_tick, time.time() 42 | age = new_tick - old_tick 43 | if age > EWMA.TICK_INTERVAL_S: 44 | self.last_tick = new_tick - age % EWMA.TICK_INTERVAL_S 45 | required_ticks = int(age / EWMA.TICK_INTERVAL_S) 46 | for _ in range_(required_ticks): 47 | self.m1_rate.tick() 48 | self.m5_rate.tick() 49 | self.m15_rate.tick() 50 | 51 | def get_count(self): 52 | return self.count 53 | 54 | def get_one_minute_rate(self): 55 | self.tick_if_necessary() 56 | return self.m1_rate.get_rate() 57 | 58 | def get_five_minute_rate(self): 59 | self.tick_if_necessary() 60 | return self.m5_rate.get_rate() 61 | 62 | def get_fifteen_minute_rate(self): 63 | self.tick_if_necessary() 64 | return self.m15_rate.get_rate() 65 | 66 | def get_mean_rate(self): 67 | elapsed = time.time() - self.start_time 68 | if elapsed == 0: 69 | return 0.0 70 | return self.count / elapsed 71 | 72 | def view(self): 73 | result = { 74 | 'count': self.get_count(), 75 | 'm15_rate': self.get_fifteen_minute_rate(), 76 | 'm1_rate': self.get_one_minute_rate(), 77 | 'm5_rate': self.get_five_minute_rate(), 78 | 'mean_rate': self.get_mean_rate(), 79 | 'units': 'events/second', 80 | } 81 | return result 82 | -------------------------------------------------------------------------------- /uwsgi_metrics/metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This is the uWSGI-specific part of uwsgi_metrics. 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | import contextlib 7 | import logging 8 | import marshal 9 | import mmap 10 | import time 11 | 12 | import six 13 | 14 | try: 15 | import uwsgi 16 | except ImportError: 17 | # The uwsgi module is only available when running under uWSGI. 18 | # Mock it out for tests and documentation building. 19 | class uwsgi(object): 20 | @staticmethod 21 | def lock(): 22 | pass 23 | 24 | @staticmethod 25 | def unlock(): 26 | pass 27 | 28 | @staticmethod 29 | def add_timer(signal, period): 30 | pass 31 | 32 | @staticmethod 33 | def register_signal(signal, target, func): 34 | pass 35 | 36 | 37 | try: 38 | import uwsgidecorators 39 | except ImportError: 40 | class uwsgidecorators(object): 41 | @staticmethod 42 | def mulefunc(target): 43 | def decorator(fun): 44 | return fun 45 | return decorator 46 | 47 | 48 | from uwsgi_metrics.__about__ import __version__ 49 | from uwsgi_metrics.counter import Counter 50 | from uwsgi_metrics.histogram import Histogram 51 | from uwsgi_metrics.meter import Meter 52 | from uwsgi_metrics.timer import Timer 53 | 54 | DEFAULT_UPDATE_PERIOD_S = 5 55 | DEFAULT_TIMER_SIGNAL_NUMBER = 42 56 | MAX_MARSHALLED_VIEW_SIZE = 2 ** 20 57 | MULE = 'mule1' 58 | 59 | log = logging.getLogger('uwsgi_metrics.metrics') 60 | 61 | # Map of all metrics of type: metric_type -> module -> metric_name -> metric 62 | # where 'module' and 'metric_name' are both string values. 63 | # These metrics are periodically marshalled to the memory mapped buffer for 64 | # viewing by regular workers. 65 | all_metrics = {} 66 | 67 | # The memory mapped buffer 68 | marshalled_metrics_mmap = mmap.mmap(-1, MAX_MARSHALLED_VIEW_SIZE) 69 | marshalled_metrics_mmap.write(marshal.dumps({ 70 | 'version': __version__, 71 | 'counters': {}, 72 | 'gauges': {}, 73 | 'histograms': {}, 74 | 'meters': {}, 75 | 'timers': {}, 76 | })) 77 | 78 | # Set when initialized() has been invoked 79 | initialized = False 80 | 81 | 82 | class NotInitialized(Exception): 83 | """Raised when the initialize() method has not been invoked.""" 84 | 85 | 86 | def get_metric(ty, module, name, default): 87 | key = (ty, module, name) 88 | return all_metrics.setdefault(key, default) 89 | 90 | 91 | def initialize(signal_number=DEFAULT_TIMER_SIGNAL_NUMBER, 92 | update_period_s=DEFAULT_UPDATE_PERIOD_S): 93 | """Initialize metrics, must be invoked at least once prior to invoking any 94 | other method.""" 95 | global initialized 96 | if initialized: 97 | return 98 | initialized = True 99 | uwsgi.add_timer(signal_number, update_period_s) 100 | uwsgi.register_signal(signal_number, MULE, emit) 101 | 102 | 103 | def reset(): 104 | """Test-only method""" 105 | global all_metrics, initialized 106 | initialized = False 107 | all_metrics = {} 108 | 109 | 110 | def emit(_): 111 | """Serialize metrics to the memory mapped buffer.""" 112 | if not initialized: 113 | raise NotInitialized 114 | 115 | view = { 116 | 'version': __version__, 117 | 'counters': {}, 118 | 'gauges': {}, 119 | 'histograms': {}, 120 | 'meters': {}, 121 | 'timers': {}, 122 | } 123 | 124 | for (ty, module, name), metric in six.iteritems(all_metrics): 125 | view[ty]['%s.%s' % (module, name)] = metric.view() 126 | 127 | marshalled_view = marshal.dumps(view) 128 | if len(marshalled_view) > MAX_MARSHALLED_VIEW_SIZE: 129 | log.warn( 130 | 'Marshalled length too large, got %d, max %d. ' 131 | 'Try recording fewer metrics or increasing ' 132 | 'MAX_MARSHALLED_VIEW_SIZE' 133 | % (len(marshalled_view), MAX_MARSHALLED_VIEW_SIZE)) 134 | return 135 | marshalled_metrics_mmap.seek(0) 136 | try: 137 | # Reading and writing to/from an mmap'ed buffer is not guaranteed 138 | # to be atomic, so we must serialize access to it. 139 | uwsgi.lock() 140 | marshalled_metrics_mmap.write(marshalled_view) 141 | finally: 142 | uwsgi.unlock() 143 | 144 | 145 | def view(): 146 | """Get a dictionary representation of current metrics.""" 147 | if not initialized: 148 | raise NotInitialized 149 | 150 | marshalled_metrics_mmap.seek(0) 151 | try: 152 | uwsgi.lock() 153 | marshalled_view = marshalled_metrics_mmap.read( 154 | MAX_MARSHALLED_VIEW_SIZE) 155 | finally: 156 | uwsgi.unlock() 157 | return marshal.loads(marshalled_view) 158 | 159 | 160 | @contextlib.contextmanager 161 | def timing(module, name): 162 | """ 163 | Context manager to time a section of code:: 164 | 165 | with timing(__name__, 'my_timer'): 166 | do_some_operation() 167 | """ 168 | start_time_s = time.time() 169 | try: 170 | yield 171 | finally: 172 | end_time_s = time.time() 173 | delta_s = end_time_s - start_time_s 174 | delta_ms = delta_s * 1000 175 | timer(module, name, delta_ms) 176 | 177 | 178 | @uwsgidecorators.mulefunc(1) 179 | def timer(module, name, delta, duration_units='milliseconds'): 180 | """ 181 | Record a timing delta: 182 | :: 183 | 184 | start_time_s = time.time() 185 | do_some_operation() 186 | end_time_s = time.time() 187 | delta_s = end_time_s - start_time_s 188 | delta_ms = delta_s * 1000 189 | timer(__name__, 'my_timer', delta_ms) 190 | """ 191 | timer = get_metric('timers', module, name, Timer(duration_units)) 192 | timer.update(delta) 193 | 194 | 195 | @uwsgidecorators.mulefunc(1) 196 | def histogram(module, name, value): 197 | """ 198 | Record a value in a histogram: 199 | :: 200 | 201 | histogram(__name__, 'my_histogram', len(queue)) 202 | """ 203 | histogram = get_metric('histograms', module, name, Histogram()) 204 | histogram.update(value) 205 | 206 | 207 | @uwsgidecorators.mulefunc(1) 208 | def counter(module, name, count=1): 209 | """ 210 | Record an event's occurence in a counter: 211 | :: 212 | 213 | counter(__name__, 'my_counter') 214 | """ 215 | counter = get_metric('counters', module, name, Counter()) 216 | counter.inc(count) 217 | 218 | 219 | @uwsgidecorators.mulefunc(1) 220 | def meter(module, name, count=1): 221 | """ 222 | Record an event rate: 223 | :: 224 | 225 | meter(__name__, 'my_meter', 'event_type') 226 | """ 227 | meter = get_metric('meters', module, name, Meter()) 228 | meter.mark(count) 229 | -------------------------------------------------------------------------------- /uwsgi_metrics/reservoir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | import math 6 | import random 7 | import time 8 | 9 | try: 10 | import treap 11 | except ImportError: 12 | # treap using cython, which means that it cannot be installed on 13 | # readthedocs. Catch the error here so that we can continue building docs. 14 | pass 15 | 16 | from uwsgi_metrics.snapshot import Snapshot 17 | 18 | 19 | # By default, store 1028 elements in the reservoir. This offers a 99.9% 20 | # confidence level with a 5% margin of error assuming a normal distribution, 21 | # and an alpha factor of 0.015, which heavily biases the reservoir to the past 22 | # 5 minutes of measurements. 23 | DEFAULT_SIZE = 1028 24 | DEFAULT_ALPHA = 0.015 25 | 26 | # Rescale the priorities (keys) every hour. 27 | RESCALE_THRESHOLD_S = 60 * 60 28 | 29 | 30 | class Reservoir(object): 31 | """An exponentially-decaying random reservoir of integers. Uses Cormode et 32 | al's forward-decaying priority reservoir sampling method to produce a 33 | statistically representative sampling reservoir, exponentially biased 34 | towards newer entries. 35 | 36 | See http://dimacs.rutgers.edu/~graham/pubs/papers/fwddecay.pdf 37 | "Cormode et al. Forward Decay: A Practical Time Decay Model for Streaming 38 | Systems. ICDE '09: Proceedings of the 2009 IEEE International Conference on 39 | Data Engineering (2009)" 40 | 41 | Translated from ExponentiallyDecayingReservoir.java 42 | """ 43 | 44 | def __init__(self, unit=None, size=DEFAULT_SIZE, 45 | alpha=DEFAULT_ALPHA): 46 | """Create a new exponentially-decaying random reservoir. 47 | 48 | :param size: the number of samples to keep in the sampling reservoir 49 | :param alpha: the exponential decay factor; the higher this is, the 50 | more biased the reservoir will be towards newer values. 51 | """ 52 | 53 | self.unit = unit 54 | self.size = size 55 | self.alpha = alpha 56 | self.start_time = self.current_time_in_fractional_seconds() 57 | self.next_scale_time = self.start_time + RESCALE_THRESHOLD_S 58 | self.values = treap.treap() 59 | 60 | def current_time_in_fractional_seconds(self): 61 | return time.time() 62 | 63 | def weight(self, t): 64 | return math.exp(self.alpha * t) 65 | 66 | def update(self, value, timestamp=None): 67 | """Add a value to the reservoir. 68 | 69 | :param value: the value to be added 70 | :param timestamp: the epoch timestamp of the value in seconds, defaults 71 | to the current timestamp if not specified. 72 | """ 73 | 74 | if timestamp is None: 75 | timestamp = self.current_time_in_fractional_seconds() 76 | self.rescale_if_needed() 77 | priority = self.weight(timestamp - self.start_time) / random.random() 78 | self.values[priority] = value 79 | if len(self.values) > self.size: 80 | self.values.remove_min() 81 | 82 | def rescale_if_needed(self): 83 | now = self.current_time_in_fractional_seconds() 84 | if now >= self.next_scale_time: 85 | self.rescale(now) 86 | 87 | def rescale(self, now): 88 | """ "A common feature of the above techniques—indeed, the key technique 89 | that allows us to track the decayed weights efficiently—is that they 90 | maintain counts and other quantities based on g(ti − L), and only scale 91 | by g(t − L) at query time. But while g(ti −L)/g(t−L) is guaranteed to 92 | lie between zero and one, the intermediate values of g(ti − L) could 93 | become very large. For polynomial functions, these values should not 94 | grow too large, and should be effectively represented in practice by 95 | floating point values without loss of precision. For exponential 96 | functions, these values could grow quite large as new values of 97 | (ti − L) become large, and potentially exceed the capacity of common 98 | floating point types. However, since the values stored by the 99 | algorithms are linear combinations of g values (scaled sums), they 100 | can be rescaled relative to a new landmark. That is, by the analysis 101 | of exponential decay in Section VI-A, the choice of L does not affect 102 | the final result. We can therefore multiply each value based on L by a 103 | factor of exp(−α(L′ − L)), and obtain the correct value as if we had 104 | instead computed relative to a new landmark L′ (and then use this new 105 | L′ at query time). This can be done with a linear pass over whatever 106 | data structure is being used." 107 | """ 108 | 109 | old_start_time = self.start_time 110 | self.start_time = self.current_time_in_fractional_seconds() 111 | self.next_scale_time = now + RESCALE_THRESHOLD_S 112 | new_values = treap.treap() 113 | for key in self.values.keys(): 114 | new_key = key * math.exp( 115 | -self.alpha * (self.start_time - old_start_time)) 116 | new_values[new_key] = self.values[key] 117 | self.values = new_values 118 | 119 | def get_snapshot(self): 120 | return Snapshot(self.values.values()) 121 | -------------------------------------------------------------------------------- /uwsgi_metrics/snapshot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | import math 6 | 7 | 8 | class Snapshot(object): 9 | """A snapshot of a reservoir state. 10 | 11 | Translated from Snapshot.java 12 | """ 13 | 14 | def __init__(self, values=None): 15 | if values is None: 16 | self.values = [] 17 | else: 18 | self.values = sorted(values) 19 | 20 | def get_value(self, quantile): 21 | if quantile < 0.0 or quantile > 1.0: 22 | raise ValueError(quantile + " is not in [0..1]") 23 | 24 | if len(self.values) == 0: 25 | return 0.0 26 | 27 | pos = quantile * (len(self.values) + 1) 28 | 29 | if pos < 1: 30 | return self.values[0] 31 | 32 | if pos >= len(self.values): 33 | return self.values[-1] 34 | 35 | lower = self.values[int(pos) - 1] 36 | upper = self.values[int(pos)] 37 | return lower + (pos - math.floor(pos)) * (upper - lower) 38 | 39 | def get_median(self): 40 | return self.get_value(0.5) 41 | 42 | def get_75th_percentile(self): 43 | return self.get_value(0.75) 44 | 45 | def get_95th_percentile(self): 46 | return self.get_value(0.95) 47 | 48 | def get_98th_percentile(self): 49 | return self.get_value(0.98) 50 | 51 | def get_99th_percentile(self): 52 | return self.get_value(0.99) 53 | 54 | def get_999th_percentile(self): 55 | return self.get_value(0.999) 56 | 57 | def get_mean(self): 58 | if len(self.values) == 0: 59 | return 0.0 60 | return sum(self.values) / float(len(self.values)) 61 | 62 | def get_std_dev(self): 63 | # Two-pass algorithm for variance, avoids numeric overflow 64 | 65 | if len(self.values) <= 1: 66 | return 0.0 67 | 68 | mean = self.get_mean() 69 | sum_ = 0 70 | 71 | for value in self.values: 72 | diff = value - mean 73 | sum_ += diff * diff 74 | 75 | variance = sum_ / (len(self.values) - 1.0) 76 | return math.sqrt(variance) 77 | 78 | def get_min(self): 79 | if len(self.values) == 0: 80 | return 0.0 81 | return self.values[0] 82 | 83 | def get_max(self): 84 | if len(self.values) == 0: 85 | return 0.0 86 | return self.values[-1] 87 | 88 | def size(self): 89 | return len(self.values) 90 | 91 | def view(self): 92 | result = { 93 | 'max': self.get_max(), 94 | 'mean': self.get_mean(), 95 | 'min': self.get_min(), 96 | 'p50': self.get_median(), 97 | 'p75': self.get_75th_percentile(), 98 | 'p95': self.get_95th_percentile(), 99 | 'p98': self.get_98th_percentile(), 100 | 'p99': self.get_99th_percentile(), 101 | 'p999': self.get_999th_percentile(), 102 | 'stddev': self.get_std_dev(), 103 | } 104 | return result 105 | -------------------------------------------------------------------------------- /uwsgi_metrics/timer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | from uwsgi_metrics.meter import Meter 6 | from uwsgi_metrics.histogram import Histogram 7 | 8 | 9 | class Timer(object): 10 | """A timer metric which aggregates timing durations and provides duration 11 | statistics, plus throughput statistics via a Meter. 12 | 13 | Translated from Timer.java 14 | """ 15 | 16 | def __init__(self, duration_units): 17 | self.meter = Meter() 18 | self.histogram = Histogram() 19 | self.duration_units = duration_units 20 | 21 | def view(self): 22 | snapshot = self.get_snapshot() 23 | result = { 24 | 'count': self.meter.get_count(), 25 | 'max': snapshot.get_max(), 26 | 'mean': snapshot.get_mean(), 27 | 'min': snapshot.get_min(), 28 | 'p50': snapshot.get_median(), 29 | 'p75': snapshot.get_75th_percentile(), 30 | 'p95': snapshot.get_95th_percentile(), 31 | 'p98': snapshot.get_98th_percentile(), 32 | 'p99': snapshot.get_99th_percentile(), 33 | 'p999': snapshot.get_999th_percentile(), 34 | 'stddev': snapshot.get_std_dev(), 35 | 'm15_rate': self.meter.get_fifteen_minute_rate(), 36 | 'm1_rate': self.meter.get_one_minute_rate(), 37 | 'm5_rate': self.meter.get_five_minute_rate(), 38 | 'mean_rate': self.meter.get_mean_rate(), 39 | 'duration_units': self.duration_units, 40 | 'rate_units': 'calls/second' 41 | } 42 | return result 43 | 44 | def update(self, duration): 45 | """Add a recorded duration.""" 46 | if duration >= 0: 47 | self.histogram.update(duration) 48 | self.meter.mark() 49 | 50 | def get_count(self): 51 | return self.histogram.get_count() 52 | 53 | def get_fifteen_minute_rate(self): 54 | return self.meter.get_fifteen_minute_rate() 55 | 56 | def get_five_minute_rate(self): 57 | return self.meter.get_five_minute_rate() 58 | 59 | def get_one_minute_rate(self): 60 | return self.meter.get_one_minute_rate() 61 | 62 | def get_mean_rate(self): 63 | return self.meter.get_mean_rate() 64 | 65 | def get_snapshot(self): 66 | return self.histogram.get_snapshot() 67 | --------------------------------------------------------------------------------