Skip to content

Commit e825809

Browse files
authored
Merge pull request #482 from itamarst/476-infinite-exception
Fix infinite exception when destination errors
2 parents d769e25 + 98b5c94 commit e825809

File tree

6 files changed

+117
-99
lines changed

6 files changed

+117
-99
lines changed

docs/source/news.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Features:
99
* ``Action.continue_task`` now takes ``action_task`` and extra fields to use for the action, so the default ``eliot:remote_task`` can be changed.
1010
* Added support for Python 3.10.
1111

12+
Bug fixes:
13+
14+
* Fix infinite recursion when a logging destination raises exceptions forever. Thanks to to @alextatarinov.
15+
1216
1.13.0
1317
^^^^^^
1418

eliot/_action.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,9 @@ def log(self, message_type, **fields):
440440
fields[TASK_UUID_FIELD] = self._identification[TASK_UUID_FIELD]
441441
fields[TASK_LEVEL_FIELD] = self._nextTaskLevel().as_list()
442442
fields[MESSAGE_TYPE_FIELD] = message_type
443-
self._logger.write(fields, fields.pop("__eliot_serializer__", None))
443+
# Loggers will hopefully go away...
444+
logger = fields.pop("__eliot_logger__", self._logger)
445+
logger.write(fields, fields.pop("__eliot_serializer__", None))
444446

445447

446448
class WrongTask(Exception):
@@ -951,10 +953,10 @@ def log_message(message_type, **fields):
951953
952954
If there is no current action, a new UUID will be generated.
953955
"""
954-
# Loggers will hopefully go away...
955-
logger = fields.pop("__eliot_logger__", None)
956956
action = current_action()
957957
if action is None:
958+
# Loggers will hopefully go away...
959+
logger = fields.pop("__eliot_logger__", None)
958960
action = Action(logger, str(uuid4()), TaskLevel(level=[]), "")
959961
action.log(message_type, **fields)
960962

eliot/_message.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,19 @@ def write(self, logger=None, action=None):
116116
Byte field names will be converted to Unicode.
117117
118118
@type logger: L{eliot.ILogger} or C{None} indicating the default one.
119+
Should not be set if the action is also set.
119120
120-
@param action: The L{Action} which is the context for this message. If
121-
C{None}, the L{Action} will be deduced from the current call
122-
stack.
121+
@param action: The L{Action} which is the context for this message. If
122+
C{None}, the L{Action} will be deduced from the current call stack.
123123
"""
124124
fields = dict(self._contents)
125125
if "message_type" not in fields:
126126
fields["message_type"] = ""
127127
if self._serializer is not None:
128128
fields["__eliot_serializer__"] = self._serializer
129129
if action is None:
130-
fields["__eliot_logger__"] = logger
130+
if logger is not None:
131+
fields["__eliot_logger__"] = logger
131132
log_message(**fields)
132133
else:
133134
action.log(**fields)

eliot/_output.py

Lines changed: 64 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Implementation of hooks and APIs for outputting log messages.
33
"""
44

5-
import sys
65
import traceback
76
import inspect
87
import json as pyjson
@@ -22,16 +21,9 @@
2221
from ._validation import ValidationError
2322

2423

25-
class _DestinationsSendError(Exception):
26-
"""
27-
An error occured sending to one or more destinations.
28-
29-
@ivar errors: A list of tuples output from C{sys.exc_info()}.
30-
"""
31-
32-
def __init__(self, errors):
33-
self.errors = errors
34-
Exception.__init__(self, errors)
24+
# Action type for log messages due to a (hopefully temporarily) broken
25+
# destination.
26+
DESTINATION_FAILURE = "eliot:destination_failure"
3527

3628

3729
class BufferingDestination(object):
@@ -70,24 +62,57 @@ def addGlobalFields(self, **fields):
7062
"""
7163
self._globalFields.update(fields)
7264

