Skip to content

Commit 6d15aac

Browse files
authored
Revert log shipping removals (#2383)
* Revert "Drop deprecated logging handler (#2348)" This reverts commit 4b0c8e9. * Revert "contrib/flask: remove deprecated log shipping integration (#2346)" This reverts commit 39f4191. * Revert "contrib/django: remove deprecated LoggingHandler (#2345)" This reverts commit eb34e89.
1 parent f5c1a92 commit 6d15aac

File tree

7 files changed

+595
-11
lines changed

7 files changed

+595
-11
lines changed

elasticapm/conf/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,15 @@ 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+
890+
Within Django:
891+
892+
>>> from elasticapm.contrib.django.handlers import LoggingHandler
893+
>>> setup_logging(LoggingHandler())
894+
886895
Returns a boolean based on if logging was configured or not.
887896
"""
888897
# TODO We should probably revisit this. Does it make more sense as

elasticapm/contrib/django/handlers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,44 @@
3131

3232
from __future__ import absolute_import
3333

34+
import logging
3435
import sys
3536
import warnings
3637

3738
from django.conf import settings as django_settings
3839

40+
from elasticapm import get_client
41+
from elasticapm.handlers.logging import LoggingHandler as BaseLoggingHandler
42+
from elasticapm.utils.logging import get_logger
43+
44+
logger = get_logger("elasticapm.logging")
45+
46+
47+
class LoggingHandler(BaseLoggingHandler):
48+
def __init__(self, level=logging.NOTSET) -> None:
49+
warnings.warn(
50+
"The LoggingHandler is deprecated and will be removed in v7.0 of the agent. "
51+
"Please use `log_ecs_reformatting` and ship the logs with Elastic "
52+
"Agent or Filebeat instead. "
53+
"https://www.elastic.co/guide/en/apm/agent/python/current/logs.html",
54+
DeprecationWarning,
55+
)
56+
# skip initialization of BaseLoggingHandler
57+
logging.Handler.__init__(self, level=level)
58+
59+
@property
60+
def client(self):
61+
return get_client()
62+
63+
def _emit(self, record, **kwargs):
64+
from elasticapm.contrib.django.middleware import LogMiddleware
65+
66+
# Fetch the request from a threadlocal variable, if available
67+
request = getattr(LogMiddleware.thread, "request", None)
68+
request = getattr(record, "request", request)
69+
70+
return super(LoggingHandler, self)._emit(record, request=request, **kwargs)
71+
3972

4073
def exception_handler(client, request=None, **kwargs):
4174
def actually_do_stuff(request=None, **kwargs) -> None:

elasticapm/contrib/flask/__init__.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@
3131

3232
from __future__ import absolute_import
3333

34+
import logging
35+
import warnings
36+
3437
import flask
3538
from flask import request, signals
3639

3740
import elasticapm
3841
import elasticapm.instrumentation.control
3942
from elasticapm import get_client
4043
from elasticapm.base import Client
41-
from elasticapm.conf import constants
44+
from elasticapm.conf import constants, setup_logging
4245
from elasticapm.contrib.flask.utils import get_data_from_request, get_data_from_response
46+
from elasticapm.handlers.logging import LoggingHandler
4347
from elasticapm.traces import execution_context
4448
from elasticapm.utils import build_name_with_http_method_prefix
4549
from elasticapm.utils.disttracing import TraceParent
@@ -77,14 +81,17 @@ class ElasticAPM(object):
7781
>>> elasticapm.capture_message('hello, world!')
7882
"""
7983

80-
def __init__(self, app=None, client=None, client_cls=Client, **defaults) -> None:
84+
def __init__(self, app=None, client=None, client_cls=Client, logging=False, **defaults) -> None:
8185
self.app = app
86+
self.logging = logging
87+
if self.logging:
88+
warnings.warn(
89+
"Flask log shipping is deprecated. See the Flask docs for more info and alternatives.",
90+
DeprecationWarning,
91+
)
8292
self.client = client or get_client()
8393
self.client_cls = client_cls
8494

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

@@ -120,6 +127,14 @@ def init_app(self, app, **defaults) -> None:
120127

121128
self.client = self.client_cls(config, **defaults)
122129

130+
# 0 is a valid log level (NOTSET), so we need to check explicitly for it
131+
if self.logging or self.logging is logging.NOTSET:
132+
if self.logging is not True:
133+
kwargs = {"level": self.logging}
134+
else:
135+
kwargs = {}
136+
setup_logging(LoggingHandler(self.client, **kwargs))
137+
123138
signals.got_request_exception.connect(self.handle_exception, sender=app, weak=False)
124139

125140
try:

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/django/django_tests.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from elasticapm.conf.constants import ERROR, SPAN, TRANSACTION
6363
from elasticapm.contrib.django.apps import ElasticAPMConfig
6464
from elasticapm.contrib.django.client import client, get_client
65+
from elasticapm.contrib.django.handlers import LoggingHandler
6566
from elasticapm.contrib.django.middleware.wsgi import ElasticAPM
6667
from elasticapm.utils.disttracing import TraceParent
6768
from tests.contrib.django.conftest import BASE_TEMPLATE_DIR
@@ -409,6 +410,25 @@ def test_ignored_exception_is_ignored(django_elasticapm_client, client):
409410
assert len(django_elasticapm_client.events[ERROR]) == 0
410411

411412

413+
def test_record_none_exc_info(django_elasticapm_client):
414+
# sys.exc_info can return (None, None, None) if no exception is being
415+
# handled anywhere on the stack. See:
416+
# http://docs.python.org/library/sys.html#sys.exc_info
417+
record = logging.LogRecord(
418+
"foo", logging.INFO, pathname=None, lineno=None, msg="test", args=(), exc_info=(None, None, None)
419+
)
420+
handler = LoggingHandler()
421+
handler.emit(record)
422+
423+
assert len(django_elasticapm_client.events[ERROR]) == 1
424+
event = django_elasticapm_client.events[ERROR][0]
425+
426+
assert event["log"]["param_message"] == "test"
427+
assert event["log"]["logger_name"] == "foo"
428+
assert event["log"]["level"] == "info"
429+
assert "exception" not in event
430+
431+
412432
def test_404_middleware(django_elasticapm_client, client):
413433
with override_settings(
414434
**middleware_setting(django.VERSION, ["elasticapm.contrib.django.middleware.Catch404Middleware"])
@@ -1012,6 +1032,54 @@ def test_filter_matches_module_only(django_sending_elasticapm_client):
10121032
assert len(django_sending_elasticapm_client.httpserver.requests) == 1
10131033

10141034

1035+
def test_django_logging_request_kwarg(django_elasticapm_client):
1036+
handler = LoggingHandler()
1037+
1038+
logger = logging.getLogger(__name__)
1039+
logger.handlers = []
1040+
logger.addHandler(handler)
1041+
1042+
logger.error(
1043+
"This is a test error",
1044+
extra={
1045+
"request": WSGIRequest(
1046+
environ={
1047+
"wsgi.input": io.StringIO(),
1048+
"REQUEST_METHOD": "POST",
1049+
"SERVER_NAME": "testserver",
1050+
"SERVER_PORT": "80",
1051+
"CONTENT_TYPE": "application/json",
1052+
"ACCEPT": "application/json",
1053+
}
1054+
)
1055+
},
1056+
)
1057+
1058+
assert len(django_elasticapm_client.events[ERROR]) == 1
1059+
event = django_elasticapm_client.events[ERROR][0]
1060+
assert "request" in event["context"]
1061+
request = event["context"]["request"]
1062+
assert request["method"] == "POST"
1063+
1064+
1065+
def test_django_logging_middleware(django_elasticapm_client, client):
1066+
handler = LoggingHandler()
1067+
1068+
logger = logging.getLogger("logmiddleware")
1069+
logger.handlers = []
1070+
logger.addHandler(handler)
1071+
logger.level = logging.INFO
1072+
1073+
with override_settings(
1074+
**middleware_setting(django.VERSION, ["elasticapm.contrib.django.middleware.LogMiddleware"])
1075+
):
1076+
client.get(reverse("elasticapm-logging"))
1077+
assert len(django_elasticapm_client.events[ERROR]) == 1
1078+
event = django_elasticapm_client.events[ERROR][0]
1079+
assert "request" in event["context"]
1080+
assert event["context"]["request"]["url"]["pathname"] == reverse("elasticapm-logging")
1081+
1082+
10151083
def client_get(client, url):
10161084
return client.get(url)
10171085

0 commit comments

Comments
 (0)