├── setup.cfg ├── requirements.txt ├── requirements_test.txt ├── run_tests.sh ├── MANIFEST.in ├── prometheus ├── __init__.py ├── utils.py ├── exporter.py ├── test_utils.py ├── registry.py ├── metricdict.py ├── negotiator.py ├── pusher.py ├── test_registry.py ├── test_negotiator.py ├── test_metricdict.py ├── test_pusher.py ├── collectors.py ├── test_exporter.py ├── formats.py ├── test_collectors.py ├── pb2 │ └── metrics_pb2.py └── test_formats.py ├── .coveragerc ├── circle.yml ├── CHANGELOG.rst ├── .gitignore ├── LICENSE ├── examples ├── input_example.py ├── trigonometry_example.py ├── timing_write_io_example.py └── memory_cpu_usage_example.py ├── setup.py └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | quantile-python==1.1 2 | requests==2.5.1 3 | protobuf-py3==2.5.1 -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | coveralls==0.5 4 | psutil==2.2.1 -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python -m unittest discover -s prometheus -p 'test*.py' -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE NOTICE CHANGELOG.rst HISTORY.rst run_tests.sh requirements*.txt -------------------------------------------------------------------------------- /prometheus/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'prometheus' 2 | __version__ = '0.3.0' 3 | __author__ = 'Xabier Larrakoetxea' 4 | __license__ = 'MIT License' 5 | __copyright__ = 'Copyright 2015 Xabier Larrakoetxea' -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | 5 | [report] 6 | omit = 7 | *quantile* 8 | *requests* 9 | *protobuf* 10 | *metrics_pb2* 11 | 12 | # Regexes for lines to exclude from consideration 13 | exclude_lines = 14 | 15 | def __repr__ 16 | def __str__ 17 | 18 | if 0: 19 | if __name__ == .__main__.: -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | timezone: 3 | Europe/Madrid 4 | python: 5 | version: 3.4.2 6 | 7 | dependencies: 8 | override: 9 | - pip install -r requirements_test.txt 10 | 11 | test: 12 | override: 13 | - coverage run --rcfile=./.coveragerc -m unittest discover -s prometheus -p 'test_*.py' 14 | 15 | post: 16 | - coveralls # need to set up the coveralls token in "COVERALLS_REPO_TOKEN" env var -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Release History 4 | --------------- 5 | 6 | 7 | 0.3.0 (2015-02-20) 8 | ++++++++++++++++++ 9 | 10 | * Improve README 11 | * Implement pushgateway (push metrics) 12 | 13 | 0.2.1 (2015-02-19) 14 | ++++++++++++++++++ 15 | 16 | * Fix installation problem 17 | 18 | 0.2.0 (2015-02-19) 19 | ++++++++++++++++++ 20 | 21 | * Added Protobuf format 22 | * Add summary example (disk IO write) 23 | 24 | 0.1.3 (2015-02-11) 25 | ++++++++++++++++++ 26 | 27 | * Added MANIFEST 28 | 29 | 0.1.2 (2015-02-11) 30 | ++++++++++++++++++ 31 | 32 | * Fix requirements in setup 33 | 34 | 0.1.1 (2015-02-11) 35 | ++++++++++++++++++ 36 | 37 | * Improve documents 38 | * Add requirements to the setup 39 | 40 | 0.1 (2015-02-11) 41 | ++++++++++++++++++ 42 | 43 | * Setup basic stuff 44 | * Exporting server 45 | * Gauge, counter and summary metrics 46 | * Text 0.0.4 format 47 | -------------------------------------------------------------------------------- /prometheus/utils.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from datetime import datetime, timezone 3 | 4 | 5 | def unify_labels(labels, const_labels, ordered=False): 6 | if const_labels: 7 | result = const_labels.copy() 8 | if labels: 9 | # Add labels to const labels 10 | for k, v in labels.items(): 11 | result[k] = v 12 | else: 13 | result = labels 14 | 15 | if ordered and result: 16 | result = collections.OrderedDict(sorted(result.items(), 17 | key=lambda t: t[0])) 18 | 19 | return result 20 | 21 | 22 | def get_timestamp(): 23 | """ Timestamp is the number of milliseconds since the epoch 24 | (1970-01-01 00:00 UTC) excluding leap seconds. 25 | """ 26 | return int(datetime.utcnow().replace( 27 | tzinfo=timezone.utc).timestamp() * 1000) 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Xabier Larrakoetxea Gallego 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /prometheus/exporter.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler 2 | 3 | 4 | from prometheus.negotiator import Negotiator 5 | 6 | 7 | class PrometheusMetricHandler(BaseHTTPRequestHandler): 8 | 9 | METRICS_PATH = "/metrics" 10 | 11 | def __init__(self, registry, *args, **kwargs): 12 | self.registry = registry 13 | 14 | super().__init__(*args, **kwargs) 15 | 16 | def do_GET(self): 17 | if self.path == self.METRICS_PATH: 18 | # select formatter (without timestamp) 19 | formatter = Negotiator.negotiate(self.headers)(False) 20 | 21 | # Response OK 22 | self.send_response(200) 23 | 24 | # Add headers (type, encoding... and stuff) 25 | for k, v in formatter.get_headers().items(): 26 | self.send_header(k, v) 27 | self.end_headers() 28 | 29 | # Get the juice and serve! 30 | response = formatter.marshall(self.registry) 31 | 32 | # Maybe is protobuf bytes 33 | if isinstance(response, str): 34 | response = response.encode("utf8") 35 | 36 | self.wfile.write(response) 37 | return 38 | -------------------------------------------------------------------------------- /prometheus/test_utils.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import unittest 3 | 4 | from prometheus import utils 5 | 6 | 7 | class TestUnifyLabels(unittest.TestCase): 8 | 9 | def test_no_const_labels(self): 10 | 11 | labels = {'a': 'b', 'c': 'd'} 12 | result = utils.unify_labels(labels, None) 13 | 14 | self.assertEqual(labels, result) 15 | 16 | def test_no_labels(self): 17 | 18 | const_labels = {'a': 'b', 'c': 'd'} 19 | result = utils.unify_labels(None, const_labels) 20 | 21 | self.assertEqual(const_labels, result) 22 | 23 | def test_union(self): 24 | 25 | const_labels = {'a': 'b', 'c': 'd'} 26 | labels = {'e': 'f', 'g': 'h'} 27 | result = utils.unify_labels(labels, const_labels) 28 | 29 | valid_result = {'g': 'h', 'c': 'd', 'e': 'f', 'a': 'b'} 30 | 31 | self.assertEqual(valid_result, result) 32 | 33 | def test_union_order(self): 34 | 35 | const_labels = {'a': 'b', 'c': 'd'} 36 | labels = {'e': 'f', 'g': 'h'} 37 | result = utils.unify_labels(labels, const_labels, True) 38 | 39 | valid_result = OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h')]) 40 | 41 | self.assertEqual(valid_result, result) -------------------------------------------------------------------------------- /prometheus/registry.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Lock 2 | 3 | from prometheus.collectors import Collector 4 | 5 | # Used so only one thread can access the values at the same time 6 | mutex = Lock() 7 | 8 | 9 | class Registry(object): 10 | """" Registry registers all the collectors""" 11 | 12 | def __init__(self): 13 | self.collectors = {} 14 | 15 | def register(self, collector): 16 | """ Registers a collector""" 17 | if not isinstance(collector, Collector): 18 | raise TypeError( 19 | "Can't register instance, not a valid type of collector") 20 | 21 | if collector.name in self.collectors: 22 | raise ValueError("Collector already exists or name colision") 23 | 24 | with mutex: 25 | self.collectors[collector.name] = collector 26 | 27 | def deregister(self, name): 28 | """ eregisters a collector based on the name""" 29 | with mutex: 30 | del self.collectors[name] 31 | 32 | def get(self, name): 33 | """ Get a collector""" 34 | 35 | with mutex: 36 | return self.collectors[name] 37 | 38 | def get_all(self): 39 | """Get a list with all the collectors""" 40 | with mutex: 41 | return [v for k, v in self.collectors.items()] 42 | -------------------------------------------------------------------------------- /examples/input_example.py: -------------------------------------------------------------------------------- 1 | # Run the pushgatway: 2 | # docker run --rm -p 9091:9091 prom/pushgateway 3 | # 4 | # Configure the Pushgateway in prometheus: 5 | # 6 | # job: { 7 | # name: "pushgateway" 8 | # scrape_interval: "1s" 9 | # target_group: { 10 | # target: "http://172.17.42.1:9091/metrics" 11 | # } 12 | # } 13 | # 14 | 15 | # Set the python path 16 | import inspect 17 | import os 18 | import sys 19 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))) 20 | 21 | from prometheus.pusher import Pusher 22 | from prometheus.registry import Registry 23 | from prometheus.collectors import Gauge 24 | 25 | PUSHGATEWAY_URL = "http://127.0.0.1:9091" 26 | 27 | if __name__ == "__main__": 28 | job_name = "example" 29 | p = Pusher(job_name, PUSHGATEWAY_URL) 30 | registry = Registry() 31 | g = Gauge("up_and_down", "Up and down counter.", {}) 32 | registry.register(g) 33 | 34 | user = input("Hi! enter your username: ") 35 | 36 | while True: 37 | try: 38 | n = int(input("Enter a positive or negative number: ")) 39 | 40 | g.add({'user': user, }, n) 41 | # Push to the pushgateway 42 | p.add(registry) 43 | except ValueError: 44 | print("Wrong input") 45 | -------------------------------------------------------------------------------- /prometheus/metricdict.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | import re 4 | 5 | 6 | # Sometimes python will access by string for example iterating objects, and 7 | # it has this notation 8 | regex = re.compile("\{.*:.*,?\}") 9 | 10 | 11 | # http://stackoverflow.com/questions/3387691/python-how-to-perfectly-override-a-dict 12 | class MetricDict(collections.MutableMapping): 13 | """ MetricDict stores the data based on the labels so we need to generate 14 | custom hash keys based on the labels 15 | """ 16 | 17 | EMPTY_KEY = "__EMPTY__" 18 | 19 | def __init__(self, *args, **kwargs): 20 | self.store = dict() 21 | self.update(dict(*args, **kwargs)) 22 | 23 | def __getitem__(self, key): 24 | return self.store[self.__keytransform__(key)] 25 | 26 | def __setitem__(self, key, value): 27 | self.store[self.__keytransform__(key)] = value 28 | 29 | def __delitem__(self, key): 30 | del self.store[self.__keytransform__(key)] 31 | 32 | def __iter__(self): 33 | return iter(self.store) 34 | 35 | def __len__(self): 36 | return len(self.store) 37 | 38 | def __keytransform__(self, key): 39 | 40 | # Sometimes we need empty keys 41 | if not key or key == MetricDict.EMPTY_KEY: 42 | return MetricDict.EMPTY_KEY 43 | 44 | # Python accesses by string key so we allow if is str and 45 | # 'our custom' format 46 | if type(key) == str and regex.match(key): 47 | return key 48 | 49 | if type(key) is not dict: 50 | raise TypeError("Only accepts dicts as keys") 51 | 52 | return json.dumps(key, sort_keys=True) 53 | -------------------------------------------------------------------------------- /prometheus/negotiator.py: -------------------------------------------------------------------------------- 1 | from prometheus.formats import TextFormat, ProtobufFormat, ProtobufTextFormat 2 | 3 | 4 | class Negotiator(object): 5 | """ Negotiator selects the best format based on request header 6 | """ 7 | 8 | FALLBACK = TextFormat 9 | TEXT = { 10 | 'default': ('text/plain',), 11 | '0.0.4': ('text/plain', 12 | 'version=0.0.4'), 13 | 14 | } 15 | PROTOBUF = { 16 | "default": ("application/vnd.google.protobuf", 17 | "proto=io.prometheus.client.MetricFamily", 18 | "encoding=delimited"), 19 | 20 | "text": ("application/vnd.google.protobuf", 21 | "proto=io.prometheus.client.MetricFamily", 22 | "encoding=text") 23 | } 24 | 25 | @classmethod 26 | def negotiate(cls, headers): 27 | """ Process headers dict to return the format class 28 | (not the instance) 29 | """ 30 | # set lower keys 31 | headers = {k.lower(): v for k, v in headers.items()} 32 | 33 | accept = headers.get('accept', "*/*") 34 | 35 | parsed_accept = accept.split(";") 36 | parsed_accept = [i.strip() for i in parsed_accept] 37 | 38 | # Protobuffer (only one version) 39 | if all([i in parsed_accept for i in cls.PROTOBUF['default']]): 40 | return ProtobufFormat 41 | elif all([i in parsed_accept for i in cls.PROTOBUF['text']]): 42 | return ProtobufTextFormat 43 | # Text 0.0.4 44 | elif all([i in parsed_accept for i in cls.TEXT['0.0.4']]): 45 | return TextFormat 46 | # Text (Default) 47 | elif all([i in parsed_accept for i in cls.TEXT['default']]): 48 | return TextFormat 49 | # Default 50 | else: 51 | return cls.FALLBACK 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | from pip.req import parse_requirements 6 | from pip.download import PipSession 7 | import prometheus 8 | 9 | from codecs import open 10 | 11 | try: 12 | from setuptools import setup 13 | except ImportError: 14 | from distutils.core import setup 15 | 16 | if sys.argv[-1] == 'publish': 17 | os.system('python setup.py sdist upload') 18 | sys.exit() 19 | 20 | 21 | packages = [ 22 | 'prometheus', 23 | 'prometheus.pb2', 24 | ] 25 | 26 | install_reqs = parse_requirements("requirements.txt", session=PipSession()) 27 | requires = [str(ir.req) for ir in install_reqs] 28 | 29 | print(requires) 30 | with open('README.md', 'r', 'utf-8') as f: 31 | readme = f.read() 32 | 33 | setup( 34 | name='prometheus', 35 | version=prometheus.__version__, 36 | description='Python Prometheus client', 37 | long_description=readme, 38 | author='Xabier Larrakoetxea', 39 | author_email='slok69@gmail.com', 40 | url='https://github.com/slok/prometheus-python', 41 | packages=packages, 42 | package_data={'': ['LICENSE', 'requirements.txt']}, 43 | package_dir={'prometheus': 'prometheus'}, 44 | include_package_data=True, 45 | install_requires=requires, 46 | license='MIT License', 47 | zip_safe=False, 48 | download_url='https://github.com/slok/prometheus-python/tarball/{0}'.format(prometheus.__version__), 49 | keywords=['prometheus', 'client', 'metrics'], 50 | classifiers=[ 51 | 'Development Status :: 3 - Alpha', 52 | 'Intended Audience :: Developers', 53 | 'Natural Language :: English', 54 | 'License :: OSI Approved :: MIT License', 55 | 'Programming Language :: Python', 56 | 'Programming Language :: Python :: 3', 57 | 'Programming Language :: Python :: 3.3', 58 | 'Programming Language :: Python :: 3.4' 59 | ], 60 | ) 61 | -------------------------------------------------------------------------------- /examples/trigonometry_example.py: -------------------------------------------------------------------------------- 1 | # Set the python path 2 | import inspect 3 | import os 4 | import sys 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))) 6 | 7 | import math 8 | import threading 9 | from http.server import HTTPServer 10 | import socket 11 | import time 12 | 13 | from prometheus.collectors import Gauge 14 | from prometheus.registry import Registry 15 | from prometheus.exporter import PrometheusMetricHandler 16 | 17 | PORT_NUMBER = 4444 18 | 19 | 20 | def gather_data(registry): 21 | """Gathers the metrics""" 22 | 23 | # Get the host name of the machine 24 | host = socket.gethostname() 25 | 26 | # Create our collectors 27 | trig_metric = Gauge("trigonometry_example", 28 | "Various trigonometry examples.", 29 | {'host': host}) 30 | 31 | # register the metric collectors 32 | registry.register(trig_metric) 33 | 34 | # Start gathering metrics every second 35 | counter = 0 36 | while True: 37 | time.sleep(1) 38 | 39 | sine = math.sin(math.radians(counter % 360)) 40 | cosine = math.cos(math.radians(counter % 360)) 41 | trig_metric.set({'type': "sine"}, sine) 42 | trig_metric.set({'type': "cosine"}, cosine) 43 | 44 | counter += 1 45 | 46 | if __name__ == "__main__": 47 | 48 | # Create the registry 49 | registry = Registry() 50 | 51 | # Create the thread that gathers the data while we serve it 52 | thread = threading.Thread(target=gather_data, args=(registry, )) 53 | thread.start() 54 | 55 | # Set a server to export (expose to prometheus) the data (in a thread) 56 | try: 57 | # We make this to set the registry in the handler 58 | def handler(*args, **kwargs): 59 | PrometheusMetricHandler(registry, *args, **kwargs) 60 | 61 | server = HTTPServer(('', PORT_NUMBER), handler) 62 | server.serve_forever() 63 | 64 | except KeyboardInterrupt: 65 | server.socket.close() 66 | thread.join() 67 | -------------------------------------------------------------------------------- /prometheus/pusher.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | import requests 4 | 5 | from prometheus.formats import ProtobufFormat 6 | 7 | 8 | # TODO: Fire and forget pushes 9 | class Pusher(object): 10 | """ This class is used to push a registry to a pushgateway""" 11 | 12 | PATH = "/metrics/jobs/{0}" 13 | INSTANCE_PATH = "/metrics/jobs/{0}/instances/{1}" 14 | 15 | def __init__(self, job_name, addr, instance_name=None): 16 | self.job_name = job_name 17 | self.instance_name = instance_name 18 | self.addr = addr 19 | 20 | # push format 21 | self.formatter = ProtobufFormat() 22 | self.headers = self.formatter.get_headers() 23 | 24 | # Set paths 25 | if instance_name: 26 | self.path = urljoin(self.addr, self.__class__.INSTANCE_PATH).format( 27 | job_name, instance_name) 28 | else: 29 | self.path = urljoin(self.addr, self.__class__.PATH).format(job_name) 30 | 31 | def add(self, registry): 32 | """ Add works like replace, but only previously pushed metrics with the 33 | same name (and the same job and instance) will be replaced. 34 | (It uses HTTP method 'POST' to push to the Pushgateway.) 35 | """ 36 | # POST 37 | payload = self.formatter.marshall(registry) 38 | r = requests.post(self.path, data=payload, headers=self.headers) 39 | 40 | def replace(self, registry): 41 | """ Push triggers a metric collection and pushes all collected metrics 42 | to the Pushgateway specified by addr 43 | Note that all previously pushed metrics with the same job and 44 | instance will be replaced with the metrics pushed by this call. 45 | (It uses HTTP method 'PUT' to push to the Pushgateway.) 46 | """ 47 | # PUT 48 | payload = self.formatter.marshall(registry) 49 | r = requests.put(self.path, data=payload, headers=self.headers) 50 | 51 | def delete(self, registry): 52 | # DELETE 53 | payload = self.formatter.marshall(registry) 54 | r = requests.delete(self.path, data=payload, headers=self.headers) 55 | -------------------------------------------------------------------------------- /examples/timing_write_io_example.py: -------------------------------------------------------------------------------- 1 | # Set the python path 2 | import inspect 3 | import os 4 | import sys 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))) 6 | 7 | import math 8 | import threading 9 | from http.server import HTTPServer 10 | import socket 11 | import time 12 | 13 | from prometheus.collectors import Summary 14 | from prometheus.registry import Registry 15 | from prometheus.exporter import PrometheusMetricHandler 16 | 17 | PORT_NUMBER = 4444 18 | 19 | 20 | def gather_data(registry): 21 | """Gathers the metrics""" 22 | 23 | # Get the host name of the machine 24 | host = socket.gethostname() 25 | 26 | # Create our collectors 27 | io_metric = Summary("write_file_io_example", 28 | "Writing io file in disk example.", 29 | {'host': host}) 30 | 31 | # register the metric collectors 32 | registry.register(io_metric) 33 | chunk = b'\xff'*4000 # 4000 bytes 34 | filename_path = "/tmp/prometheus_test" 35 | blocksizes = (100, 10000, 1000000, 100000000) 36 | 37 | # Start gathering metrics every 0.7 seconds 38 | while True: 39 | time.sleep(0.7) 40 | 41 | for i in blocksizes: 42 | time_start = time.time() 43 | # Action 44 | with open(filename_path, "wb") as f: 45 | for _ in range(i // 10000): 46 | f.write(chunk) 47 | 48 | io_metric.add({"file": filename_path, "block": i}, 49 | time.time() - time_start) 50 | 51 | if __name__ == "__main__": 52 | 53 | # Create the registry 54 | registry = Registry() 55 | 56 | # Create the thread that gathers the data while we serve it 57 | thread = threading.Thread(target=gather_data, args=(registry, )) 58 | thread.start() 59 | 60 | # Set a server to export (expose to prometheus) the data (in a thread) 61 | try: 62 | # We make this to set the registry in the handler 63 | def handler(*args, **kwargs): 64 | PrometheusMetricHandler(registry, *args, **kwargs) 65 | 66 | server = HTTPServer(('', PORT_NUMBER), handler) 67 | server.serve_forever() 68 | 69 | except KeyboardInterrupt: 70 | server.socket.close() 71 | thread.join() 72 | -------------------------------------------------------------------------------- /examples/memory_cpu_usage_example.py: -------------------------------------------------------------------------------- 1 | # Set the python path 2 | import inspect 3 | import os 4 | import sys 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))) 6 | 7 | import threading 8 | from http.server import HTTPServer 9 | import socket 10 | import time 11 | 12 | from prometheus.collectors import Gauge 13 | from prometheus.registry import Registry 14 | from prometheus.exporter import PrometheusMetricHandler 15 | import psutil 16 | 17 | PORT_NUMBER = 4444 18 | 19 | 20 | def gather_data(registry): 21 | """Gathers the metrics""" 22 | 23 | # Get the host name of the machine 24 | host = socket.gethostname() 25 | 26 | # Create our collectors 27 | ram_metric = Gauge("memory_usage_bytes", "Memory usage in bytes.", 28 | {'host': host}) 29 | cpu_metric = Gauge("cpu_usage_percent", "CPU usage percent.", 30 | {'host': host}) 31 | 32 | # register the metric collectors 33 | registry.register(ram_metric) 34 | registry.register(cpu_metric) 35 | 36 | # Start gathering metrics every second 37 | while True: 38 | time.sleep(1) 39 | 40 | # Add ram metrics 41 | ram = psutil.virtual_memory() 42 | swap = psutil.swap_memory() 43 | 44 | ram_metric.set({'type': "virtual", }, ram.used) 45 | ram_metric.set({'type': "virtual", 'status': "cached"}, ram.cached) 46 | ram_metric.set({'type': "swap"}, swap.used) 47 | 48 | # Add cpu metrics 49 | for c, p in enumerate(psutil.cpu_percent(interval=1, percpu=True)): 50 | cpu_metric.set({'core': c}, p) 51 | 52 | if __name__ == "__main__": 53 | 54 | # Create the registry 55 | registry = Registry() 56 | 57 | # Create the thread that gathers the data while we serve it 58 | thread = threading.Thread(target=gather_data, args=(registry, )) 59 | thread.start() 60 | 61 | # Set a server to export (expose to prometheus) the data (in a thread) 62 | try: 63 | # We make this to set the registry in the handler 64 | def handler(*args, **kwargs): 65 | PrometheusMetricHandler(registry, *args, **kwargs) 66 | 67 | server = HTTPServer(('', PORT_NUMBER), handler) 68 | server.serve_forever() 69 | 70 | except KeyboardInterrupt: 71 | server.socket.close() 72 | thread.join() 73 | -------------------------------------------------------------------------------- /prometheus/test_registry.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from prometheus.collectors import Collector, Counter, Gauge, Summary 4 | from prometheus.registry import Registry 5 | 6 | 7 | class TestRegistry(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.data = { 11 | 'name': "logged_users_total", 12 | 'help_text': "Logged users in the application", 13 | 'const_labels': {"app": "my_app"}, 14 | } 15 | 16 | def test_register(self): 17 | 18 | q = 100 19 | collectors = [Collector('test'+str(i), 'Test'+str(i)) for i in range(q)] 20 | 21 | r = Registry() 22 | 23 | for i in collectors: 24 | r.register(i) 25 | 26 | self.assertEqual(q, len(r.collectors)) 27 | 28 | def test_register_sames(self): 29 | r = Registry() 30 | 31 | r.register(Collector(**self.data)) 32 | 33 | with self.assertRaises(ValueError) as context: 34 | r.register(Collector(**self.data)) 35 | 36 | self.assertEqual("Collector already exists or name colision", 37 | str(context.exception)) 38 | 39 | def test_register_counter(self): 40 | r = Registry() 41 | r.register(Counter(**self.data)) 42 | 43 | self.assertEqual(1, len(r.collectors)) 44 | 45 | def test_register_gauge(self): 46 | r = Registry() 47 | r.register(Gauge(**self.data)) 48 | 49 | self.assertEqual(1, len(r.collectors)) 50 | 51 | def test_register_summary(self): 52 | r = Registry() 53 | r.register(Summary(**self.data)) 54 | 55 | self.assertEqual(1, len(r.collectors)) 56 | 57 | def test_register_wrong_type(self): 58 | r = Registry() 59 | 60 | with self.assertRaises(TypeError) as context: 61 | r.register("This will fail") 62 | self.assertEqual("Can't register instance, not a valid type of collector", 63 | str(context.exception)) 64 | 65 | def test_deregister(self): 66 | r = Registry() 67 | r.register(Collector(**self.data)) 68 | 69 | r.deregister(self.data['name']) 70 | 71 | self.assertEqual(0, len(r.collectors)) 72 | 73 | def test_get(self): 74 | r = Registry() 75 | c = Collector(**self.data) 76 | r.register(c) 77 | 78 | self.assertEqual(c, r.get(c.name)) 79 | 80 | def test_get_all(self): 81 | q = 100 82 | collectors = [Collector('test'+str(i), 'Test'+str(i)) for i in range(q)] 83 | 84 | r = Registry() 85 | 86 | for i in collectors: 87 | r.register(i) 88 | 89 | result = r.get_all() 90 | 91 | self.assertTrue(isinstance(result, list)) 92 | self.assertEqual(q, len(result)) 93 | -------------------------------------------------------------------------------- /prometheus/test_negotiator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from prometheus.formats import TextFormat, ProtobufFormat, ProtobufTextFormat 4 | from prometheus.negotiator import Negotiator 5 | 6 | 7 | class TestNegotiator(unittest.TestCase): 8 | 9 | def test_protobuffer(self): 10 | headers = ({ 11 | 'accept': "proto=io.prometheus.client.MetricFamily;application/vnd.google.protobuf;encoding=delimited", 12 | 'accept-encoding': "gzip, deflate, sdch", 13 | 'accept-language': "es-ES,es;q=0.8", 14 | }, { 15 | 'Accept': "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited", 16 | 'accept-encoding': "gzip, deflate, sdch", 17 | 'accept-language': "es-ES,es;q=0.8", 18 | }, { 19 | 'ACCEPT': "encoding=delimited;application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily", 20 | 'accept-encoding': "gzip, deflate, sdch", 21 | 'accept-language': "es-ES,es;q=0.8", 22 | }) 23 | 24 | for i in headers: 25 | self.assertEqual(ProtobufFormat, Negotiator.negotiate(i)) 26 | 27 | def test_protobuffer_debug(self): 28 | headers = ({ 29 | 'accept': "proto=io.prometheus.client.MetricFamily;application/vnd.google.protobuf;encoding=text", 30 | 'accept-encoding': "gzip, deflate, sdch", 31 | 'accept-language': "es-ES,es;q=0.8", 32 | }, { 33 | 'Accept': "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=text", 34 | 'accept-encoding': "gzip, deflate, sdch", 35 | 'accept-language': "es-ES,es;q=0.8", 36 | }, { 37 | 'ACCEPT': "encoding=text;application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily", 38 | 'accept-encoding': "gzip, deflate, sdch", 39 | 'accept-language': "es-ES,es;q=0.8", 40 | }) 41 | 42 | for i in headers: 43 | self.assertEqual(ProtobufTextFormat, Negotiator.negotiate(i)) 44 | 45 | def test_text_004(self): 46 | headers = ({ 47 | 'accept': "text/plain; version=0.0.4", 48 | 'accept-encoding': "gzip, deflate, sdch", 49 | 'accept-language': "es-ES,es;q=0.8", 50 | }, { 51 | 'Accept': "text/plain;version=0.0.4", 52 | 'accept-encoding': "gzip, deflate, sdch", 53 | 'accept-language': "es-ES,es;q=0.8", 54 | }, { 55 | 'ACCEPT': " version=0.0.4; text/plain", 56 | 'accept-encoding': "gzip, deflate, sdch", 57 | 'accept-language': "es-ES,es;q=0.8", 58 | }) 59 | 60 | for i in headers: 61 | self.assertEqual(TextFormat, Negotiator.negotiate(i)) 62 | 63 | def test_text_default(self): 64 | headers = ({ 65 | 'Accept': "text/plain;", 66 | 'accept-encoding': "gzip, deflate, sdch", 67 | 'accept-language': "es-ES,es;q=0.8", 68 | }, { 69 | 'accept': "text/plain", 70 | 'accept-encoding': "gzip, deflate, sdch", 71 | 'accept-language': "es-ES,es;q=0.8", 72 | }) 73 | 74 | for i in headers: 75 | self.assertEqual(TextFormat, Negotiator.negotiate(i)) 76 | 77 | def test_default(self): 78 | headers = ({ 79 | 'accept': "application/json", 80 | 'accept-encoding': "gzip, deflate, sdch", 81 | 'accept-language': "es-ES,es;q=0.8", 82 | }, { 83 | 'Accept': "*/*", 84 | 'accept-encoding': "gzip, deflate, sdch", 85 | 'accept-language': "es-ES,es;q=0.8", 86 | }, { 87 | 'ACCEPT': "application/nothing", 88 | 'accept-encoding': "gzip, deflate, sdch", 89 | 'accept-language': "es-ES,es;q=0.8", 90 | }) 91 | 92 | for i in headers: 93 | self.assertEqual(TextFormat, Negotiator.negotiate(i)) 94 | -------------------------------------------------------------------------------- /prometheus/test_metricdict.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from prometheus.metricdict import MetricDict 4 | 5 | 6 | class TestMetricDict(unittest.TestCase): 7 | 8 | def setUp(self): 9 | pass 10 | 11 | def test_bad_keys(self): 12 | with self.assertRaises(TypeError) as context: 13 | metrics = MetricDict() 14 | metrics['not_valid'] = "value" 15 | 16 | self.assertEqual('Only accepts dicts as keys', str(context.exception)) 17 | 18 | def test_set(self): 19 | metrics = MetricDict() 20 | data = ( 21 | ({'a': 1}, 1000), 22 | ({'b': 2, 'c': 3}, 2000), 23 | ({'d': 4, 'e': 5, 'f': 6}, 3000), 24 | ) 25 | 26 | for i in data: 27 | metrics[i[0]] = i[1] 28 | 29 | self.assertEqual(len(data), len(metrics)) 30 | 31 | def test_get(self): 32 | metrics = MetricDict() 33 | data = ( 34 | ({'a': 1}, 1000), 35 | ({'b': 2, 'c': 3}, 2000), 36 | ({'d': 4, 'e': 5, 'f': 6}, 3000), 37 | ) 38 | 39 | for i in data: 40 | metrics[i[0]] = i[1] 41 | 42 | for i in data: 43 | self.assertEqual(i[1], metrics[i[0]]) 44 | 45 | def test_override(self): 46 | metrics = MetricDict() 47 | key = {'a': 1} 48 | 49 | for i in range(100): 50 | metrics[key] = i 51 | self.assertEqual(i, metrics[key]) 52 | 53 | self.assertEqual(1, len(metrics)) 54 | 55 | def test_similar(self): 56 | metrics = MetricDict() 57 | data = ( 58 | ({'d': 4, 'e': 5, 'f': 6}, 3000), 59 | ({'e': 5, 'd': 4, 'f': 6}, 4000), 60 | ({'d': 4, 'f': 6, 'e': 5}, 5000), 61 | ) 62 | 63 | for i in data: 64 | metrics[i[0]] = i[1] 65 | 66 | self.assertEqual(1, len(metrics)) 67 | 68 | def test_access_by_str(self): 69 | label = {'b': 2, 'c': 3, 'a': 1} 70 | access_key = '{"a": 1, "b": 2, "c": 3}' 71 | bad_access_key = '{"b": 2, "c": 3, "a": 1}' 72 | value = 100 73 | 74 | metrics = MetricDict() 75 | metrics[label] = 100 76 | 77 | # Wrong string 78 | with self.assertRaises(TypeError) as context: 79 | metrics['dasdasd'] 80 | self.assertEqual('Only accepts dicts as keys', str(context.exception)) 81 | 82 | # Access ok with string 83 | self.assertEqual(value, metrics[access_key]) 84 | 85 | # Access ok but wrong key by order 86 | with self.assertRaises(KeyError) as context: 87 | metrics[bad_access_key] 88 | self.assertEqual("'{0}'".format(bad_access_key), 89 | str(context.exception)) 90 | 91 | def test_empty_key(self): 92 | metrics = MetricDict() 93 | iterations = 100 94 | 95 | for i in range(iterations): 96 | metrics[None] = i 97 | self.assertEqual(metrics[None], i) 98 | 99 | # Last check (different empty) 100 | self.assertEqual(iterations-1, metrics[""]) 101 | self.assertEqual(iterations-1, metrics[{}]) 102 | 103 | def test_delete(self): 104 | metrics = MetricDict() 105 | data = ( 106 | ({'d': 4, 'e': 5, 'f': 6}, 3000), 107 | ({'e': 5, 'd': 4, 'f': 6}, 4000), 108 | ({'d': 4, 'f': 6, 'e': 5}, 5000), 109 | ({'d': 41, 'f': 61, 'e': 51}, 6000), 110 | ({'d': 41, 'e': 51, 'f': 61}, 7000), 111 | ({'f': 61, 'e': 51, 'd': 41}, 8000), 112 | ) 113 | 114 | for i in data: 115 | metrics[i[0]] = i[1] 116 | 117 | del metrics[i[0]] 118 | 119 | self.assertEqual(1, len(metrics)) 120 | 121 | def test_all(self): 122 | metrics = MetricDict() 123 | data = ( 124 | ({'d': 4, 'e': 5, 'f': 6}, 3000), 125 | ({'e': 5, 'd': 4, 'f': 6}, 4000), 126 | ({'d': 4, 'f': 6, 'e': 5}, 5000), 127 | ({'d': 41, 'f': 61, 'e': 51}, 6000), 128 | ({'d': 41, 'e': 51, 'f': 61}, 7000), 129 | ({'f': 61, 'e': 51, 'd': 41}, 8000), 130 | ) 131 | 132 | for i in data: 133 | metrics[i[0]] = i[1] 134 | 135 | self.assertEqual(2, len(metrics)) 136 | 137 | self.assertEqual(5000, metrics[{'d': 4, 'e': 5, 'f': 6}]) 138 | self.assertEqual(8000, metrics[{'d': 41, 'f': 61, 'e': 51}]) 139 | 140 | -------------------------------------------------------------------------------- /prometheus/test_pusher.py: -------------------------------------------------------------------------------- 1 | from http.server import HTTPServer 2 | import unittest 3 | import threading 4 | 5 | from prometheus.collectors import Counter 6 | from prometheus.pusher import Pusher 7 | from prometheus.registry import Registry 8 | 9 | TEST_PORT = 61423 10 | TEST_HOST = "127.0.0.1" 11 | TEST_URL = "http://{host}:{port}".format(host=TEST_HOST, port=TEST_PORT) 12 | 13 | 14 | from http.server import BaseHTTPRequestHandler 15 | 16 | 17 | class PusherTestHandler(BaseHTTPRequestHandler): 18 | 19 | def __init__(self, test_instance, *args, **kwargs): 20 | self.test_instance = test_instance 21 | 22 | super().__init__(*args, **kwargs) 23 | 24 | # Silence! 25 | def log_message(self, format, *args): 26 | pass 27 | 28 | def do_POST(self): 29 | self.send_response(200) 30 | self.end_headers() 31 | 32 | length = int(self.headers['Content-Length']) 33 | data = self.rfile.read(length) 34 | 35 | # Set the request data to access from the test 36 | self.test_instance.request = { 37 | 'path': self.path, 38 | 'headers': self.headers, 39 | 'method': "POST", 40 | 'body': data, 41 | } 42 | return 43 | 44 | def do_PUT(self): 45 | self.send_response(200) 46 | self.end_headers() 47 | 48 | length = int(self.headers['Content-Length']) 49 | data = self.rfile.read(length) 50 | 51 | # Set the request data to access from the test 52 | self.test_instance.request = { 53 | 'path': self.path, 54 | 'headers': self.headers, 55 | 'method': "PUT", 56 | 'body': data, 57 | } 58 | 59 | return 60 | 61 | def do_DELETE(self): 62 | self.send_response(200) 63 | self.end_headers() 64 | 65 | length = int(self.headers['Content-Length']) 66 | data = self.rfile.read(length) 67 | 68 | # Set the request data to access from the test 69 | self.test_instance.request = { 70 | 'path': self.path, 71 | 'headers': self.headers, 72 | 'method': "DELETE", 73 | 'body': data, 74 | } 75 | return 76 | 77 | 78 | class TestPusher(unittest.TestCase): 79 | 80 | def setUp(self): 81 | # Handler hack 82 | def handler(*args, **kwargs): 83 | PusherTestHandler(self, *args, **kwargs) 84 | 85 | self.server = HTTPServer(('', TEST_PORT), handler) 86 | 87 | # Start a server 88 | thread = threading.Thread(target=self.server.serve_forever) 89 | thread.start() 90 | 91 | def tearDown(self): 92 | self.server.shutdown() 93 | self.server.socket.close() 94 | 95 | def test_push_job_ping(self): 96 | job_name = "my-job" 97 | p = Pusher(job_name, TEST_URL) 98 | registry = Registry() 99 | c = Counter("total_requests", "Total requests.", {}) 100 | registry.register(c) 101 | 102 | c.inc({'url': "/p/user", }) 103 | 104 | # Push to the pushgateway 105 | p.replace(registry) 106 | 107 | # Check the objects that setted the server thread 108 | self.assertEqual(Pusher.PATH.format(job_name), self.request['path']) 109 | 110 | def test_push_instance_ping(self): 111 | job_name = "my-job" 112 | instance_name = "my-instance" 113 | p = Pusher(job_name, TEST_URL, instance_name) 114 | registry = Registry() 115 | c = Counter("total_requests", "Total requests.", {}) 116 | registry.register(c) 117 | 118 | c.inc({'url': "/p/user", }) 119 | 120 | # Push to the pushgateway 121 | p.replace(registry) 122 | 123 | # Check the object that setted the server thread 124 | self.assertEqual(Pusher.INSTANCE_PATH.format(job_name, instance_name), 125 | self.request['path']) 126 | 127 | def test_push_add(self): 128 | job_name = "my-job" 129 | p = Pusher(job_name, TEST_URL) 130 | registry = Registry() 131 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 132 | registry.register(counter) 133 | 134 | counter_data = ( 135 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 136 | ) 137 | 138 | [counter.set(c[0], c[1]) for c in counter_data] 139 | valid_result = b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00\x00\x00\x00\x00\x00y@' 140 | 141 | # Push to the pushgateway 142 | p.add(registry) 143 | 144 | # Check the object that setted the server thread 145 | self.assertEqual("POST", self.request['method']) 146 | self.assertEqual(valid_result, self.request['body']) 147 | 148 | def test_push_replace(self): 149 | job_name = "my-job" 150 | p = Pusher(job_name, TEST_URL) 151 | registry = Registry() 152 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 153 | registry.register(counter) 154 | 155 | counter_data = ( 156 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 157 | ) 158 | 159 | [counter.set(c[0], c[1]) for c in counter_data] 160 | valid_result = b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00\x00\x00\x00\x00\x00y@' 161 | 162 | # Push to the pushgateway 163 | p.replace(registry) 164 | 165 | # Check the object that setted the server thread 166 | self.assertEqual("PUT", self.request['method']) 167 | self.assertEqual(valid_result, self.request['body']) 168 | 169 | def test_push_delete(self): 170 | job_name = "my-job" 171 | p = Pusher(job_name, TEST_URL) 172 | registry = Registry() 173 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 174 | registry.register(counter) 175 | 176 | counter_data = ( 177 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 178 | ) 179 | 180 | [counter.set(c[0], c[1]) for c in counter_data] 181 | valid_result = b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00\x00\x00\x00\x00\x00y@' 182 | 183 | # Push to the pushgateway 184 | p.delete(registry) 185 | 186 | # Check the object that setted the server thread 187 | self.assertEqual("DELETE", self.request['method']) 188 | self.assertEqual(valid_result, self.request['body']) 189 | -------------------------------------------------------------------------------- /prometheus/collectors.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | from multiprocessing import Lock 4 | 5 | import quantile 6 | 7 | from prometheus.metricdict import MetricDict 8 | 9 | 10 | # Used so only one thread can access the values at the same time 11 | mutex = Lock() 12 | 13 | # Used to return the value ordered (not necessary byt for consistency useful) 14 | decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict) 15 | 16 | RESTRICTED_LABELS_NAMES = ('job',) 17 | RESTRICTED_LABELS_PREFIXES = ('__',) 18 | 19 | 20 | class Collector(object): 21 | """Collector is the base class for all the collectors/metrics""" 22 | 23 | REPR_STR = "untyped" 24 | 25 | def __init__(self, name, help_text, const_labels=None): 26 | self.name = name 27 | self.help_text = help_text 28 | self.const_labels = const_labels 29 | 30 | if const_labels: 31 | self._label_names_correct(const_labels) 32 | self.const_labels = const_labels 33 | 34 | # This is a map that contains all the metrics 35 | # This variable should be syncronized 36 | self.values = MetricDict() 37 | 38 | def set_value(self, labels, value): 39 | """ Sets a value in the container""" 40 | 41 | if labels: 42 | self._label_names_correct(labels) 43 | 44 | with mutex: 45 | self.values[labels] = value 46 | 47 | def get_value(self, labels): 48 | """ Gets a value in the container, exception if isn't present""" 49 | 50 | with mutex: 51 | return self.values[labels] 52 | 53 | def get(self, labels): 54 | """Handy alias""" 55 | return self.get_value(labels) 56 | 57 | def _label_names_correct(self, labels): 58 | """Raise exception (ValueError) if labels not correct""" 59 | 60 | for k, v in labels.items(): 61 | # Check reserved labels 62 | if k in RESTRICTED_LABELS_NAMES: 63 | raise ValueError("Labels not correct") 64 | 65 | # Check prefixes 66 | if any(k.startswith(i) for i in RESTRICTED_LABELS_PREFIXES): 67 | raise ValueError("Labels not correct") 68 | 69 | return True 70 | 71 | def get_all(self): 72 | """ Returns a list populated by tuples of 2 elements, first one is 73 | a dict with all the labels and the second elemnt is the value 74 | of the metric itself 75 | """ 76 | with mutex: 77 | items = self.values.items() 78 | 79 | result = [] 80 | for k, v in items: 81 | # Check if is a single value dict (custom empty key) 82 | if not k or k == MetricDict.EMPTY_KEY: 83 | key = None 84 | else: 85 | key = decoder.decode(k) 86 | result.append((key, self.get(k))) 87 | 88 | return result 89 | 90 | 91 | class Counter(Collector): 92 | """ Counter is a Metric that represents a single numerical value that only 93 | ever goes up. 94 | """ 95 | 96 | REPR_STR = "counter" 97 | 98 | def set(self, labels, value): 99 | """ Set is used to set the Counter to an arbitrary value. """ 100 | 101 | self.set_value(labels, value) 102 | 103 | def get(self, labels): 104 | """ Get gets the counter of an arbitrary group of labels""" 105 | 106 | return self.get_value(labels) 107 | 108 | def inc(self, labels): 109 | """ Inc increments the counter by 1.""" 110 | self.add(labels, 1) 111 | 112 | def add(self, labels, value): 113 | """ Add adds the given value to the counter. It panics if the value 114 | is < 0. 115 | """ 116 | 117 | if value < 0: 118 | raise ValueError("Counters can't decrease") 119 | 120 | try: 121 | current = self.get_value(labels) 122 | except KeyError: 123 | current = 0 124 | 125 | self.set_value(labels, current + value) 126 | 127 | 128 | class Gauge(Collector): 129 | """ Gauge is a Metric that represents a single numerical value that can 130 | arbitrarily go up and down. 131 | """ 132 | 133 | REPR_STR = "gauge" 134 | 135 | def set(self, labels, value): 136 | """ Set sets the Gauge to an arbitrary value.""" 137 | 138 | self.set_value(labels, value) 139 | 140 | def get(self, labels): 141 | """ Get gets the Gauge of an arbitrary group of labels""" 142 | 143 | return self.get_value(labels) 144 | 145 | def inc(self, labels): 146 | """ Inc increments the Gauge by 1.""" 147 | self.add(labels, 1) 148 | 149 | def dec(self, labels): 150 | """ Dec decrements the Gauge by 1.""" 151 | self.add(labels, -1) 152 | 153 | def add(self, labels, value): 154 | """ Add adds the given value to the Gauge. (The value can be 155 | negative, resulting in a decrease of the Gauge.) 156 | """ 157 | 158 | try: 159 | current = self.get_value(labels) 160 | except KeyError: 161 | current = 0 162 | 163 | self.set_value(labels, current + value) 164 | 165 | def sub(self, labels, value): 166 | """ Sub subtracts the given value from the Gauge. (The value can be 167 | negative, resulting in an increase of the Gauge.) 168 | """ 169 | self.add(labels, -value) 170 | 171 | 172 | class Summary(Collector): 173 | """ A Summary captures individual observations from an event or sample 174 | stream and summarizes them in a manner similar to traditional summary 175 | statistics: 1. sum of observations, 2. observation count, 176 | 3. rank estimations. 177 | """ 178 | 179 | REPR_STR = "summary" 180 | DEFAULT_INVARIANTS = [(0.50, 0.05), (0.90, 0.01), (0.99, 0.001)] 181 | SUM_KEY = "sum" 182 | COUNT_KEY = "count" 183 | 184 | # Reimplement the setter and getter without mutex because we need to use 185 | # it in a higher level (with the estimator object) 186 | def get_value(self, labels): 187 | return self.values[labels] 188 | 189 | def set_value(self, labels, value): 190 | if labels: 191 | self._label_names_correct(labels) 192 | 193 | self.values[labels] = value 194 | 195 | def add(self, labels, value): 196 | """Add adds a single observation to the summary.""" 197 | 198 | if type(value) not in (float, int): 199 | raise TypeError("Summary only works with digits (int, float)") 200 | 201 | # We have already a lock for data but not for the estimator 202 | with mutex: 203 | try: 204 | e = self.get_value(labels) 205 | except KeyError: 206 | # Initialize quantile estimator 207 | e = quantile.Estimator(*self.__class__.DEFAULT_INVARIANTS) 208 | self.set_value(labels, e) 209 | e.observe(float(value)) 210 | 211 | def get(self, labels): 212 | """ Get gets the data in the form of 0.5, 0.9 and 0.99 percentiles. Also 213 | you get sum and count, all in a dict 214 | """ 215 | 216 | return_data = {} 217 | 218 | # We have already a lock for data but not for the estimator 219 | with mutex: 220 | e = self.get_value(labels) 221 | 222 | # Set invariants data (default to 0.50, 0.90 and 0.99) 223 | for i in e._invariants: 224 | q = i._quantile 225 | return_data[q] = e.query(q) 226 | 227 | # Set sum and count 228 | return_data[self.__class__.SUM_KEY] = e._sum 229 | return_data[self.__class__.COUNT_KEY] = e._observations 230 | 231 | return return_data 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Prometheus python client 3 | ================== 4 | 5 | ## Deprecated, use: https://github.com/prometheus/client_python 6 | 7 | Python 3 client library for [Prometheus](http://prometheus.io) that can 8 | serve data to prometheus (in text and protobuf formats) and also push data 9 | to a pushgateway. 10 | 11 | [![CircleCI](https://circleci.com/gh/slok/prometheus-python.png?style=shield&circle-token=:circle-token)](https://circleci.com/gh/slok/prometheus-python) 12 | [![Coverage Status](https://coveralls.io/repos/slok/prometheus-python/badge.svg?branch=master)](https://coveralls.io/r/slok/prometheus-python?branch=master) 13 | [![PyPI version](https://badge.fury.io/py/prometheus.svg)](http://badge.fury.io/py/prometheus) 14 | 15 | **Table of Contents** 16 | 17 | - [Prometheus python client](#) 18 | - [Status](#) 19 | - [Install](#) 20 | - [Why Python 3 and not python 2?](#) 21 | - [Usage](#) 22 | - [Serve data](#) 23 | - [Push data (to pushgateway)](#) 24 | - [Metrics/Collectors](#) 25 | - [Counter](#) 26 | - [Gauge](#) 27 | - [Summary](#) 28 | - [Labels](#) 29 | - [Const labels](#) 30 | - [Examples](#) 31 | - [Serve examples](#) 32 | - [Gauges](#) 33 | - [Summaries](#) 34 | - [How to use the examples](#) 35 | - [PushGateway examples](#) 36 | - [Gauges](#) 37 | - [How to use the examples](#) 38 | - [Tests](#) 39 | - [TODO](#) 40 | - [Author](#) 41 | - [License](#) 42 | 43 | 44 | Status 45 | ------ 46 | Under *heavy* development 47 | 48 | 49 | Install 50 | ------- 51 | 52 | $ pip install prometheus 53 | 54 | 55 | Why Python 3 and not python 2? 56 | ------------------------------- 57 | 58 | I think that everyone should start adopting the "new" Python version and let 59 | python2 be the old man that every one likes talking to but don't want live be with him. 60 | 61 | And the only way doing this is by "forcing people" to use py3. 62 | 63 | Also Maintaining code for one version is hard, imagine 2... error prone, slower updates... 64 | 65 | So, don't use Python 2 and start using Python 3! 66 | 67 | Usage 68 | ----- 69 | 70 | ### Serve data 71 | 72 | ```python 73 | from http.server import HTTPServer 74 | from prometheus.exporter import PrometheusMetricHandler 75 | from prometheus.registry import Registry 76 | 77 | registry = Registry() 78 | 79 | def handler(*args, **kwargs): 80 | PrometheusMetricHandler(registry, *args, **kwargs) 81 | 82 | server = HTTPServer(('', 8888), handler) 83 | server.serve_forever() 84 | ``` 85 | 86 | ### Push data (to pushgateway) 87 | 88 | TODO 89 | 90 | Metrics/Collectors 91 | ------------------- 92 | 93 | ### Counter 94 | 95 | ```python 96 | from prometheus.collectors import Counter 97 | 98 | uploads_metric = Counter("file_uploads_total", "File total uploads.") 99 | uploads_metric.inc({'type': "png", }) 100 | ``` 101 | 102 | ### Gauge 103 | 104 | ```python 105 | from prometheus.collectors import Gauge 106 | 107 | ram_metric = Gauge("memory_usage_bytes", "Memory usage in bytes.", {'host': host}) 108 | ram_metric.set({'type': "virtual", }, 100) 109 | ``` 110 | 111 | ### Summary 112 | 113 | ```python 114 | from prometheus.collectors import Summary 115 | 116 | http_access = Summary("http_access_time", "HTTP access time", {'time': 'ms'}) 117 | 118 | values = [3, 5.2, 13, 4] 119 | 120 | for i in values: 121 | http_access.add({'time': '/static'}, i) 122 | ``` 123 | 124 | Labels 125 | ------ 126 | 127 | Labels define the multidimensional magic in prometheus. To add a metric to a collector 128 | you identify with a label for example we have this collector that stores the cosumed 129 | memory: 130 | 131 | ```python 132 | ram_metric = Gauge("memory_usage_bytes", "Memory usage in bytes.") 133 | ``` 134 | 135 | And then we add our RAM user MBs: 136 | 137 | ```python 138 | ram_metric.set({'type': "virtual", }, 100) 139 | ``` 140 | 141 | aplying mutidimensional capacity we can store in the same metric the memory consumed by the 142 | swap of our system too: 143 | 144 | ```python 145 | ram_metric.set({'type': "swap", }, 100) 146 | ``` 147 | 148 | Const labels 149 | ------------ 150 | 151 | When you create a `collector` you can put to than collector constant labels, 152 | these constant labels will apply to all the metrics gathered by that collector 153 | apart from the ones that we put. For example this example without const labels 154 | 155 | ```python 156 | ram_metric = Gauge("memory_usage_bytes", "Memory usage in bytes.") 157 | ram_metric.set({'type': "virtual", 'host': host}, 100) 158 | ram_metric.set({'type': "swap", 'host': host}, 100) 159 | ``` 160 | 161 | is the same as this one with const labels: 162 | 163 | ```python 164 | ram_metric = Gauge("memory_usage_bytes", "Memory usage in bytes.", {'host': host}) 165 | ram_metric.set({'type': "virtual", }, 100) 166 | ram_metric.set({'type': "swap", }, 100) 167 | ``` 168 | 169 | Examples 170 | -------- 171 | 172 | ### Serve examples 173 | 174 | #### Gauges 175 | * [Memory and cpu usage](examples/memory_cpu_usage_example.py) (Requires psutil) 176 | * [Trigonometry samples](examples/trigonometry_example.py) 177 | 178 | #### Summaries 179 | * [Disk write IO timing](examples/timing_write_io_example.py) 180 | 181 | #### How to use the examples 182 | 183 | First some examples need requirements, install them: 184 | 185 | pip install requirements_test.txt 186 | 187 | Now run an example, for example [timing_write_io_example.py](examples/timing_write_io_example.py) 188 | 189 | python ./examples/timing_write_io_example.py 190 | 191 | All examples run on port `4444`. You can point prometheus conf like this to 192 | point to one of the examples: 193 | 194 | job: { 195 | name: "python-client-test" 196 | scrape_interval: "1s" 197 | target_group: { 198 | target: "http://xxx.xxx.xxx.xxx:4444/metrics" 199 | } 200 | } 201 | 202 | Or you can test the different formats available with curl: 203 | 204 | Default (Text 0.0.4): 205 | 206 | curl 'http://127.0.0.1:4444/metrics' 207 | 208 | 209 | Text (0.0.4): 210 | 211 | curl 'http://127.0.0.1:4444/metrics' -H 'Accept: text/plain; version=0.0.4' 212 | 213 | 214 | Protobuf debug (0.0.4): 215 | 216 | curl 'http://127.0.0.1:4444/metrics' -H 'Accept: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text' 217 | 218 | Protobuf (0.0.4): 219 | 220 | curl 'http://127.0.0.1:4444/metrics' -H 'Accept: application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited' 221 | 222 | 223 | ### PushGateway examples 224 | 225 | #### Gauges 226 | 227 | * [input digits](examples/input_example.py) 228 | 229 | 230 | #### How to use the examples 231 | 232 | First you need to run a gateway, for example with docker: 233 | 234 | docker run --rm -p 9091:9091 prom/pushgateway 235 | 236 | Now configure prometheus to grab the metrics from the push gateway example 237 | 238 | job: { 239 | name: "pushgateway" 240 | scrape_interval: "1s" 241 | target_group: { 242 | target: "http://172.17.42.1:9091/metrics" 243 | } 244 | } 245 | 246 | Ready to launch the example: 247 | 248 | python ./examples/input_example.py 249 | 250 | As the serve explanation, you can debug de pushgateway serving data by 251 | accessing its URL (in the example: `http://localhost:9091/metrics`) with `Curl` 252 | 253 | Tests 254 | ----- 255 | 256 | $ pip install -r requirements_test.txt 257 | $ ./run_tests.sh 258 | 259 | 260 | TODO 261 | ---- 262 | 263 | * Moaaaar examples 264 | * implement handy utils 265 | 266 | 267 | Author 268 | ------ 269 | 270 | [Xabier (slok) Larrakoetxea](http://xlarrakoetxea.org) 271 | 272 | License 273 | ------- 274 | 275 | [See License](/LICENSE) 276 | -------------------------------------------------------------------------------- /prometheus/test_exporter.py: -------------------------------------------------------------------------------- 1 | from http.server import HTTPServer 2 | import unittest 3 | import threading 4 | import urllib 5 | 6 | import requests 7 | 8 | from prometheus.collectors import Collector, Counter, Gauge, Summary 9 | from prometheus.exporter import PrometheusMetricHandler 10 | from prometheus.formats import TextFormat 11 | from prometheus.registry import Registry 12 | 13 | TEST_PORT = 61423 14 | TEST_HOST = "127.0.0.1" 15 | TEST_URL = "http://{host}:{port}".format(host=TEST_HOST, port=TEST_PORT) 16 | TEST_METRICS_PATH = PrometheusMetricHandler.METRICS_PATH 17 | 18 | 19 | class TestPrometheusMetricHandler(PrometheusMetricHandler): 20 | """Custom handler to not show request messages when running tests""" 21 | def log_message(self, format, *args): 22 | pass 23 | 24 | 25 | class TestTextExporter(unittest.TestCase): 26 | 27 | def setUp(self): 28 | # Create the registry 29 | self.registry = Registry() 30 | 31 | # Handler hack 32 | def handler(*args, **kwargs): 33 | TestPrometheusMetricHandler(self.registry, *args, **kwargs) 34 | 35 | self.server = HTTPServer(('', TEST_PORT), handler) 36 | 37 | # Start a server 38 | thread = threading.Thread(target=self.server.serve_forever) 39 | thread.start() 40 | 41 | def test_counter(self): 42 | 43 | # Add some metrics 44 | data = ( 45 | ({'data': 1}, 100), 46 | ({'data': "2"}, 200), 47 | ({'data': 3}, 300), 48 | ({'data': 1}, 400), 49 | ) 50 | c = Counter("test_counter", "Test Counter.", {'test': "test_counter"}) 51 | self.registry.register(c) 52 | 53 | for i in data: 54 | c.set(i[0], i[1]) 55 | 56 | headers = {'accept': 'text/plain; version=0.0.4'} 57 | url = urllib.parse.urljoin(TEST_URL, TEST_METRICS_PATH[1:]) 58 | r = requests.get(url, headers=headers) 59 | 60 | valid_data = """# HELP test_counter Test Counter. 61 | # TYPE test_counter counter 62 | test_counter{data="1",test="test_counter"} 400 63 | test_counter{data="2",test="test_counter"} 200 64 | test_counter{data="3",test="test_counter"} 300 65 | """ 66 | self.assertEqual("text/plain; version=0.0.4; charset=utf-8", 67 | r.headers['content-type']) 68 | self.assertEqual(200, r.status_code) 69 | self.assertEqual(valid_data, r.text) 70 | 71 | def test_gauge(self): 72 | 73 | # Add some metrics 74 | data = ( 75 | ({'data': 1}, 100), 76 | ({'data': "2"}, 200), 77 | ({'data': 3}, 300), 78 | ({'data': 1}, 400), 79 | ) 80 | g = Gauge("test_gauge", "Test Gauge.", {'test': "test_gauge"}) 81 | self.registry.register(g) 82 | 83 | for i in data: 84 | g.set(i[0], i[1]) 85 | 86 | headers = {'accept': 'text/plain; version=0.0.4'} 87 | url = urllib.parse.urljoin(TEST_URL, TEST_METRICS_PATH[1:]) 88 | r = requests.get(url, headers=headers) 89 | 90 | valid_data = """# HELP test_gauge Test Gauge. 91 | # TYPE test_gauge gauge 92 | test_gauge{data="1",test="test_gauge"} 400 93 | test_gauge{data="2",test="test_gauge"} 200 94 | test_gauge{data="3",test="test_gauge"} 300 95 | """ 96 | self.assertEqual("text/plain; version=0.0.4; charset=utf-8", 97 | r.headers['content-type']) 98 | self.assertEqual(200, r.status_code) 99 | self.assertEqual(valid_data, r.text) 100 | 101 | def test_summary(self): 102 | 103 | # Add some metrics 104 | data = [3, 5.2, 13, 4] 105 | label = {'data': 1} 106 | 107 | s = Summary("test_summary", "Test Summary.", {'test': "test_summary"}) 108 | self.registry.register(s) 109 | 110 | for i in data: 111 | s.add(label, i) 112 | 113 | headers = {'accept': 'text/plain; version=0.0.4'} 114 | url = urllib.parse.urljoin(TEST_URL, TEST_METRICS_PATH[1:]) 115 | r = requests.get(url, headers=headers) 116 | 117 | valid_data = """# HELP test_summary Test Summary. 118 | # TYPE test_summary summary 119 | test_summary_count{data="1",test="test_summary"} 4 120 | test_summary_sum{data="1",test="test_summary"} 25.2 121 | test_summary{data="1",quantile="0.5",test="test_summary"} 4.0 122 | test_summary{data="1",quantile="0.9",test="test_summary"} 5.2 123 | test_summary{data="1",quantile="0.99",test="test_summary"} 5.2 124 | """ 125 | self.assertEqual("text/plain; version=0.0.4; charset=utf-8", 126 | r.headers['content-type']) 127 | self.assertEqual(200, r.status_code) 128 | self.assertEqual(valid_data, r.text) 129 | 130 | def test_all(self): 131 | format_times = 10 132 | 133 | counter_data = ( 134 | ({'c_sample': '1'}, 100), 135 | ({'c_sample': '2'}, 200), 136 | ({'c_sample': '3'}, 300), 137 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 138 | ) 139 | 140 | gauge_data = ( 141 | ({'g_sample': '1'}, 500), 142 | ({'g_sample': '2'}, 600), 143 | ({'g_sample': '3'}, 700), 144 | ({'g_sample': '1', 'g_subsample': 'b'}, 800), 145 | ) 146 | 147 | summary_data = ( 148 | ({'s_sample': '1'}, range(1000, 2000, 4)), 149 | ({'s_sample': '2'}, range(2000, 3000, 20)), 150 | ({'s_sample': '3'}, range(3000, 4000, 13)), 151 | ({'s_sample': '1', 's_subsample': 'b'}, range(4000, 5000, 47)), 152 | ) 153 | 154 | registry = Registry() 155 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 156 | gauge = Gauge("gauge_test", "A gauge.", {'type': "gauge"}) 157 | summary = Summary("summary_test", "A summary.", {'type': "summary"}) 158 | 159 | self.registry.register(counter) 160 | self.registry.register(gauge) 161 | self.registry.register(summary) 162 | 163 | # Add data 164 | [counter.set(c[0], c[1]) for c in counter_data] 165 | [gauge.set(g[0], g[1]) for g in gauge_data] 166 | [summary.add(i[0], s) for i in summary_data for s in i[1]] 167 | 168 | registry.register(counter) 169 | registry.register(gauge) 170 | registry.register(summary) 171 | 172 | valid_data = """# HELP counter_test A counter. 173 | # TYPE counter_test counter 174 | counter_test{c_sample="1",c_subsample="b",type="counter"} 400 175 | counter_test{c_sample="1",type="counter"} 100 176 | counter_test{c_sample="2",type="counter"} 200 177 | counter_test{c_sample="3",type="counter"} 300 178 | # HELP gauge_test A gauge. 179 | # TYPE gauge_test gauge 180 | gauge_test{g_sample="1",g_subsample="b",type="gauge"} 800 181 | gauge_test{g_sample="1",type="gauge"} 500 182 | gauge_test{g_sample="2",type="gauge"} 600 183 | gauge_test{g_sample="3",type="gauge"} 700 184 | # HELP summary_test A summary. 185 | # TYPE summary_test summary 186 | summary_test_count{s_sample="1",s_subsample="b",type="summary"} 22 187 | summary_test_count{s_sample="1",type="summary"} 250 188 | summary_test_count{s_sample="2",type="summary"} 50 189 | summary_test_count{s_sample="3",type="summary"} 77 190 | summary_test_sum{s_sample="1",s_subsample="b",type="summary"} 98857.0 191 | summary_test_sum{s_sample="1",type="summary"} 374500.0 192 | summary_test_sum{s_sample="2",type="summary"} 124500.0 193 | summary_test_sum{s_sample="3",type="summary"} 269038.0 194 | summary_test{quantile="0.5",s_sample="1",s_subsample="b",type="summary"} 4235.0 195 | summary_test{quantile="0.5",s_sample="1",type="summary"} 1272.0 196 | summary_test{quantile="0.5",s_sample="2",type="summary"} 2260.0 197 | summary_test{quantile="0.5",s_sample="3",type="summary"} 3260.0 198 | summary_test{quantile="0.9",s_sample="1",s_subsample="b",type="summary"} 4470.0 199 | summary_test{quantile="0.9",s_sample="1",type="summary"} 1452.0 200 | summary_test{quantile="0.9",s_sample="2",type="summary"} 2440.0 201 | summary_test{quantile="0.9",s_sample="3",type="summary"} 3442.0 202 | summary_test{quantile="0.99",s_sample="1",s_subsample="b",type="summary"} 4517.0 203 | summary_test{quantile="0.99",s_sample="1",type="summary"} 1496.0 204 | summary_test{quantile="0.99",s_sample="2",type="summary"} 2500.0 205 | summary_test{quantile="0.99",s_sample="3",type="summary"} 3494.0 206 | """ 207 | 208 | headers = {'accept': 'text/plain; version=0.0.4'} 209 | url = urllib.parse.urljoin(TEST_URL, TEST_METRICS_PATH[1:]) 210 | r = requests.get(url, headers=headers) 211 | 212 | self.assertEqual("text/plain; version=0.0.4; charset=utf-8", 213 | r.headers['content-type']) 214 | self.assertEqual(200, r.status_code) 215 | self.assertEqual(valid_data, r.text) 216 | 217 | def tearDown(self): 218 | self.server.shutdown() 219 | self.server.socket.close() 220 | -------------------------------------------------------------------------------- /prometheus/formats.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | import collections 3 | 4 | from google.protobuf.internal import encoder 5 | 6 | from prometheus import collectors 7 | from prometheus import utils 8 | from prometheus.pb2 import metrics_pb2 9 | 10 | 11 | class PrometheusFormat(object): 12 | 13 | __metaclass__ = ABCMeta 14 | 15 | @abstractmethod 16 | def get_headers(self): 17 | """ Returns the headers of the communication format""" 18 | pass 19 | 20 | @abstractmethod 21 | def _format_counter(self, counter, name): 22 | """ Returns a representation of a counter value in the implemented 23 | format. Receives a tuple with the labels (a dict) as first element 24 | and the value as a second element 25 | """ 26 | pass 27 | 28 | @abstractmethod 29 | def _format_gauge(self, gauge, name): 30 | """ Returns a representation of a gauge value in the implemented 31 | format. Receives a tuple with the labels (a dict) as first element 32 | and the value as a second element 33 | """ 34 | pass 35 | 36 | @abstractmethod 37 | def _format_sumary(self, summary, name): 38 | """ Returns a representation of a summary value in the implemented 39 | format. Receives a tuple with the labels (a dict) as first element 40 | and the value as a second element 41 | """ 42 | pass 43 | 44 | @abstractmethod 45 | def marshall(self, registry): 46 | """ Marshalls a registry and returns the storage/transfer format """ 47 | pass 48 | 49 | 50 | class TextFormat(PrometheusFormat): 51 | # Header information 52 | CONTENT = 'text/plain' 53 | VERSION = '0.0.4' 54 | 55 | # Formats for values 56 | HELP_FMT = "# HELP {name} {help_text}" 57 | TYPE_FMT = "# TYPE {name} {value_type}" 58 | COMMENT_FMT = "# {comment}" 59 | LABEL_FMT = "{key}=\"{value}\"" 60 | LABEL_SEPARATOR_FMT = "," 61 | LINE_SEPARATOR_FMT = "\n" 62 | COUNTER_FMT = "{name}{labels} {value} {timestamp}" 63 | GAUGE_FMT = COUNTER_FMT 64 | SUMMARY_FMTS = { 65 | 'quantile': "{name}{labels} {value} {timestamp}", 66 | 'sum': "{name}_sum{labels} {value} {timestamp}", 67 | 'count': "{name}_count{labels} {value} {timestamp}", 68 | } 69 | 70 | def __init__(self, timestamp=False): 71 | """timestamp is a boolean, if you want timestamp in each metric""" 72 | self.timestamp = timestamp 73 | 74 | def get_headers(self): 75 | headers = { 76 | 'Content-Type': "{0}; version={1}; charset=utf-8".format( 77 | TextFormat.CONTENT, 78 | TextFormat.VERSION), 79 | } 80 | 81 | return headers 82 | 83 | def _format_line(self, name, labels, value, const_labels=None): 84 | labels_str = "" 85 | ts = "" 86 | # Unify the const_labels and labels 87 | # Consta labels have lower priority than labels 88 | labels = utils.unify_labels(labels, const_labels, True) 89 | 90 | # Create the label string 91 | if labels: 92 | labels_str = [TextFormat.LABEL_FMT.format(key=k, value=v) 93 | for k, v in labels.items()] 94 | labels_str = TextFormat.LABEL_SEPARATOR_FMT.join(labels_str) 95 | labels_str = "{{{labels}}}".format(labels=labels_str) 96 | 97 | if self.timestamp: 98 | ts = utils.get_timestamp() 99 | 100 | result = TextFormat.COUNTER_FMT.format(name=name, labels=labels_str, 101 | value=value, timestamp=ts) 102 | 103 | return result.strip() 104 | 105 | def _format_counter(self, counter, name, const_labels): 106 | return self._format_line(name, counter[0], counter[1], const_labels) 107 | 108 | def _format_gauge(self, gauge, name, const_labels): 109 | return self._format_line(name, gauge[0], gauge[1], const_labels) 110 | 111 | def _format_summary(self, summary, name, const_labels): 112 | results = [] 113 | 114 | for k, v in summary[1].items(): 115 | # Stat from a fresh dict for the labels (new or with preset data) 116 | if summary[0]: 117 | labels = summary[0].copy() 118 | else: 119 | labels = {} 120 | 121 | # Quantiles need labels and not special name (like sum and count) 122 | if type(k) is not float: 123 | name_str = "{0}_{1}".format(name, k) 124 | else: 125 | labels['quantile'] = k 126 | name_str = name 127 | results.append(self._format_line(name_str, labels, v, 128 | const_labels)) 129 | 130 | return results 131 | 132 | def marshall_lines(self, collector): 133 | """ Marshalls a collector and returns the storage/transfer format in 134 | a tuple, this tuple has reprensentation format per element. 135 | """ 136 | 137 | if isinstance(collector, collectors.Counter): 138 | exec_method = self._format_counter 139 | elif isinstance(collector, collectors.Gauge): 140 | exec_method = self._format_gauge 141 | elif isinstance(collector, collectors.Summary): 142 | exec_method = self._format_summary 143 | else: 144 | raise TypeError("Not a valid object format") 145 | 146 | # create headers 147 | help_header = TextFormat.HELP_FMT.format(name=collector.name, 148 | help_text=collector.help_text) 149 | 150 | type_header = TextFormat.TYPE_FMT.format(name=collector.name, 151 | value_type=collector.REPR_STR) 152 | 153 | # Prepare start headers 154 | lines = [help_header, type_header] 155 | 156 | for i in collector.get_all(): 157 | r = exec_method(i, collector.name, collector.const_labels) 158 | 159 | # Check if it returns one or multiple lines 160 | if not isinstance(r, str) and isinstance(r, collections.Iterable): 161 | lines.extend(r) 162 | else: 163 | lines.append(r) 164 | 165 | return lines 166 | 167 | def marshall_collector(self, collector): 168 | # need sort? 169 | result = sorted(self.marshall_lines(collector)) 170 | return self.__class__.LINE_SEPARATOR_FMT.join(result) 171 | 172 | def marshall(self, registry): 173 | """Marshalls a full registry (various collectors)""" 174 | 175 | blocks = [] 176 | for i in registry.get_all(): 177 | blocks.append(self.marshall_collector(i)) 178 | 179 | # Sort? used in tests 180 | blocks = sorted(blocks) 181 | 182 | # Needs EOF 183 | blocks.append("") 184 | 185 | return self.__class__.LINE_SEPARATOR_FMT.join(blocks) 186 | 187 | 188 | class ProtobufFormat(PrometheusFormat): 189 | # Header information 190 | CONTENT = 'application/vnd.google.protobuf' 191 | PROTO = 'io.prometheus.client.MetricFamily' 192 | ENCODING = 'delimited' 193 | VERSION = '0.0.4' 194 | LINE_SEPARATOR_FMT = "\n" 195 | 196 | def __init__(self, timestamp=False): 197 | """timestamp is a boolean, if you want timestamp in each metric""" 198 | self.timestamp = timestamp 199 | 200 | def get_headers(self): 201 | headers = { 202 | 'Content-Type': "{0}; proto={1}; encoding={2}".format( 203 | self.__class__.CONTENT, 204 | self.__class__.PROTO, 205 | self.__class__.ENCODING, 206 | ), 207 | } 208 | 209 | return headers 210 | 211 | def _create_pb2_labels(self, labels): 212 | result = [] 213 | for k, v in labels.items(): 214 | l = metrics_pb2.LabelPair(name=k, value=str(v)) 215 | result.append(l) 216 | return result 217 | 218 | def _format_counter(self, counter, name, const_labels): 219 | labels = utils.unify_labels(counter[0], const_labels, ordered=True) 220 | 221 | # With a counter and labelpairs we do a Metric 222 | pb2_labels = self._create_pb2_labels(labels) 223 | counter = metrics_pb2.Counter(value=counter[1]) 224 | 225 | metric = metrics_pb2.Metric(label=pb2_labels, counter=counter) 226 | if self.timestamp: 227 | metric.timestamp_ms = utils.get_timestamp() 228 | 229 | return metric 230 | 231 | def _format_gauge(self, gauge, name, const_labels): 232 | labels = utils.unify_labels(gauge[0], const_labels, ordered=True) 233 | 234 | pb2_labels = self._create_pb2_labels(labels) 235 | gauge = metrics_pb2.Gauge(value=gauge[1]) 236 | 237 | metric = metrics_pb2.Metric(label=pb2_labels, gauge=gauge) 238 | if self.timestamp: 239 | metric.timestamp_ms = utils.get_timestamp() 240 | return metric 241 | 242 | def _format_summary(self, summary, name, const_labels): 243 | labels = utils.unify_labels(summary[0], const_labels, ordered=True) 244 | 245 | pb2_labels = self._create_pb2_labels(labels) 246 | 247 | # Create the quantiles 248 | quantiles = [] 249 | 250 | for k, v in summary[1].items(): 251 | if not isinstance(k, str): 252 | q = metrics_pb2.Quantile(quantile=k, value=v) 253 | quantiles.append(q) 254 | 255 | summary = metrics_pb2.Summary(sample_count=summary[1]['count'], 256 | sample_sum=summary[1]['sum'], 257 | quantile=quantiles) 258 | 259 | metric = metrics_pb2.Metric(label=pb2_labels, summary=summary) 260 | if self.timestamp: 261 | metric.timestamp_ms = utils.get_timestamp() 262 | 263 | return metric 264 | 265 | def marshall_collector(self, collector): 266 | 267 | if isinstance(collector, collectors.Counter): 268 | metric_type = metrics_pb2.COUNTER 269 | exec_method = self._format_counter 270 | elif isinstance(collector, collectors.Gauge): 271 | metric_type = metrics_pb2.GAUGE 272 | exec_method = self._format_gauge 273 | elif isinstance(collector, collectors.Summary): 274 | metric_type = metrics_pb2.SUMMARY 275 | exec_method = self._format_summary 276 | else: 277 | raise TypeError("Not a valid object format") 278 | 279 | metrics = [] 280 | 281 | for i in collector.get_all(): 282 | r = exec_method(i, collector.name, collector.const_labels) 283 | metrics.append(r) 284 | 285 | pb2_collector = metrics_pb2.MetricFamily(name=collector.name, 286 | help=collector.help_text, 287 | type=metric_type, 288 | metric=metrics) 289 | return pb2_collector 290 | 291 | def marshall(self, registry): 292 | """Returns bytes""" 293 | result = b"" 294 | 295 | for i in registry.get_all(): 296 | # Each message needs to be prefixed with a varint with the size of 297 | # the message (MetrycType) 298 | # https://github.com/matttproud/golang_protobuf_extensions/blob/master/ext/encode.go 299 | # http://zombietetris.de/blog/building-your-own-writedelimitedto-for-python-protobuf/ 300 | body = self.marshall_collector(i).SerializeToString() 301 | msg = encoder._VarintBytes(len(body)) + body 302 | result += msg 303 | 304 | return result 305 | 306 | 307 | class ProtobufTextFormat(ProtobufFormat): 308 | """Return protobuf data as text, only for debugging""" 309 | 310 | ENCODING = 'test' 311 | 312 | def marshall(self, registry): 313 | blocks = [] 314 | 315 | for i in registry.get_all(): 316 | blocks.append(str(self.marshall_collector(i))) 317 | 318 | return self.__class__.LINE_SEPARATOR_FMT.join(blocks) 319 | -------------------------------------------------------------------------------- /prometheus/test_collectors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from prometheus.collectors import Collector, Counter, Gauge, Summary 4 | 5 | 6 | class TestCollectorDict(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.data = { 10 | 'name': "logged_users_total", 11 | 'help_text': "Logged users in the application", 12 | 'const_labels': {"app": "my_app"}, 13 | } 14 | 15 | self.c = Collector(**self.data) 16 | 17 | def test_initialization(self): 18 | self.assertEqual(self.data['name'], self.c.name) 19 | self.assertEqual(self.data['help_text'], self.c.help_text) 20 | self.assertEqual(self.data['const_labels'], self.c.const_labels) 21 | 22 | def test_set_value(self): 23 | data = ( 24 | ({'country': "sp", "device": "desktop"}, 520), 25 | ({'country': "us", "device": "mobile"}, 654), 26 | ({'country': "uk", "device": "desktop"}, 1001), 27 | ({'country': "de", "device": "desktop"}, 995), 28 | ) 29 | 30 | for m in data: 31 | self.c.set_value(m[0], m[1]) 32 | 33 | self.assertEqual(len(data), len(self.c.values)) 34 | 35 | def test_same_value(self): 36 | data = ( 37 | ({'country': "sp", "device": "desktop", "ts": "GMT+1"}, 520), 38 | ({"ts": "GMT+1", 'country': "sp", "device": "desktop"}, 521), 39 | ({'country': "sp", "ts": "GMT+1", "device": "desktop"}, 522), 40 | ({"device": "desktop", "ts": "GMT+1", 'country': "sp"}, 523), 41 | ) 42 | 43 | for m in data: 44 | self.c.set_value(m[0], m[1]) 45 | 46 | self.assertEqual(1, len(self.c.values)) 47 | self.assertEqual(523, self.c.values[data[0][0]]) 48 | 49 | def test_get_value(self): 50 | data = ( 51 | ({'country': "sp", "device": "desktop"}, 520), 52 | ({'country': "us", "device": "mobile"}, 654), 53 | ({'country': "uk", "device": "desktop"}, 1001), 54 | ({'country': "de", "device": "desktop"}, 995), 55 | ) 56 | 57 | for m in data: 58 | self.c.set_value(m[0], m[1]) 59 | 60 | for m in data: 61 | self.assertEqual(m[1], self.c.get_value(m[0])) 62 | 63 | def test_not_const_labels(self): 64 | del self.data['const_labels'] 65 | self.c = Collector(**self.data) 66 | 67 | def test_not_name(self): 68 | with self.assertRaises(TypeError) as context: 69 | del self.data['name'] 70 | self.c = Collector(**self.data) 71 | 72 | self.assertEqual( 73 | "__init__() missing 1 required positional argument: 'name'", 74 | str(context.exception)) 75 | 76 | def test_not_help_text(self): 77 | with self.assertRaises(TypeError) as context: 78 | del self.data['help_text'] 79 | self.c = Collector(**self.data) 80 | 81 | self.assertEqual( 82 | "__init__() missing 1 required positional argument: 'help_text'", 83 | str(context.exception)) 84 | 85 | def test_without_labels(self): 86 | data = ( 87 | ({}, 520), 88 | (None, 654), 89 | ("", 1001), 90 | ) 91 | 92 | for i in data: 93 | self.c.set_value(i[0], i[1]) 94 | 95 | self.assertEqual(1, len(self.c.values)) 96 | self.assertEqual((data)[len(data)-1][1], self.c.values[data[0][0]]) 97 | 98 | #def test_set_value_mutex(self): 99 | # # TODO: Check mutex 100 | # pass 101 | 102 | def test_wrong_labels(self): 103 | 104 | # Normal set 105 | with self.assertRaises(ValueError) as context: 106 | self.c.set_value({'job': 1, 'ok': 2}, 1) 107 | 108 | self.assertEqual('Labels not correct', str(context.exception)) 109 | 110 | with self.assertRaises(ValueError) as context: 111 | self.c.set_value({'__not_ok': 1, 'ok': 2}, 1) 112 | 113 | self.assertEqual('Labels not correct', str(context.exception)) 114 | 115 | # Constructor set 116 | with self.assertRaises(ValueError) as context: 117 | Collector("x", "y", {'job': 1, 'ok': 2}) 118 | 119 | self.assertEqual('Labels not correct', str(context.exception)) 120 | 121 | with self.assertRaises(ValueError) as context: 122 | Collector("x", "y", {'__not_ok': 1, 'ok': 2}) 123 | 124 | self.assertEqual('Labels not correct', str(context.exception)) 125 | 126 | def test_get_all(self): 127 | data = ( 128 | ({'country': "sp", "device": "desktop"}, 520), 129 | ({'country': "us", "device": "mobile"}, 654), 130 | ({'country': "uk", "device": "desktop"}, 1001), 131 | ({'country': "de", "device": "desktop"}, 995), 132 | ({'country': "zh", "device": "desktop"}, 520), 133 | ({'country': "ch", "device": "mobile"}, 654), 134 | ({'country': "ca", "device": "desktop"}, 1001), 135 | ({'country': "jp", "device": "desktop"}, 995), 136 | ({'country': "au", "device": "desktop"}, 520), 137 | ({'country': "py", "device": "mobile"}, 654), 138 | ({'country': "ar", "device": "desktop"}, 1001), 139 | ({'country': "pt", "device": "desktop"}, 995), 140 | ) 141 | 142 | for i in data: 143 | self.c.set_value(i[0], i[1]) 144 | 145 | sort_fn = lambda x: x[0]['country'] 146 | sorted_data = sorted(data, key=sort_fn) 147 | sorted_result = sorted(self.c.get_all(), key=sort_fn) 148 | self.assertEqual(sorted_data, sorted_result) 149 | 150 | 151 | class TestCounter(unittest.TestCase): 152 | 153 | def setUp(self): 154 | self.data = { 155 | 'name': "logged_users_total", 156 | 'help_text': "Logged users in the application", 157 | 'const_labels': {"app": "my_app"}, 158 | } 159 | 160 | self.c = Counter(**self.data) 161 | 162 | def test_set(self): 163 | 164 | data = ( 165 | { 166 | 'labels': {'country': "sp", "device": "desktop"}, 167 | 'values': range(10) 168 | }, 169 | { 170 | 'labels': {'country': "us", "device": "mobile"}, 171 | 'values': range(10, 20) 172 | }, 173 | { 174 | 'labels': {'country': "uk", "device": "desktop"}, 175 | 'values': range(20, 30) 176 | } 177 | ) 178 | 179 | for i in data: 180 | for j in i['values']: 181 | self.c.set(i['labels'], j) 182 | 183 | self.assertEqual(len(data), len(self.c.values)) 184 | 185 | def test_get(self): 186 | data = ( 187 | { 188 | 'labels': {'country': "sp", "device": "desktop"}, 189 | 'values': range(10) 190 | }, 191 | { 192 | 'labels': {'country': "us", "device": "mobile"}, 193 | 'values': range(10, 20) 194 | }, 195 | { 196 | 'labels': {'country': "uk", "device": "desktop"}, 197 | 'values': range(20, 30) 198 | } 199 | ) 200 | 201 | for i in data: 202 | for j in i['values']: 203 | self.c.set(i['labels'], j) 204 | self.assertEqual(j, self.c.get(i['labels'])) 205 | 206 | # Last check 207 | for i in data: 208 | self.assertEqual(max(i['values']), self.c.get(i['labels'])) 209 | 210 | def test_set_get_without_labels(self): 211 | data = { 212 | 'labels': {}, 213 | 'values': range(100) 214 | } 215 | 216 | for i in data['values']: 217 | self.c.set(data['labels'], i) 218 | 219 | self.assertEqual(1, len(self.c.values)) 220 | 221 | self.assertEqual(max(data['values']), self.c.get(data['labels'])) 222 | 223 | def test_inc(self): 224 | iterations = 100 225 | labels = {'country': "sp", "device": "desktop"} 226 | 227 | for i in range(iterations): 228 | self.c.inc(labels) 229 | 230 | self.assertEqual(iterations, self.c.get(labels)) 231 | 232 | def test_add(self): 233 | labels = {'country': "sp", "device": "desktop"} 234 | iterations = 100 235 | 236 | for i in range(iterations): 237 | self.c.add(labels, i) 238 | 239 | self.assertEqual(sum(range(iterations)), self.c.get(labels)) 240 | 241 | def test_negative_add(self): 242 | labels = {'country': "sp", "device": "desktop"} 243 | 244 | with self.assertRaises(ValueError) as context: 245 | self.c.add(labels, -1) 246 | self.assertEqual('Counters can\'t decrease', str(context.exception)) 247 | 248 | 249 | class TestGauge(unittest.TestCase): 250 | 251 | def setUp(self): 252 | self.data = { 253 | 'name': "hdd_disk_used", 254 | 'help_text': "Disk space used", 255 | 'const_labels': {"server": "1.db.production.my-app"}, 256 | } 257 | 258 | self.g = Gauge(**self.data) 259 | 260 | def test_set(self): 261 | data = ( 262 | { 263 | 'labels': {'max': "500G", 'dev': "sda"}, 264 | 'values': range(0, 500, 50) 265 | }, 266 | { 267 | 'labels': {'max': "1T", 'dev': "sdb"}, 268 | 'values': range(0, 1000, 100) 269 | }, 270 | { 271 | 'labels': {'max': "10T", 'dev': "sdc"}, 272 | 'values': range(0, 10000, 1000) 273 | } 274 | ) 275 | 276 | for i in data: 277 | for j in i['values']: 278 | self.g.set(i['labels'], j) 279 | 280 | self.assertEqual(len(data), len(self.g.values)) 281 | 282 | def test_get(self): 283 | data = ( 284 | { 285 | 'labels': {'max': "500G", 'dev': "sda"}, 286 | 'values': range(0, 500, 50) 287 | }, 288 | { 289 | 'labels': {'max': "1T", 'dev': "sdb"}, 290 | 'values': range(0, 1000, 100) 291 | }, 292 | { 293 | 'labels': {'max': "10T", 'dev': "sdc"}, 294 | 'values': range(0, 10000, 1000) 295 | } 296 | ) 297 | 298 | for i in data: 299 | for j in i['values']: 300 | self.g.set(i['labels'], j) 301 | self.assertEqual(j, self.g.get(i['labels'])) 302 | 303 | for i in data: 304 | self.assertEqual(max(i['values']), self.g.get(i['labels'])) 305 | 306 | def test_set_get_without_labels(self): 307 | data = { 308 | 'labels': {}, 309 | 'values': range(100) 310 | } 311 | 312 | for i in data['values']: 313 | self.g.set(data['labels'], i) 314 | 315 | self.assertEqual(1, len(self.g.values)) 316 | 317 | self.assertEqual(max(data['values']), self.g.get(data['labels'])) 318 | 319 | def test_inc(self): 320 | iterations = 100 321 | labels = {'max': "10T", 'dev': "sdc"} 322 | 323 | for i in range(iterations): 324 | self.g.inc(labels) 325 | self.assertEqual(i+1, self.g.get(labels)) 326 | 327 | self.assertEqual(iterations, self.g.get(labels)) 328 | 329 | def test_dec(self): 330 | iterations = 100 331 | labels = {'max': "10T", 'dev': "sdc"} 332 | self.g.set(labels, iterations) 333 | 334 | for i in range(iterations): 335 | self.g.dec(labels) 336 | self.assertEqual(iterations-(i+1), self.g.get(labels)) 337 | 338 | self.assertEqual(0, self.g.get(labels)) 339 | 340 | def test_add(self): 341 | iterations = 100 342 | labels = {'max': "10T", 'dev': "sdc"} 343 | 344 | for i in range(iterations): 345 | self.g.add(labels, i) 346 | 347 | self.assertEqual(sum(range(iterations)), self.g.get(labels)) 348 | 349 | def test_add_negative(self): 350 | iterations = 100 351 | labels = {'max': "10T", 'dev': "sdc"} 352 | 353 | for i in range(iterations): 354 | self.g.add(labels, -i) 355 | 356 | self.assertEqual(sum(map(lambda x: -x, range(iterations))), 357 | self.g.get(labels)) 358 | 359 | def test_sub(self): 360 | iterations = 100 361 | labels = {'max': "10T", 'dev': "sdc"} 362 | 363 | for i in range(iterations): 364 | self.g.sub(labels, i) 365 | 366 | self.assertEqual(sum(map(lambda x: -x, range(iterations))), 367 | self.g.get(labels)) 368 | 369 | def test_sub_positive(self): 370 | iterations = 100 371 | labels = {'max': "10T", 'dev': "sdc"} 372 | 373 | for i in range(iterations): 374 | self.g.sub(labels, -i) 375 | 376 | self.assertEqual(sum(range(iterations)), self.g.get(labels)) 377 | 378 | 379 | class TestSummary(unittest.TestCase): 380 | 381 | def setUp(self): 382 | self.data = { 383 | 'name': "http_request_duration_microseconds", 384 | 'help_text': "Request duration per application", 385 | 'const_labels': {"app": "my_app"}, 386 | } 387 | 388 | self.s = Summary(**self.data) 389 | 390 | def test_add(self): 391 | data = ( 392 | { 393 | 'labels': {'handler': '/static'}, 394 | 'values': range(0, 500, 50) 395 | }, 396 | { 397 | 'labels': {'handler': '/p'}, 398 | 'values': range(0, 1000, 100) 399 | }, 400 | { 401 | 'labels': {'handler': '/p/login'}, 402 | 'values': range(0, 10000, 1000) 403 | } 404 | ) 405 | 406 | for i in data: 407 | for j in i['values']: 408 | self.s.add(i['labels'], j) 409 | 410 | for i in data: 411 | self.assertEqual(len(i['values']), 412 | self.s.values[i['labels']]._observations) 413 | 414 | def test_get(self): 415 | labels = {'handler': '/static'} 416 | values = [3, 5.2, 13, 4] 417 | 418 | for i in values: 419 | self.s.add(labels, i) 420 | 421 | data = self.s.get(labels) 422 | correct_data = { 423 | 'sum': 25.2, 424 | 'count': 4, 425 | 0.50: 4.0, 426 | 0.90: 5.2, 427 | 0.99: 5.2, 428 | } 429 | 430 | self.assertEqual(correct_data, data) 431 | 432 | def test_add_get_without_labels(self): 433 | labels = None 434 | values = [3, 5.2, 13, 4] 435 | 436 | for i in values: 437 | self.s.add(labels, i) 438 | 439 | self.assertEqual(1, len(self.s.values)) 440 | 441 | correct_data = { 442 | 'sum': 25.2, 443 | 'count': 4, 444 | 0.50: 4.0, 445 | 0.90: 5.2, 446 | 0.99: 5.2, 447 | } 448 | self.assertEqual(correct_data, self.s.get(labels)) 449 | 450 | def test_add_wrong_types(self): 451 | labels = None 452 | values = ["3", (1, 2), {'1': 2}, True] 453 | 454 | for i in values: 455 | with self.assertRaises(TypeError) as context: 456 | self.s.add(labels, i) 457 | self.assertEqual("Summary only works with digits (int, float)", 458 | str(context.exception)) 459 | -------------------------------------------------------------------------------- /prometheus/pb2/metrics_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: metrics.proto 3 | 4 | from google.protobuf.internal import enum_type_wrapper 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | import sys 9 | if sys.version_info >= (3,): 10 | #some constants that are python2 only 11 | unicode = str 12 | long = int 13 | range = range 14 | unichr = chr 15 | def b(s): 16 | return s.encode("latin-1") 17 | def u(s): 18 | return s 19 | else: 20 | #some constants that are python2 only 21 | range = xrange 22 | unicode = unicode 23 | long = long 24 | unichr = unichr 25 | def b(s): 26 | return s 27 | # Workaround for standalone backslash 28 | def u(s): 29 | return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") 30 | 31 | from google.protobuf import descriptor_pb2 32 | # @@protoc_insertion_point(imports) 33 | 34 | 35 | 36 | 37 | DESCRIPTOR = _descriptor.FileDescriptor( 38 | name='metrics.proto', 39 | package='io.prometheus.client', 40 | serialized_pb=b('\n\rmetrics.proto\x12\x14io.prometheus.client\"(\n\tLabelPair\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x16\n\x05Gauge\x12\r\n\x05value\x18\x01 \x01(\x01\"\x18\n\x07\x43ounter\x12\r\n\x05value\x18\x01 \x01(\x01\"+\n\x08Quantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"e\n\x07Summary\x12\x14\n\x0csample_count\x18\x01 \x01(\x04\x12\x12\n\nsample_sum\x18\x02 \x01(\x01\x12\x30\n\x08quantile\x18\x03 \x03(\x0b\x32\x1e.io.prometheus.client.Quantile\"\x18\n\x07Untyped\x12\r\n\x05value\x18\x01 \x01(\x01\"\x8a\x02\n\x06Metric\x12.\n\x05label\x18\x01 \x03(\x0b\x32\x1f.io.prometheus.client.LabelPair\x12*\n\x05gauge\x18\x02 \x01(\x0b\x32\x1b.io.prometheus.client.Gauge\x12.\n\x07\x63ounter\x18\x03 \x01(\x0b\x32\x1d.io.prometheus.client.Counter\x12.\n\x07summary\x18\x04 \x01(\x0b\x32\x1d.io.prometheus.client.Summary\x12.\n\x07untyped\x18\x05 \x01(\x0b\x32\x1d.io.prometheus.client.Untyped\x12\x14\n\x0ctimestamp_ms\x18\x06 \x01(\x03\"\x88\x01\n\x0cMetricFamily\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04help\x18\x02 \x01(\t\x12.\n\x04type\x18\x03 \x01(\x0e\x32 .io.prometheus.client.MetricType\x12,\n\x06metric\x18\x04 \x03(\x0b\x32\x1c.io.prometheus.client.Metric*>\n\nMetricType\x12\x0b\n\x07\x43OUNTER\x10\x00\x12\t\n\x05GAUGE\x10\x01\x12\x0b\n\x07SUMMARY\x10\x02\x12\x0b\n\x07UNTYPED\x10\x03\x42\x16\n\x14io.prometheus.client')) 41 | 42 | _METRICTYPE = _descriptor.EnumDescriptor( 43 | name='MetricType', 44 | full_name='io.prometheus.client.MetricType', 45 | filename=None, 46 | file=DESCRIPTOR, 47 | values=[ 48 | _descriptor.EnumValueDescriptor( 49 | name='COUNTER', index=0, number=0, 50 | options=None, 51 | type=None), 52 | _descriptor.EnumValueDescriptor( 53 | name='GAUGE', index=1, number=1, 54 | options=None, 55 | type=None), 56 | _descriptor.EnumValueDescriptor( 57 | name='SUMMARY', index=2, number=2, 58 | options=None, 59 | type=None), 60 | _descriptor.EnumValueDescriptor( 61 | name='UNTYPED', index=3, number=3, 62 | options=None, 63 | type=None), 64 | ], 65 | containing_type=None, 66 | options=None, 67 | serialized_start=713, 68 | serialized_end=775, 69 | ) 70 | 71 | MetricType = enum_type_wrapper.EnumTypeWrapper(_METRICTYPE) 72 | COUNTER = 0 73 | GAUGE = 1 74 | SUMMARY = 2 75 | UNTYPED = 3 76 | 77 | 78 | 79 | _LABELPAIR = _descriptor.Descriptor( 80 | name='LabelPair', 81 | full_name='io.prometheus.client.LabelPair', 82 | filename=None, 83 | file=DESCRIPTOR, 84 | containing_type=None, 85 | fields=[ 86 | _descriptor.FieldDescriptor( 87 | name='name', full_name='io.prometheus.client.LabelPair.name', index=0, 88 | number=1, type=9, cpp_type=9, label=1, 89 | has_default_value=False, default_value=unicode(b(""), "utf-8"), 90 | message_type=None, enum_type=None, containing_type=None, 91 | is_extension=False, extension_scope=None, 92 | options=None), 93 | _descriptor.FieldDescriptor( 94 | name='value', full_name='io.prometheus.client.LabelPair.value', index=1, 95 | number=2, type=9, cpp_type=9, label=1, 96 | has_default_value=False, default_value=unicode(b(""), "utf-8"), 97 | message_type=None, enum_type=None, containing_type=None, 98 | is_extension=False, extension_scope=None, 99 | options=None), 100 | ], 101 | extensions=[ 102 | ], 103 | nested_types=[], 104 | enum_types=[ 105 | ], 106 | options=None, 107 | is_extendable=False, 108 | extension_ranges=[], 109 | serialized_start=39, 110 | serialized_end=79, 111 | ) 112 | 113 | 114 | _GAUGE = _descriptor.Descriptor( 115 | name='Gauge', 116 | full_name='io.prometheus.client.Gauge', 117 | filename=None, 118 | file=DESCRIPTOR, 119 | containing_type=None, 120 | fields=[ 121 | _descriptor.FieldDescriptor( 122 | name='value', full_name='io.prometheus.client.Gauge.value', index=0, 123 | number=1, type=1, cpp_type=5, label=1, 124 | has_default_value=False, default_value=0, 125 | message_type=None, enum_type=None, containing_type=None, 126 | is_extension=False, extension_scope=None, 127 | options=None), 128 | ], 129 | extensions=[ 130 | ], 131 | nested_types=[], 132 | enum_types=[ 133 | ], 134 | options=None, 135 | is_extendable=False, 136 | extension_ranges=[], 137 | serialized_start=81, 138 | serialized_end=103, 139 | ) 140 | 141 | 142 | _COUNTER = _descriptor.Descriptor( 143 | name='Counter', 144 | full_name='io.prometheus.client.Counter', 145 | filename=None, 146 | file=DESCRIPTOR, 147 | containing_type=None, 148 | fields=[ 149 | _descriptor.FieldDescriptor( 150 | name='value', full_name='io.prometheus.client.Counter.value', index=0, 151 | number=1, type=1, cpp_type=5, label=1, 152 | has_default_value=False, default_value=0, 153 | message_type=None, enum_type=None, containing_type=None, 154 | is_extension=False, extension_scope=None, 155 | options=None), 156 | ], 157 | extensions=[ 158 | ], 159 | nested_types=[], 160 | enum_types=[ 161 | ], 162 | options=None, 163 | is_extendable=False, 164 | extension_ranges=[], 165 | serialized_start=105, 166 | serialized_end=129, 167 | ) 168 | 169 | 170 | _QUANTILE = _descriptor.Descriptor( 171 | name='Quantile', 172 | full_name='io.prometheus.client.Quantile', 173 | filename=None, 174 | file=DESCRIPTOR, 175 | containing_type=None, 176 | fields=[ 177 | _descriptor.FieldDescriptor( 178 | name='quantile', full_name='io.prometheus.client.Quantile.quantile', index=0, 179 | number=1, type=1, cpp_type=5, label=1, 180 | has_default_value=False, default_value=0, 181 | message_type=None, enum_type=None, containing_type=None, 182 | is_extension=False, extension_scope=None, 183 | options=None), 184 | _descriptor.FieldDescriptor( 185 | name='value', full_name='io.prometheus.client.Quantile.value', index=1, 186 | number=2, type=1, cpp_type=5, label=1, 187 | has_default_value=False, default_value=0, 188 | message_type=None, enum_type=None, containing_type=None, 189 | is_extension=False, extension_scope=None, 190 | options=None), 191 | ], 192 | extensions=[ 193 | ], 194 | nested_types=[], 195 | enum_types=[ 196 | ], 197 | options=None, 198 | is_extendable=False, 199 | extension_ranges=[], 200 | serialized_start=131, 201 | serialized_end=174, 202 | ) 203 | 204 | 205 | _SUMMARY = _descriptor.Descriptor( 206 | name='Summary', 207 | full_name='io.prometheus.client.Summary', 208 | filename=None, 209 | file=DESCRIPTOR, 210 | containing_type=None, 211 | fields=[ 212 | _descriptor.FieldDescriptor( 213 | name='sample_count', full_name='io.prometheus.client.Summary.sample_count', index=0, 214 | number=1, type=4, cpp_type=4, label=1, 215 | has_default_value=False, default_value=0, 216 | message_type=None, enum_type=None, containing_type=None, 217 | is_extension=False, extension_scope=None, 218 | options=None), 219 | _descriptor.FieldDescriptor( 220 | name='sample_sum', full_name='io.prometheus.client.Summary.sample_sum', index=1, 221 | number=2, type=1, cpp_type=5, label=1, 222 | has_default_value=False, default_value=0, 223 | message_type=None, enum_type=None, containing_type=None, 224 | is_extension=False, extension_scope=None, 225 | options=None), 226 | _descriptor.FieldDescriptor( 227 | name='quantile', full_name='io.prometheus.client.Summary.quantile', index=2, 228 | number=3, type=11, cpp_type=10, label=3, 229 | has_default_value=False, default_value=[], 230 | message_type=None, enum_type=None, containing_type=None, 231 | is_extension=False, extension_scope=None, 232 | options=None), 233 | ], 234 | extensions=[ 235 | ], 236 | nested_types=[], 237 | enum_types=[ 238 | ], 239 | options=None, 240 | is_extendable=False, 241 | extension_ranges=[], 242 | serialized_start=176, 243 | serialized_end=277, 244 | ) 245 | 246 | 247 | _UNTYPED = _descriptor.Descriptor( 248 | name='Untyped', 249 | full_name='io.prometheus.client.Untyped', 250 | filename=None, 251 | file=DESCRIPTOR, 252 | containing_type=None, 253 | fields=[ 254 | _descriptor.FieldDescriptor( 255 | name='value', full_name='io.prometheus.client.Untyped.value', index=0, 256 | number=1, type=1, cpp_type=5, label=1, 257 | has_default_value=False, default_value=0, 258 | message_type=None, enum_type=None, containing_type=None, 259 | is_extension=False, extension_scope=None, 260 | options=None), 261 | ], 262 | extensions=[ 263 | ], 264 | nested_types=[], 265 | enum_types=[ 266 | ], 267 | options=None, 268 | is_extendable=False, 269 | extension_ranges=[], 270 | serialized_start=279, 271 | serialized_end=303, 272 | ) 273 | 274 | 275 | _METRIC = _descriptor.Descriptor( 276 | name='Metric', 277 | full_name='io.prometheus.client.Metric', 278 | filename=None, 279 | file=DESCRIPTOR, 280 | containing_type=None, 281 | fields=[ 282 | _descriptor.FieldDescriptor( 283 | name='label', full_name='io.prometheus.client.Metric.label', index=0, 284 | number=1, type=11, cpp_type=10, label=3, 285 | has_default_value=False, default_value=[], 286 | message_type=None, enum_type=None, containing_type=None, 287 | is_extension=False, extension_scope=None, 288 | options=None), 289 | _descriptor.FieldDescriptor( 290 | name='gauge', full_name='io.prometheus.client.Metric.gauge', index=1, 291 | number=2, type=11, cpp_type=10, label=1, 292 | has_default_value=False, default_value=None, 293 | message_type=None, enum_type=None, containing_type=None, 294 | is_extension=False, extension_scope=None, 295 | options=None), 296 | _descriptor.FieldDescriptor( 297 | name='counter', full_name='io.prometheus.client.Metric.counter', index=2, 298 | number=3, type=11, cpp_type=10, label=1, 299 | has_default_value=False, default_value=None, 300 | message_type=None, enum_type=None, containing_type=None, 301 | is_extension=False, extension_scope=None, 302 | options=None), 303 | _descriptor.FieldDescriptor( 304 | name='summary', full_name='io.prometheus.client.Metric.summary', index=3, 305 | number=4, type=11, cpp_type=10, label=1, 306 | has_default_value=False, default_value=None, 307 | message_type=None, enum_type=None, containing_type=None, 308 | is_extension=False, extension_scope=None, 309 | options=None), 310 | _descriptor.FieldDescriptor( 311 | name='untyped', full_name='io.prometheus.client.Metric.untyped', index=4, 312 | number=5, type=11, cpp_type=10, label=1, 313 | has_default_value=False, default_value=None, 314 | message_type=None, enum_type=None, containing_type=None, 315 | is_extension=False, extension_scope=None, 316 | options=None), 317 | _descriptor.FieldDescriptor( 318 | name='timestamp_ms', full_name='io.prometheus.client.Metric.timestamp_ms', index=5, 319 | number=6, type=3, cpp_type=2, label=1, 320 | has_default_value=False, default_value=0, 321 | message_type=None, enum_type=None, containing_type=None, 322 | is_extension=False, extension_scope=None, 323 | options=None), 324 | ], 325 | extensions=[ 326 | ], 327 | nested_types=[], 328 | enum_types=[ 329 | ], 330 | options=None, 331 | is_extendable=False, 332 | extension_ranges=[], 333 | serialized_start=306, 334 | serialized_end=572, 335 | ) 336 | 337 | 338 | _METRICFAMILY = _descriptor.Descriptor( 339 | name='MetricFamily', 340 | full_name='io.prometheus.client.MetricFamily', 341 | filename=None, 342 | file=DESCRIPTOR, 343 | containing_type=None, 344 | fields=[ 345 | _descriptor.FieldDescriptor( 346 | name='name', full_name='io.prometheus.client.MetricFamily.name', index=0, 347 | number=1, type=9, cpp_type=9, label=1, 348 | has_default_value=False, default_value=unicode(b(""), "utf-8"), 349 | message_type=None, enum_type=None, containing_type=None, 350 | is_extension=False, extension_scope=None, 351 | options=None), 352 | _descriptor.FieldDescriptor( 353 | name='help', full_name='io.prometheus.client.MetricFamily.help', index=1, 354 | number=2, type=9, cpp_type=9, label=1, 355 | has_default_value=False, default_value=unicode(b(""), "utf-8"), 356 | message_type=None, enum_type=None, containing_type=None, 357 | is_extension=False, extension_scope=None, 358 | options=None), 359 | _descriptor.FieldDescriptor( 360 | name='type', full_name='io.prometheus.client.MetricFamily.type', index=2, 361 | number=3, type=14, cpp_type=8, label=1, 362 | has_default_value=False, default_value=0, 363 | message_type=None, enum_type=None, containing_type=None, 364 | is_extension=False, extension_scope=None, 365 | options=None), 366 | _descriptor.FieldDescriptor( 367 | name='metric', full_name='io.prometheus.client.MetricFamily.metric', index=3, 368 | number=4, type=11, cpp_type=10, label=3, 369 | has_default_value=False, default_value=[], 370 | message_type=None, enum_type=None, containing_type=None, 371 | is_extension=False, extension_scope=None, 372 | options=None), 373 | ], 374 | extensions=[ 375 | ], 376 | nested_types=[], 377 | enum_types=[ 378 | ], 379 | options=None, 380 | is_extendable=False, 381 | extension_ranges=[], 382 | serialized_start=575, 383 | serialized_end=711, 384 | ) 385 | 386 | _SUMMARY.fields_by_name['quantile'].message_type = _QUANTILE 387 | _METRIC.fields_by_name['label'].message_type = _LABELPAIR 388 | _METRIC.fields_by_name['gauge'].message_type = _GAUGE 389 | _METRIC.fields_by_name['counter'].message_type = _COUNTER 390 | _METRIC.fields_by_name['summary'].message_type = _SUMMARY 391 | _METRIC.fields_by_name['untyped'].message_type = _UNTYPED 392 | _METRICFAMILY.fields_by_name['type'].enum_type = _METRICTYPE 393 | _METRICFAMILY.fields_by_name['metric'].message_type = _METRIC 394 | DESCRIPTOR.message_types_by_name['LabelPair'] = _LABELPAIR 395 | DESCRIPTOR.message_types_by_name['Gauge'] = _GAUGE 396 | DESCRIPTOR.message_types_by_name['Counter'] = _COUNTER 397 | DESCRIPTOR.message_types_by_name['Quantile'] = _QUANTILE 398 | DESCRIPTOR.message_types_by_name['Summary'] = _SUMMARY 399 | DESCRIPTOR.message_types_by_name['Untyped'] = _UNTYPED 400 | DESCRIPTOR.message_types_by_name['Metric'] = _METRIC 401 | DESCRIPTOR.message_types_by_name['MetricFamily'] = _METRICFAMILY 402 | 403 | LabelPair = _reflection.GeneratedProtocolMessageType('LabelPair', (_message.Message,), 404 | { 405 | 'DESCRIPTOR': _LABELPAIR, 406 | # @@protoc_insertion_point(class_scope:io.prometheus.client.LabelPair) 407 | }) 408 | 409 | Gauge = _reflection.GeneratedProtocolMessageType('Gauge', (_message.Message,), 410 | { 411 | 'DESCRIPTOR': _GAUGE, 412 | # @@protoc_insertion_point(class_scope:io.prometheus.client.Gauge) 413 | }) 414 | 415 | Counter = _reflection.GeneratedProtocolMessageType('Counter', (_message.Message,), 416 | { 417 | 'DESCRIPTOR': _COUNTER, 418 | # @@protoc_insertion_point(class_scope:io.prometheus.client.Counter) 419 | }) 420 | 421 | Quantile = _reflection.GeneratedProtocolMessageType('Quantile', (_message.Message,), 422 | { 423 | 'DESCRIPTOR': _QUANTILE, 424 | # @@protoc_insertion_point(class_scope:io.prometheus.client.Quantile) 425 | }) 426 | 427 | Summary = _reflection.GeneratedProtocolMessageType('Summary', (_message.Message,), 428 | { 429 | 'DESCRIPTOR': _SUMMARY, 430 | # @@protoc_insertion_point(class_scope:io.prometheus.client.Summary) 431 | }) 432 | 433 | Untyped = _reflection.GeneratedProtocolMessageType('Untyped', (_message.Message,), 434 | { 435 | 'DESCRIPTOR': _UNTYPED, 436 | # @@protoc_insertion_point(class_scope:io.prometheus.client.Untyped) 437 | }) 438 | 439 | Metric = _reflection.GeneratedProtocolMessageType('Metric', (_message.Message,), 440 | { 441 | 'DESCRIPTOR': _METRIC, 442 | # @@protoc_insertion_point(class_scope:io.prometheus.client.Metric) 443 | }) 444 | 445 | MetricFamily = _reflection.GeneratedProtocolMessageType('MetricFamily', (_message.Message,), 446 | { 447 | 'DESCRIPTOR': _METRICFAMILY, 448 | # @@protoc_insertion_point(class_scope:io.prometheus.client.MetricFamily) 449 | }) 450 | 451 | 452 | DESCRIPTOR.has_options = True 453 | DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), b('\n\024io.prometheus.client')) 454 | # @@protoc_insertion_point(module_scope) 455 | -------------------------------------------------------------------------------- /prometheus/test_formats.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import unittest 4 | 5 | from prometheus.collectors import Collector, Counter, Gauge, Summary 6 | from prometheus.formats import TextFormat, ProtobufFormat, ProtobufTextFormat 7 | from prometheus.pb2 import metrics_pb2 8 | from prometheus.registry import Registry 9 | from prometheus import utils 10 | 11 | 12 | class TestTextFormat(unittest.TestCase): 13 | 14 | def test_headers(self): 15 | f = TextFormat() 16 | result = { 17 | 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8' 18 | } 19 | 20 | self.assertEqual(result, f.get_headers()) 21 | 22 | def test_wrong_format(self): 23 | self.data = { 24 | 'name': "logged_users_total", 25 | 'help_text': "Logged users in the application", 26 | 'const_labels': {"app": "my_app"}, 27 | } 28 | 29 | f = TextFormat() 30 | 31 | c = Collector(**self.data) 32 | 33 | with self.assertRaises(TypeError) as context: 34 | f.marshall_collector(c) 35 | 36 | self.assertEqual('Not a valid object format', str(context.exception)) 37 | 38 | def test_counter_format(self): 39 | 40 | self.data = { 41 | 'name': "logged_users_total", 42 | 'help_text': "Logged users in the application", 43 | 'const_labels': None, 44 | } 45 | c = Counter(**self.data) 46 | 47 | counter_data = ( 48 | ({'country': "sp", "device": "desktop"}, 520), 49 | ({'country': "us", "device": "mobile"}, 654), 50 | ({'country': "uk", "device": "desktop"}, 1001), 51 | ({'country': "de", "device": "desktop"}, 995), 52 | ({'country': "zh", "device": "desktop"}, 520), 53 | ({'country': "ch", "device": "mobile"}, 654), 54 | ({'country': "ca", "device": "desktop"}, 1001), 55 | ({'country': "jp", "device": "desktop"}, 995), 56 | ({'country': "au", "device": "desktop"}, 520), 57 | ({'country': "py", "device": "mobile"}, 654), 58 | ({'country': "ar", "device": "desktop"}, 1001), 59 | ({'country': "pt", "device": "desktop"}, 995), 60 | ) 61 | 62 | valid_result = ( 63 | "# HELP logged_users_total Logged users in the application", 64 | "# TYPE logged_users_total counter", 65 | "logged_users_total{country=\"ch\",device=\"mobile\"} 654", 66 | "logged_users_total{country=\"zh\",device=\"desktop\"} 520", 67 | "logged_users_total{country=\"jp\",device=\"desktop\"} 995", 68 | "logged_users_total{country=\"de\",device=\"desktop\"} 995", 69 | "logged_users_total{country=\"pt\",device=\"desktop\"} 995", 70 | "logged_users_total{country=\"ca\",device=\"desktop\"} 1001", 71 | "logged_users_total{country=\"sp\",device=\"desktop\"} 520", 72 | "logged_users_total{country=\"au\",device=\"desktop\"} 520", 73 | "logged_users_total{country=\"uk\",device=\"desktop\"} 1001", 74 | "logged_users_total{country=\"py\",device=\"mobile\"} 654", 75 | "logged_users_total{country=\"us\",device=\"mobile\"} 654", 76 | "logged_users_total{country=\"ar\",device=\"desktop\"} 1001", 77 | ) 78 | 79 | # Add data to the collector 80 | for i in counter_data: 81 | c.set_value(i[0], i[1]) 82 | 83 | # Select format 84 | f = TextFormat() 85 | result = f.marshall_lines(c) 86 | 87 | result = sorted(result) 88 | valid_result = sorted(valid_result) 89 | 90 | self.assertEqual(valid_result, result) 91 | 92 | def test_counter_format_with_const_labels(self): 93 | 94 | self.data = { 95 | 'name': "logged_users_total", 96 | 'help_text': "Logged users in the application", 97 | 'const_labels': {"app": "my_app"}, 98 | } 99 | c = Counter(**self.data) 100 | 101 | counter_data = ( 102 | ({'country': "sp", "device": "desktop"}, 520), 103 | ({'country': "us", "device": "mobile"}, 654), 104 | ({'country': "uk", "device": "desktop"}, 1001), 105 | ({'country': "de", "device": "desktop"}, 995), 106 | ({'country': "zh", "device": "desktop"}, 520), 107 | ({'country': "ch", "device": "mobile"}, 654), 108 | ({'country': "ca", "device": "desktop"}, 1001), 109 | ({'country': "jp", "device": "desktop"}, 995), 110 | ({'country': "au", "device": "desktop"}, 520), 111 | ({'country': "py", "device": "mobile"}, 654), 112 | ({'country': "ar", "device": "desktop"}, 1001), 113 | ({'country': "pt", "device": "desktop"}, 995), 114 | ) 115 | 116 | valid_result = ( 117 | "# HELP logged_users_total Logged users in the application", 118 | "# TYPE logged_users_total counter", 119 | "logged_users_total{app=\"my_app\",country=\"ch\",device=\"mobile\"} 654", 120 | "logged_users_total{app=\"my_app\",country=\"zh\",device=\"desktop\"} 520", 121 | "logged_users_total{app=\"my_app\",country=\"jp\",device=\"desktop\"} 995", 122 | "logged_users_total{app=\"my_app\",country=\"de\",device=\"desktop\"} 995", 123 | "logged_users_total{app=\"my_app\",country=\"pt\",device=\"desktop\"} 995", 124 | "logged_users_total{app=\"my_app\",country=\"ca\",device=\"desktop\"} 1001", 125 | "logged_users_total{app=\"my_app\",country=\"sp\",device=\"desktop\"} 520", 126 | "logged_users_total{app=\"my_app\",country=\"au\",device=\"desktop\"} 520", 127 | "logged_users_total{app=\"my_app\",country=\"uk\",device=\"desktop\"} 1001", 128 | "logged_users_total{app=\"my_app\",country=\"py\",device=\"mobile\"} 654", 129 | "logged_users_total{app=\"my_app\",country=\"us\",device=\"mobile\"} 654", 130 | "logged_users_total{app=\"my_app\",country=\"ar\",device=\"desktop\"} 1001", 131 | ) 132 | 133 | # Add data to the collector 134 | for i in counter_data: 135 | c.set_value(i[0], i[1]) 136 | 137 | # Select format 138 | f = TextFormat() 139 | result = f.marshall_lines(c) 140 | 141 | result = sorted(result) 142 | valid_result = sorted(valid_result) 143 | 144 | self.assertEqual(valid_result, result) 145 | 146 | def test_counter_format_text(self): 147 | 148 | name = "container_cpu_usage_seconds_total" 149 | help_text = "Total seconds of cpu time consumed." 150 | 151 | valid_result = """# HELP container_cpu_usage_seconds_total Total seconds of cpu time consumed. 152 | # TYPE container_cpu_usage_seconds_total counter 153 | container_cpu_usage_seconds_total{id="110863b5395f7f3476d44e7cb8799f2643abbd385dd544bcc379538ac6ffc5ca",name="container-extractor",type="kernel"} 0 154 | container_cpu_usage_seconds_total{id="110863b5395f7f3476d44e7cb8799f2643abbd385dd544bcc379538ac6ffc5ca",name="container-extractor",type="user"} 0 155 | container_cpu_usage_seconds_total{id="7c1ae8f404be413a6413d0792123092446f694887f52ae6403356215943d3c36",name="calendall_db_1",type="kernel"} 0 156 | container_cpu_usage_seconds_total{id="7c1ae8f404be413a6413d0792123092446f694887f52ae6403356215943d3c36",name="calendall_db_1",type="user"} 0 157 | container_cpu_usage_seconds_total{id="c863b092d1ecdc68f54a6a4ed0d24fe629696be2337ccafb44c279c7c2d1c172",name="calendall_web_run_8",type="kernel"} 0 158 | container_cpu_usage_seconds_total{id="c863b092d1ecdc68f54a6a4ed0d24fe629696be2337ccafb44c279c7c2d1c172",name="calendall_web_run_8",type="user"} 0 159 | container_cpu_usage_seconds_total{id="cefa0b389a634a0b2f3c2f52ade668d71de75e5775e91297bd65bebe745ba054",name="prometheus",type="kernel"} 0 160 | container_cpu_usage_seconds_total{id="cefa0b389a634a0b2f3c2f52ade668d71de75e5775e91297bd65bebe745ba054",name="prometheus",type="user"} 0""" 161 | 162 | data = ( 163 | ({'id': "110863b5395f7f3476d44e7cb8799f2643abbd385dd544bcc379538ac6ffc5ca", 'name': "container-extractor", 'type': "kernel"}, 0), 164 | ({'id': "110863b5395f7f3476d44e7cb8799f2643abbd385dd544bcc379538ac6ffc5ca", 'name': "container-extractor", 'type': "user"}, 0), 165 | ({'id': "7c1ae8f404be413a6413d0792123092446f694887f52ae6403356215943d3c36", 'name': "calendall_db_1", 'type': "kernel"}, 0), 166 | ({'id': "7c1ae8f404be413a6413d0792123092446f694887f52ae6403356215943d3c36", 'name': "calendall_db_1", 'type': "user"}, 0), 167 | ({'id': "c863b092d1ecdc68f54a6a4ed0d24fe629696be2337ccafb44c279c7c2d1c172", 'name': "calendall_web_run_8", 'type': "kernel"}, 0), 168 | ({'id': "c863b092d1ecdc68f54a6a4ed0d24fe629696be2337ccafb44c279c7c2d1c172", 'name': "calendall_web_run_8", 'type': "user"}, 0), 169 | ({'id': "cefa0b389a634a0b2f3c2f52ade668d71de75e5775e91297bd65bebe745ba054", 'name': "prometheus", 'type': "kernel"}, 0), 170 | ({'id': "cefa0b389a634a0b2f3c2f52ade668d71de75e5775e91297bd65bebe745ba054", 'name': "prometheus", 'type': "user"}, 0), 171 | ) 172 | 173 | # Create the counter 174 | c = Counter(name=name, help_text=help_text, const_labels={}) 175 | 176 | for i in data: 177 | c.set_value(i[0], i[1]) 178 | 179 | # Select format 180 | f = TextFormat() 181 | 182 | result = f.marshall_collector(c) 183 | 184 | self.assertEqual(valid_result, result) 185 | 186 | def test_counter_format_with_timestamp(self): 187 | self.data = { 188 | 'name': "logged_users_total", 189 | 'help_text': "Logged users in the application", 190 | 'const_labels': {}, 191 | } 192 | c = Counter(**self.data) 193 | 194 | counter_data = ({'country': "ch", "device": "mobile"}, 654) 195 | 196 | c.set_value(counter_data[0], counter_data[1]) 197 | 198 | result_regex = """# HELP logged_users_total Logged users in the application 199 | # TYPE logged_users_total counter 200 | logged_users_total{country="ch",device="mobile"} 654 \d*(?:.\d*)?$""" 201 | 202 | f_with_ts = TextFormat(True) 203 | result = f_with_ts.marshall_collector(c) 204 | 205 | self.assertTrue(re.match(result_regex, result)) 206 | 207 | def test_single_counter_format_text(self): 208 | 209 | name = "prometheus_dns_sd_lookups_total" 210 | help_text = "The number of DNS-SD lookups." 211 | 212 | valid_result = """# HELP prometheus_dns_sd_lookups_total The number of DNS-SD lookups. 213 | # TYPE prometheus_dns_sd_lookups_total counter 214 | prometheus_dns_sd_lookups_total 10""" 215 | 216 | data = ( 217 | (None, 10), 218 | ) 219 | 220 | # Create the counter 221 | c = Counter(name=name, help_text=help_text, const_labels={}) 222 | 223 | for i in data: 224 | c.set_value(i[0], i[1]) 225 | 226 | # Select format 227 | f = TextFormat() 228 | 229 | result = f.marshall_collector(c) 230 | 231 | self.assertEqual(valid_result, result) 232 | 233 | def test_gauge_format(self): 234 | 235 | self.data = { 236 | 'name': "logged_users_total", 237 | 'help_text': "Logged users in the application", 238 | 'const_labels': {}, 239 | } 240 | g = Gauge(**self.data) 241 | 242 | counter_data = ( 243 | ({'country': "sp", "device": "desktop"}, 520), 244 | ({'country': "us", "device": "mobile"}, 654), 245 | ({'country': "uk", "device": "desktop"}, 1001), 246 | ({'country': "de", "device": "desktop"}, 995), 247 | ({'country': "zh", "device": "desktop"}, 520), 248 | ({'country': "ch", "device": "mobile"}, 654), 249 | ({'country': "ca", "device": "desktop"}, 1001), 250 | ({'country': "jp", "device": "desktop"}, 995), 251 | ({'country': "au", "device": "desktop"}, 520), 252 | ({'country': "py", "device": "mobile"}, 654), 253 | ({'country': "ar", "device": "desktop"}, 1001), 254 | ({'country': "pt", "device": "desktop"}, 995), 255 | ) 256 | 257 | valid_result = ( 258 | "# HELP logged_users_total Logged users in the application", 259 | "# TYPE logged_users_total gauge", 260 | "logged_users_total{country=\"ch\",device=\"mobile\"} 654", 261 | "logged_users_total{country=\"zh\",device=\"desktop\"} 520", 262 | "logged_users_total{country=\"jp\",device=\"desktop\"} 995", 263 | "logged_users_total{country=\"de\",device=\"desktop\"} 995", 264 | "logged_users_total{country=\"pt\",device=\"desktop\"} 995", 265 | "logged_users_total{country=\"ca\",device=\"desktop\"} 1001", 266 | "logged_users_total{country=\"sp\",device=\"desktop\"} 520", 267 | "logged_users_total{country=\"au\",device=\"desktop\"} 520", 268 | "logged_users_total{country=\"uk\",device=\"desktop\"} 1001", 269 | "logged_users_total{country=\"py\",device=\"mobile\"} 654", 270 | "logged_users_total{country=\"us\",device=\"mobile\"} 654", 271 | "logged_users_total{country=\"ar\",device=\"desktop\"} 1001", 272 | ) 273 | 274 | # Add data to the collector 275 | for i in counter_data: 276 | g.set_value(i[0], i[1]) 277 | 278 | # Select format 279 | f = TextFormat() 280 | result = f.marshall_lines(g) 281 | 282 | result = sorted(result) 283 | valid_result = sorted(valid_result) 284 | 285 | self.assertEqual(valid_result, result) 286 | 287 | def test_gauge_format_with_const_labels(self): 288 | 289 | self.data = { 290 | 'name': "logged_users_total", 291 | 'help_text': "Logged users in the application", 292 | 'const_labels': {"app": "my_app"}, 293 | } 294 | g = Gauge(**self.data) 295 | 296 | counter_data = ( 297 | ({'country': "sp", "device": "desktop"}, 520), 298 | ({'country': "us", "device": "mobile"}, 654), 299 | ({'country': "uk", "device": "desktop"}, 1001), 300 | ({'country': "de", "device": "desktop"}, 995), 301 | ({'country': "zh", "device": "desktop"}, 520), 302 | ({'country': "ch", "device": "mobile"}, 654), 303 | ({'country': "ca", "device": "desktop"}, 1001), 304 | ({'country': "jp", "device": "desktop"}, 995), 305 | ({'country': "au", "device": "desktop"}, 520), 306 | ({'country': "py", "device": "mobile"}, 654), 307 | ({'country': "ar", "device": "desktop"}, 1001), 308 | ({'country': "pt", "device": "desktop"}, 995), 309 | ) 310 | 311 | valid_result = ( 312 | "# HELP logged_users_total Logged users in the application", 313 | "# TYPE logged_users_total gauge", 314 | "logged_users_total{app=\"my_app\",country=\"ch\",device=\"mobile\"} 654", 315 | "logged_users_total{app=\"my_app\",country=\"zh\",device=\"desktop\"} 520", 316 | "logged_users_total{app=\"my_app\",country=\"jp\",device=\"desktop\"} 995", 317 | "logged_users_total{app=\"my_app\",country=\"de\",device=\"desktop\"} 995", 318 | "logged_users_total{app=\"my_app\",country=\"pt\",device=\"desktop\"} 995", 319 | "logged_users_total{app=\"my_app\",country=\"ca\",device=\"desktop\"} 1001", 320 | "logged_users_total{app=\"my_app\",country=\"sp\",device=\"desktop\"} 520", 321 | "logged_users_total{app=\"my_app\",country=\"au\",device=\"desktop\"} 520", 322 | "logged_users_total{app=\"my_app\",country=\"uk\",device=\"desktop\"} 1001", 323 | "logged_users_total{app=\"my_app\",country=\"py\",device=\"mobile\"} 654", 324 | "logged_users_total{app=\"my_app\",country=\"us\",device=\"mobile\"} 654", 325 | "logged_users_total{app=\"my_app\",country=\"ar\",device=\"desktop\"} 1001", 326 | ) 327 | 328 | # Add data to the collector 329 | for i in counter_data: 330 | g.set_value(i[0], i[1]) 331 | 332 | # Select format 333 | f = TextFormat() 334 | result = f.marshall_lines(g) 335 | 336 | result = sorted(result) 337 | valid_result = sorted(valid_result) 338 | 339 | self.assertEqual(valid_result, result) 340 | 341 | def test_gauge_format_text(self): 342 | 343 | name = "container_memory_max_usage_bytes" 344 | help_text = "Maximum memory usage ever recorded in bytes." 345 | 346 | valid_result = """# HELP container_memory_max_usage_bytes Maximum memory usage ever recorded in bytes. 347 | # TYPE container_memory_max_usage_bytes gauge 348 | container_memory_max_usage_bytes{id="4f70875bb57986783064fe958f694c9e225643b0d18e9cde6bdee56d47b7ce76",name="prometheus"} 0 349 | container_memory_max_usage_bytes{id="89042838f24f0ec0aa2a6c93ff44fd3f3e43057d35cfd32de89558112ecb92a0",name="calendall_web_run_3"} 0 350 | container_memory_max_usage_bytes{id="d11c6bc95459822e995fac4d4ae527f6cac442a1896a771dbb307ba276beceb9",name="db"} 0 351 | container_memory_max_usage_bytes{id="e4260cc9dca3e4e50ad2bffb0ec7432442197f135023ab629fe3576485cc65dd",name="container-extractor"} 0 352 | container_memory_max_usage_bytes{id="f30d1caaa142b1688a0684ed744fcae6d202a36877617b985e20a5d33801b311",name="calendall_db_1"} 0 353 | container_memory_max_usage_bytes{id="f835d921ffaf332f8d88ef5231ba149e389a2f37276f081878d6f982ef89a981",name="cocky_fermat"} 0""" 354 | 355 | data = ( 356 | ({'id': "4f70875bb57986783064fe958f694c9e225643b0d18e9cde6bdee56d47b7ce76", 'name': "prometheus"}, 0), 357 | ({'id': "89042838f24f0ec0aa2a6c93ff44fd3f3e43057d35cfd32de89558112ecb92a0", 'name': "calendall_web_run_3"}, 0), 358 | ({'id': "d11c6bc95459822e995fac4d4ae527f6cac442a1896a771dbb307ba276beceb9", 'name': "db"}, 0), 359 | ({'id': "e4260cc9dca3e4e50ad2bffb0ec7432442197f135023ab629fe3576485cc65dd", 'name': "container-extractor"}, 0), 360 | ({'id': "f30d1caaa142b1688a0684ed744fcae6d202a36877617b985e20a5d33801b311", 'name': "calendall_db_1"}, 0), 361 | ({'id': "f835d921ffaf332f8d88ef5231ba149e389a2f37276f081878d6f982ef89a981", 'name': "cocky_fermat"}, 0), 362 | ) 363 | 364 | # Create the counter 365 | g = Gauge(name=name, help_text=help_text, const_labels={}) 366 | 367 | for i in data: 368 | g.set_value(i[0], i[1]) 369 | 370 | # Select format 371 | f = TextFormat() 372 | 373 | result = f.marshall_collector(g) 374 | 375 | self.assertEqual(valid_result, result) 376 | 377 | def test_gauge_format_with_timestamp(self): 378 | self.data = { 379 | 'name': "logged_users_total", 380 | 'help_text': "Logged users in the application", 381 | 'const_labels': {}, 382 | } 383 | g = Gauge(**self.data) 384 | 385 | counter_data = ({'country': "ch", "device": "mobile"}, 654) 386 | 387 | g.set_value(counter_data[0], counter_data[1]) 388 | 389 | result_regex = """# HELP logged_users_total Logged users in the application 390 | # TYPE logged_users_total gauge 391 | logged_users_total{country="ch",device="mobile"} 654 \d*(?:.\d*)?$""" 392 | 393 | f_with_ts = TextFormat(True) 394 | result = f_with_ts.marshall_collector(g) 395 | 396 | self.assertTrue(re.match(result_regex, result)) 397 | 398 | def test_single_gauge_format_text(self): 399 | 400 | name = "prometheus_local_storage_indexing_queue_capacity" 401 | help_text = "The capacity of the indexing queue." 402 | 403 | valid_result = """# HELP prometheus_local_storage_indexing_queue_capacity The capacity of the indexing queue. 404 | # TYPE prometheus_local_storage_indexing_queue_capacity gauge 405 | prometheus_local_storage_indexing_queue_capacity 16384""" 406 | 407 | data = ( 408 | (None, 16384), 409 | ) 410 | 411 | # Create the counter 412 | g = Gauge(name=name, help_text=help_text, const_labels={}) 413 | 414 | for i in data: 415 | g.set_value(i[0], i[1]) 416 | 417 | # Select format 418 | f = TextFormat() 419 | 420 | result = f.marshall_collector(g) 421 | 422 | self.assertEqual(valid_result, result) 423 | 424 | def test_one_summary_format(self): 425 | data = { 426 | 'name': "logged_users_total", 427 | 'help_text': "Logged users in the application", 428 | 'const_labels': {}, 429 | } 430 | 431 | labels = {'handler': '/static'} 432 | values = [3, 5.2, 13, 4] 433 | 434 | valid_result = ( 435 | "# HELP logged_users_total Logged users in the application", 436 | "# TYPE logged_users_total summary", 437 | "logged_users_total{handler=\"/static\",quantile=\"0.5\"} 4.0", 438 | "logged_users_total{handler=\"/static\",quantile=\"0.9\"} 5.2", 439 | "logged_users_total{handler=\"/static\",quantile=\"0.99\"} 5.2", 440 | "logged_users_total_count{handler=\"/static\"} 4", 441 | "logged_users_total_sum{handler=\"/static\"} 25.2", 442 | ) 443 | 444 | s = Summary(**data) 445 | 446 | for i in values: 447 | s.add(labels, i) 448 | 449 | f = TextFormat() 450 | result = f.marshall_lines(s) 451 | 452 | result = sorted(result) 453 | valid_result = sorted(valid_result) 454 | 455 | self.assertEqual(valid_result, result) 456 | 457 | def test_summary_format_text(self): 458 | data = { 459 | 'name': "prometheus_target_interval_length_seconds", 460 | 'help_text': "Actual intervals between scrapes.", 461 | 'const_labels': {}, 462 | } 463 | 464 | labels = {'interval': '5s'} 465 | values = [3, 5.2, 13, 4] 466 | 467 | valid_result = """# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes. 468 | # TYPE prometheus_target_interval_length_seconds summary 469 | prometheus_target_interval_length_seconds_count{interval="5s"} 4 470 | prometheus_target_interval_length_seconds_sum{interval="5s"} 25.2 471 | prometheus_target_interval_length_seconds{interval="5s",quantile="0.5"} 4.0 472 | prometheus_target_interval_length_seconds{interval="5s",quantile="0.9"} 5.2 473 | prometheus_target_interval_length_seconds{interval="5s",quantile="0.99"} 5.2""" 474 | 475 | s = Summary(**data) 476 | 477 | for i in values: 478 | s.add(labels, i) 479 | 480 | f = TextFormat() 481 | result = f.marshall_collector(s) 482 | 483 | self.assertEqual(valid_result, result) 484 | 485 | def test_multiple_summary_format(self): 486 | data = { 487 | 'name': "prometheus_target_interval_length_seconds", 488 | 'help_text': "Actual intervals between scrapes.", 489 | 'const_labels': {}, 490 | } 491 | 492 | summary_data = ( 493 | ({'interval': "5s"}, [3, 5.2, 13, 4]), 494 | ({'interval': "10s"}, [1.3, 1.2, 32.1, 59.2, 109.46, 70.9]), 495 | ({'interval': "10s", 'method': "fast"}, [5, 9.8, 31, 9.7, 101.4]), 496 | ) 497 | 498 | valid_result = ( 499 | "# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes.", 500 | "# TYPE prometheus_target_interval_length_seconds summary", 501 | "prometheus_target_interval_length_seconds{interval=\"5s\",quantile=\"0.5\"} 4.0", 502 | "prometheus_target_interval_length_seconds{interval=\"5s\",quantile=\"0.9\"} 5.2", 503 | "prometheus_target_interval_length_seconds{interval=\"5s\",quantile=\"0.99\"} 5.2", 504 | "prometheus_target_interval_length_seconds_count{interval=\"5s\"} 4", 505 | "prometheus_target_interval_length_seconds_sum{interval=\"5s\"} 25.2", 506 | "prometheus_target_interval_length_seconds{interval=\"10s\",quantile=\"0.5\"} 32.1", 507 | "prometheus_target_interval_length_seconds{interval=\"10s\",quantile=\"0.9\"} 59.2", 508 | "prometheus_target_interval_length_seconds{interval=\"10s\",quantile=\"0.99\"} 59.2", 509 | "prometheus_target_interval_length_seconds_count{interval=\"10s\"} 6", 510 | "prometheus_target_interval_length_seconds_sum{interval=\"10s\"} 274.15999999999997", 511 | "prometheus_target_interval_length_seconds{interval=\"10s\",method=\"fast\",quantile=\"0.5\"} 9.7", 512 | "prometheus_target_interval_length_seconds{interval=\"10s\",method=\"fast\",quantile=\"0.9\"} 9.8", 513 | "prometheus_target_interval_length_seconds{interval=\"10s\",method=\"fast\",quantile=\"0.99\"} 9.8", 514 | "prometheus_target_interval_length_seconds_count{interval=\"10s\",method=\"fast\"} 5", 515 | "prometheus_target_interval_length_seconds_sum{interval=\"10s\",method=\"fast\"} 156.9", 516 | ) 517 | 518 | s = Summary(**data) 519 | 520 | for i in summary_data: 521 | for j in i[1]: 522 | s.add(i[0], j) 523 | 524 | f = TextFormat() 525 | result = f.marshall_lines(s) 526 | 527 | self.assertEqual(sorted(valid_result), sorted(result)) 528 | 529 | # # This one hans't got labels 530 | def test_single_summary_format(self): 531 | data = { 532 | 'name': "logged_users_total", 533 | 'help_text': "Logged users in the application", 534 | 'const_labels': {}, 535 | } 536 | 537 | labels = {} 538 | values = [3, 5.2, 13, 4] 539 | 540 | valid_result = ( 541 | "# HELP logged_users_total Logged users in the application", 542 | "# TYPE logged_users_total summary", 543 | "logged_users_total{quantile=\"0.5\"} 4.0", 544 | "logged_users_total{quantile=\"0.9\"} 5.2", 545 | "logged_users_total{quantile=\"0.99\"} 5.2", 546 | "logged_users_total_count 4", 547 | "logged_users_total_sum 25.2", 548 | ) 549 | 550 | s = Summary(**data) 551 | 552 | for i in values: 553 | s.add(labels, i) 554 | 555 | f = TextFormat() 556 | result = f.marshall_lines(s) 557 | 558 | result = sorted(result) 559 | valid_result = sorted(valid_result) 560 | 561 | self.assertEqual(valid_result, result) 562 | 563 | def test_summary_format_timestamp(self): 564 | data = { 565 | 'name': "prometheus_target_interval_length_seconds", 566 | 'help_text': "Actual intervals between scrapes.", 567 | 'const_labels': {}, 568 | } 569 | 570 | labels = {'interval': '5s'} 571 | values = [3, 5.2, 13, 4] 572 | 573 | result_regex = """# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes. 574 | # TYPE prometheus_target_interval_length_seconds summary 575 | prometheus_target_interval_length_seconds_count{interval="5s"} 4 \d*(?:.\d*)? 576 | prometheus_target_interval_length_seconds_sum{interval="5s"} 25.2 \d*(?:.\d*)? 577 | prometheus_target_interval_length_seconds{interval="5s",quantile="0.5"} 4.0 \d*(?:.\d*)? 578 | prometheus_target_interval_length_seconds{interval="5s",quantile="0.9"} 5.2 \d*(?:.\d*)? 579 | prometheus_target_interval_length_seconds{interval="5s",quantile="0.99"} 5.2 \d*(?:.\d*)?$""" 580 | 581 | s = Summary(**data) 582 | 583 | for i in values: 584 | s.add(labels, i) 585 | 586 | f = TextFormat(True) 587 | result = f.marshall_collector(s) 588 | 589 | self.assertTrue(re.match(result_regex, result)) 590 | 591 | def test_registry_marshall(self): 592 | 593 | format_times = 10 594 | 595 | counter_data = ( 596 | ({'c_sample': '1'}, 100), 597 | ({'c_sample': '2'}, 200), 598 | ({'c_sample': '3'}, 300), 599 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 600 | ) 601 | 602 | gauge_data = ( 603 | ({'g_sample': '1'}, 500), 604 | ({'g_sample': '2'}, 600), 605 | ({'g_sample': '3'}, 700), 606 | ({'g_sample': '1', 'g_subsample': 'b'}, 800), 607 | ) 608 | 609 | summary_data = ( 610 | ({'s_sample': '1'}, range(1000, 2000, 4)), 611 | ({'s_sample': '2'}, range(2000, 3000, 20)), 612 | ({'s_sample': '3'}, range(3000, 4000, 13)), 613 | ({'s_sample': '1', 's_subsample': 'b'}, range(4000, 5000, 47)), 614 | ) 615 | 616 | registry = Registry() 617 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 618 | gauge = Gauge("gauge_test", "A gauge.", {'type': "gauge"}) 619 | summary = Summary("summary_test", "A summary.", {'type': "summary"}) 620 | 621 | # Add data 622 | [counter.set(c[0], c[1]) for c in counter_data] 623 | [gauge.set(g[0], g[1]) for g in gauge_data] 624 | [summary.add(i[0], s) for i in summary_data for s in i[1]] 625 | 626 | registry.register(counter) 627 | registry.register(gauge) 628 | registry.register(summary) 629 | 630 | valid_regex = """# HELP counter_test A counter. 631 | # TYPE counter_test counter 632 | counter_test{c_sample="1",c_subsample="b",type="counter"} 400 633 | counter_test{c_sample="1",type="counter"} 100 634 | counter_test{c_sample="2",type="counter"} 200 635 | counter_test{c_sample="3",type="counter"} 300 636 | # HELP gauge_test A gauge. 637 | # TYPE gauge_test gauge 638 | gauge_test{g_sample="1",g_subsample="b",type="gauge"} 800 639 | gauge_test{g_sample="1",type="gauge"} 500 640 | gauge_test{g_sample="2",type="gauge"} 600 641 | gauge_test{g_sample="3",type="gauge"} 700 642 | # HELP summary_test A summary. 643 | # TYPE summary_test summary 644 | summary_test_count{s_sample="1",s_subsample="b",type="summary"} \d*(?:.\d*)? 645 | summary_test_count{s_sample="1",type="summary"} \d*(?:.\d*)? 646 | summary_test_count{s_sample="2",type="summary"} \d*(?:.\d*)? 647 | summary_test_count{s_sample="3",type="summary"} \d*(?:.\d*)? 648 | summary_test_sum{s_sample="1",s_subsample="b",type="summary"} \d*(?:.\d*)? 649 | summary_test_sum{s_sample="1",type="summary"} \d*(?:.\d*)? 650 | summary_test_sum{s_sample="2",type="summary"} \d*(?:.\d*)? 651 | summary_test_sum{s_sample="3",type="summary"} \d*(?:.\d*)? 652 | summary_test{quantile="0.5",s_sample="1",s_subsample="b",type="summary"} \d*(?:.\d*)? 653 | summary_test{quantile="0.5",s_sample="1",type="summary"} \d*(?:.\d*)? 654 | summary_test{quantile="0.5",s_sample="2",type="summary"} \d*(?:.\d*)? 655 | summary_test{quantile="0.5",s_sample="3",type="summary"} \d*(?:.\d*)? 656 | summary_test{quantile="0.9",s_sample="1",s_subsample="b",type="summary"} \d*(?:.\d*)? 657 | summary_test{quantile="0.9",s_sample="1",type="summary"} \d*(?:.\d*)? 658 | summary_test{quantile="0.9",s_sample="2",type="summary"} 2\d*(?:.\d*)? 659 | summary_test{quantile="0.9",s_sample="3",type="summary"} \d*(?:.\d*)? 660 | summary_test{quantile="0.99",s_sample="1",s_subsample="b",type="summary"} \d*(?:.\d*)? 661 | summary_test{quantile="0.99",s_sample="1",type="summary"} \d*(?:.\d*)? 662 | summary_test{quantile="0.99",s_sample="2",type="summary"} \d*(?:.\d*)? 663 | summary_test{quantile="0.99",s_sample="3",type="summary"} \d*(?:.\d*)? 664 | """ 665 | f = TextFormat() 666 | # print(f.marshall(registry)) 667 | self.maxDiff = None 668 | # Check multiple times to ensure multiple marshalling requests 669 | for i in range(format_times): 670 | self.assertTrue(re.match(valid_regex, f.marshall(registry))) 671 | 672 | 673 | class TestProtobufFormat(unittest.TestCase): 674 | 675 | # Test Utils 676 | def _create_protobuf_object(self, data, metrics, metric_type, const_labels={}, ts=False): 677 | pb2_metrics = [] 678 | for i in metrics: 679 | labels = [metrics_pb2.LabelPair(name=k, value=v) for k, v in i[0].items()] 680 | c_labels = [metrics_pb2.LabelPair(name=k, value=v) for k, v in const_labels.items()] 681 | labels.extend(c_labels) 682 | 683 | if metric_type == metrics_pb2.COUNTER: 684 | metric = metrics_pb2.Metric( 685 | counter=metrics_pb2.Counter(value=i[1]), 686 | label=labels) 687 | elif metric_type == metrics_pb2.GAUGE: 688 | metric = metrics_pb2.Metric( 689 | gauge=metrics_pb2.Gauge(value=i[1]), 690 | label=labels) 691 | elif metric_type == metrics_pb2.SUMMARY: 692 | quantiles = [] 693 | 694 | for k, v in i[1].items(): 695 | if not isinstance(k, str): 696 | q = metrics_pb2.Quantile(quantile=k, value=v) 697 | quantiles.append(q) 698 | 699 | metric = metrics_pb2.Metric( 700 | summary=metrics_pb2.Summary(quantile=quantiles, 701 | sample_sum=i[1]['sum'], 702 | sample_count=i[1]['count']), 703 | label=labels) 704 | else: 705 | raise TypeError("Not a valid metric") 706 | 707 | if ts: 708 | metric.timestamp_ms = utils.get_timestamp() 709 | 710 | pb2_metrics.append(metric) 711 | 712 | valid_result = metrics_pb2.MetricFamily( 713 | name=data['name'], 714 | help=data['help_text'], 715 | type=metric_type, 716 | metric=pb2_metrics 717 | ) 718 | 719 | return valid_result 720 | 721 | def _protobuf_metric_equal(self, ptb1, ptb2): 722 | if ptb1 is ptb2: 723 | return True 724 | 725 | if not ptb1 or not ptb2: 726 | return False 727 | 728 | # start all the filters 729 | # 1st level: Metric Family 730 | if (ptb1.name != ptb2.name) or\ 731 | (ptb1.help != ptb2.help) or\ 732 | (ptb1.type != ptb2.type) or\ 733 | (len(ptb1.metric) != len(ptb2.metric)): 734 | return False 735 | 736 | def sort_metric(v): 737 | """Small function to order the lists of protobuf""" 738 | x = sorted(v.label, key=lambda x: x.name+x.value) 739 | return("".join([i.name+i.value for i in x])) 740 | 741 | # Before continuing, sort stuff 742 | mts1 = sorted(ptb1.metric, key=sort_metric) 743 | mts2 = sorted(ptb2.metric, key=sort_metric) 744 | 745 | # Now that they are ordered we can compare each element with each 746 | for k, m1 in enumerate(mts1): 747 | m2 = mts2[k] 748 | 749 | # Check ts 750 | if m1.timestamp_ms != m2.timestamp_ms: 751 | return False 752 | 753 | # Check value 754 | if ptb1.type == metrics_pb2.COUNTER and ptb2.type == metrics_pb2.COUNTER: 755 | if m1.counter != m2.counter: 756 | return False 757 | elif ptb1.type == metrics_pb2.GAUGE and ptb2.type == metrics_pb2.GAUGE: 758 | if m1.gauge != m2.gauge: 759 | return False 760 | elif ptb1.type == metrics_pb2.SUMMARY and ptb2.type == metrics_pb2.SUMMARY: 761 | mm1, mm2 = m1.summary, m2.summary 762 | if mm1.sample_count != mm2.sample_count or\ 763 | mm1.sample_sum != mm2.sample_sum: 764 | return False 765 | 766 | # order quantiles to test 767 | mm1_quantiles = sorted(mm1.quantile, key=lambda x: x.quantile) 768 | mm2_quantiles = sorted(mm2.quantile, key=lambda x: x.quantile) 769 | 770 | if mm1_quantiles != mm2_quantiles: 771 | return False 772 | 773 | else: 774 | return False 775 | 776 | # Check labels 777 | # Sort labels 778 | l1 = sorted(m1.label, key=lambda x: x.name+x.value) 779 | l2 = sorted(m2.label, key=lambda x: x.name+x.value) 780 | if not all([l.name == l2[k].name and l.value == l2[k].value for k, l in enumerate(l1)]): 781 | return False 782 | 783 | return True 784 | 785 | def test_create_protobuf_object_wrong(self): 786 | data = { 787 | 'name': "logged_users_total", 788 | 'help_text': "Logged users in the application", 789 | 'const_labels': None, 790 | } 791 | 792 | values =( 793 | ({'country': "sp", "device": "desktop"}, 520), 794 | ({'country': "us", "device": "mobile"}, 654), 795 | ) 796 | 797 | with self.assertRaises(TypeError) as context: 798 | self._create_protobuf_object(data, values, 7) 799 | 800 | self.assertEqual("Not a valid metric", str(context.exception)) 801 | 802 | def test_test_timestamp(self): 803 | data = { 804 | 'name': "logged_users_total", 805 | 'help_text': "Logged users in the application", 806 | 'const_labels': None, 807 | } 808 | 809 | values = ( 810 | ({'country': "sp", "device": "desktop"}, 520), 811 | ({'country': "us", "device": "mobile"}, 654), 812 | ) 813 | 814 | c = self._create_protobuf_object(data, values, metrics_pb2.COUNTER, {}) 815 | for i in c.metric: 816 | self.assertEqual(0, i.timestamp_ms) 817 | 818 | c = self._create_protobuf_object(data, values, metrics_pb2.COUNTER, {}, True) 819 | for i in c.metric: 820 | self.assertIsNotNone(i.timestamp_ms) 821 | 822 | self.assertEqual(c, c) 823 | self.assertTrue(self._protobuf_metric_equal(c, c)) 824 | time.sleep(0.5) 825 | c2 = self._create_protobuf_object(data, values, metrics_pb2.COUNTER, {}, True) 826 | self.assertFalse(self._protobuf_metric_equal(c, c2)) 827 | 828 | def test_test_protobuf_metric_equal_not_metric(self): 829 | data = { 830 | 'name': "logged_users_total", 831 | 'help_text': "Logged users in the application", 832 | 'const_labels': None, 833 | } 834 | 835 | values = (({"device": "mobile", 'country': "us"}, 654), 836 | ({'country': "sp", "device": "desktop"}, 520)) 837 | pt1 = self._create_protobuf_object(data, values, metrics_pb2.COUNTER) 838 | 839 | self.assertFalse(self._protobuf_metric_equal(pt1, None)) 840 | self.assertFalse(self._protobuf_metric_equal(None, pt1)) 841 | 842 | def test_test_protobuf_metric_equal_not_basic_data(self): 843 | data = { 844 | 'name': "logged_users_total", 845 | 'help_text': "Logged users in the application", 846 | 'const_labels': None, 847 | } 848 | 849 | pt1 = self._create_protobuf_object(data, (), metrics_pb2.COUNTER) 850 | 851 | data2 = data.copy() 852 | data2['name'] = "other" 853 | pt2 = self._create_protobuf_object(data2, (), metrics_pb2.COUNTER) 854 | self.assertFalse(self._protobuf_metric_equal(pt1, pt2)) 855 | 856 | data2 = data.copy() 857 | data2['help_text'] = "other" 858 | pt2 = self._create_protobuf_object(data2, (), metrics_pb2.COUNTER) 859 | self.assertFalse(self._protobuf_metric_equal(pt1, pt2)) 860 | 861 | pt2 = self._create_protobuf_object(data, (), metrics_pb2.SUMMARY) 862 | self.assertFalse(self._protobuf_metric_equal(pt1, pt2)) 863 | 864 | def test_test_protobuf_metric_equal_not_labels(self): 865 | data = { 866 | 'name': "logged_users_total", 867 | 'help_text': "Logged users in the application", 868 | 'const_labels': None, 869 | } 870 | 871 | values = (({"device": "mobile", 'country': "us"}, 654),) 872 | pt1 = self._create_protobuf_object(data, values, metrics_pb2.COUNTER) 873 | 874 | values2 = (({"device": "mobile", 'country': "es"}, 654),) 875 | pt2 = self._create_protobuf_object(data, values2, metrics_pb2.COUNTER) 876 | 877 | self.assertFalse(self._protobuf_metric_equal(pt1, pt2)) 878 | 879 | def test_test_protobuf_metric_equal_counter(self): 880 | data = { 881 | 'name': "logged_users_total", 882 | 'help_text': "Logged users in the application", 883 | 'const_labels': None, 884 | } 885 | 886 | counter_data = ( 887 | { 888 | 'pt1': (({'country': "sp", "device": "desktop"}, 520), 889 | ({'country': "us", "device": "mobile"}, 654),), 890 | 'pt2': (({'country': "sp", "device": "desktop"}, 520), 891 | ({'country': "us", "device": "mobile"}, 654),), 892 | 'ok': True 893 | }, 894 | { 895 | 'pt1': (({'country': "sp", "device": "desktop"}, 521), 896 | ({'country': "us", "device": "mobile"}, 654),), 897 | 'pt2': (({'country': "sp", "device": "desktop"}, 520), 898 | ({'country': "us", "device": "mobile"}, 654),), 899 | 'ok': False 900 | }, 901 | { 902 | 'pt1': (({'country': "sp", "device": "desktop"}, 520), 903 | ({"device": "mobile", 'country': "us"}, 654),), 904 | 'pt2': (({"device": "desktop", 'country': "sp"}, 520), 905 | ({'country': "us", "device": "mobile"}, 654),), 906 | 'ok': True 907 | }, 908 | { 909 | 'pt1': (({"device": "mobile", 'country': "us"}, 654), 910 | ({'country': "sp", "device": "desktop"}, 520)), 911 | 'pt2': (({"device": "desktop", 'country': "sp"}, 520), 912 | ({'country': "us", "device": "mobile"}, 654),), 913 | 'ok': True 914 | }, 915 | ) 916 | 917 | for i in counter_data: 918 | p1 = self._create_protobuf_object(data, i['pt1'], metrics_pb2.COUNTER) 919 | p2 = self._create_protobuf_object(data, i['pt2'], metrics_pb2.COUNTER) 920 | 921 | if i['ok']: 922 | self.assertTrue(self._protobuf_metric_equal(p1, p2)) 923 | else: 924 | self.assertFalse(self._protobuf_metric_equal(p1, p2)) 925 | 926 | def test_test_protobuf_metric_equal_gauge(self): 927 | data = { 928 | 'name': "logged_users_total", 929 | 'help_text': "Logged users in the application", 930 | 'const_labels': None, 931 | } 932 | 933 | gauge_data = ( 934 | { 935 | 'pt1': (({'country': "sp", "device": "desktop"}, 520), 936 | ({'country': "us", "device": "mobile"}, 654),), 937 | 'pt2': (({'country': "sp", "device": "desktop"}, 520), 938 | ({'country': "us", "device": "mobile"}, 654),), 939 | 'ok': True 940 | }, 941 | { 942 | 'pt1': (({'country': "sp", "device": "desktop"}, 521), 943 | ({'country': "us", "device": "mobile"}, 654),), 944 | 'pt2': (({'country': "sp", "device": "desktop"}, 520), 945 | ({'country': "us", "device": "mobile"}, 654),), 946 | 'ok': False 947 | }, 948 | { 949 | 'pt1': (({'country': "sp", "device": "desktop"}, 520), 950 | ({"device": "mobile", 'country': "us"}, 654),), 951 | 'pt2': (({"device": "desktop", 'country': "sp"}, 520), 952 | ({'country': "us", "device": "mobile"}, 654),), 953 | 'ok': True 954 | }, 955 | { 956 | 'pt1': (({"device": "mobile", 'country': "us"}, 654), 957 | ({'country': "sp", "device": "desktop"}, 520)), 958 | 'pt2': (({"device": "desktop", 'country': "sp"}, 520), 959 | ({'country': "us", "device": "mobile"}, 654),), 960 | 'ok': True 961 | }, 962 | ) 963 | 964 | for i in gauge_data: 965 | p1 = self._create_protobuf_object(data, i['pt1'], metrics_pb2.GAUGE) 966 | p2 = self._create_protobuf_object(data, i['pt2'], metrics_pb2.GAUGE) 967 | 968 | if i['ok']: 969 | self.assertTrue(self._protobuf_metric_equal(p1, p2)) 970 | else: 971 | self.assertFalse(self._protobuf_metric_equal(p1, p2)) 972 | 973 | def test_test_protobuf_metric_equal_summary(self): 974 | data = { 975 | 'name': "logged_users_total", 976 | 'help_text': "Logged users in the application", 977 | 'const_labels': None, 978 | } 979 | 980 | gauge_data = ( 981 | { 982 | 'pt1': (({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 983 | ({'interval': "10s"}, {0.5: 90, 0.9: 149, 0.99: 150, "sum": 385, "count": 10}),), 984 | 'pt2': (({'interval': "10s"}, {0.5: 90, 0.9: 149, 0.99: 150, "sum": 385, "count": 10}), 985 | ({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4})), 986 | 'ok': True 987 | }, 988 | { 989 | 'pt1': (({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 990 | ({'interval': "10s"}, {0.5: 90, 0.9: 149, 0.99: 150, "sum": 385, "count": 10}),), 991 | 'pt2': (({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 992 | ({'interval': "10s"}, {0.5: 90, 0.9: 150, 0.99: 150, "sum": 385, "count": 10}),), 993 | 'ok': False 994 | }, 995 | { 996 | 'pt1': (({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 997 | ({'interval': "10s"}, {0.5: 90, 0.9: 149, 0.99: 150, "sum": 385, "count": 10}),), 998 | 'pt2': (({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 999 | ({'interval': "10s"}, {0.5: 90, 0.9: 149, 0.99: 150, "sum": 385, "count": 11}),), 1000 | 'ok': False 1001 | }, 1002 | ) 1003 | 1004 | for i in gauge_data: 1005 | p1 = self._create_protobuf_object(data, i['pt1'], metrics_pb2.SUMMARY) 1006 | p2 = self._create_protobuf_object(data, i['pt2'], metrics_pb2.SUMMARY) 1007 | 1008 | if i['ok']: 1009 | self.assertTrue(self._protobuf_metric_equal(p1, p2)) 1010 | else: 1011 | self.assertFalse(self._protobuf_metric_equal(p1, p2)) 1012 | 1013 | # Finish Test Utils 1014 | # ###################################### 1015 | def test_headers(self): 1016 | f = ProtobufFormat() 1017 | result = { 1018 | 'Content-Type': 'application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited' 1019 | } 1020 | 1021 | self.assertEqual(result, f.get_headers()) 1022 | 1023 | def test_wrong_format(self): 1024 | data = { 1025 | 'name': "logged_users_total", 1026 | 'help_text': "Logged users in the application", 1027 | 'const_labels': {"app": "my_app"}, 1028 | } 1029 | 1030 | f = ProtobufFormat() 1031 | 1032 | c = Collector(**data) 1033 | 1034 | with self.assertRaises(TypeError) as context: 1035 | f.marshall_collector(c) 1036 | 1037 | self.assertEqual('Not a valid object format', str(context.exception)) 1038 | 1039 | def test_counter_format(self): 1040 | 1041 | data = { 1042 | 'name': "logged_users_total", 1043 | 'help_text': "Logged users in the application", 1044 | 'const_labels': None, 1045 | } 1046 | c = Counter(**data) 1047 | 1048 | counter_data = ( 1049 | ({'country': "sp", "device": "desktop"}, 520), 1050 | ({'country': "us", "device": "mobile"}, 654), 1051 | ({'country': "uk", "device": "desktop"}, 1001), 1052 | ({'country': "de", "device": "desktop"}, 995), 1053 | ({'country': "zh", "device": "desktop"}, 520), 1054 | ) 1055 | 1056 | # Construct the result to compare 1057 | valid_result = self._create_protobuf_object( 1058 | data, counter_data, metrics_pb2.COUNTER) 1059 | 1060 | # Add data to the collector 1061 | for i in counter_data: 1062 | c.set_value(i[0], i[1]) 1063 | 1064 | f = ProtobufFormat() 1065 | 1066 | result = f.marshall_collector(c) 1067 | 1068 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1069 | 1070 | def test_counter_format_with_const_labels(self): 1071 | data = { 1072 | 'name': "logged_users_total", 1073 | 'help_text': "Logged users in the application", 1074 | 'const_labels': {"app": "my_app"}, 1075 | } 1076 | c = Counter(**data) 1077 | 1078 | counter_data = ( 1079 | ({'country': "sp", "device": "desktop"}, 520), 1080 | ({'country': "us", "device": "mobile"}, 654), 1081 | ({'country': "uk", "device": "desktop"}, 1001), 1082 | ({'country': "de", "device": "desktop"}, 995), 1083 | ({'country': "zh", "device": "desktop"}, 520), 1084 | ) 1085 | 1086 | # Construct the result to compare 1087 | valid_result = self._create_protobuf_object( 1088 | data, counter_data, metrics_pb2.COUNTER, data['const_labels']) 1089 | 1090 | # Add data to the collector 1091 | for i in counter_data: 1092 | c.set_value(i[0], i[1]) 1093 | 1094 | f = ProtobufFormat() 1095 | 1096 | result = f.marshall_collector(c) 1097 | 1098 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1099 | 1100 | def test_gauge_format(self): 1101 | 1102 | data = { 1103 | 'name': "logged_users_total", 1104 | 'help_text': "Logged users in the application", 1105 | 'const_labels': None, 1106 | } 1107 | g = Gauge(**data) 1108 | 1109 | gauge_data = ( 1110 | ({'country': "sp", "device": "desktop"}, 520), 1111 | ({'country': "us", "device": "mobile"}, 654), 1112 | ({'country': "uk", "device": "desktop"}, 1001), 1113 | ({'country': "de", "device": "desktop"}, 995), 1114 | ({'country': "zh", "device": "desktop"}, 520), 1115 | ) 1116 | 1117 | # Construct the result to compare 1118 | valid_result = self._create_protobuf_object( 1119 | data, gauge_data, metrics_pb2.GAUGE) 1120 | 1121 | # Add data to the collector 1122 | for i in gauge_data: 1123 | g.set_value(i[0], i[1]) 1124 | 1125 | f = ProtobufFormat() 1126 | 1127 | result = f.marshall_collector(g) 1128 | 1129 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1130 | 1131 | def test_gauge_format_with_const_labels(self): 1132 | data = { 1133 | 'name': "logged_users_total", 1134 | 'help_text': "Logged users in the application", 1135 | 'const_labels': {"app": "my_app"}, 1136 | } 1137 | g = Gauge(**data) 1138 | 1139 | gauge_data = ( 1140 | ({'country': "sp", "device": "desktop"}, 520), 1141 | ({'country': "us", "device": "mobile"}, 654), 1142 | ({'country': "uk", "device": "desktop"}, 1001), 1143 | ({'country': "de", "device": "desktop"}, 995), 1144 | ({'country': "zh", "device": "desktop"}, 520), 1145 | ) 1146 | 1147 | # Construct the result to compare 1148 | valid_result = self._create_protobuf_object( 1149 | data, gauge_data, metrics_pb2.GAUGE, data['const_labels']) 1150 | 1151 | # Add data to the collector 1152 | for i in gauge_data: 1153 | g.set_value(i[0], i[1]) 1154 | 1155 | f = ProtobufFormat() 1156 | 1157 | result = f.marshall_collector(g) 1158 | 1159 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1160 | 1161 | def test_one_summary_format(self): 1162 | data = { 1163 | 'name': "logged_users_total", 1164 | 'help_text': "Logged users in the application", 1165 | 'const_labels': {}, 1166 | } 1167 | 1168 | labels = {'handler': '/static'} 1169 | values = [3, 5.2, 13, 4] 1170 | 1171 | s = Summary(**data) 1172 | 1173 | for i in values: 1174 | s.add(labels, i) 1175 | 1176 | tmp_valid_data = [ 1177 | (labels, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 1178 | ] 1179 | valid_result = self._create_protobuf_object(data, tmp_valid_data, 1180 | metrics_pb2.SUMMARY) 1181 | 1182 | f = ProtobufFormat() 1183 | 1184 | result = f.marshall_collector(s) 1185 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1186 | 1187 | def test_one_summary_format_with_const_labels(self): 1188 | data = { 1189 | 'name': "logged_users_total", 1190 | 'help_text': "Logged users in the application", 1191 | 'const_labels': {"app": "my_app"}, 1192 | } 1193 | 1194 | labels = {'handler': '/static'} 1195 | values = [3, 5.2, 13, 4] 1196 | 1197 | s = Summary(**data) 1198 | 1199 | for i in values: 1200 | s.add(labels, i) 1201 | 1202 | tmp_valid_data = [ 1203 | (labels, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 1204 | ] 1205 | valid_result = self._create_protobuf_object(data, tmp_valid_data, 1206 | metrics_pb2.SUMMARY, 1207 | data['const_labels']) 1208 | 1209 | f = ProtobufFormat() 1210 | 1211 | result = f.marshall_collector(s) 1212 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1213 | 1214 | def test_summary_format(self): 1215 | data = { 1216 | 'name': "logged_users_total", 1217 | 'help_text': "Logged users in the application", 1218 | 'const_labels': {}, 1219 | } 1220 | 1221 | summary_data = ( 1222 | ({'interval': "5s"}, [3, 5.2, 13, 4]), 1223 | ({'interval': "10s"}, [1.3, 1.2, 32.1, 59.2, 109.46, 70.9]), 1224 | ({'interval': "10s", 'method': "fast"}, [5, 9.8, 31, 9.7, 101.4]), 1225 | ) 1226 | 1227 | s = Summary(**data) 1228 | 1229 | for i in summary_data: 1230 | for j in i[1]: 1231 | s.add(i[0], j) 1232 | 1233 | tmp_valid_data = [ 1234 | ({'interval': "5s"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), 1235 | ({'interval': "10s"}, {0.5: 32.1, 0.9: 59.2, 0.99: 59.2, "sum": 274.15999999999997, "count": 6}), 1236 | ({'interval': "10s", 'method': "fast"}, {0.5: 9.7, 0.9: 9.8, 0.99: 9.8, "sum": 156.9, "count": 5}), 1237 | ] 1238 | valid_result = self._create_protobuf_object(data, tmp_valid_data, 1239 | metrics_pb2.SUMMARY) 1240 | 1241 | f = ProtobufFormat() 1242 | 1243 | result = f.marshall_collector(s) 1244 | self.assertTrue(self._protobuf_metric_equal(valid_result, result)) 1245 | 1246 | def test_registry_marshall_counter(self): 1247 | 1248 | format_times = 10 1249 | 1250 | counter_data = ( 1251 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 1252 | ) 1253 | 1254 | registry = Registry() 1255 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 1256 | 1257 | # Add data 1258 | [counter.set(c[0], c[1]) for c in counter_data] 1259 | 1260 | registry.register(counter) 1261 | 1262 | valid_result = b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00\x00\x00\x00\x00\x00y@' 1263 | f = ProtobufFormat() 1264 | 1265 | # Check multiple times to ensure multiple marshalling requests 1266 | for i in range(format_times): 1267 | self.assertEqual(valid_result, f.marshall(registry)) 1268 | 1269 | def test_registry_marshall_gauge(self): 1270 | format_times = 10 1271 | 1272 | gauge_data = ( 1273 | ({'g_sample': '1', 'g_subsample': 'b'}, 800), 1274 | ) 1275 | 1276 | registry = Registry() 1277 | gauge = Gauge("gauge_test", "A gauge.", {'type': "gauge"}) 1278 | 1279 | # Add data 1280 | [gauge.set(g[0], g[1]) for g in gauge_data] 1281 | 1282 | registry.register(gauge) 1283 | 1284 | valid_result = b'U\n\ngauge_test\x12\x08A gauge.\x18\x01";\n\r\n\x08g_sample\x12\x011\n\x10\n\x0bg_subsample\x12\x01b\n\r\n\x04type\x12\x05gauge\x12\t\t\x00\x00\x00\x00\x00\x00\x89@' 1285 | 1286 | f = ProtobufFormat() 1287 | 1288 | # Check multiple times to ensure multiple marshalling requests 1289 | for i in range(format_times): 1290 | self.assertEqual(valid_result, f.marshall(registry)) 1291 | 1292 | def test_registry_marshall_summary(self): 1293 | format_times = 10 1294 | 1295 | summary_data = ( 1296 | ({'s_sample': '1', 's_subsample': 'b'}, range(4000, 5000, 47)), 1297 | ) 1298 | 1299 | registry = Registry() 1300 | summary = Summary("summary_test", "A summary.", {'type': "summary"}) 1301 | 1302 | # Add data 1303 | [summary.add(i[0], s) for i in summary_data for s in i[1]] 1304 | 1305 | registry.register(summary) 1306 | 1307 | valid_result = b'\x99\x01\n\x0csummary_test\x12\nA summary.\x18\x02"{\n\r\n\x08s_sample\x12\x011\n\x10\n\x0bs_subsample\x12\x01b\n\x0f\n\x04type\x12\x07summary"G\x08\x16\x11\x00\x00\x00\x00\x90"\xf8@\x1a\x12\t\x00\x00\x00\x00\x00\x00\xe0?\x11\x00\x00\x00\x00\x00\x8b\xb0@\x1a\x12\t\xcd\xcc\xcc\xcc\xcc\xcc\xec?\x11\x00\x00\x00\x00\x00v\xb1@\x1a\x12\t\xaeG\xe1z\x14\xae\xef?\x11\x00\x00\x00\x00\x00\xa5\xb1@' 1308 | 1309 | f = ProtobufFormat() 1310 | 1311 | # Check multiple times to ensure multiple marshalling requests 1312 | for i in range(format_times): 1313 | self.assertEqual(valid_result, f.marshall(registry)) 1314 | 1315 | 1316 | class TestProtobufTextFormat(unittest.TestCase): 1317 | 1318 | # Small tests because of the ordenation 1319 | def test_registry_marshall_counter(self): 1320 | format_times = 10 1321 | 1322 | counter_data = ( 1323 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 1324 | ) 1325 | 1326 | registry = Registry() 1327 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 1328 | 1329 | # Add data 1330 | [counter.set(c[0], c[1]) for c in counter_data] 1331 | 1332 | registry.register(counter) 1333 | 1334 | valid_result = """name: "counter_test" 1335 | help: "A counter." 1336 | type: COUNTER 1337 | metric { 1338 | label { 1339 | name: "c_sample" 1340 | value: "1" 1341 | } 1342 | label { 1343 | name: "c_subsample" 1344 | value: "b" 1345 | } 1346 | label { 1347 | name: "type" 1348 | value: "counter" 1349 | } 1350 | counter { 1351 | value: 400 1352 | } 1353 | } 1354 | """ 1355 | 1356 | f = ProtobufTextFormat() 1357 | # Check multiple times to ensure multiple marshalling requests 1358 | for i in range(format_times): 1359 | self.assertEqual(valid_result, f.marshall(registry)) 1360 | 1361 | def test_registry_marshall_counter_with_timestamp(self): 1362 | format_times = 10 1363 | 1364 | counter_data = ( 1365 | ({'c_sample': '1', 'c_subsample': 'b'}, 400), 1366 | ) 1367 | 1368 | registry = Registry() 1369 | counter = Counter("counter_test", "A counter.", {'type': "counter"}) 1370 | 1371 | # Add data 1372 | [counter.set(c[0], c[1]) for c in counter_data] 1373 | 1374 | registry.register(counter) 1375 | 1376 | valid_regex = """name: "counter_test" 1377 | help: "A counter." 1378 | type: COUNTER 1379 | metric { 1380 | label { 1381 | name: "c_sample" 1382 | value: "1" 1383 | } 1384 | label { 1385 | name: "c_subsample" 1386 | value: "b" 1387 | } 1388 | label { 1389 | name: "type" 1390 | value: "counter" 1391 | } 1392 | counter { 1393 | value: 400 1394 | } 1395 | timestamp_ms: (\d+) 1396 | } 1397 | """ 1398 | 1399 | f = ProtobufTextFormat(timestamp=True) 1400 | 1401 | # Check multiple times to ensure multiple marshalling requests 1402 | for i in range(format_times): 1403 | self.assertTrue(re.match(valid_regex, f.marshall(registry))) 1404 | 1405 | def test_registry_marshall_gauge(self): 1406 | format_times = 10 1407 | 1408 | gauge_data = ( 1409 | ({'g_sample': '1', 'g_subsample': 'b'}, 800), 1410 | ) 1411 | 1412 | registry = Registry() 1413 | gauge = Gauge("gauge_test", "A gauge.", {'type': "gauge"}) 1414 | 1415 | # Add data 1416 | [gauge.set(g[0], g[1]) for g in gauge_data] 1417 | 1418 | registry.register(gauge) 1419 | valid_result = """name: "gauge_test" 1420 | help: "A gauge." 1421 | type: GAUGE 1422 | metric { 1423 | label { 1424 | name: "g_sample" 1425 | value: "1" 1426 | } 1427 | label { 1428 | name: "g_subsample" 1429 | value: "b" 1430 | } 1431 | label { 1432 | name: "type" 1433 | value: "gauge" 1434 | } 1435 | gauge { 1436 | value: 800 1437 | } 1438 | } 1439 | """ 1440 | f = ProtobufTextFormat() 1441 | # Check multiple times to ensure multiple marshalling requests 1442 | for i in range(format_times): 1443 | self.assertEqual(valid_result, f.marshall(registry)) 1444 | 1445 | def test_registry_marshall_gauge_with_timestamp(self): 1446 | format_times = 10 1447 | 1448 | gauge_data = ( 1449 | ({'g_sample': '1', 'g_subsample': 'b'}, 800), 1450 | ) 1451 | 1452 | registry = Registry() 1453 | gauge = Gauge("gauge_test", "A gauge.", {'type': "gauge"}) 1454 | 1455 | # Add data 1456 | [gauge.set(g[0], g[1]) for g in gauge_data] 1457 | 1458 | registry.register(gauge) 1459 | valid_regex = """name: "gauge_test" 1460 | help: "A gauge." 1461 | type: GAUGE 1462 | metric { 1463 | label { 1464 | name: "g_sample" 1465 | value: "1" 1466 | } 1467 | label { 1468 | name: "g_subsample" 1469 | value: "b" 1470 | } 1471 | label { 1472 | name: "type" 1473 | value: "gauge" 1474 | } 1475 | gauge { 1476 | value: 800 1477 | } 1478 | timestamp_ms: (\d+) 1479 | } 1480 | """ 1481 | f = ProtobufTextFormat(True) 1482 | # Check multiple times to ensure multiple marshalling requests 1483 | for i in range(format_times): 1484 | self.assertTrue(re.match(valid_regex, f.marshall(registry))) 1485 | 1486 | def test_registry_marshall_summary(self): 1487 | format_times = 10 1488 | 1489 | summary_data = ( 1490 | ({'s_sample': '1', 's_subsample': 'b'}, range(4000, 5000, 47)), 1491 | ) 1492 | 1493 | registry = Registry() 1494 | summary = Summary("summary_test", "A summary.", {'type': "summary"}) 1495 | 1496 | # Add data 1497 | [summary.add(i[0], s) for i in summary_data for s in i[1]] 1498 | 1499 | registry.register(summary) 1500 | valid_result = """name: "summary_test" 1501 | help: "A summary." 1502 | type: SUMMARY 1503 | metric { 1504 | label { 1505 | name: "s_sample" 1506 | value: "1" 1507 | } 1508 | label { 1509 | name: "s_subsample" 1510 | value: "b" 1511 | } 1512 | label { 1513 | name: "type" 1514 | value: "summary" 1515 | } 1516 | summary { 1517 | sample_count: 22 1518 | sample_sum: 98857.0 1519 | quantile { 1520 | quantile: 0.5 1521 | value: 4235.0 1522 | } 1523 | quantile { 1524 | quantile: 0.9 1525 | value: 4470.0 1526 | } 1527 | quantile { 1528 | quantile: 0.99 1529 | value: 4517.0 1530 | } 1531 | } 1532 | } 1533 | """ 1534 | f = ProtobufTextFormat() 1535 | 1536 | # Check multiple times to ensure multiple marshalling requests 1537 | for i in range(format_times): 1538 | self.assertEqual(valid_result, f.marshall(registry)) 1539 | 1540 | def test_registry_marshall_summary_with_timestamp(self): 1541 | format_times = 10 1542 | 1543 | summary_data = ( 1544 | ({'s_sample': '1', 's_subsample': 'b'}, range(4000, 5000, 47)), 1545 | ) 1546 | 1547 | registry = Registry() 1548 | summary = Summary("summary_test", "A summary.", {'type': "summary"}) 1549 | 1550 | # Add data 1551 | [summary.add(i[0], s) for i in summary_data for s in i[1]] 1552 | 1553 | registry.register(summary) 1554 | valid_regex = """name: "summary_test" 1555 | help: "A summary." 1556 | type: SUMMARY 1557 | metric { 1558 | label { 1559 | name: "s_sample" 1560 | value: "1" 1561 | } 1562 | label { 1563 | name: "s_subsample" 1564 | value: "b" 1565 | } 1566 | label { 1567 | name: "type" 1568 | value: "summary" 1569 | } 1570 | summary { 1571 | sample_count: 22 1572 | sample_sum: 98857.0 1573 | quantile { 1574 | quantile: 0.5 1575 | value: 4235.0 1576 | } 1577 | quantile { 1578 | quantile: 0.9 1579 | value: 4470.0 1580 | } 1581 | quantile { 1582 | quantile: 0.99 1583 | value: 4517.0 1584 | } 1585 | } 1586 | timestamp_ms: (\d+) 1587 | } 1588 | """ 1589 | f = ProtobufTextFormat(True) 1590 | # Check multiple times to ensure multiple marshalling requests 1591 | for i in range(format_times): 1592 | self.assertTrue(re.match(valid_regex, f.marshall(registry))) --------------------------------------------------------------------------------