elasticapm/handlers/logging.py (181 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 sys
import traceback
import warnings
import wrapt
from elasticapm import get_client
from elasticapm.base import Client
from elasticapm.traces import execution_context
from elasticapm.utils.stacks import iter_stack_frames
class LoggingHandler(logging.Handler):
def __init__(self, *args, **kwargs) -> None:
warnings.warn(
"The LoggingHandler is deprecated and will be removed in v7.0 of "
"the agent. Please use `log_ecs_reformatting` and ship the logs "
"with Elastic Agent or Filebeat instead. "
"https://www.elastic.co/guide/en/apm/agent/python/current/logs.html",
DeprecationWarning,
)
self.client = None
if "client" in kwargs:
self.client = kwargs.pop("client")
elif len(args) > 0:
arg = args[0]
if isinstance(arg, Client):
self.client = arg
if not self.client:
client_cls = kwargs.pop("client_cls", None)
if client_cls:
self.client = client_cls(*args, **kwargs)
else:
warnings.warn(
"LoggingHandler requires a Client instance. No Client was received.",
DeprecationWarning,
)
self.client = Client(*args, **kwargs)
logging.Handler.__init__(self, level=kwargs.get("level", logging.NOTSET))
def emit(self, record):
self.format(record)
# Avoid typical config issues by overriding loggers behavior
if record.name.startswith(("elasticapm.errors",)):
sys.stderr.write(record.getMessage() + "\n")
return
try:
return self._emit(record)
except Exception:
sys.stderr.write("Top level ElasticAPM exception caught - failed creating log record.\n")
sys.stderr.write(record.getMessage() + "\n")
sys.stderr.write(traceback.format_exc() + "\n")
try:
self.client.capture("Exception")
except Exception:
pass
def _emit(self, record, **kwargs):
data = {}
for k, v in record.__dict__.items():
if "." not in k and k not in ("culprit",):
continue
data[k] = v
stack = getattr(record, "stack", None)
if stack is True:
stack = iter_stack_frames(config=self.client.config)
if stack:
frames = []
started = False
last_mod = ""
for item in stack:
if isinstance(item, (list, tuple)):
frame, lineno = item
else:
frame, lineno = item, item.f_lineno
if not started:
f_globals = getattr(frame, "f_globals", {})
module_name = f_globals.get("__name__", "")
if last_mod.startswith("logging") and not module_name.startswith("logging"):
started = True
else:
last_mod = module_name
continue
frames.append((frame, lineno))
stack = frames
custom = getattr(record, "data", {})
# Add in all of the data from the record that we aren't already capturing
for k in record.__dict__.keys():
if k in (
"stack",
"name",
"args",
"msg",
"levelno",
"exc_text",
"exc_info",
"data",
"created",
"levelname",
"msecs",
"relativeCreated",
):
continue
if k.startswith("_"):
continue
custom[k] = record.__dict__[k]
# If there's no exception being processed,
# exc_info may be a 3-tuple of None
# http://docs.python.org/library/sys.html#sys.exc_info
if record.exc_info and all(record.exc_info):
handler = self.client.get_handler("elasticapm.events.Exception")
exception = handler.capture(self.client, exc_info=record.exc_info)
else:
exception = None
return self.client.capture(
"Message",
param_message={"message": str(record.msg), "params": record.args},
stack=stack,
custom=custom,
exception=exception,
level=record.levelno,
logger_name=record.name,
**kwargs,
)
class LoggingFilter(logging.Filter):
"""
This filter doesn't actually do any "filtering" -- rather, it just adds
three new attributes to any "filtered" LogRecord objects:
* elasticapm_transaction_id
* elasticapm_trace_id
* elasticapm_span_id
* elasticapm_service_name
These attributes can then be incorporated into your handlers and formatters,
so that you can tie log messages to transactions in elasticsearch.
This filter also adds these fields to a dictionary attribute,
`elasticapm_labels`, using the official tracing fields names as documented
here: https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
Note that if you're using Python 3.2+, by default we will add a
LogRecordFactory to your root logger which will add these attributes
automatically.
"""
def __init__(self, name=""):
super().__init__(name=name)
warnings.warn(
"The LoggingFilter is deprecated and will be removed in v7.0 of "
"the agent. On Python 3.2+, by default we add a LogRecordFactory to "
"your root logger automatically"
"https://www.elastic.co/guide/en/apm/agent/python/current/logs.html",
DeprecationWarning,
)
def filter(self, record):
"""
Add elasticapm attributes to `record`.
"""
_add_attributes_to_log_record(record)
return True
@wrapt.decorator
def log_record_factory(wrapped, instance, args, kwargs):
"""
Decorator, designed to wrap the python log record factory (fetched by
logging.getLogRecordFactory), adding the same custom attributes as in
the LoggingFilter provided above.
:return:
LogRecord object, with custom attributes for APM tracing fields
"""
record = wrapped(*args, **kwargs)
return _add_attributes_to_log_record(record)
def _add_attributes_to_log_record(record):
"""
Add custom attributes for APM tracing fields to a LogRecord object
:param record: LogRecord object
:return: Updated LogRecord object with new APM tracing fields
"""
transaction = execution_context.get_transaction()
transaction_id = transaction.id if transaction else None
record.elasticapm_transaction_id = transaction_id
trace_id = transaction.trace_parent.trace_id if transaction and transaction.trace_parent else None
record.elasticapm_trace_id = trace_id
span = execution_context.get_span()
span_id = span.id if span else None
record.elasticapm_span_id = span_id
client = get_client()
service_name = client.config.service_name if client else None
record.elasticapm_service_name = service_name
service_environment = client.config.environment if client else None
record.elasticapm_service_environment = service_environment
record.elasticapm_labels = {
"transaction.id": transaction_id,
"trace.id": trace_id,
"span.id": span_id,
"service.name": service_name,
"service.environment": service_environment,
}
return record
class Formatter(logging.Formatter):
"""
Custom formatter to automatically append the elasticapm format string,
as well as ensure that LogRecord objects actually have the required fields
(so as to avoid errors which could occur for logs before we override the
LogRecordFactory):
formatstring = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
formatstring = formatstring + " | elasticapm " \
"transaction.id=%(elasticapm_transaction_id)s " \
"trace.id=%(elasticapm_trace_id)s " \
"span.id=%(elasticapm_span_id)s"
"""
def __init__(self, fmt=None, datefmt=None, style="%") -> None:
if fmt is None:
fmt = "%(message)s"
fmt = (
fmt + " | elasticapm "
"transaction.id=%(elasticapm_transaction_id)s "
"trace.id=%(elasticapm_trace_id)s "
"span.id=%(elasticapm_span_id)s"
)
super(Formatter, self).__init__(fmt=fmt, datefmt=datefmt, style=style)
def format(self, record):
if not hasattr(record, "elasticapm_transaction_id"):
record.elasticapm_transaction_id = None
record.elasticapm_trace_id = None
record.elasticapm_span_id = None
record.elasticapm_service_name = None
record.elasticapm_service_environment = None
return super(Formatter, self).format(record=record)
def formatTime(self, record, datefmt=None):
if not hasattr(record, "elasticapm_transaction_id"):
record.elasticapm_transaction_id = None
record.elasticapm_trace_id = None
record.elasticapm_span_id = None
record.elasticapm_service_name = None
record.elasticapm_service_environment = None
return super(Formatter, self).formatTime(record=record, datefmt=datefmt)