Skip to content

Commit

Permalink
feat: add opentelemetry service
Browse files Browse the repository at this point in the history
It's only useful for metrics, but it adds the core of tracing and
logging.
  • Loading branch information
alexppg committed Oct 31, 2020
1 parent ed104b4 commit d7afe30
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 0 deletions.
25 changes: 25 additions & 0 deletions pyms/flask/app/create_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def example():
request: Optional[DriverService] = None
tracer: Optional[DriverService] = None
metrics: Optional[DriverService] = None
opentelemetry: Optional[DriverService] = None
_singleton = True

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

def init_opentelemetry(self) -> None:
if self.opentelemetry:
if self.opentelemetry.config.metrics.enabled:
# Set metrics backend
self.opentelemetry.set_metrics_backend()
# Set the metrics blueprint
# DISCLAIMER this endpoint may be only necessary with prometheus client
self.application.register_blueprint(self.opentelemetry.blueprint)
# Set instrumentations
if self.opentelemetry.config.metrics.instrumentations.flask:
self.opentelemetry.monitor(
self.application.config["APP_NAME"], self.application
)
if self.opentelemetry.config.metrics.instrumentations.logger:
self.opentelemetry.add_logger_handler(
self.application.logger, self.application.config["APP_NAME"]
)
if self.opentelemetry.config.tracing.enabled:
self.opentelemetry.set_tracing_backend()
if self.opentelemetry.config.logging.enabled:
self.opentelemetry.set_logging_backend()

def reload_conf(self):
self.delete_services()
self.config.reload()
Expand Down Expand Up @@ -237,6 +260,8 @@ def create_app(self) -> Flask:

self.init_metrics()

self.init_opentelemetry()

logger.debug(
"Started app with PyMS and this services: {}".format(self.services)
)
Expand Down
133 changes: 133 additions & 0 deletions pyms/flask/services/opentelemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import logging
import time
from typing import Text

from flask import Blueprint, Response, request
from pyms.flask.services.driver import DriverService

from opentelemetry import metrics
from opentelemetry.exporter.prometheus import PrometheusMetricsExporter
from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder
from opentelemetry.sdk.metrics.export.controller import PushController
from prometheus_client import generate_latest

# TODO set sane defaults
# https://github.com/python-microservices/pyms/issues/218
# TODO validate config
# https://github.com/python-microservices/pyms/issues/219
PROMETHEUS_CLIENT = "prometheus"


class FlaskMetricsWrapper:
def __init__(self, app_name: str, meter: MeterProvider):
self.app_name = app_name
# TODO add Histogram support for flask when available
# https://github.com/open-telemetry/opentelemetry-python/issues/1255
self.flask_request_latency = meter.create_metric(
"http_server_requests_seconds",
"Flask Request Latency",
"http_server_requests_seconds",
float,
ValueRecorder,
("service", "method", "uri", "status"),
)
self.flask_request_count = meter.create_metric(
"http_server_requests_count",
"Flask Request Count",
"http_server_requests_count",
int,
Counter,
["service", "method", "uri", "status"],
)

def before_request(self): # pylint: disable=R0201
request.start_time = time.time()

def after_request(self, response: Response) -> Response:
if hasattr(request.url_rule, "rule"):
path = request.url_rule.rule
else:
path = request.path
request_latency = time.time() - request.start_time
labels = {
"service": self.app_name,
"method": str(request.method),
"uri": path,
"status": str(response.status_code),
}

self.flask_request_latency.record(request_latency, labels)
self.flask_request_count.add(1, labels)

return response


class Service(DriverService):
"""
Adds [OpenTelemetry](https://opentelemetry.io/) metrics using the [Opentelemetry Client Library](https://opentelemetry-python.readthedocs.io/en/latest/exporter/).
"""

config_resource: Text = "opentelemetry"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.blueprint = Blueprint("opentelemetry", __name__)
self.serve_metrics()

def set_metrics_backend(self):
# Set meter provider
metrics.set_meter_provider(MeterProvider())
self.meter = metrics.get_meter(__name__)
if self.config.metrics.backend.lower() == PROMETHEUS_CLIENT:
exporter = PrometheusMetricsExporter()
else:
pass
# Create the push controller that will update the metrics when the
# interval is met
PushController(self.meter, exporter, self.config.metrics.interval)

def set_tracing_backend(self):
pass

def set_logging_backend(self):
pass

def monitor(self, app_name, app):
metric = FlaskMetricsWrapper(app_name, self.meter)
app.before_request(metric.before_request)
app.after_request(metric.after_request)

def serve_metrics(self):
@self.blueprint.route("/metrics", methods=["GET"])
def metrics(): # pylint: disable=unused-variable
return Response(
generate_latest(),
mimetype="text/print()lain",
content_type="text/plain; charset=utf-8",
)

def add_logger_handler(
self, logger: logging.Logger, service_name: str
) -> logging.Logger:
logger.addHandler(MetricsLogHandler(service_name, self.meter))
return logger


class MetricsLogHandler(logging.Handler):
"""A LogHandler that exports logging metrics for OpenTelemetry."""

def __init__(self, app_name: str, meter: MeterProvider):
super().__init__()
self.app_name = str(app_name)
self.logger_total_messages = meter.create_metric(
"logger_messages_total",
"Count of log entries by service and level.",
"logger_messages_total",
int,
Counter,
["service", "level"],
)

def emit(self, record) -> None:
labels = {"service": self.app_name, "level": record.levelname}
self.logger_total_messages.add(1, labels)
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
'prometheus_client>=0.8.0',
]

install_opentelemetry_requires = [
'opentelemetry-exporter-prometheus>=0.14b0',
'opentelemetry-sdk>=0.14b0',
]

install_tests_requires = [
'requests-mock>=1.8.0',
'coverage>=5.3',
Expand Down

0 comments on commit d7afe30

Please sign in to comment.