Skip to content

Commit 0953fb9

Browse files
committed
Revert "Drop deprecated logging handler (elastic#2348)"
This reverts commit 4b0c8e9.
1 parent b533c96 commit 0953fb9

File tree

5 files changed

+443
-9
lines changed

5 files changed

+443
-9
lines changed

elasticapm/conf/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,10 @@ def setup_logging(handler):
883883
884884
For a typical Python install:
885885
886+
>>> from elasticapm.handlers.logging import LoggingHandler
887+
>>> client = ElasticAPM(...)
888+
>>> setup_logging(LoggingHandler(client))
889+
886890
Returns a boolean based on if logging was configured or not.
887891
"""
888892
# TODO We should probably revisit this. Does it make more sense as

elasticapm/contrib/flask/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ def __init__(self, app=None, client=None, client_cls=Client, **defaults) -> None
8282
self.client = client or get_client()
8383
self.client_cls = client_cls
8484

85-
if "logging" in defaults:
86-
raise ValueError("Flask log shipping has been removed, drop the ElasticAPM logging parameter")
87-
8885
if app:
8986
self.init_app(app, **defaults)
9087

elasticapm/handlers/logging.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,181 @@
3232
from __future__ import absolute_import
3333

3434
import logging
35+
import sys
36+
import traceback
37+
import warnings
3538

3639
import wrapt
3740

3841
from elasticapm import get_client
42+
from elasticapm.base import Client
3943
from elasticapm.traces import execution_context
44+
from elasticapm.utils.stacks import iter_stack_frames
45+
46+
47+
class LoggingHandler(logging.Handler):
48+
def __init__(self, *args, **kwargs) -> None:
49+
warnings.warn(
50+
"The LoggingHandler is deprecated and will be removed in v7.0 of "
51+
"the agent. Please use `log_ecs_reformatting` and ship the logs "
52+
"with Elastic Agent or Filebeat instead. "
53+
"https://www.elastic.co/guide/en/apm/agent/python/current/logs.html",
54+
DeprecationWarning,
55+
)
56+
self.client = None
57+
if "client" in kwargs:
58+
self.client = kwargs.pop("client")
59+
elif len(args) > 0:
60+
arg = args[0]
61+
if isinstance(arg, Client):
62+
self.client = arg
63+
64+
if not self.client:
65+
client_cls = kwargs.pop("client_cls", None)
66+
if client_cls:
67+
self.client = client_cls(*args, **kwargs)
68+
else:
69+
warnings.warn(
70+
"LoggingHandler requires a Client instance. No Client was received.",
71+
DeprecationWarning,
72+
)
73+
self.client = Client(*args, **kwargs)
74+
logging.Handler.__init__(self, level=kwargs.get("level", logging.NOTSET))
75+
76+
def emit(self, record):
77+
self.format(record)
78+
79+
# Avoid typical config issues by overriding loggers behavior
80+
if record.name.startswith(("elasticapm.errors",)):
81+
sys.stderr.write(record.getMessage() + "\n")
82+
return
83+
84+
try:
85+
return self._emit(record)
86+
except Exception:
87+
sys.stderr.write("Top level ElasticAPM exception caught - failed creating log record.\n")
88+
sys.stderr.write(record.getMessage() + "\n")
89+
sys.stderr.write(traceback.format_exc() + "\n")
90+
91+
try:
92+
self.client.capture("Exception")
93+
except Exception:
94+
pass
95+
96+
def _emit(self, record, **kwargs):
97+
data = {}
98+
99+
for k, v in record.__dict__.items():
100+
if "." not in k and k not in ("culprit",):
101+
continue
102+
data[k] = v
103+
104+
stack = getattr(record, "stack", None)
105+
if stack is True:
106+
stack = iter_stack_frames(config=self.client.config)
107+
108+
if stack:
109+
frames = []
110+
started = False
111+
last_mod = ""
112+
for item in stack:
113+
if isinstance(item, (list, tuple)):
114+
frame, lineno = item
115+
else:
116+
frame, lineno = item, item.f_lineno
117+
118+
if not started:
119+
f_globals = getattr(frame, "f_globals", {})
120+
module_name = f_globals.get("__name__", "")
121+
if last_mod.startswith("logging") and not module_name.startswith("logging"):
122+
started = True
123+
else:
124+
last_mod = module_name
125+
continue
126+
frames.append((frame, lineno))
127+
stack = frames
128+
129+
custom = getattr(record, "data", {})
130+
# Add in all of the data from the record that we aren't already capturing
131+
for k in record.__dict__.keys():
132+
if k in (
133+
"stack",
134+
"name",
135+
"args",
136+
"msg",
137+
"levelno",
138+
"exc_text",
139+
"exc_info",
140+
"data",
141+
"created",
142+
"levelname",
143+
"msecs",
144+
"relativeCreated",
145+
):
146+
continue
147+
if k.startswith("_"):
148+
continue
149+
custom[k] = record.__dict__[k]
150+
151+
# If there's no exception being processed,
152+
# exc_info may be a 3-tuple of None
153+
# http://docs.python.org/library/sys.html#sys.exc_info
154+
if record.exc_info and all(record.exc_info):
155+
handler = self.client.get_handler("elasticapm.events.Exception")
156+
exception = handler.capture(self.client, exc_info=record.exc_info)
157+
else:
158+
exception = None
159+
160+
return self.client.capture(
161+
"Message",
162+
param_message={"message": str(record.msg), "params": record.args},
163+
stack=stack,
164+
custom=custom,
165+
exception=exception,
166+
level=record.levelno,
167+
logger_name=record.name,
168+
**kwargs,
169+
)
170+
171+
172+
class LoggingFilter(logging.Filter):
173+
"""
174+
This filter doesn't actually do any "filtering" -- rather, it just adds
175+
three new attributes to any "filtered" LogRecord objects:
176+
177+
* elasticapm_transaction_id
178+
* elasticapm_trace_id
179+
* elasticapm_span_id
180+
* elasticapm_service_name
181+
182+
These attributes can then be incorporated into your handlers and formatters,
183+
so that you can tie log messages to transactions in elasticsearch.
184+
185+
This filter also adds these fields to a dictionary attribute,
186+
`elasticapm_labels`, using the official tracing fields names as documented
187+
here: https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
188+
189+
Note that if you're using Python 3.2+, by default we will add a
190+
LogRecordFactory to your root logger which will add these attributes
191+
automatically.
192+
"""
193+
194+
def __init__(self, name=""):
195+
super().__init__(name=name)
196+
warnings.warn(
197+
"The LoggingFilter is deprecated and will be removed in v7.0 of "
198+
"the agent. On Python 3.2+, by default we add a LogRecordFactory to "
199+
"your root logger automatically"
200+
"https://www.elastic.co/guide/en/apm/agent/python/current/logs.html",
201+
DeprecationWarning,
202+
)
203+
204+
def filter(self, record):
205+
"""
206+
Add elasticapm attributes to `record`.
207+
"""
208+
_add_attributes_to_log_record(record)
209+
return True
40210

41211

42212
@wrapt.decorator

tests/contrib/flask/flask_tests.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@
5050
pytestmark = pytest.mark.flask
5151

5252

53-
def test_logging_parameter_raises_exception():
54-
with pytest.raises(ValueError, match="Flask log shipping has been removed, drop the ElasticAPM logging parameter"):
55-
ElasticAPM(config=None, logging=True)
56-
57-
5853
def test_error_handler(flask_apm_client):
5954
client = flask_apm_client.app.test_client()
6055
response = client.get("/an-error/")

0 commit comments

Comments
 (0)