elasticapm/contrib/flask/__init__.py (116 lines of code) (raw):
# BSD 3-Clause License
#
# Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
from __future__ import absolute_import
import logging
import warnings
import flask
from flask import request, signals
import elasticapm
import elasticapm.instrumentation.control
from elasticapm import get_client
from elasticapm.base import Client
from elasticapm.conf import constants, setup_logging
from elasticapm.contrib.flask.utils import get_data_from_request, get_data_from_response
from elasticapm.handlers.logging import LoggingHandler
from elasticapm.traces import execution_context
from elasticapm.utils import build_name_with_http_method_prefix
from elasticapm.utils.disttracing import TraceParent
from elasticapm.utils.logging import get_logger
logger = get_logger("elasticapm.errors.client")
class ElasticAPM(object):
"""
Flask application for Elastic APM.
Look up configuration from ``os.environ.get('ELASTIC_APM_SERVICE_NAME')`` and
``os.environ.get('ELASTIC_APM_SECRET_TOKEN')``::
>>> elasticapm = ElasticAPM(app)
Pass an arbitrary SERVICE_NAME and SECRET_TOKEN::
>>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd')
Pass an explicit client::
>>> elasticapm = ElasticAPM(app, client=client)
Capture an exception::
>>> try:
>>> 1 / 0
>>> except ZeroDivisionError:
>>> elasticapm.capture_exception()
Capture a message::
>>> elasticapm.capture_message('hello, world!')
"""
def __init__(self, app=None, client=None, client_cls=Client, logging=False, **defaults) -> None:
self.app = app
self.logging = logging
if self.logging:
warnings.warn(
"Flask log shipping is deprecated. See the Flask docs for more info and alternatives.",
DeprecationWarning,
)
self.client = client or get_client()
self.client_cls = client_cls
if app:
self.init_app(app, **defaults)
def handle_exception(self, *args, **kwargs) -> None:
if not self.client:
return
if self.app.debug and not self.client.config.debug:
return
self.client.capture_exception(
exc_info=kwargs.get("exc_info"),
context={"request": get_data_from_request(request, self.client.config, constants.ERROR)},
custom={"app": self.app},
handled=False,
)
# End the transaction here, as `request_finished` won't be called when an
# unhandled exception occurs.
#
# Unfortunately, that also means that we can't capture any response data,
# as the response isn't ready at this point in time.
elasticapm.set_transaction_outcome(outcome=constants.OUTCOME.FAILURE, override=False)
self.client.end_transaction(result="HTTP 5xx")
def init_app(self, app, **defaults) -> None:
self.app = app
if not self.client:
config = self.app.config.get("ELASTIC_APM", {})
if "framework_name" not in defaults:
defaults["framework_name"] = "flask"
defaults["framework_version"] = getattr(flask, "__version__", "<0.7")
self.client = self.client_cls(config, **defaults)
# 0 is a valid log level (NOTSET), so we need to check explicitly for it
if self.logging or self.logging is logging.NOTSET:
if self.logging is not True:
kwargs = {"level": self.logging}
else:
kwargs = {}
setup_logging(LoggingHandler(self.client, **kwargs))
signals.got_request_exception.connect(self.handle_exception, sender=app, weak=False)
try:
from elasticapm.contrib.celery import register_exception_tracking
register_exception_tracking(self.client)
except ImportError:
pass
# Instrument to get spans
if self.client.config.instrument and self.client.config.enabled:
elasticapm.instrumentation.control.instrument()
signals.request_started.connect(self.request_started, sender=app)
signals.request_finished.connect(self.request_finished, sender=app)
try:
from elasticapm.contrib.celery import register_instrumentation
register_instrumentation(self.client)
except ImportError:
pass
else:
logger.debug("Skipping instrumentation. INSTRUMENT is set to False.")
@app.context_processor
def rum_tracing():
"""
Adds APM related IDs to the context used for correlating the backend transaction with the RUM transaction
"""
transaction = execution_context.get_transaction()
if transaction and transaction.trace_parent:
return {
"apm": {
"trace_id": transaction.trace_parent.trace_id,
"span_id": lambda: transaction.ensure_parent_id(),
"is_sampled": transaction.is_sampled,
"is_sampled_js": "true" if transaction.is_sampled else "false",
}
}
return {}
def request_started(self, app) -> None:
if (not self.app.debug or self.client.config.debug) and not self.client.should_ignore_url(request.path):
trace_parent = TraceParent.from_headers(request.headers)
self.client.begin_transaction("request", trace_parent=trace_parent)
elasticapm.set_context(
lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request"
)
rule = request.url_rule.rule if request.url_rule is not None else ""
rule = build_name_with_http_method_prefix(rule, request)
elasticapm.set_transaction_name(rule, override=False)
def request_finished(self, app, response) -> None:
if not self.app.debug or self.client.config.debug:
elasticapm.set_context(
lambda: get_data_from_response(response, self.client.config, constants.TRANSACTION), "response"
)
if response.status_code:
result = "HTTP {}xx".format(response.status_code // 100)
elasticapm.set_transaction_outcome(http_status_code=response.status_code, override=False)
else:
result = response.status
elasticapm.set_transaction_outcome(http_status_code=response.status, override=False)
elasticapm.set_transaction_result(result, override=False)
# Instead of calling end_transaction here, we defer the call until the response is closed.
# This ensures that we capture things that happen until the WSGI server closes the response.
response.call_on_close(self.client.end_transaction)
def capture_exception(self, *args, **kwargs):
assert self.client, "capture_exception called before application configured"
return self.client.capture_exception(*args, **kwargs)
def capture_message(self, *args, **kwargs):
assert self.client, "capture_message called before application configured"
return self.client.capture_message(*args, **kwargs)