Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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‎

Lines changed: 25 additions & 0 deletions
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‎

Lines changed: 133 additions & 0 deletions
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‎

Lines changed: 5 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /