Skip to content

Commit d7afe30

Browse files
committed
feat: add opentelemetry service
It's only useful for metrics, but it adds the core of tracing and logging.
1 parent ed104b4 commit d7afe30

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

pyms/flask/app/create_app.py

+25
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def example():
8080
request: Optional[DriverService] = None
8181
tracer: Optional[DriverService] = None
8282
metrics: Optional[DriverService] = None
83+
opentelemetry: Optional[DriverService] = None
8384
_singleton = True
8485

8586
def __init__(self, *args, **kwargs):
@@ -204,6 +205,28 @@ def init_metrics(self) -> None:
204205
)
205206
self.metrics.monitor(self.application.config["APP_NAME"], self.application)
206207

208+
def init_opentelemetry(self) -> None:
209+
if self.opentelemetry:
210+
if self.opentelemetry.config.metrics.enabled:
211+
# Set metrics backend
212+
self.opentelemetry.set_metrics_backend()
213+
# Set the metrics blueprint
214+
# DISCLAIMER this endpoint may be only necessary with prometheus client
215+
self.application.register_blueprint(self.opentelemetry.blueprint)
216+
# Set instrumentations
217+
if self.opentelemetry.config.metrics.instrumentations.flask:
218+
self.opentelemetry.monitor(
219+
self.application.config["APP_NAME"], self.application
220+
)
221+
if self.opentelemetry.config.metrics.instrumentations.logger:
222+
self.opentelemetry.add_logger_handler(
223+
self.application.logger, self.application.config["APP_NAME"]
224+
)
225+
if self.opentelemetry.config.tracing.enabled:
226+
self.opentelemetry.set_tracing_backend()
227+
if self.opentelemetry.config.logging.enabled:
228+
self.opentelemetry.set_logging_backend()
229+
207230
def reload_conf(self):
208231
self.delete_services()
209232
self.config.reload()
@@ -237,6 +260,8 @@ def create_app(self) -> Flask:
237260

238261
self.init_metrics()
239262

263+
self.init_opentelemetry()
264+
240265
logger.debug(
241266
"Started app with PyMS and this services: {}".format(self.services)
242267
)

pyms/flask/services/opentelemetry.py

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import logging
2+
import time
3+
from typing import Text
4+
5+
from flask import Blueprint, Response, request
6+
from pyms.flask.services.driver import DriverService
7+
8+
from opentelemetry import metrics
9+
from opentelemetry.exporter.prometheus import PrometheusMetricsExporter
10+
from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder
11+
from opentelemetry.sdk.metrics.export.controller import PushController
12+
from prometheus_client import generate_latest
13+
14+
# TODO set sane defaults
15+
# https://github.com/python-microservices/pyms/issues/218
16+
# TODO validate config
17+
# https://github.com/python-microservices/pyms/issues/219
18+
PROMETHEUS_CLIENT = "prometheus"
19+
20+
21+
class FlaskMetricsWrapper:
22+
def __init__(self, app_name: str, meter: MeterProvider):
23+
self.app_name = app_name
24+
# TODO add Histogram support for flask when available
25+
# https://github.com/open-telemetry/opentelemetry-python/issues/1255
26+
self.flask_request_latency = meter.create_metric(
27+
"http_server_requests_seconds",
28+
"Flask Request Latency",
29+
"http_server_requests_seconds",
30+
float,
31+
ValueRecorder,
32+
("service", "method", "uri", "status"),
33+
)
34+
self.flask_request_count = meter.create_metric(
35+
"http_server_requests_count",
36+
"Flask Request Count",
37+
"http_server_requests_count",
38+
int,
39+
Counter,
40+
["service", "method", "uri", "status"],
41+
)
42+
43+
def before_request(self): # pylint: disable=R0201
44+
request.start_time = time.time()
45+
46+
def after_request(self, response: Response) -> Response:
47+
if hasattr(request.url_rule, "rule"):
48+
path = request.url_rule.rule
49+
else:
50+
path = request.path
51+
request_latency = time.time() - request.start_time
52+
labels = {
53+
"service": self.app_name,
54+
"method": str(request.method),
55+
"uri": path,
56+
"status": str(response.status_code),
57+
}
58+
59+
self.flask_request_latency.record(request_latency, labels)
60+
self.flask_request_count.add(1, labels)
61+
62+
return response
63+
64+
65+
class Service(DriverService):
66+
"""
67+
Adds [OpenTelemetry](https://opentelemetry.io/) metrics using the [Opentelemetry Client Library](https://opentelemetry-python.readthedocs.io/en/latest/exporter/).
68+
"""
69+
70+
config_resource: Text = "opentelemetry"
71+
72+
def __init__(self, *args, **kwargs):
73+
super().__init__(*args, **kwargs)
74+
self.blueprint = Blueprint("opentelemetry", __name__)
75+
self.serve_metrics()
76+
77+
def set_metrics_backend(self):
78+
# Set meter provider
79+
metrics.set_meter_provider(MeterProvider())
80+
self.meter = metrics.get_meter(__name__)
81+
if self.config.metrics.backend.lower() == PROMETHEUS_CLIENT:
82+
exporter = PrometheusMetricsExporter()
83+
else:
84+
pass
85+
# Create the push controller that will update the metrics when the
86+
# interval is met
87+
PushController(self.meter, exporter, self.config.metrics.interval)
88+
89+
def set_tracing_backend(self):
90+
pass
91+
92+
def set_logging_backend(self):
93+
pass
94+
95+
def monitor(self, app_name, app):
96+
metric = FlaskMetricsWrapper(app_name, self.meter)
97+
app.before_request(metric.before_request)
98+
app.after_request(metric.after_request)
99+
100+
def serve_metrics(self):
101+
@self.blueprint.route("/metrics", methods=["GET"])
102+
def metrics(): # pylint: disable=unused-variable
103+
return Response(
104+
generate_latest(),
105+
mimetype="text/print()lain",
106+
content_type="text/plain; charset=utf-8",
107+
)
108+
109+
def add_logger_handler(
110+
self, logger: logging.Logger, service_name: str
111+
) -> logging.Logger:
112+
logger.addHandler(MetricsLogHandler(service_name, self.meter))
113+
return logger
114+
115+
116+
class MetricsLogHandler(logging.Handler):
117+
"""A LogHandler that exports logging metrics for OpenTelemetry."""
118+
119+
def __init__(self, app_name: str, meter: MeterProvider):
120+
super().__init__()
121+
self.app_name = str(app_name)
122+
self.logger_total_messages = meter.create_metric(
123+
"logger_messages_total",
124+
"Count of log entries by service and level.",
125+
"logger_messages_total",
126+
int,
127+
Counter,
128+
["service", "level"],
129+
)
130+
131+
def emit(self, record) -> None:
132+
labels = {"service": self.app_name, "level": record.levelname}
133+
self.logger_total_messages.add(1, labels)

setup.py

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
'prometheus_client>=0.8.0',
5353
]
5454

55+
install_opentelemetry_requires = [
56+
'opentelemetry-exporter-prometheus>=0.14b0',
57+
'opentelemetry-sdk>=0.14b0',
58+
]
59+
5560
install_tests_requires = [
5661
'requests-mock>=1.8.0',
5762
'coverage>=5.3',

0 commit comments

Comments
 (0)