73-
def send(self, message):
65+
def send(self, message, logger=None):
7466
"""
7567
Deliver a message to all destinations.
7668
7769
The passed in message might be mutated.
7870
71+
This should never raise an exception.
72+
7973
@param message: A message dictionary that can be serialized to JSON.
8074
@type message: L{dict}
75+
76+
@param logger: The ``ILogger`` that wrote the message, if any.
8177
"""
8278
message.update(self._globalFields)
8379
errors = []
80+
is_destination_error_message = (
81+
message.get("message_type", None) == DESTINATION_FAILURE
82+
)
8483
for dest in self._destinations:
8584
try:
8685
dest(message)
86+
except Exception as e:
87+
# If the destination is broken not because of a specific
88+
# message, but rather continously, we will get a
89+
# "eliot:destination_failure" log message logged, and so we
90+
# want to ensure it doesn't do infinite recursion.
91+
if not is_destination_error_message:
92+
errors.append(e)
93+
94+
for exception in errors:
95+
from ._action import log_message
96+
97+
try:
98+
new_msg = {
99+
MESSAGE_TYPE_FIELD: DESTINATION_FAILURE,
100+
REASON_FIELD: safeunicode(exception),
101+
EXCEPTION_FIELD: exception.__class__.__module__
102+
+ "."
103+
+ exception.__class__.__name__,
104+
"message": _safe_unicode_dictionary(message),
105+
}
106+
if logger is not None:
107+
# This is really only useful for testing, should really
108+
# figure out way to get rid of this mechanism...
109+
new_msg["__eliot_logger__"] = logger
110+
log_message(**new_msg)
87111
except:
88-
errors.append(sys.exc_info())
89-
if errors:
90-
raise _DestinationsSendError(errors)
112+
# Nothing we can do here, raising exception to caller will
113+
# break business logic, better to have that continue to
114+
# work even if logging isn't.
115+
pass
91116

92117
def add(self, *destinations):
93118
"""
@@ -144,6 +169,28 @@ def write(dictionary, serializer=None):
144169
"""
145170

146171

172+
def _safe_unicode_dictionary(dictionary):
173+
"""
174+
Serialize a dictionary to a unicode string no matter what it contains.
175+
176+
The resulting dictionary will loosely follow Python syntax but it is
177+
not expected to actually be a lossless encoding in all cases.
178+
179+
@param dictionary: A L{dict} to serialize.
180+
181+
@return: A L{unicode} string representing the input dictionary as
182+
faithfully as can be done without putting in too much effort.
183+
"""
184+
try:
185+
return str(
186+
dict(
187+
(saferepr(key), saferepr(value)) for (key, value) in dictionary.items()
188+
)
189+
)
190+
except:
191+
return saferepr(dictionary)
192+
193+
147194
@implementer(ILogger)
148195
class Logger(object):
149196
"""
@@ -155,29 +202,6 @@ class Logger(object):
155202
"""
156203

157204
_destinations = Destinations()
158-
_log_tracebacks = True
159-
160-
def _safeUnicodeDictionary(self, dictionary):
161-
"""
162-
Serialize a dictionary to a unicode string no matter what it contains.
163-
164-
The resulting dictionary will loosely follow Python syntax but it is
165-
not expected to actually be a lossless encoding in all cases.
166-
167-
@param dictionary: A L{dict} to serialize.
168-
169-
@return: A L{unicode} string representing the input dictionary as
170-
faithfully as can be done without putting in too much effort.
171-
"""
172-
try:
173-
return str(
174-
dict(
175-
(saferepr(key), saferepr(value))
176-
for (key, value) in dictionary.items()
177-
)
178-
)
179-
except:
180-
return saferepr(dictionary)
181205

182206
def write(self, dictionary, serializer=None):
183207
"""
@@ -193,38 +217,12 @@ def write(self, dictionary, serializer=None):
193217

194218
log_message(
195219
"eliot:serialization_failure",
196-
message=self._safeUnicodeDictionary(dictionary),
220+
message=_safe_unicode_dictionary(dictionary),
197221
__eliot_logger__=self,
198222
)
199223
return
200224

201-
try:
202-
self._destinations.send(dictionary)
203-
except _DestinationsSendError as e:
204-
from ._action import log_message
205-
206-
if self._log_tracebacks:
207-
for (exc_type, exception, exc_traceback) in e.errors:
208-
# Can't use same Logger as serialization errors because
209-
# if destination continues to error out we will get
210-
# infinite recursion. So instead we have to manually
211-
# construct a Logger that won't retry.
212-
logger = Logger()
213-
logger._log_tracebacks = False
214-
logger._destinations = self._destinations
215-
msg = {
216-
MESSAGE_TYPE_FIELD: "eliot:destination_failure",
217-
REASON_FIELD: safeunicode(exception),
218-
EXCEPTION_FIELD: exc_type.__module__ + "." + exc_type.__name__,
219-
"message": self._safeUnicodeDictionary(dictionary),
220-
"__eliot_logger__": logger,
221-
}
222-
log_message(**msg)
223-
else:
224-
# Nothing we can do here, raising exception to caller will
225-
# break business logic, better to have that continue to
226-
# work even if logging isn't.
227-
pass
225+
self._destinations.send(dictionary, self)
228226

229227

230228
def exclusively(f):

0 commit comments

Comments
 (0)