elasticapm/events.py (142 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
import random
import sys
from elasticapm.conf.constants import EXCEPTION_CHAIN_MAX_DEPTH
from elasticapm.utils import encoding, varmap
from elasticapm.utils.encoding import keyword_field, shorten, to_unicode
from elasticapm.utils.logging import get_logger
from elasticapm.utils.stacks import get_culprit, get_stack_info, iter_traceback_frames
__all__ = ("BaseEvent", "Exception", "Message")
logger = get_logger("elasticapm.events")
class BaseEvent(object):
@staticmethod
def to_string(client, data):
raise NotImplementedError
@staticmethod
def capture(client, **kwargs):
return {}
class Exception(BaseEvent):
"""
Exceptions store the following metadata:
- value: 'My exception value'
- type: 'ClassName'
- module '__builtin__' (i.e. __builtin__.TypeError)
- frames: a list of serialized frames (see _get_traceback_frames)
"""
@staticmethod
def to_string(client, data):
exc = data["exception"]
if exc["value"]:
return "%s: %s" % (exc["type"], exc["value"])
return exc["type"]
@staticmethod
def get_hash(data):
exc = data["exception"]
output = [exc["type"]]
for frame in data["stacktrace"]["frames"]:
output.append(frame["module"])
output.append(frame["function"])
return output
@staticmethod
def capture(client, exc_info=None, **kwargs):
culprit = exc_value = exc_type = exc_module = frames = exc_traceback = None
new_exc_info = False
if not exc_info or exc_info is True:
new_exc_info = True
exc_info = sys.exc_info()
if exc_info == (None, None, None):
raise ValueError("No exception found: capture_exception requires an active exception.")
try:
exc_type, exc_value, exc_traceback = exc_info
frames = get_stack_info(
iter_traceback_frames(exc_traceback, config=client.config),
with_locals=client.config.collect_local_variables in ("errors", "all"),
library_frame_context_lines=client.config.source_lines_error_library_frames,
in_app_frame_context_lines=client.config.source_lines_error_app_frames,
include_paths_re=client.include_paths_re,
exclude_paths_re=client.exclude_paths_re,
locals_processor_func=lambda local_var: varmap(
lambda k, val: shorten(
val,
list_length=client.config.local_var_list_max_length,
string_length=client.config.local_var_max_length,
dict_length=client.config.local_var_dict_max_length,
),
local_var,
),
)
culprit = kwargs.get("culprit", None) or get_culprit(
frames, client.config.include_paths, client.config.exclude_paths
)
if hasattr(exc_type, "__module__"):
exc_module = exc_type.__module__
exc_type = exc_type.__name__
else:
exc_module = None
exc_type = exc_type.__name__
finally:
if new_exc_info:
try:
del exc_info
del exc_traceback
except Exception as e:
logger.exception(e)
if "message" in kwargs:
message = kwargs["message"]
else:
message = "%s: %s" % (exc_type, to_unicode(exc_value)) if exc_value else str(exc_type)
message = encoding.long_field(message)
data = {
"id": "%032x" % random.getrandbits(128),
"culprit": keyword_field(culprit),
"exception": {
"message": message,
"type": keyword_field(str(exc_type)),
"module": keyword_field(str(exc_module)),
"stacktrace": frames,
},
}
if hasattr(exc_value, "_elastic_apm_span_id"):
data["parent_id"] = exc_value._elastic_apm_span_id
del exc_value._elastic_apm_span_id
depth = kwargs.get("_exc_chain_depth", 0)
if depth > EXCEPTION_CHAIN_MAX_DEPTH:
return
cause = exc_value.__cause__
chained_context = exc_value.__context__
# we follow the pattern of Python itself here and only capture the chained exception
# if cause is not None and __suppress_context__ is False
if chained_context and not (exc_value.__suppress_context__ and cause is None):
if cause:
chained_exc_type = type(cause)
chained_exc_value = cause
else:
chained_exc_type = type(chained_context)
chained_exc_value = chained_context
chained_exc_info = chained_exc_type, chained_exc_value, chained_context.__traceback__
chained_cause = Exception.capture(
client, exc_info=chained_exc_info, culprit="None", _exc_chain_depth=depth + 1
)
if chained_cause:
data["exception"]["cause"] = [chained_cause["exception"]]
return data
class Message(BaseEvent):
"""
Messages store the following metadata:
- message: 'My message from %s about %s'
- params: ('foo', 'bar')
"""
@staticmethod
def to_string(client, data):
return data["log"]["message"]
@staticmethod
def get_hash(data):
msg = data["param_message"]
return [msg["message"]]
@staticmethod
def capture(client, param_message=None, message=None, level=None, logger_name=None, **kwargs):
if message:
param_message = {"message": message}
params = param_message.get("params")
message = param_message["message"] % params if params else param_message["message"]
data = kwargs.get("data", {})
message_data = {
"id": "%032x" % random.getrandbits(128),
"log": {
"level": keyword_field(level or "error"),
"logger_name": keyword_field(logger_name or "__root__"),
"message": message,
"param_message": keyword_field(param_message["message"]),
},
}
if isinstance(data.get("stacktrace"), dict):
message_data["log"]["stacktrace"] = data["stacktrace"]["frames"]
if kwargs.get("exception"):
message_data["culprit"] = kwargs["exception"]["culprit"]
message_data["exception"] = kwargs["exception"]["exception"]
return message_data