From e7dac801787dd55ef5c86678ba67824f53b72fcb Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Tue, 26 Nov 2019 22:21:42 +0100 Subject: [PATCH 01/19] Add metrics support out of the box --- pyms/flask/app/create_app.py | 3 +++ pyms/flask/metrics/__init__.py | 1 + pyms/flask/metrics/metrics.py | 43 ++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 pyms/flask/metrics/__init__.py create mode 100644 pyms/flask/metrics/metrics.py diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index 293974e..50d33d3 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -8,6 +8,7 @@ from pyms.config.conf import get_conf from pyms.constants import LOGGER_NAME, SERVICE_ENVIRONMENT from pyms.flask.healthcheck import healthcheck_blueprint +from pyms.flask.metrics import metrics_blueprint, monitor from pyms.flask.services.driver import ServicesManager from pyms.logger import CustomJsonFormatter from pyms.utils.utils import check_package_exists @@ -113,6 +114,8 @@ def create_app(self): # Initialize Blueprints self.application.register_blueprint(healthcheck_blueprint) + self.application.register_blueprint(metrics_blueprint) + monitor(self.application) self.init_libs() self.add_error_handlers() diff --git a/pyms/flask/metrics/__init__.py b/pyms/flask/metrics/__init__.py new file mode 100644 index 0000000..9192276 --- /dev/null +++ b/pyms/flask/metrics/__init__.py @@ -0,0 +1 @@ +from pyms.flask.metrics.metrics import metrics_blueprint, monitor diff --git a/pyms/flask/metrics/metrics.py b/pyms/flask/metrics/metrics.py new file mode 100644 index 0000000..7c2a942 --- /dev/null +++ b/pyms/flask/metrics/metrics.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals, print_function, absolute_import, division + +import time + +from flask import Blueprint, Response, request +from prometheus_client import Counter, Histogram, generate_latest + +# Based on https://github.com/sbarratt/flask-prometheus + +metrics_blueprint = Blueprint("metrics", __name__) + +FLASK_REQUEST_LATENCY = Histogram( + "flask_request_latency_seconds", "Flask Request Latency", ["method", "endpoint"] +) +FLASK_REQUEST_COUNT = Counter( + "flask_request_count", "Flask Request Count", ["method", "endpoint", "http_status"] +) + + +def before_request(): + request.start_time = time.time() + + +def after_request(response): + request_latency = time.time() - request.start_time + FLASK_REQUEST_LATENCY.labels(request.method, request.path).observe(request_latency) + FLASK_REQUEST_COUNT.labels(request.method, request.path, response.status_code).inc() + + return response + + +def monitor(app): + app.before_request(before_request) + app.after_request(after_request) + + +@metrics_blueprint.route("/metrics", methods=["GET"]) +def metrics(): + return Response( + generate_latest(), + mimetype="text/plain", + content_type="text/plain; charset=utf-8", + ) From 0613b32da4bd18918f8d1a0f68d889f73fee8e2f Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 00:36:22 +0100 Subject: [PATCH 02/19] Split to ease modularization --- pyms/flask/metrics/__init__.py | 2 +- pyms/flask/metrics/{metrics.py => flask.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pyms/flask/metrics/{metrics.py => flask.py} (100%) diff --git a/pyms/flask/metrics/__init__.py b/pyms/flask/metrics/__init__.py index 9192276..30f45d5 100644 --- a/pyms/flask/metrics/__init__.py +++ b/pyms/flask/metrics/__init__.py @@ -1 +1 @@ -from pyms.flask.metrics.metrics import metrics_blueprint, monitor +from pyms.flask.metrics.flask import metrics_blueprint, monitor diff --git a/pyms/flask/metrics/metrics.py b/pyms/flask/metrics/flask.py similarity index 100% rename from pyms/flask/metrics/metrics.py rename to pyms/flask/metrics/flask.py From cb089212a46de5fecb2e2d3789913ca417ba8fd6 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 00:36:54 +0100 Subject: [PATCH 03/19] Add logger metrics --- pyms/flask/app/create_app.py | 6 +++++- pyms/flask/metrics/__init__.py | 1 + pyms/flask/metrics/logger.py | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 pyms/flask/metrics/logger.py diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index 50d33d3..bc95c80 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -8,7 +8,7 @@ from pyms.config.conf import get_conf from pyms.constants import LOGGER_NAME, SERVICE_ENVIRONMENT from pyms.flask.healthcheck import healthcheck_blueprint -from pyms.flask.metrics import metrics_blueprint, monitor +from pyms.flask.metrics import metrics_blueprint, monitor, logger_metrics from pyms.flask.services.driver import ServicesManager from pyms.logger import CustomJsonFormatter from pyms.utils.utils import check_package_exists @@ -63,6 +63,10 @@ def init_tracer(self): def init_logger(self): self.application.logger = logger + self.application.logger = logger_metrics( + self.application.config["APP_NAME"], + self.application.logger + ) os.environ['WERKZEUG_RUN_MAIN'] = "true" formatter = CustomJsonFormatter('(timestamp) (level) (name) (module) (funcName) (lineno) (message)') diff --git a/pyms/flask/metrics/__init__.py b/pyms/flask/metrics/__init__.py index 30f45d5..b75043d 100644 --- a/pyms/flask/metrics/__init__.py +++ b/pyms/flask/metrics/__init__.py @@ -1 +1,2 @@ from pyms.flask.metrics.flask import metrics_blueprint, monitor +from pyms.flask.metrics.logger import logger_metrics diff --git a/pyms/flask/metrics/logger.py b/pyms/flask/metrics/logger.py new file mode 100644 index 0000000..686069d --- /dev/null +++ b/pyms/flask/metrics/logger.py @@ -0,0 +1,22 @@ +import logging + +import prometheus_client + +# Based on https://github.com/korfuri/python-logging-prometheus/ + +log_entries = prometheus_client.Counter( + "python_logging_messages_total", + "Count of log entries by service and level.", + ["service", "level"], +) + + +def logger_metrics(app_name, logger): + class ExportingLogHandler(logging.Handler): + """A LogHandler that exports logging metrics for Prometheus.io.""" + + def emit(self, record): + log_entries.labels(app_name, record.levelname).inc() + + logger.addHandler(ExportingLogHandler()) + return logger From 8f76186326b3a00b49e6c8701c2c41976b3f6d55 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 09:37:45 +0100 Subject: [PATCH 04/19] Add requirements --- Pipfile | 1 + requirements-tests.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/Pipfile b/Pipfile index 663f689..b01301c 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ jaeger-client = "==4.1.0" flask-opentracing = "*" opentracing = ">=2.1" opentracing-instrumentation = "==3.2.1" +prometheus_client = ">=0.7.1" [dev-packages] requests-mock = "*" coverage = "==4.4.0" diff --git a/requirements-tests.txt b/requirements-tests.txt index d9626ad..cf61cef 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -85,3 +85,4 @@ webencodings==0.5.1 Werkzeug==0.16.0 wrapt==1.11.2 zipp==0.6.0 +prometheus_client==0.7.1 From b306496525842028098d45bdca41212741872fdb Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 10:31:38 +0100 Subject: [PATCH 05/19] Update pipfile lock --- Pipfile.lock | 227 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 143 insertions(+), 84 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 7b1f45c..7690967 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "1388af3b70241985d77207fcf801afa2cc9ce5b2ec2363b282b88e405fb5fef4" + "sha256": "bb33289bcd92a4ca7abd12b0fb31150f96f4ce8be22e2c9da795897902e5fce5" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.6" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -186,6 +184,13 @@ "index": "pypi", "version": "==3.2.1" }, + "prometheus-client": { + "hashes": [ + "sha256:71cd24a2b3eb335cb800c7159f423df1bd4dcd5171b234be15e3f31ec9f622da" + ], + "index": "pypi", + "version": "==0.7.1" + }, "python-json-logger": { "hashes": [ "sha256:b7a31162f2a01965a5efb94453ce69230ed208468b0bbc7fdfc56e6d8df2e281" @@ -195,22 +200,22 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:05418379e70ae2e986d31cfb51b50bd0f93bf5ab9d9b40dabdb4616727c4c26e", + "sha256:37c31e6087df09321539c18b5b02382538354c350dc76f3b458a6c93745a545c", + "sha256:3fd57916529381a46619e1cbfe1d372c7e008d5945fb1953da4a03b195630c33", + "sha256:52559ed9a06e2775d5c7ec5d86932371a439d6594a21991589475894a399939b", + "sha256:79288cdd596f9b77687f9e363fabf74a71f0399034b2742a74b1ca1f0ba5285f", + "sha256:7f737a46c65635898a1cb19b2ddf4e0c906d3f2e422c995f828fb621f8fa856b", + "sha256:93c09bcfe50adc03bbfb74f665e680d984b1023e83d0b48c93f7e2a8e70ac4be", + "sha256:94641ee1659be00239882b74e824ca6bc6b0c42f3f63b772f8f42cfaacfc83ab", + "sha256:9af87170b8a6c8e3219139a7146835bde3f5532cc47553701829a111cb2a9313", + "sha256:b6bd554afb21407f503d7821b9b8a4c3e36d3eb3e8fee329aa138cb5bc4f4809", + "sha256:c41a87796b705a473db39d06c220b1f25616b8c92fb5ea5c7fe327d2dcd63eb3", + "sha256:f00cd88db394bab373bcdcc58ab2eb5c3c3e75a520c6fab084a66d0ecd8cf90c", + "sha256:f65ea27155c5401e493935abdb10929b6df66256f202b91d00e967e54411f3a0" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2b1" }, "requests": { "hashes": [ @@ -221,19 +226,19 @@ }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "swagger-ui-bundle": { "hashes": [ - "sha256:01ae8fdb1fa4e034933e0874afdda0d433dcb94476fccb231b66fd5f49dac96c", - "sha256:802f160dd6fe1d6b8fa92c6a40f593ef52f87ad0f507b1170ad2067f03de4c01", - "sha256:e88bd0d8334d685440a85210ff1e1083a0caabd4c36fa061843067ff4c2ac680" + "sha256:49d2e12d60a6499e9d37ea37953b5d700f4e114edc7520fe918bae5eb693a20e", + "sha256:c5373b683487b1b914dccd23bcd9a3016afa2c2d1cda10f8713c0a9af0f91dd3", + "sha256:f776811855092c086dbb08216c8810a84accef8c76c796a135caa13645c5cc68" ], "index": "pypi", - "version": "==0.0.5" + "version": "==0.0.6" }, "threadloop": { "hashes": [ @@ -244,9 +249,9 @@ }, "thrift": { "hashes": [ - "sha256:7d59ac4fdcb2c58037ebd4a9da5f9a49e3e034bf75b3f26d9fe48ba3d8806e6b" + "sha256:9af1c86bf73433afc6010ed376a6c6aca2b54099cc0d61895f640870a9ae7d89" ], - "version": "==0.11.0" + "version": "==0.13.0" }, "tornado": { "hashes": [ @@ -262,10 +267,10 @@ }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], - "version": "==1.25.6" + "version": "==1.25.7" }, "werkzeug": { "hashes": [ @@ -284,17 +289,10 @@ "develop": { "astroid": { "hashes": [ - "sha256:09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", - "sha256:5a79c9b4bd6c4be777424593f957c996e20beb5f74e0bc332f47713c6f675efe" + "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", + "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" ], - "version": "==2.3.2" - }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "version": "==1.3.0" + "version": "==2.3.3" }, "attrs": { "hashes": [ @@ -311,6 +309,12 @@ "index": "pypi", "version": "==1.6.2" }, + "basictracer": { + "hashes": [ + "sha256:00871fba2a3f5f1bad185ea28f5d4b70d337cba0807a5399043789b48fd6be99" + ], + "version": "==3.0.0" + }, "certifi": { "hashes": [ "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", @@ -391,10 +395,16 @@ }, "gitpython": { "hashes": [ - "sha256:3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2", - "sha256:a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389" + "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", + "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" ], - "version": "==3.0.4" + "version": "==3.0.5" + }, + "googleapis-common-protos": { + "hashes": [ + "sha256:e61b8ed5e36b976b487c6e7b15f31bb10c7a0ca7bd5c0e837f4afab64b53a0c6" + ], + "version": "==1.6.0" }, "idna": { "hashes": [ @@ -425,6 +435,13 @@ ], "version": "==2.10.3" }, + "jsonpickle": { + "hashes": [ + "sha256:d0c5a4e6cb4e58f6d5406bdded44365c2bcf9c836c4f52910cc9ba7245a59dc2", + "sha256:d3e922d781b1d0096df2dad89a2e1f47177d7969b596aea806a9d91b4626b29b" + ], + "version": "==1.2" + }, "lazy-object-proxy": { "hashes": [ "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", @@ -451,6 +468,14 @@ ], "version": "==1.4.3" }, + "lightstep": { + "hashes": [ + "sha256:149ffc031498405950e4c1ef972c58f1a6e641c9ca3961347ed4e1948b9c8bff", + "sha256:f565d8731c3a583c8e5ca4ba3be124d83103bf54cac2aa2952a5d194900dda79" + ], + "index": "pypi", + "version": "==4.1.0" + }, "livereload": { "hashes": [ "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", @@ -520,6 +545,13 @@ ], "version": "==7.2.0" }, + "opentracing": { + "hashes": [ + "sha256:cfd231ba5c58f90bc277787e62861eb0c6e4af76e42957bec240bbdf71fb7e0e" + ], + "index": "pypi", + "version": "==2.2.0" + }, "packaging": { "hashes": [ "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", @@ -529,17 +561,38 @@ }, "pbr": { "hashes": [ - "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", - "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" + "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", + "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" ], - "version": "==5.4.3" + "version": "==5.4.4" }, "pluggy": { "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.13.0" + "version": "==0.13.1" + }, + "protobuf": { + "hashes": [ + "sha256:0ba5d7626dbc4ce78971c3e62ec37f84c8139ea7008c008660d3312cf11e0db8", + "sha256:189b706f72e8b7ddc965168a79ff296ca5b7bdd95b5b05208afb9818a681c712", + "sha256:340965444aafc7aac7e3586e930f5b3f8347ca9b350afab60bac84dcc0b94437", + "sha256:44fbc7b1786ab975ec9eba9da765398d58ec705d1c8e856b0523b8f9c1c53cf7", + "sha256:48d96b559fab3063feaebd316352e3418424629d59b77dbcb96ecc4c594d7f5f", + "sha256:5e32923c7896c49b1d3a327fe25a76363d200acdfa97844f5647f1bf9f298da8", + "sha256:6662442fbf22796dbd942bb15b664d70dcc25ae28d371b7e4ca6261e9bc495b7", + "sha256:6bb5d999faceee281bc4a2fc77866c61af7be4b7e5efadc930c42f234a99cafd", + "sha256:83b38b7b61b7c60af0fa03a71c27c4232117453a62ccf69a511284793a400751", + "sha256:90c22f4fd4e01279efc4e4911dafe308f35fcc4310bcb89bcee4d3ca20210d20", + "sha256:97b08853b9bb71512ed52381f05cf2d4179f4234825b505d8f8d2bb9d9429939", + "sha256:aef47082114428b47db73876ecb7751802548830ce5c95dba7ebe24d5e196d7c", + "sha256:b89ed3ba88ea5ec8b2c704a5ae747c9038ee1faff277fcddac75f850e645f7e1", + "sha256:be5afc2e1f5c320bd4a38e73d8b02c67d72dbee370a004732c923c7c8a472f72", + "sha256:d1c18853c7ad3c8e34edfafc6488fc24f4221c15b516c14796032cc53f8cde94", + "sha256:f4370d0e3d6e1ac2f80911651691ac540901f661b372036ea72637546ba98202" + ], + "version": "==3.11.0" }, "py": { "hashes": [ @@ -550,26 +603,26 @@ }, "pylint": { "hashes": [ - "sha256:7b76045426c650d2b0f02fc47c14d7934d17898779da95288a74c2a7ec440702", - "sha256:856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0" + "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", + "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" ], "index": "pypi", - "version": "==2.4.3" + "version": "==2.4.4" }, "pyparsing": { "hashes": [ - "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", - "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" + "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", + "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" ], - "version": "==2.4.2" + "version": "==2.4.5" }, "pytest": { "hashes": [ - "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6", - "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4" + "sha256:63344a2e3bce2e4d522fd62b4fdebb647c019f1f9e4ca075debbd13219db4418", + "sha256:f67403f33b2b1d25a6756184077394167fe5e2f9d8bdaab30707d19ccec35427" ], "index": "pypi", - "version": "==5.2.2" + "version": "==5.3.1" }, "pytest-cov": { "hashes": [ @@ -581,22 +634,22 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:05418379e70ae2e986d31cfb51b50bd0f93bf5ab9d9b40dabdb4616727c4c26e", + "sha256:37c31e6087df09321539c18b5b02382538354c350dc76f3b458a6c93745a545c", + "sha256:3fd57916529381a46619e1cbfe1d372c7e008d5945fb1953da4a03b195630c33", + "sha256:52559ed9a06e2775d5c7ec5d86932371a439d6594a21991589475894a399939b", + "sha256:79288cdd596f9b77687f9e363fabf74a71f0399034b2742a74b1ca1f0ba5285f", + "sha256:7f737a46c65635898a1cb19b2ddf4e0c906d3f2e422c995f828fb621f8fa856b", + "sha256:93c09bcfe50adc03bbfb74f665e680d984b1023e83d0b48c93f7e2a8e70ac4be", + "sha256:94641ee1659be00239882b74e824ca6bc6b0c42f3f63b772f8f42cfaacfc83ab", + "sha256:9af87170b8a6c8e3219139a7146835bde3f5532cc47553701829a111cb2a9313", + "sha256:b6bd554afb21407f503d7821b9b8a4c3e36d3eb3e8fee329aa138cb5bc4f4809", + "sha256:c41a87796b705a473db39d06c220b1f25616b8c92fb5ea5c7fe327d2dcd63eb3", + "sha256:f00cd88db394bab373bcdcc58ab2eb5c3c3e75a520c6fab084a66d0ecd8cf90c", + "sha256:f65ea27155c5401e493935abdb10929b6df66256f202b91d00e967e54411f3a0" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2b1" }, "requests": { "hashes": [ @@ -623,10 +676,10 @@ }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "smmap2": { "hashes": [ @@ -642,6 +695,12 @@ ], "version": "==1.31.0" }, + "thrift": { + "hashes": [ + "sha256:9af1c86bf73433afc6010ed376a6c6aca2b54099cc0d61895f640870a9ae7d89" + ], + "version": "==0.13.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -663,11 +722,11 @@ }, "tox": { "hashes": [ - "sha256:0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e", - "sha256:c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1" + "sha256:1d1368ac86e8332f79e2bcef9fefe2b077469f08449eadf0183759b34f3b2070", + "sha256:bcfa3e40abc1e9b70607b56adfd976fe7dc8286ad56aab44e3151daca7d2d0d0" ], "index": "pypi", - "version": "==3.14.0" + "version": "==3.14.1" }, "typed-ast": { "hashes": [ @@ -697,17 +756,17 @@ }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], - "version": "==1.25.6" + "version": "==1.25.7" }, "virtualenv": { "hashes": [ - "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", - "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" + "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0", + "sha256:b57776b44f91511866594e477dd10e76a6eb44439cdd7f06dcd30ba4c5bd854f" ], - "version": "==16.7.7" + "version": "==16.7.8" }, "wcwidth": { "hashes": [ From 4dc4dc65fb86e55ec752ae9fe67dd4678550cf11 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 10:36:22 +0100 Subject: [PATCH 06/19] Delete unnecesary line --- pyms/flask/app/create_app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index bc95c80..2640917 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -62,10 +62,9 @@ def init_tracer(self): self.application.tracer = FlaskTracing(client, True, self.application) def init_logger(self): - self.application.logger = logger self.application.logger = logger_metrics( self.application.config["APP_NAME"], - self.application.logger + logger ) os.environ['WERKZEUG_RUN_MAIN'] = "true" From 5b36a906c7927fa3e442b63515c9773c5822eb5a Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 11:00:11 +0100 Subject: [PATCH 07/19] Deacoplate class of logger function --- pyms/flask/metrics/logger.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyms/flask/metrics/logger.py b/pyms/flask/metrics/logger.py index 686069d..19d42e4 100644 --- a/pyms/flask/metrics/logger.py +++ b/pyms/flask/metrics/logger.py @@ -11,12 +11,19 @@ ) -def logger_metrics(app_name, logger): - class ExportingLogHandler(logging.Handler): - """A LogHandler that exports logging metrics for Prometheus.io.""" +class ExportingLogHandler(logging.Handler): + """A LogHandler that exports logging metrics for Prometheus.io.""" + + # def __init__(self, app_name, *args, **kwargs): + def __init__(self, app_name): + super(ExportingLogHandler, self).__init__() + self.app_name = app_name + + def emit(self, record): + log_entries.labels(self.app_name, record.levelname).inc() - def emit(self, record): - log_entries.labels(app_name, record.levelname).inc() - logger.addHandler(ExportingLogHandler()) +def logger_metrics(app_name, logger): + + logger.addHandler(ExportingLogHandler(app_name)) return logger From 96ccc098c6e2da5cdda8315427952df5f0358982 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 16:15:01 +0100 Subject: [PATCH 08/19] Deacoplate logger metrics from metrics --- pyms/flask/app/create_app.py | 10 +++++----- pyms/flask/metrics/__init__.py | 2 +- pyms/flask/metrics/logger.py | 7 ------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index 2640917..ae70537 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -8,7 +8,7 @@ from pyms.config.conf import get_conf from pyms.constants import LOGGER_NAME, SERVICE_ENVIRONMENT from pyms.flask.healthcheck import healthcheck_blueprint -from pyms.flask.metrics import metrics_blueprint, monitor, logger_metrics +from pyms.flask.metrics import metrics_blueprint, monitor, ExportingLogHandler from pyms.flask.services.driver import ServicesManager from pyms.logger import CustomJsonFormatter from pyms.utils.utils import check_package_exists @@ -62,10 +62,6 @@ def init_tracer(self): self.application.tracer = FlaskTracing(client, True, self.application) def init_logger(self): - self.application.logger = logger_metrics( - self.application.config["APP_NAME"], - logger - ) os.environ['WERKZEUG_RUN_MAIN'] = "true" formatter = CustomJsonFormatter('(timestamp) (level) (name) (module) (funcName) (lineno) (message)') @@ -74,6 +70,10 @@ def init_logger(self): log_handler.setFormatter(formatter) self.application.logger.addHandler(log_handler) + self.application.logger.addHandler( + ExportingLogHandler(self.application.config["APP_NAME"]) + ) + self.application.logger.propagate = False if self.application.config["DEBUG"]: diff --git a/pyms/flask/metrics/__init__.py b/pyms/flask/metrics/__init__.py index b75043d..bdfa700 100644 --- a/pyms/flask/metrics/__init__.py +++ b/pyms/flask/metrics/__init__.py @@ -1,2 +1,2 @@ from pyms.flask.metrics.flask import metrics_blueprint, monitor -from pyms.flask.metrics.logger import logger_metrics +from pyms.flask.metrics.logger import ExportingLogHandler diff --git a/pyms/flask/metrics/logger.py b/pyms/flask/metrics/logger.py index 19d42e4..a35abdc 100644 --- a/pyms/flask/metrics/logger.py +++ b/pyms/flask/metrics/logger.py @@ -14,16 +14,9 @@ class ExportingLogHandler(logging.Handler): """A LogHandler that exports logging metrics for Prometheus.io.""" - # def __init__(self, app_name, *args, **kwargs): def __init__(self, app_name): super(ExportingLogHandler, self).__init__() self.app_name = app_name def emit(self, record): log_entries.labels(self.app_name, record.levelname).inc() - - -def logger_metrics(app_name, logger): - - logger.addHandler(ExportingLogHandler(app_name)) - return logger From 7d771d920eeee2b00a89e2c4d7192eac14768406 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Wed, 27 Nov 2019 16:16:31 +0100 Subject: [PATCH 09/19] Delete unused imported functions --- pyms/flask/metrics/flask.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyms/flask/metrics/flask.py b/pyms/flask/metrics/flask.py index 7c2a942..318de95 100644 --- a/pyms/flask/metrics/flask.py +++ b/pyms/flask/metrics/flask.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals, print_function, absolute_import, division - import time from flask import Blueprint, Response, request From 4864eeef26716078ec639b163587d062a90c24b0 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Thu, 28 Nov 2019 10:10:13 +0100 Subject: [PATCH 10/19] Create metrics test --- tests/test_metrics.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/test_metrics.py diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..2823fce --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import unittest.mock +from flask import Flask +from pyms.flask.metrics import metrics_blueprint, monitor +from prometheus_client import generate_latest + + +class TestMetricsFlask(unittest.TestCase): + def setUp(self): + self.app = Flask(__name__) + self.app.register_blueprint(metrics_blueprint) + self.client = self.app.test_client() + + def test_metrics_latency(self): + monitor(self.app) + generated_latency = b'flask_request_latency_seconds_bucket{endpoint="/",le="0.005",method="GET"}' + assert generated_latency in generate_latest() + + def test_metrics_count(self): + monitor(self.app) + generated_count = ( + b'flask_request_count_total{endpoint="/",http_status="404",method="GET"}' + ) + assert generated_count in generate_latest() From b18a316eb1abce2b22ba3b45b070fd87fd6d89b8 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Thu, 28 Nov 2019 20:33:51 +0100 Subject: [PATCH 11/19] Fix vulnerable dependencies --- requirements-tests.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index cf61cef..a61f2d2 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -42,7 +42,7 @@ mkdocs==1.0.4 mock==2.0.0 more-itertools==7.2.0 nose==1.3.7 -numpy==1.13.3 +numpy==1.16.1 openapi-spec-validator==0.2.8 opentracing==2.2.0 opentracing-instrumentation==3.2.1 @@ -52,7 +52,7 @@ pkginfo==1.5.0.1 pluggy==0.13.0 protobuf==3.9.0rc1 py==1.8.0 -py-ms==1.0.0 +py-ms==1.0.1 Pygments==2.3.1 pylint==2.4.3 pyparsing==2.4.2 From b5f235cd4bd0b6b777a5974d0814386a02eb17af Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Thu, 28 Nov 2019 20:34:19 +0100 Subject: [PATCH 12/19] Test metrics endpoint --- tests/test_metrics.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 2823fce..a2bdf3a 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -5,7 +5,6 @@ from pyms.flask.metrics import metrics_blueprint, monitor from prometheus_client import generate_latest - class TestMetricsFlask(unittest.TestCase): def setUp(self): self.app = Flask(__name__) @@ -14,12 +13,18 @@ def setUp(self): def test_metrics_latency(self): monitor(self.app) - generated_latency = b'flask_request_latency_seconds_bucket{endpoint="/",le="0.005",method="GET"}' - assert generated_latency in generate_latest() + self.client.get("/") + self.client.get("/metrics") + generated_latency_root = b'flask_request_latency_seconds_bucket{endpoint="/",le="0.005",method="GET"}' + generated_latency_metrics = b'flask_request_latency_seconds_bucket{endpoint="/metrics",le="0.005",method="GET"}' + assert generated_latency_root in generate_latest() + assert generated_latency_metrics in generate_latest() def test_metrics_count(self): monitor(self.app) - generated_count = ( - b'flask_request_count_total{endpoint="/",http_status="404",method="GET"}' - ) - assert generated_count in generate_latest() + self.client.get("/") + self.client.get("/metrics") + generated_count_root = b'flask_request_count_total{endpoint="/",http_status="404",method="GET"}' + generated_count_metrics = b'flask_request_count_total{endpoint="/metrics",http_status="200",method="GET"}' + assert generated_count_root in generate_latest() + assert generated_count_metrics in generate_latest() From d47f4943a49ad148a9fab85ec067a287924c2450 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Thu, 28 Nov 2019 23:40:01 +0100 Subject: [PATCH 13/19] Restore deleted line --- pyms/flask/app/create_app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index ae70537..86983fb 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -62,6 +62,7 @@ def init_tracer(self): self.application.tracer = FlaskTracing(client, True, self.application) def init_logger(self): + self.application.logger = logger os.environ['WERKZEUG_RUN_MAIN'] = "true" formatter = CustomJsonFormatter('(timestamp) (level) (name) (module) (funcName) (lineno) (message)') From 4b1c507d1b0f5304b1d1b1f5a8ecc372aaf98426 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Sat, 30 Nov 2019 22:19:38 +0100 Subject: [PATCH 14/19] Migrate metrics implementation to service --- pyms/flask/app/create_app.py | 17 +++++--- pyms/flask/metrics/__init__.py | 2 - pyms/flask/metrics/flask.py | 41 -------------------- pyms/flask/metrics/logger.py | 22 ----------- pyms/flask/services/metrics.py | 71 ++++++++++++++++++++++++++++++++++ tests/config-tests-metrics.yml | 8 ++++ tests/test_metrics.py | 27 +++++++++---- 7 files changed, 110 insertions(+), 78 deletions(-) delete mode 100644 pyms/flask/metrics/__init__.py delete mode 100644 pyms/flask/metrics/flask.py delete mode 100644 pyms/flask/metrics/logger.py create mode 100644 pyms/flask/services/metrics.py create mode 100644 tests/config-tests-metrics.yml diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index 86983fb..9f8f414 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -8,7 +8,6 @@ from pyms.config.conf import get_conf from pyms.constants import LOGGER_NAME, SERVICE_ENVIRONMENT from pyms.flask.healthcheck import healthcheck_blueprint -from pyms.flask.metrics import metrics_blueprint, monitor, ExportingLogHandler from pyms.flask.services.driver import ServicesManager from pyms.logger import CustomJsonFormatter from pyms.utils.utils import check_package_exists @@ -71,9 +70,6 @@ def init_logger(self): log_handler.setFormatter(formatter) self.application.logger.addHandler(log_handler) - self.application.logger.addHandler( - ExportingLogHandler(self.application.config["APP_NAME"]) - ) self.application.logger.propagate = False @@ -104,6 +100,15 @@ def init_app(self) -> Flask: return application + def init_metrics(self): + if getattr(self, "metrics", False) and self.metrics: + self.application.register_blueprint(self.metrics.metrics_blueprint) + self.metrics.add_logger_handler( + self.application.logger, + self.application.config["APP_NAME"] + ) + self.metrics.monitor(self.application) + def create_app(self): """Initialize the Flask app, register blueprints and initialize all libraries like Swagger, database, @@ -118,8 +123,6 @@ def create_app(self): # Initialize Blueprints self.application.register_blueprint(healthcheck_blueprint) - self.application.register_blueprint(metrics_blueprint) - monitor(self.application) self.init_libs() self.add_error_handlers() @@ -128,6 +131,8 @@ def create_app(self): self.init_logger() + self.init_metrics() + return self.application def add_error_handlers(self): diff --git a/pyms/flask/metrics/__init__.py b/pyms/flask/metrics/__init__.py deleted file mode 100644 index bdfa700..0000000 --- a/pyms/flask/metrics/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from pyms.flask.metrics.flask import metrics_blueprint, monitor -from pyms.flask.metrics.logger import ExportingLogHandler diff --git a/pyms/flask/metrics/flask.py b/pyms/flask/metrics/flask.py deleted file mode 100644 index 318de95..0000000 --- a/pyms/flask/metrics/flask.py +++ /dev/null @@ -1,41 +0,0 @@ -import time - -from flask import Blueprint, Response, request -from prometheus_client import Counter, Histogram, generate_latest - -# Based on https://github.com/sbarratt/flask-prometheus - -metrics_blueprint = Blueprint("metrics", __name__) - -FLASK_REQUEST_LATENCY = Histogram( - "flask_request_latency_seconds", "Flask Request Latency", ["method", "endpoint"] -) -FLASK_REQUEST_COUNT = Counter( - "flask_request_count", "Flask Request Count", ["method", "endpoint", "http_status"] -) - - -def before_request(): - request.start_time = time.time() - - -def after_request(response): - request_latency = time.time() - request.start_time - FLASK_REQUEST_LATENCY.labels(request.method, request.path).observe(request_latency) - FLASK_REQUEST_COUNT.labels(request.method, request.path, response.status_code).inc() - - return response - - -def monitor(app): - app.before_request(before_request) - app.after_request(after_request) - - -@metrics_blueprint.route("/metrics", methods=["GET"]) -def metrics(): - return Response( - generate_latest(), - mimetype="text/plain", - content_type="text/plain; charset=utf-8", - ) diff --git a/pyms/flask/metrics/logger.py b/pyms/flask/metrics/logger.py deleted file mode 100644 index a35abdc..0000000 --- a/pyms/flask/metrics/logger.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging - -import prometheus_client - -# Based on https://github.com/korfuri/python-logging-prometheus/ - -log_entries = prometheus_client.Counter( - "python_logging_messages_total", - "Count of log entries by service and level.", - ["service", "level"], -) - - -class ExportingLogHandler(logging.Handler): - """A LogHandler that exports logging metrics for Prometheus.io.""" - - def __init__(self, app_name): - super(ExportingLogHandler, self).__init__() - self.app_name = app_name - - def emit(self, record): - log_entries.labels(self.app_name, record.levelname).inc() diff --git a/pyms/flask/services/metrics.py b/pyms/flask/services/metrics.py new file mode 100644 index 0000000..81575dd --- /dev/null +++ b/pyms/flask/services/metrics.py @@ -0,0 +1,71 @@ +import time +import logging + +from flask import Blueprint, Response, request +from prometheus_client import Counter, Histogram, generate_latest +from pyms.flask.services.driver import DriverService + +# Based on https://github.com/sbarratt/flask-prometheus +# and https://github.com/korfuri/python-logging-prometheus/ + +FLASK_REQUEST_LATENCY = Histogram( + "flask_request_latency_seconds", "Flask Request Latency", ["method", "endpoint"] +) +FLASK_REQUEST_COUNT = Counter( + "flask_request_count", "Flask Request Count", ["method", "endpoint", "http_status"] +) + +LOGGER_TOTAL_MESSAGES = Counter( + "python_logging_messages_total", + "Count of log entries by service and level.", + ["service", "level"], +) + + +def before_request(): + request.start_time = time.time() + + +def after_request(response): + request_latency = time.time() - request.start_time + FLASK_REQUEST_LATENCY.labels(request.method, request.path).observe(request_latency) + FLASK_REQUEST_COUNT.labels(request.method, request.path, response.status_code).inc() + + return response + + +class Service(DriverService): + service = "metrics" + + def __init__(self, service, *args, **kwargs): + super().__init__(service, *args, **kwargs) + self.metrics_blueprint = Blueprint("metrics", __name__) + self.serve_metrics() + + def monitor(self, app): + app.before_request(before_request) + app.after_request(after_request) + + def serve_metrics(self): + @self.metrics_blueprint.route("/metrics", methods=["GET"]) + def metrics(): + return Response( + generate_latest(), + mimetype="text/print()lain", + content_type="text/plain; charset=utf-8", + ) + + def add_logger_handler(self, logger, service_name): + logger.addHandler(MetricsLogHandler(service_name)) + return logger + + +class MetricsLogHandler(logging.Handler): + """A LogHandler that exports logging metrics for Prometheus.io.""" + + def __init__(self, app_name): + super(MetricsLogHandler, self).__init__() + self.app_name = app_name + + def emit(self, record): + LOGGER_TOTAL_MESSAGES.labels(self.app_name, record.levelname).inc() diff --git a/tests/config-tests-metrics.yml b/tests/config-tests-metrics.yml new file mode 100644 index 0000000..8244aa6 --- /dev/null +++ b/tests/config-tests-metrics.yml @@ -0,0 +1,8 @@ +--- +pyms: + metrics: true +my-ms: + DEBUG: true + TESTING: true + APP_NAME: "Python Microservice" + APPLICATION_ROOT: / diff --git a/tests/test_metrics.py b/tests/test_metrics.py index a2bdf3a..d09d463 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,18 +1,26 @@ #!/usr/bin/env python +import os import unittest.mock -from flask import Flask -from pyms.flask.metrics import metrics_blueprint, monitor + +from flask import current_app from prometheus_client import generate_latest +from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT +from pyms.flask.app import Microservice + class TestMetricsFlask(unittest.TestCase): + + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + def setUp(self): - self.app = Flask(__name__) - self.app.register_blueprint(metrics_blueprint) + os.environ[CONFIGMAP_FILE_ENVIRONMENT] = os.path.join(self.BASE_DIR, "config-tests-metrics.yml") + ms = Microservice(service="my-ms", path=__file__) + self.app = ms.create_app() self.client = self.app.test_client() + # self.init_test_metrics() def test_metrics_latency(self): - monitor(self.app) self.client.get("/") self.client.get("/metrics") generated_latency_root = b'flask_request_latency_seconds_bucket{endpoint="/",le="0.005",method="GET"}' @@ -21,10 +29,15 @@ def test_metrics_latency(self): assert generated_latency_metrics in generate_latest() def test_metrics_count(self): - monitor(self.app) self.client.get("/") self.client.get("/metrics") - generated_count_root = b'flask_request_count_total{endpoint="/",http_status="404",method="GET"}' + generated_count_root = b'flask_request_count_total{endpoint="/",http_status="200",method="GET"}' generated_count_metrics = b'flask_request_count_total{endpoint="/metrics",http_status="200",method="GET"}' assert generated_count_root in generate_latest() assert generated_count_metrics in generate_latest() + + def test_metrics_logger(self): + self.client.get("/") + self.client.get("/metrics") + generated_logger = b'python_logging_messages_total{level="INFO",service="Python Microservice With Flask and Lightstep"}' + assert generated_logger in generate_latest() From e3f7093f51a5aa9949537aaae72a046f20685d83 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Sat, 30 Nov 2019 22:38:32 +0100 Subject: [PATCH 15/19] Add metrics documentation --- docs/services.md | 21 ++++++++++++++++++++- docs/structure.md | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/services.md b/docs/services.md index dd03d5d..93f8c48 100644 --- a/docs/services.md +++ b/docs/services.md @@ -14,7 +14,26 @@ Extends the Microservice with [Connexion](https://github.com/zalando/connexion) Extend the [requests library](http://docs.python-requests.org/en/master/) with trace headers and parsing JSON objects. Encapsulate common rest operations between business services propagating trace headers if set up. +## Metrics +Adds ![Prometheus](https://prometheus.io/) metrics using the ![Prometheus Client +Library](https://github.com/prometheus/client_python). + +At the moment, the next metrics are available: +- Incoming requests latency as a histogram +- Incoming requests number as a counter, divided by HTTP method, endpoint and + HTTP status +- Total number of log events divided by level + +To use this service, you may add the next to you configuration file: + +```yaml +pyms: + metrics: true +``` + +This will add the endpoint `/metrics` to your microservice, which will expose +the metrics. ## How to contrib: create your own service: -TODO \ No newline at end of file +TODO diff --git a/docs/structure.md b/docs/structure.md index 855a51a..3d61791 100644 --- a/docs/structure.md +++ b/docs/structure.md @@ -8,7 +8,7 @@ With the function `create_app` initialize the Flask app, register [blueprints](h and initialize all libraries such as Swagger, database, trace system, custom logger format, etc. ### pyms/flask/services -Integrations and wrappers over common libs like request, swagger, connexion +Integrations and wrappers over common libs like request, swagger, connexion or metrics. ### pyms/flask/healthcheck This view is usually used by Kubernetes, Eureka and other systems to check if our application is running. @@ -17,4 +17,4 @@ This view is usually used by Kubernetes, Eureka and other systems to check if ou Print logger in JSON format to send to server like Elasticsearch. Inject span traces in logger. ### pyms/tracer -Create an injector `flask_opentracing.FlaskTracer` to use in our projects. \ No newline at end of file +Create an injector `flask_opentracing.FlaskTracer` to use in our projects. From 421892cc974fd27696f7cf55e14877103c4e4da6 Mon Sep 17 00:00:00 2001 From: Mike Rubin Date: Sat, 30 Nov 2019 13:15:27 +0100 Subject: [PATCH 16/19] Memoize config per service (#63) * Memoizes config file per service * Updates dependencies --- Pipfile | 2 +- Pipfile.lock | 61 +++++++++++++++++++++++++------- pyms/config/conf.py | 32 +++++------------ pyms/flask/app/create_app.py | 2 +- pyms/flask/services/driver.py | 2 +- tests/test_config.py | 65 +++++++++++++++++++++++++---------- tests/test_flask.py | 31 ++++++++++++----- tests/test_requests.py | 29 ++++++++++++++++ 8 files changed, 158 insertions(+), 66 deletions(-) diff --git a/Pipfile b/Pipfile index b01301c..d07442a 100644 --- a/Pipfile +++ b/Pipfile @@ -9,7 +9,7 @@ python-json-logger = ">=0.1.10" pyyaml = ">=5.1.2" anyconfig = ">=0.9.8" swagger-ui-bundle = ">=0.0.2" -connexion = {extras = ["swagger-ui"],version = ">=2.2.0"} +connexion = {extras = ["swagger-ui"],version = "==2.4.0"} jaeger-client = "==4.1.0" flask-opentracing = "*" opentracing = ">=2.1" diff --git a/Pipfile.lock b/Pipfile.lock index 7690967..b71885e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bb33289bcd92a4ca7abd12b0fb31150f96f4ce8be22e2c9da795897902e5fce5" + "sha256": "46717e53a0a7b838196e1d835cd574369f0cccc9a4ecb42c2e96577663ff8ee1" }, "pipfile-spec": 6, "requires": {}, @@ -21,12 +21,19 @@ "index": "pypi", "version": "==0.9.10" }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ @@ -54,10 +61,11 @@ "swagger-ui" ], "hashes": [ - "sha256:e74544e382f732f97ec1c7d05a9d79e2c101e135f606a776e5f8dd1c478ff7e0" + "sha256:6e0569b646f2e6229923dc4e4c6e0325e223978bd19105779fd81e16bcb22fdf", + "sha256:7b4268e9ea837241e530738b35040345b78c8748d05d2c22805350aca0cd5b1c" ], "index": "pypi", - "version": "==2018.0.dev1" + "version": "==2.4.0" }, "contextlib2": { "hashes": [ @@ -94,6 +102,14 @@ ], "version": "==2.8" }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "markers": "python_version < '3.8'", + "version": "==0.23" + }, "inflection": { "hashes": [ "sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca" @@ -123,10 +139,10 @@ }, "jsonschema": { "hashes": [ - "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", - "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" + "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", + "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" ], - "version": "==2.6.0" + "version": "==3.2.0" }, "markupsafe": { "hashes": [ @@ -161,6 +177,13 @@ ], "version": "==1.1.1" }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, "openapi-spec-validator": { "hashes": [ "sha256:0caacd9829e9e3051e830165367bf58d436d9487b29a09220fa7edb9f47ff81b", @@ -191,6 +214,13 @@ "index": "pypi", "version": "==0.7.1" }, + "pyrsistent": { + "hashes": [ + "sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b" + ], + "index": "pypi", + "version": "==0.15.6" + }, "python-json-logger": { "hashes": [ "sha256:b7a31162f2a01965a5efb94453ce69230ed208468b0bbc7fdfc56e6d8df2e281" @@ -284,6 +314,13 @@ "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" ], "version": "==1.11.2" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" } }, "develop": { @@ -317,10 +354,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ diff --git a/pyms/config/conf.py b/pyms/config/conf.py index 26ca191..993ded2 100644 --- a/pyms/config/conf.py +++ b/pyms/config/conf.py @@ -2,28 +2,14 @@ from pyms.exceptions import ServiceDoesNotExistException -class Config: - service = None - _config = False +__service_configs = {} - def __init__(self): - pass - def config(self, *args, **kwargs): - """Set the configuration, if our yaml file is like: - myservice: - myservice1: - myvar1 - and we want to get the configuration of service1, our self.service will be "myservice.myservice1" - """ - if not self._config: - self._config = ConfFile(*args, **kwargs) - if not self.service: - raise ServiceDoesNotExistException("Service not defined") - return getattr(self._config, self.service) - - -def get_conf(service=None, *args, **kwargs): - config = Config() - config.service = service - return config.config(*args, **kwargs) +def get_conf(*args, **kwargs): + service = kwargs.pop('service', None) + memoize = kwargs.pop('memoize', True) + if not service: + raise ServiceDoesNotExistException("Service not defined") + if not memoize or service not in __service_configs: + __service_configs[service] = ConfFile(*args, **kwargs) + return getattr(__service_configs[service], service) diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index 9f8f414..6d4f99b 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -44,7 +44,7 @@ class Microservice(metaclass=SingletonMeta): def __init__(self, *args, **kwargs): self.service = kwargs.get("service", os.environ.get(SERVICE_ENVIRONMENT, "ms")) self.path = os.path.dirname(kwargs.get("path", __file__)) - self.config = get_conf(service=self.service) + self.config = get_conf(service=self.service, memoize=self._singleton) self.init_services() def init_services(self): diff --git a/pyms/flask/services/driver.py b/pyms/flask/services/driver.py index 26a2cef..de60fe9 100644 --- a/pyms/flask/services/driver.py +++ b/pyms/flask/services/driver.py @@ -25,7 +25,7 @@ class ServicesManager: def __init__(self, service=None): self.service = (service if service else SERVICE_BASE) - self.config = get_conf(service=self.service, empty_init=True) + self.config = get_conf(service=self.service, empty_init=True, memoize=False) def get_services(self): return ((k, self.get_service(k)) for k in self.config.__dict__.keys() if k not in ['empty_init', ]) diff --git a/tests/test_config.py b/tests/test_config.py index d014ee3..7b00697 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,8 +1,9 @@ import logging import os import unittest +from unittest import mock -from pyms.config.conf import Config +from pyms.config.conf import get_conf from pyms.config.confile import ConfFile from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT, LOGGER_NAME from pyms.exceptions import AttrDoesNotExistException, ConfigDoesNotFoundException, ServiceDoesNotExistException @@ -67,6 +68,14 @@ def test_equal_instances_ok2(self): config2 = {"test_1": {"test_1_1": "a", "test_1_2": "b"}} self.assertEqual(config1, config2) + def test_equal_instances_ko(self): + config = ConfFile(config={"test-1": {"test-1-1": "a"}}) + no_valid_type = ConfigDoesNotFoundException + + result = config == no_valid_type + + self.assertEqual(result, False) + def test_dictionary_attribute_not_exists(self): config = ConfFile(config={"test-1": "a"}) with self.assertRaises(AttrDoesNotExistException): @@ -89,22 +98,6 @@ def test_example_test_json_file(self): self.assertEqual(config.my_ms.test_var, "general") -class ConfServiceTests(unittest.TestCase): - - def test_config_with_service(self): - class MyService(Config): - service = "service" - - config = MyService() - configuration = config.config(config={"service": {"service1": "a", "service2": "b"}}) - self.assertEqual(configuration.service1, "a") - - def test_config_with_service_not_exist(self): - config = Config() - with self.assertRaises(ServiceDoesNotExistException): - configuration = config.config(config={"service": {"service1": "a", "service2": "b"}}) - - class ConfNotExistTests(unittest.TestCase): def test_empty_conf(self): config = ConfFile(empty_init=True) @@ -119,5 +112,39 @@ def test_empty_conf_three_levels(self): self.assertEqual(config.my_ms.level_two.level_three, {}) -if __name__ == '__main__': - unittest.main() + +class GetConfig(unittest.TestCase): + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + def setUp(self): + os.environ[CONFIGMAP_FILE_ENVIRONMENT] = os.path.join(self.BASE_DIR, "config-tests.yml") + + def tearDown(self): + del os.environ[CONFIGMAP_FILE_ENVIRONMENT] + + def test_default(self): + config = get_conf(service="my-ms") + + assert config.APP_NAME == "Python Microservice" + assert config.subservice1.test == "input" + + @mock.patch('pyms.config.conf.ConfFile') + def test_memoized(self, mock_confile): + mock_confile.pyms = {} + get_conf(service="pyms") + get_conf(service="pyms") + + mock_confile.assert_called_once() + + @mock.patch('pyms.config.conf.ConfFile') + def test_without_memoize(self, mock_confile): + mock_confile.pyms = {} + get_conf(service="pyms", memoize=False) + get_conf(service="pyms", memoize=False) + + assert mock_confile.call_count == 2 + + @mock.patch('pyms.config.conf.ConfFile') + def test_without_params(self, mock_confile): + with self.assertRaises(ServiceDoesNotExistException): + get_conf() diff --git a/tests/test_flask.py b/tests/test_flask.py index 14da4d6..e2548c9 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -1,5 +1,6 @@ import os import unittest +from unittest import mock import pytest from flask import current_app @@ -38,6 +39,12 @@ def test_home(self): response = self.client.get('/') self.assertEqual(404, response.status_code) + def test_healthcheck(self): + response = self.client.get('/healthcheck') + self.assertEqual(b"OK", response.data) + self.assertEqual(200, response.status_code) + + class MicroserviceTest(unittest.TestCase): """ @@ -68,22 +75,28 @@ def test_import_config_without_create_app(self): ms1 = MyMicroservice(service="my-ms", path=__file__, override_instance=True) self.assertEqual(ms1.config.subservice1, config().subservice1) + def test_config_singleton(self): + conf_one = config().subservice1 + conf_two = config().subservice1 + + assert conf_one is conf_two + @pytest.mark.parametrize("payload, configfile, status_code", [ ( - "Python Microservice", - "config-tests.yml", - 200 + "Python Microservice", + "config-tests.yml", + 200 ), ( - "Python Microservice With Flask", - "config-tests-flask.yml", - 404 + "Python Microservice With Flask", + "config-tests-flask.yml", + 404 ), ( - "Python Microservice With Flask and Lightstep", - "config-tests-flask-trace-lightstep.yml", - 200 + "Python Microservice With Flask and Lightstep", + "config-tests-flask-trace-lightstep.yml", + 200 ) ]) def test_configfiles(payload, configfile, status_code): diff --git a/tests/test_requests.py b/tests/test_requests.py index d8529e1..bf27c2a 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -268,6 +268,35 @@ def test_propagate_headers_propagate_no_override(self): self.assertEqual(expected_headers, headers) + def test_propagate_headers_on_get(self): + url = "http://www.my-site.com/users" + mock_headers = { + 'A': 'b', + } + self.request.propagate_headers = unittest.mock.Mock() + self.request.propagate_headers.return_value = mock_headers + with self.app.test_request_context( + '/tests/', data={'format': 'short'}, headers=mock_headers): + self.request.get(url, propagate_headers=True) + + self.request.propagate_headers.assert_called_once_with({}) + + def test_propagate_headers_on_get_with_headers(self): + url = "http://www.my-site.com/users" + mock_headers = { + 'A': 'b', + } + get_headers = { + 'C': 'd', + } + self.request.propagate_headers = unittest.mock.Mock() + self.request.propagate_headers.return_value = mock_headers + with self.app.test_request_context( + '/tests/', data={'format': 'short'}, headers=mock_headers): + self.request.get(url, headers=get_headers, propagate_headers=True) + + self.request.propagate_headers.assert_called_once_with(get_headers) + @requests_mock.Mocker() def test_retries_with_500(self, mock_request): url = 'http://localhost:9999' From 1fb2a02f65a4ff2adba68ea880ffd90d85c07a95 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Sat, 30 Nov 2019 23:24:54 +0100 Subject: [PATCH 17/19] Add jaeger metrics --- docs/services.md | 1 + pyms/flask/services/tracer.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/services.md b/docs/services.md index 93f8c48..1cea7a8 100644 --- a/docs/services.md +++ b/docs/services.md @@ -23,6 +23,7 @@ At the moment, the next metrics are available: - Incoming requests number as a counter, divided by HTTP method, endpoint and HTTP status - Total number of log events divided by level +- If `tracer` service activated and it's jaeger, it will show its metrics To use this service, you may add the next to you configuration file: diff --git a/pyms/flask/services/tracer.py b/pyms/flask/services/tracer.py index ac34b6c..ffacf92 100644 --- a/pyms/flask/services/tracer.py +++ b/pyms/flask/services/tracer.py @@ -1,8 +1,11 @@ import logging +from jaeger_client.metrics.prometheus import PrometheusMetricsFactory + from pyms.constants import LOGGER_NAME from pyms.flask.services.driver import DriverService from pyms.utils.utils import check_package_exists, import_package, import_from +from pyms.config.conf import get_conf logger = logging.getLogger(LOGGER_NAME) @@ -43,7 +46,11 @@ def init_jaeger_tracer(self): 'reporting_host': self.host } } - + metrics_config = get_conf(service="pyms.metrics", empty_init=True, memoize=False) + metrics = "" + if metrics_config: + service_name = self.component_name.lower().replace("-", "_").replace(" ", "_") + metrics = PrometheusMetricsFactory(namespace=service_name) config = Config(config={ **{'sampler': { 'type': 'const', @@ -54,6 +61,7 @@ def init_jaeger_tracer(self): }, **host }, service_name=self.component_name, + metrics_factory=metrics, validate=True) return config.initialize_tracer() From 4948c14aa8e43515e5430a174aff8608181042b0 Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Sun, 1 Dec 2019 02:34:25 +0100 Subject: [PATCH 18/19] Don't use namespace for metrics See https://github.com/jaegertracing/jaeger-client-python/issues/268 --- pyms/flask/services/tracer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyms/flask/services/tracer.py b/pyms/flask/services/tracer.py index ffacf92..2a92d53 100644 --- a/pyms/flask/services/tracer.py +++ b/pyms/flask/services/tracer.py @@ -50,7 +50,7 @@ def init_jaeger_tracer(self): metrics = "" if metrics_config: service_name = self.component_name.lower().replace("-", "_").replace(" ", "_") - metrics = PrometheusMetricsFactory(namespace=service_name) + metrics = PrometheusMetricsFactory() config = Config(config={ **{'sampler': { 'type': 'const', From 221392140e759d55abfbc0220c36153139bb4a5e Mon Sep 17 00:00:00 2001 From: Alex Perez-Pujol Date: Sun, 1 Dec 2019 02:39:56 +0100 Subject: [PATCH 19/19] Add jaeger metrics test --- tests/config-tests-metrics.yml | 3 +++ tests/config-tests.yml | 3 ++- tests/test_metrics.py | 7 ++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/config-tests-metrics.yml b/tests/config-tests-metrics.yml index 8244aa6..5860384 100644 --- a/tests/config-tests-metrics.yml +++ b/tests/config-tests-metrics.yml @@ -1,6 +1,9 @@ --- pyms: metrics: true + tracer: + client: "jaeger" + component_name: "Python Microservice" my-ms: DEBUG: true TESTING: true diff --git a/tests/config-tests.yml b/tests/config-tests.yml index 99304f2..010d26d 100644 --- a/tests/config-tests.yml +++ b/tests/config-tests.yml @@ -1,4 +1,5 @@ pyms: + metrics: true requests: data: data swagger: @@ -17,4 +18,4 @@ my-ms: subservice1: test: input subservice2: - test: output \ No newline at end of file + test: output diff --git a/tests/test_metrics.py b/tests/test_metrics.py index d09d463..a8d6f80 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -18,7 +18,6 @@ def setUp(self): ms = Microservice(service="my-ms", path=__file__) self.app = ms.create_app() self.client = self.app.test_client() - # self.init_test_metrics() def test_metrics_latency(self): self.client.get("/") @@ -41,3 +40,9 @@ def test_metrics_logger(self): self.client.get("/metrics") generated_logger = b'python_logging_messages_total{level="INFO",service="Python Microservice With Flask and Lightstep"}' assert generated_logger in generate_latest() + + def test_metrics_jaeger(self): + self.client.get("/") + self.client.get("/metrics") + generated_logger = b'jaeger:reporter_spans_total' + assert generated_logger in generate_latest()