RAHHH
This commit is contained in:
136
backend/lib/python3.12/site-packages/twisted/logger/__init__.py
Normal file
136
backend/lib/python3.12/site-packages/twisted/logger/__init__.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- test-case-name: twisted.logger.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Logger: Classes and functions to do granular logging.
|
||||
|
||||
Example usage in a module C{some.module}::
|
||||
|
||||
from twisted.logger import Logger
|
||||
log = Logger()
|
||||
|
||||
def handleData(data):
|
||||
log.debug("Got data: {data!r}.", data=data)
|
||||
|
||||
Or in a class::
|
||||
|
||||
from twisted.logger import Logger
|
||||
|
||||
class Foo:
|
||||
log = Logger()
|
||||
|
||||
def oops(self, data):
|
||||
self.log.error("Oops! Invalid data from server: {data!r}",
|
||||
data=data)
|
||||
|
||||
C{Logger}s have namespaces, for which logging can be configured independently.
|
||||
Namespaces may be specified by passing in a C{namespace} argument to L{Logger}
|
||||
when instantiating it, but if none is given, the logger will derive its own
|
||||
namespace by using the module name of the callable that instantiated it, or, in
|
||||
the case of a class, by using the fully qualified name of the class.
|
||||
|
||||
In the first example above, the namespace would be C{some.module}, and in the
|
||||
second example, it would be C{some.module.Foo}.
|
||||
|
||||
@var globalLogPublisher: The L{LogPublisher} that all L{Logger} instances that
|
||||
are not otherwise parameterized will publish events to by default.
|
||||
@var globalLogBeginner: The L{LogBeginner} used to activate the main log
|
||||
observer, whether it's a log file, or an observer pointing at stderr.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
# From ._levels
|
||||
"InvalidLogLevelError",
|
||||
"LogLevel",
|
||||
# From ._format
|
||||
"formatEvent",
|
||||
"formatEventAsClassicLogText",
|
||||
"formatTime",
|
||||
"timeFormatRFC3339",
|
||||
"eventAsText",
|
||||
# From ._flatten
|
||||
"extractField",
|
||||
# From ._interfaces
|
||||
"ILogObserver",
|
||||
"LogEvent",
|
||||
# From ._logger
|
||||
"Logger",
|
||||
"Operation",
|
||||
"_loggerFor",
|
||||
# From ._observer
|
||||
"LogPublisher",
|
||||
# From ._buffer
|
||||
"LimitedHistoryLogObserver",
|
||||
# From ._file
|
||||
"FileLogObserver",
|
||||
"textFileLogObserver",
|
||||
# From ._filter
|
||||
"PredicateResult",
|
||||
"ILogFilterPredicate",
|
||||
"FilteringLogObserver",
|
||||
"LogLevelFilterPredicate",
|
||||
# From ._stdlib
|
||||
"STDLibLogObserver",
|
||||
# From ._io
|
||||
"LoggingFile",
|
||||
# From ._legacy
|
||||
"LegacyLogObserverWrapper",
|
||||
# From ._global
|
||||
"globalLogPublisher",
|
||||
"globalLogBeginner",
|
||||
"LogBeginner",
|
||||
# From ._json
|
||||
"eventAsJSON",
|
||||
"eventFromJSON",
|
||||
"jsonFileLogObserver",
|
||||
"eventsFromJSONLogFile",
|
||||
# From ._capture
|
||||
"capturedLogs",
|
||||
]
|
||||
|
||||
from ._levels import InvalidLogLevelError, LogLevel
|
||||
|
||||
from ._flatten import extractField
|
||||
|
||||
from ._format import (
|
||||
formatEvent,
|
||||
formatEventAsClassicLogText,
|
||||
formatTime,
|
||||
timeFormatRFC3339,
|
||||
eventAsText,
|
||||
)
|
||||
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
|
||||
from ._logger import Logger, _loggerFor, Operation
|
||||
|
||||
from ._observer import LogPublisher
|
||||
|
||||
from ._buffer import LimitedHistoryLogObserver
|
||||
|
||||
from ._file import FileLogObserver, textFileLogObserver
|
||||
|
||||
from ._filter import (
|
||||
PredicateResult,
|
||||
ILogFilterPredicate,
|
||||
FilteringLogObserver,
|
||||
LogLevelFilterPredicate,
|
||||
)
|
||||
|
||||
from ._stdlib import STDLibLogObserver
|
||||
|
||||
from ._io import LoggingFile
|
||||
|
||||
from ._legacy import LegacyLogObserverWrapper
|
||||
|
||||
from ._global import globalLogPublisher, globalLogBeginner, LogBeginner
|
||||
|
||||
from ._json import (
|
||||
eventAsJSON,
|
||||
eventFromJSON,
|
||||
jsonFileLogObserver,
|
||||
eventsFromJSONLogFile,
|
||||
)
|
||||
|
||||
from ._capture import capturedLogs
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,54 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_buffer -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Log observer that maintains a buffer.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from typing import Deque, Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
|
||||
_DEFAULT_BUFFER_MAXIMUM = 64 * 1024
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class LimitedHistoryLogObserver:
|
||||
"""
|
||||
L{ILogObserver} that stores events in a buffer of a fixed size::
|
||||
|
||||
>>> from twisted.logger import LimitedHistoryLogObserver
|
||||
>>> history = LimitedHistoryLogObserver(5)
|
||||
>>> for n in range(10): history({'n': n})
|
||||
...
|
||||
>>> repeats = []
|
||||
>>> history.replayTo(repeats.append)
|
||||
>>> len(repeats)
|
||||
5
|
||||
>>> repeats
|
||||
[{'n': 5}, {'n': 6}, {'n': 7}, {'n': 8}, {'n': 9}]
|
||||
>>>
|
||||
"""
|
||||
|
||||
def __init__(self, size: Optional[int] = _DEFAULT_BUFFER_MAXIMUM) -> None:
|
||||
"""
|
||||
@param size: The maximum number of events to buffer. If L{None}, the
|
||||
buffer is unbounded.
|
||||
"""
|
||||
self._buffer: Deque[LogEvent] = deque(maxlen=size)
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
self._buffer.append(event)
|
||||
|
||||
def replayTo(self, otherObserver: ILogObserver) -> None:
|
||||
"""
|
||||
Re-play the buffered events to another log observer.
|
||||
|
||||
@param otherObserver: An observer to replay events to.
|
||||
"""
|
||||
for event in self._buffer:
|
||||
otherObserver(event)
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_capture -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Context manager for capturing logs.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator, List, Sequence, cast
|
||||
|
||||
from twisted.logger import globalLogPublisher
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capturedLogs() -> Iterator[Sequence[LogEvent]]:
|
||||
events: List[LogEvent] = []
|
||||
observer = cast(ILogObserver, events.append)
|
||||
|
||||
globalLogPublisher.addObserver(observer)
|
||||
|
||||
yield events
|
||||
|
||||
globalLogPublisher.removeObserver(observer)
|
||||
77
backend/lib/python3.12/site-packages/twisted/logger/_file.py
Normal file
77
backend/lib/python3.12/site-packages/twisted/logger/_file.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_file -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
File log observer.
|
||||
"""
|
||||
|
||||
from typing import IO, Any, Callable, Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.python.compat import ioType
|
||||
from ._format import formatEventAsClassicLogText, formatTime, timeFormatRFC3339
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class FileLogObserver:
|
||||
"""
|
||||
Log observer that writes to a file-like object.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, outFile: IO[Any], formatEvent: Callable[[LogEvent], Optional[str]]
|
||||
) -> None:
|
||||
"""
|
||||
@param outFile: A file-like object. Ideally one should be passed which
|
||||
accepts text data. Otherwise, UTF-8 L{bytes} will be used.
|
||||
@param formatEvent: A callable that formats an event.
|
||||
"""
|
||||
if ioType(outFile) is not str:
|
||||
self._encoding: Optional[str] = "utf-8"
|
||||
else:
|
||||
self._encoding = None
|
||||
|
||||
self._outFile = outFile
|
||||
self.formatEvent = formatEvent
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
"""
|
||||
Write event to file.
|
||||
|
||||
@param event: An event.
|
||||
"""
|
||||
text = self.formatEvent(event)
|
||||
|
||||
if text:
|
||||
if self._encoding is None:
|
||||
self._outFile.write(text)
|
||||
else:
|
||||
self._outFile.write(text.encode(self._encoding))
|
||||
self._outFile.flush()
|
||||
|
||||
|
||||
def textFileLogObserver(
|
||||
outFile: IO[Any], timeFormat: Optional[str] = timeFormatRFC3339
|
||||
) -> FileLogObserver:
|
||||
"""
|
||||
Create a L{FileLogObserver} that emits text to a specified (writable)
|
||||
file-like object.
|
||||
|
||||
@param outFile: A file-like object. Ideally one should be passed which
|
||||
accepts text data. Otherwise, UTF-8 L{bytes} will be used.
|
||||
@param timeFormat: The format to use when adding timestamp prefixes to
|
||||
logged events. If L{None}, or for events with no C{"log_timestamp"}
|
||||
key, the default timestamp prefix of C{"-"} is used.
|
||||
|
||||
@return: A file log observer.
|
||||
"""
|
||||
|
||||
def formatEvent(event: LogEvent) -> Optional[str]:
|
||||
return formatEventAsClassicLogText(
|
||||
event, formatTime=lambda e: formatTime(e, timeFormat)
|
||||
)
|
||||
|
||||
return FileLogObserver(outFile, formatEvent)
|
||||
211
backend/lib/python3.12/site-packages/twisted/logger/_filter.py
Normal file
211
backend/lib/python3.12/site-packages/twisted/logger/_filter.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_filter -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Filtering log observer.
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
from typing import Dict, Iterable
|
||||
|
||||
from zope.interface import Interface, implementer
|
||||
|
||||
from constantly import NamedConstant, Names
|
||||
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
from ._levels import InvalidLogLevelError, LogLevel
|
||||
from ._observer import bitbucketLogObserver
|
||||
|
||||
|
||||
class PredicateResult(Names):
|
||||
"""
|
||||
Predicate results.
|
||||
|
||||
@see: L{LogLevelFilterPredicate}
|
||||
|
||||
@cvar yes: Log the specified event. When this value is used,
|
||||
L{FilteringLogObserver} will always log the message, without
|
||||
evaluating other predicates.
|
||||
|
||||
@cvar no: Do not log the specified event. When this value is used,
|
||||
L{FilteringLogObserver} will I{not} log the message, without
|
||||
evaluating other predicates.
|
||||
|
||||
@cvar maybe: Do not have an opinion on the event. When this value is used,
|
||||
L{FilteringLogObserver} will consider subsequent predicate results;
|
||||
if returned by the last predicate being considered, then the event will
|
||||
be logged.
|
||||
"""
|
||||
|
||||
yes = NamedConstant()
|
||||
no = NamedConstant()
|
||||
maybe = NamedConstant()
|
||||
|
||||
|
||||
class ILogFilterPredicate(Interface):
|
||||
"""
|
||||
A predicate that determined whether an event should be logged.
|
||||
"""
|
||||
|
||||
def __call__(event: LogEvent) -> NamedConstant:
|
||||
"""
|
||||
Determine whether an event should be logged.
|
||||
|
||||
@returns: a L{PredicateResult}.
|
||||
"""
|
||||
|
||||
|
||||
def shouldLogEvent(predicates: Iterable[ILogFilterPredicate], event: LogEvent) -> bool:
|
||||
"""
|
||||
Determine whether an event should be logged, based on the result of
|
||||
C{predicates}.
|
||||
|
||||
By default, the result is C{True}; so if there are no predicates,
|
||||
everything will be logged.
|
||||
|
||||
If any predicate returns C{yes}, then we will immediately return C{True}.
|
||||
|
||||
If any predicate returns C{no}, then we will immediately return C{False}.
|
||||
|
||||
As predicates return C{maybe}, we keep calling the next predicate until we
|
||||
run out, at which point we return C{True}.
|
||||
|
||||
@param predicates: The predicates to use.
|
||||
@param event: An event
|
||||
|
||||
@return: True if the message should be forwarded on, C{False} if not.
|
||||
"""
|
||||
for predicate in predicates:
|
||||
result = predicate(event)
|
||||
if result == PredicateResult.yes:
|
||||
return True
|
||||
if result == PredicateResult.no:
|
||||
return False
|
||||
if result == PredicateResult.maybe:
|
||||
continue
|
||||
raise TypeError(f"Invalid predicate result: {result!r}")
|
||||
return True
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class FilteringLogObserver:
|
||||
"""
|
||||
L{ILogObserver} that wraps another L{ILogObserver}, but filters out events
|
||||
based on applying a series of L{ILogFilterPredicate}s.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
observer: ILogObserver,
|
||||
predicates: Iterable[ILogFilterPredicate],
|
||||
negativeObserver: ILogObserver = bitbucketLogObserver,
|
||||
) -> None:
|
||||
"""
|
||||
@param observer: An observer to which this observer will forward
|
||||
events when C{predictates} yield a positive result.
|
||||
@param predicates: Predicates to apply to events before forwarding to
|
||||
the wrapped observer.
|
||||
@param negativeObserver: An observer to which this observer will
|
||||
forward events when C{predictates} yield a negative result.
|
||||
"""
|
||||
self._observer = observer
|
||||
self._shouldLogEvent = partial(shouldLogEvent, list(predicates))
|
||||
self._negativeObserver = negativeObserver
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
"""
|
||||
Forward to next observer if predicate allows it.
|
||||
"""
|
||||
if self._shouldLogEvent(event):
|
||||
if "log_trace" in event:
|
||||
event["log_trace"].append((self, self._observer))
|
||||
self._observer(event)
|
||||
else:
|
||||
self._negativeObserver(event)
|
||||
|
||||
|
||||
@implementer(ILogFilterPredicate)
|
||||
class LogLevelFilterPredicate:
|
||||
"""
|
||||
L{ILogFilterPredicate} that filters out events with a log level lower than
|
||||
the log level for the event's namespace.
|
||||
|
||||
Events that not not have a log level or namespace are also dropped.
|
||||
"""
|
||||
|
||||
def __init__(self, defaultLogLevel: NamedConstant = LogLevel.info) -> None:
|
||||
"""
|
||||
@param defaultLogLevel: The default minimum log level.
|
||||
"""
|
||||
self._logLevelsByNamespace: Dict[str, NamedConstant] = {}
|
||||
self.defaultLogLevel = defaultLogLevel
|
||||
self.clearLogLevels()
|
||||
|
||||
def logLevelForNamespace(self, namespace: str) -> NamedConstant:
|
||||
"""
|
||||
Determine an appropriate log level for the given namespace.
|
||||
|
||||
This respects dots in namespaces; for example, if you have previously
|
||||
invoked C{setLogLevelForNamespace("mypackage", LogLevel.debug)}, then
|
||||
C{logLevelForNamespace("mypackage.subpackage")} will return
|
||||
C{LogLevel.debug}.
|
||||
|
||||
@param namespace: A logging namespace. Use C{""} for the default
|
||||
namespace.
|
||||
|
||||
@return: The log level for the specified namespace.
|
||||
"""
|
||||
if not namespace:
|
||||
return self._logLevelsByNamespace[""]
|
||||
|
||||
if namespace in self._logLevelsByNamespace:
|
||||
return self._logLevelsByNamespace[namespace]
|
||||
|
||||
segments = namespace.split(".")
|
||||
index = len(segments) - 1
|
||||
|
||||
while index > 0:
|
||||
namespace = ".".join(segments[:index])
|
||||
if namespace in self._logLevelsByNamespace:
|
||||
return self._logLevelsByNamespace[namespace]
|
||||
index -= 1
|
||||
|
||||
return self._logLevelsByNamespace[""]
|
||||
|
||||
def setLogLevelForNamespace(self, namespace: str, level: NamedConstant) -> None:
|
||||
"""
|
||||
Sets the log level for a logging namespace.
|
||||
|
||||
@param namespace: A logging namespace.
|
||||
@param level: The log level for the given namespace.
|
||||
"""
|
||||
if level not in LogLevel.iterconstants():
|
||||
raise InvalidLogLevelError(level)
|
||||
|
||||
if namespace:
|
||||
self._logLevelsByNamespace[namespace] = level
|
||||
else:
|
||||
self._logLevelsByNamespace[""] = level
|
||||
|
||||
def clearLogLevels(self) -> None:
|
||||
"""
|
||||
Clears all log levels to the default.
|
||||
"""
|
||||
self._logLevelsByNamespace.clear()
|
||||
self._logLevelsByNamespace[""] = self.defaultLogLevel
|
||||
|
||||
def __call__(self, event: LogEvent) -> NamedConstant:
|
||||
eventLevel = event.get("log_level", None)
|
||||
if eventLevel is None:
|
||||
return PredicateResult.no
|
||||
|
||||
namespace = event.get("log_namespace", "")
|
||||
if not namespace:
|
||||
return PredicateResult.no
|
||||
|
||||
namespaceLevel = self.logLevelForNamespace(namespace)
|
||||
if eventLevel < namespaceLevel:
|
||||
return PredicateResult.no
|
||||
|
||||
return PredicateResult.maybe
|
||||
175
backend/lib/python3.12/site-packages/twisted/logger/_flatten.py
Normal file
175
backend/lib/python3.12/site-packages/twisted/logger/_flatten.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_flatten -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Code related to "flattening" events; that is, extracting a description of all
|
||||
relevant fields from the format string and persisting them for later
|
||||
examination.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from string import Formatter
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ._interfaces import LogEvent
|
||||
|
||||
aFormatter = Formatter()
|
||||
|
||||
|
||||
class KeyFlattener:
|
||||
"""
|
||||
A L{KeyFlattener} computes keys for the things within curly braces in
|
||||
PEP-3101-style format strings as parsed by L{string.Formatter.parse}.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize a L{KeyFlattener}.
|
||||
"""
|
||||
self.keys: Dict[str, int] = defaultdict(lambda: 0)
|
||||
|
||||
def flatKey(
|
||||
self, fieldName: str, formatSpec: Optional[str], conversion: Optional[str]
|
||||
) -> str:
|
||||
"""
|
||||
Compute a string key for a given field/format/conversion.
|
||||
|
||||
@param fieldName: A format field name.
|
||||
@param formatSpec: A format spec.
|
||||
@param conversion: A format field conversion type.
|
||||
|
||||
@return: A key specific to the given field, format and conversion, as
|
||||
well as the occurrence of that combination within this
|
||||
L{KeyFlattener}'s lifetime.
|
||||
"""
|
||||
if formatSpec is None:
|
||||
formatSpec = ""
|
||||
|
||||
if conversion is None:
|
||||
conversion = ""
|
||||
|
||||
result = "{fieldName}!{conversion}:{formatSpec}".format(
|
||||
fieldName=fieldName,
|
||||
formatSpec=formatSpec,
|
||||
conversion=conversion,
|
||||
)
|
||||
self.keys[result] += 1
|
||||
n = self.keys[result]
|
||||
if n != 1:
|
||||
result += "/" + str(self.keys[result])
|
||||
return result
|
||||
|
||||
|
||||
def flattenEvent(event: LogEvent) -> None:
|
||||
"""
|
||||
Flatten the given event by pre-associating format fields with specific
|
||||
objects and callable results in a L{dict} put into the C{"log_flattened"}
|
||||
key in the event.
|
||||
|
||||
@param event: A logging event.
|
||||
"""
|
||||
if event.get("log_format", None) is None:
|
||||
return
|
||||
|
||||
if "log_flattened" in event:
|
||||
fields = event["log_flattened"]
|
||||
else:
|
||||
fields = {}
|
||||
|
||||
keyFlattener = KeyFlattener()
|
||||
|
||||
for literalText, fieldName, formatSpec, conversion in aFormatter.parse(
|
||||
event["log_format"]
|
||||
):
|
||||
if fieldName is None:
|
||||
continue
|
||||
|
||||
if conversion != "r":
|
||||
conversion = "s"
|
||||
|
||||
flattenedKey = keyFlattener.flatKey(fieldName, formatSpec, conversion)
|
||||
structuredKey = keyFlattener.flatKey(fieldName, formatSpec, "")
|
||||
|
||||
if flattenedKey in fields:
|
||||
# We've already seen and handled this key
|
||||
continue
|
||||
|
||||
if fieldName.endswith("()"):
|
||||
fieldName = fieldName[:-2]
|
||||
callit = True
|
||||
else:
|
||||
callit = False
|
||||
|
||||
field = aFormatter.get_field(fieldName, (), event)
|
||||
fieldValue = field[0]
|
||||
|
||||
if conversion == "r":
|
||||
conversionFunction = repr
|
||||
else: # Above: if conversion is not "r", it's "s"
|
||||
conversionFunction = str
|
||||
|
||||
if callit:
|
||||
fieldValue = fieldValue()
|
||||
|
||||
flattenedValue = conversionFunction(fieldValue)
|
||||
fields[flattenedKey] = flattenedValue
|
||||
fields[structuredKey] = fieldValue
|
||||
|
||||
if fields:
|
||||
event["log_flattened"] = fields
|
||||
|
||||
|
||||
def extractField(field: str, event: LogEvent) -> Any:
|
||||
"""
|
||||
Extract a given format field from the given event.
|
||||
|
||||
@param field: A string describing a format field or log key. This is the
|
||||
text that would normally fall between a pair of curly braces in a
|
||||
format string: for example, C{"key[2].attribute"}. If a conversion is
|
||||
specified (the thing after the C{"!"} character in a format field) then
|
||||
the result will always be str.
|
||||
@param event: A log event.
|
||||
|
||||
@return: A value extracted from the field.
|
||||
|
||||
@raise KeyError: if the field is not found in the given event.
|
||||
"""
|
||||
keyFlattener = KeyFlattener()
|
||||
|
||||
[[literalText, fieldName, formatSpec, conversion]] = aFormatter.parse(
|
||||
"{" + field + "}"
|
||||
)
|
||||
|
||||
assert fieldName is not None
|
||||
|
||||
key = keyFlattener.flatKey(fieldName, formatSpec, conversion)
|
||||
|
||||
if "log_flattened" not in event:
|
||||
flattenEvent(event)
|
||||
|
||||
return event["log_flattened"][key]
|
||||
|
||||
|
||||
def flatFormat(event: LogEvent) -> str:
|
||||
"""
|
||||
Format an event which has been flattened with L{flattenEvent}.
|
||||
|
||||
@param event: A logging event.
|
||||
|
||||
@return: A formatted string.
|
||||
"""
|
||||
fieldValues = event["log_flattened"]
|
||||
keyFlattener = KeyFlattener()
|
||||
s = []
|
||||
|
||||
for literalText, fieldName, formatSpec, conversion in aFormatter.parse(
|
||||
event["log_format"]
|
||||
):
|
||||
s.append(literalText)
|
||||
|
||||
if fieldName is not None:
|
||||
key = keyFlattener.flatKey(fieldName, formatSpec, conversion or "s")
|
||||
s.append(str(fieldValues[key]))
|
||||
|
||||
return "".join(s)
|
||||
421
backend/lib/python3.12/site-packages/twisted/logger/_format.py
Normal file
421
backend/lib/python3.12/site-packages/twisted/logger/_format.py
Normal file
@@ -0,0 +1,421 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_format -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tools for formatting logging events.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime as DateTime
|
||||
from typing import Any, Callable, Iterator, Mapping, Optional, Union, cast
|
||||
|
||||
from constantly import NamedConstant
|
||||
|
||||
from twisted.python._tzhelper import FixedOffsetTimeZone
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import safe_repr
|
||||
from ._flatten import aFormatter, flatFormat
|
||||
from ._interfaces import LogEvent
|
||||
|
||||
timeFormatRFC3339 = "%Y-%m-%dT%H:%M:%S%z"
|
||||
|
||||
|
||||
def formatEvent(event: LogEvent) -> str:
|
||||
"""
|
||||
Formats an event as text, using the format in C{event["log_format"]}.
|
||||
|
||||
This implementation should never raise an exception; if the formatting
|
||||
cannot be done, the returned string will describe the event generically so
|
||||
that a useful message is emitted regardless.
|
||||
|
||||
@param event: A logging event.
|
||||
|
||||
@return: A formatted string.
|
||||
"""
|
||||
return eventAsText(
|
||||
event,
|
||||
includeTraceback=False,
|
||||
includeTimestamp=False,
|
||||
includeSystem=False,
|
||||
)
|
||||
|
||||
|
||||
def formatUnformattableEvent(event: LogEvent, error: BaseException) -> str:
|
||||
"""
|
||||
Formats an event as text that describes the event generically and a
|
||||
formatting error.
|
||||
|
||||
@param event: A logging event.
|
||||
@param error: The formatting error.
|
||||
|
||||
@return: A formatted string.
|
||||
"""
|
||||
try:
|
||||
return "Unable to format event {event!r}: {error}".format(
|
||||
event=event, error=error
|
||||
)
|
||||
except BaseException:
|
||||
# Yikes, something really nasty happened.
|
||||
#
|
||||
# Try to recover as much formattable data as possible; hopefully at
|
||||
# least the namespace is sane, which will help you find the offending
|
||||
# logger.
|
||||
failure = Failure()
|
||||
|
||||
text = ", ".join(
|
||||
" = ".join((safe_repr(key), safe_repr(value)))
|
||||
for key, value in event.items()
|
||||
)
|
||||
|
||||
return (
|
||||
"MESSAGE LOST: unformattable object logged: {error}\n"
|
||||
"Recoverable data: {text}\n"
|
||||
"Exception during formatting:\n{failure}".format(
|
||||
error=safe_repr(error), failure=failure, text=text
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def formatTime(
|
||||
when: Optional[float],
|
||||
timeFormat: Optional[str] = timeFormatRFC3339,
|
||||
default: str = "-",
|
||||
) -> str:
|
||||
"""
|
||||
Format a timestamp as text.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from time import time
|
||||
>>> from twisted.logger import formatTime
|
||||
>>>
|
||||
>>> t = time()
|
||||
>>> formatTime(t)
|
||||
u'2013-10-22T14:19:11-0700'
|
||||
>>> formatTime(t, timeFormat="%Y/%W") # Year and week number
|
||||
u'2013/42'
|
||||
>>>
|
||||
|
||||
@param when: A timestamp.
|
||||
@param timeFormat: A time format.
|
||||
@param default: Text to return if C{when} or C{timeFormat} is L{None}.
|
||||
|
||||
@return: A formatted time.
|
||||
"""
|
||||
if timeFormat is None or when is None:
|
||||
return default
|
||||
else:
|
||||
tz = FixedOffsetTimeZone.fromLocalTimeStamp(when)
|
||||
datetime = DateTime.fromtimestamp(when, tz)
|
||||
return str(datetime.strftime(timeFormat))
|
||||
|
||||
|
||||
def formatEventAsClassicLogText(
|
||||
event: LogEvent, formatTime: Callable[[Optional[float]], str] = formatTime
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Format an event as a line of human-readable text for, e.g. traditional log
|
||||
file output.
|
||||
|
||||
The output format is C{"{timeStamp} [{system}] {event}\\n"}, where:
|
||||
|
||||
- C{timeStamp} is computed by calling the given C{formatTime} callable
|
||||
on the event's C{"log_time"} value
|
||||
|
||||
- C{system} is the event's C{"log_system"} value, if set, otherwise,
|
||||
the C{"log_namespace"} and C{"log_level"}, joined by a C{"#"}. Each
|
||||
defaults to C{"-"} is not set.
|
||||
|
||||
- C{event} is the event, as formatted by L{formatEvent}.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from time import time
|
||||
>>> from twisted.logger import formatEventAsClassicLogText
|
||||
>>> from twisted.logger import LogLevel
|
||||
>>>
|
||||
>>> formatEventAsClassicLogText(dict()) # No format, returns None
|
||||
>>> formatEventAsClassicLogText(dict(log_format="Hello!"))
|
||||
u'- [-#-] Hello!\\n'
|
||||
>>> formatEventAsClassicLogText(dict(
|
||||
... log_format="Hello!",
|
||||
... log_time=time(),
|
||||
... log_namespace="my_namespace",
|
||||
... log_level=LogLevel.info,
|
||||
... ))
|
||||
u'2013-10-22T17:30:02-0700 [my_namespace#info] Hello!\\n'
|
||||
>>> formatEventAsClassicLogText(dict(
|
||||
... log_format="Hello!",
|
||||
... log_time=time(),
|
||||
... log_system="my_system",
|
||||
... ))
|
||||
u'2013-11-11T17:22:06-0800 [my_system] Hello!\\n'
|
||||
>>>
|
||||
|
||||
@param event: an event.
|
||||
@param formatTime: A time formatter
|
||||
|
||||
@return: A formatted event, or L{None} if no output is appropriate.
|
||||
"""
|
||||
eventText = eventAsText(event, formatTime=formatTime)
|
||||
if not eventText:
|
||||
return None
|
||||
eventText = eventText.replace("\n", "\n\t")
|
||||
return eventText + "\n"
|
||||
|
||||
|
||||
def keycall(key: str, getter: Callable[[str], Any]) -> PotentialCallWrapper:
|
||||
"""
|
||||
Check to see if C{key} ends with parentheses ("C{()}"); if not, wrap up the
|
||||
result of C{get} in a L{PotentialCallWrapper}. Otherwise, call the result
|
||||
of C{get} first, before wrapping it up.
|
||||
|
||||
@param key: The last dotted segment of a formatting key, as parsed by
|
||||
L{Formatter.vformat}, which may end in C{()}.
|
||||
|
||||
@param getter: A function which takes a string and returns some other
|
||||
object, to be formatted and stringified for a log.
|
||||
|
||||
@return: A L{PotentialCallWrapper} that will wrap up the result to allow
|
||||
for subsequent usages of parens to defer execution to log-format time.
|
||||
"""
|
||||
callit = key.endswith("()")
|
||||
realKey = key[:-2] if callit else key
|
||||
value = getter(realKey)
|
||||
if callit:
|
||||
value = value()
|
||||
return PotentialCallWrapper(value)
|
||||
|
||||
|
||||
class PotentialCallWrapper(object):
|
||||
"""
|
||||
Object wrapper that wraps C{getattr()} so as to process call-parentheses
|
||||
C{"()"} after a dotted attribute access.
|
||||
"""
|
||||
|
||||
def __init__(self, wrapped: object) -> None:
|
||||
self._wrapped = wrapped
|
||||
|
||||
def __getattr__(self, name: str) -> object:
|
||||
return keycall(name, lambda name_: getattr(self._wrapped, name_))
|
||||
|
||||
def __getitem__(self, name: str) -> object:
|
||||
# The sub-object may not be indexable, but if it isn't, that's the
|
||||
# caller's problem.
|
||||
value = self._wrapped[name] # type:ignore[index]
|
||||
return PotentialCallWrapper(value)
|
||||
|
||||
def __format__(self, format_spec: str) -> str:
|
||||
return format(self._wrapped, format_spec)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._wrapped)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self._wrapped)
|
||||
|
||||
|
||||
class CallMapping(Mapping[str, Any]):
|
||||
"""
|
||||
Read-only mapping that turns a C{()}-suffix in key names into an invocation
|
||||
of the key rather than a lookup of the key.
|
||||
|
||||
Implementation support for L{formatWithCall}.
|
||||
"""
|
||||
|
||||
def __init__(self, submapping: Mapping[str, Any]) -> None:
|
||||
"""
|
||||
@param submapping: Another read-only mapping which will be used to look
|
||||
up items.
|
||||
"""
|
||||
self._submapping = submapping
|
||||
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
return iter(self._submapping)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._submapping)
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
"""
|
||||
Look up an item in the submapping for this L{CallMapping}, calling it
|
||||
if C{key} ends with C{"()"}.
|
||||
"""
|
||||
return keycall(key, self._submapping.__getitem__)
|
||||
|
||||
|
||||
def formatWithCall(formatString: str, mapping: Mapping[str, Any]) -> str:
|
||||
"""
|
||||
Format a string like L{str.format}, but:
|
||||
|
||||
- taking only a name mapping; no positional arguments
|
||||
|
||||
- with the additional syntax that an empty set of parentheses
|
||||
correspond to a formatting item that should be called, and its result
|
||||
C{str}'d, rather than calling C{str} on the element directly as
|
||||
normal.
|
||||
|
||||
For example::
|
||||
|
||||
>>> formatWithCall("{string}, {function()}.",
|
||||
... dict(string="just a string",
|
||||
... function=lambda: "a function"))
|
||||
'just a string, a function.'
|
||||
|
||||
@param formatString: A PEP-3101 format string.
|
||||
@param mapping: A L{dict}-like object to format.
|
||||
|
||||
@return: The string with formatted values interpolated.
|
||||
"""
|
||||
return str(aFormatter.vformat(formatString, (), CallMapping(mapping)))
|
||||
|
||||
|
||||
def _formatEvent(event: LogEvent) -> str:
|
||||
"""
|
||||
Formats an event as a string, using the format in C{event["log_format"]}.
|
||||
|
||||
This implementation should never raise an exception; if the formatting
|
||||
cannot be done, the returned string will describe the event generically so
|
||||
that a useful message is emitted regardless.
|
||||
|
||||
@param event: A logging event.
|
||||
|
||||
@return: A formatted string.
|
||||
"""
|
||||
try:
|
||||
if "log_flattened" in event:
|
||||
return flatFormat(event)
|
||||
|
||||
format = cast(Optional[Union[str, bytes]], event.get("log_format", None))
|
||||
if format is None:
|
||||
return ""
|
||||
|
||||
# Make sure format is text.
|
||||
if isinstance(format, str):
|
||||
pass
|
||||
elif isinstance(format, bytes):
|
||||
format = format.decode("utf-8")
|
||||
else:
|
||||
raise TypeError(f"Log format must be str, not {format!r}")
|
||||
|
||||
return formatWithCall(format, event)
|
||||
|
||||
except BaseException as e:
|
||||
return formatUnformattableEvent(event, e)
|
||||
|
||||
|
||||
def _formatTraceback(failure: Failure) -> str:
|
||||
"""
|
||||
Format a failure traceback, assuming UTF-8 and using a replacement
|
||||
strategy for errors. Every effort is made to provide a usable
|
||||
traceback, but should not that not be possible, a message and the
|
||||
captured exception are logged.
|
||||
|
||||
@param failure: The failure to retrieve a traceback from.
|
||||
|
||||
@return: The formatted traceback.
|
||||
"""
|
||||
try:
|
||||
traceback = failure.getTraceback()
|
||||
except BaseException as e:
|
||||
traceback = "(UNABLE TO OBTAIN TRACEBACK FROM EVENT):" + str(e)
|
||||
return traceback
|
||||
|
||||
|
||||
def _formatSystem(event: LogEvent) -> str:
|
||||
"""
|
||||
Format the system specified in the event in the "log_system" key if set,
|
||||
otherwise the C{"log_namespace"} and C{"log_level"}, joined by a C{"#"}.
|
||||
Each defaults to C{"-"} is not set. If formatting fails completely,
|
||||
"UNFORMATTABLE" is returned.
|
||||
|
||||
@param event: The event containing the system specification.
|
||||
|
||||
@return: A formatted string representing the "log_system" key.
|
||||
"""
|
||||
system = cast(Optional[str], event.get("log_system", None))
|
||||
if system is None:
|
||||
level = cast(Optional[NamedConstant], event.get("log_level", None))
|
||||
if level is None:
|
||||
levelName = "-"
|
||||
else:
|
||||
levelName = level.name
|
||||
|
||||
system = "{namespace}#{level}".format(
|
||||
namespace=cast(str, event.get("log_namespace", "-")),
|
||||
level=levelName,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
system = str(system)
|
||||
except Exception:
|
||||
system = "UNFORMATTABLE"
|
||||
return system
|
||||
|
||||
|
||||
def eventAsText(
|
||||
event: LogEvent,
|
||||
includeTraceback: bool = True,
|
||||
includeTimestamp: bool = True,
|
||||
includeSystem: bool = True,
|
||||
formatTime: Callable[[float], str] = formatTime,
|
||||
) -> str:
|
||||
r"""
|
||||
Format an event as text. Optionally, attach timestamp, traceback, and
|
||||
system information.
|
||||
|
||||
The full output format is:
|
||||
C{"{timeStamp} [{system}] {event}\n{traceback}\n"} where:
|
||||
|
||||
- C{timeStamp} is the event's C{"log_time"} value formatted with
|
||||
the provided C{formatTime} callable.
|
||||
|
||||
- C{system} is the event's C{"log_system"} value, if set, otherwise,
|
||||
the C{"log_namespace"} and C{"log_level"}, joined by a C{"#"}. Each
|
||||
defaults to C{"-"} is not set.
|
||||
|
||||
- C{event} is the event, as formatted by L{formatEvent}.
|
||||
|
||||
- C{traceback} is the traceback if the event contains a
|
||||
C{"log_failure"} key. In the event the original traceback cannot
|
||||
be formatted, a message indicating the failure will be substituted.
|
||||
|
||||
If the event cannot be formatted, and no traceback exists, an empty string
|
||||
is returned, even if includeSystem or includeTimestamp are true.
|
||||
|
||||
@param event: A logging event.
|
||||
@param includeTraceback: If true and a C{"log_failure"} key exists, append
|
||||
a traceback.
|
||||
@param includeTimestamp: If true include a formatted timestamp before the
|
||||
event.
|
||||
@param includeSystem: If true, include the event's C{"log_system"} value.
|
||||
@param formatTime: A time formatter
|
||||
|
||||
@return: A formatted string with specified options.
|
||||
|
||||
@since: Twisted 18.9.0
|
||||
"""
|
||||
eventText = _formatEvent(event)
|
||||
if includeTraceback and "log_failure" in event:
|
||||
f = event["log_failure"]
|
||||
traceback = _formatTraceback(f)
|
||||
eventText = "\n".join((eventText, traceback))
|
||||
|
||||
if not eventText:
|
||||
return eventText
|
||||
|
||||
timeStamp = ""
|
||||
if includeTimestamp:
|
||||
timeStamp = "".join([formatTime(cast(float, event.get("log_time", None))), " "])
|
||||
|
||||
system = ""
|
||||
if includeSystem:
|
||||
system = "".join(["[", _formatSystem(event), "]", " "])
|
||||
|
||||
return "{timeStamp}{system}{eventText}".format(
|
||||
timeStamp=timeStamp,
|
||||
system=system,
|
||||
eventText=eventText,
|
||||
)
|
||||
226
backend/lib/python3.12/site-packages/twisted/logger/_global.py
Normal file
226
backend/lib/python3.12/site-packages/twisted/logger/_global.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_global -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module includes process-global state associated with the logging system,
|
||||
and implementation of logic for managing that global state.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from typing import IO, Any, Iterable, Optional, Type
|
||||
|
||||
from twisted.python.compat import currentframe
|
||||
from twisted.python.reflect import qual
|
||||
from ._buffer import LimitedHistoryLogObserver
|
||||
from ._file import FileLogObserver
|
||||
from ._filter import FilteringLogObserver, LogLevelFilterPredicate
|
||||
from ._format import eventAsText
|
||||
from ._interfaces import ILogObserver
|
||||
from ._io import LoggingFile
|
||||
from ._levels import LogLevel
|
||||
from ._logger import Logger
|
||||
from ._observer import LogPublisher
|
||||
|
||||
MORE_THAN_ONCE_WARNING = (
|
||||
"Warning: primary log target selected twice at <{fileNow}:{lineNow}> - "
|
||||
"previously selected at <{fileThen}:{lineThen}>. Remove one of the calls "
|
||||
"to beginLoggingTo."
|
||||
)
|
||||
|
||||
|
||||
class LogBeginner:
|
||||
"""
|
||||
A L{LogBeginner} holds state related to logging before logging has begun,
|
||||
and begins logging when told to do so. Logging "begins" when someone has
|
||||
selected a set of observers, like, for example, a L{FileLogObserver} that
|
||||
writes to a file on disk, or to standard output.
|
||||
|
||||
Applications will not typically need to instantiate this class, except
|
||||
those which intend to initialize the global logging system themselves,
|
||||
which may wish to instantiate this for testing. The global instance for
|
||||
the current process is exposed as
|
||||
L{twisted.logger.globalLogBeginner}.
|
||||
|
||||
Before logging has begun, a L{LogBeginner} will:
|
||||
|
||||
1. Log any critical messages (e.g.: unhandled exceptions) to the given
|
||||
file-like object.
|
||||
|
||||
2. Save (a limited number of) log events in a
|
||||
L{LimitedHistoryLogObserver}.
|
||||
|
||||
@cvar _DEFAULT_BUFFER_SIZE: The default size for the initial log events
|
||||
buffer.
|
||||
|
||||
@ivar _initialBuffer: A buffer of messages logged before logging began.
|
||||
@ivar _publisher: The log publisher passed in to L{LogBeginner}'s
|
||||
constructor.
|
||||
@ivar _log: The logger used to log messages about the operation of the
|
||||
L{LogBeginner} itself.
|
||||
@ivar _stdio: An object with C{stderr} and C{stdout} attributes (like the
|
||||
L{sys} module) which will be replaced when redirecting standard I/O.
|
||||
@ivar _temporaryObserver: If not L{None}, an L{ILogObserver} that observes
|
||||
events on C{_publisher} for this L{LogBeginner}.
|
||||
"""
|
||||
|
||||
_DEFAULT_BUFFER_SIZE = 200
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
publisher: LogPublisher,
|
||||
errorStream: IO[Any],
|
||||
stdio: object,
|
||||
warningsModule: Any,
|
||||
initialBufferSize: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize this L{LogBeginner}.
|
||||
|
||||
@param initialBufferSize: The size of the event buffer into which
|
||||
events are collected until C{beginLoggingTo} is called. Or
|
||||
C{None} to use the default size.
|
||||
"""
|
||||
if initialBufferSize is None:
|
||||
initialBufferSize = self._DEFAULT_BUFFER_SIZE
|
||||
self._initialBuffer = LimitedHistoryLogObserver(size=initialBufferSize)
|
||||
self._publisher = publisher
|
||||
self._log = Logger(observer=publisher)
|
||||
self._stdio = stdio
|
||||
self._warningsModule = warningsModule
|
||||
self._temporaryObserver: Optional[ILogObserver] = LogPublisher(
|
||||
self._initialBuffer,
|
||||
FilteringLogObserver(
|
||||
FileLogObserver(
|
||||
errorStream,
|
||||
lambda event: eventAsText(
|
||||
event,
|
||||
includeTimestamp=False,
|
||||
includeSystem=False,
|
||||
)
|
||||
+ "\n",
|
||||
),
|
||||
[LogLevelFilterPredicate(defaultLogLevel=LogLevel.critical)],
|
||||
),
|
||||
)
|
||||
self._previousBegin = ("", 0)
|
||||
publisher.addObserver(self._temporaryObserver)
|
||||
self._oldshowwarning = warningsModule.showwarning
|
||||
|
||||
def beginLoggingTo(
|
||||
self,
|
||||
observers: Iterable[ILogObserver],
|
||||
discardBuffer: bool = False,
|
||||
redirectStandardIO: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Begin logging to the given set of observers. This will:
|
||||
|
||||
1. Add all the observers given in C{observers} to the
|
||||
L{LogPublisher} associated with this L{LogBeginner}.
|
||||
|
||||
2. Optionally re-direct standard output and standard error streams
|
||||
to the logging system.
|
||||
|
||||
3. Re-play any messages that were previously logged to that
|
||||
publisher to the new observers, if C{discardBuffer} is not set.
|
||||
|
||||
4. Stop logging critical errors from the L{LogPublisher} as strings
|
||||
to the C{errorStream} associated with this L{LogBeginner}, and
|
||||
allow them to be logged normally.
|
||||
|
||||
5. Re-direct warnings from the L{warnings} module associated with
|
||||
this L{LogBeginner} to log messages.
|
||||
|
||||
@note: Since a L{LogBeginner} is designed to encapsulate the transition
|
||||
between process-startup and log-system-configuration, this method
|
||||
is intended to be invoked I{once}.
|
||||
|
||||
@param observers: The observers to register.
|
||||
@param discardBuffer: Whether to discard the buffer and not re-play it
|
||||
to the added observers. (This argument is provided mainly for
|
||||
compatibility with legacy concerns.)
|
||||
@param redirectStandardIO: If true, redirect standard output and
|
||||
standard error to the observers.
|
||||
"""
|
||||
caller = currentframe(1)
|
||||
filename = caller.f_code.co_filename
|
||||
lineno = caller.f_lineno
|
||||
|
||||
for observer in observers:
|
||||
self._publisher.addObserver(observer)
|
||||
|
||||
if self._temporaryObserver is not None:
|
||||
self._publisher.removeObserver(self._temporaryObserver)
|
||||
if not discardBuffer:
|
||||
self._initialBuffer.replayTo(self._publisher)
|
||||
self._temporaryObserver = None
|
||||
self._warningsModule.showwarning = self.showwarning
|
||||
else:
|
||||
previousFile, previousLine = self._previousBegin
|
||||
self._log.warn(
|
||||
MORE_THAN_ONCE_WARNING,
|
||||
fileNow=filename,
|
||||
lineNow=lineno,
|
||||
fileThen=previousFile,
|
||||
lineThen=previousLine,
|
||||
)
|
||||
|
||||
self._previousBegin = (filename, lineno)
|
||||
if redirectStandardIO:
|
||||
streams = [("stdout", LogLevel.info), ("stderr", LogLevel.error)]
|
||||
else:
|
||||
streams = []
|
||||
|
||||
for stream, level in streams:
|
||||
oldStream = getattr(self._stdio, stream)
|
||||
loggingFile = LoggingFile(
|
||||
logger=Logger(namespace=stream, observer=self._publisher),
|
||||
level=level,
|
||||
encoding=getattr(oldStream, "encoding", None),
|
||||
)
|
||||
setattr(self._stdio, stream, loggingFile)
|
||||
|
||||
def showwarning(
|
||||
self,
|
||||
message: str,
|
||||
category: Type[Warning],
|
||||
filename: str,
|
||||
lineno: int,
|
||||
file: Optional[IO[Any]] = None,
|
||||
line: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Twisted-enabled wrapper around L{warnings.showwarning}.
|
||||
|
||||
If C{file} is L{None}, the default behaviour is to emit the warning to
|
||||
the log system, otherwise the original L{warnings.showwarning} Python
|
||||
function is called.
|
||||
|
||||
@param message: A warning message to emit.
|
||||
@param category: A warning category to associate with C{message}.
|
||||
@param filename: A file name for the source code file issuing the
|
||||
warning.
|
||||
@param lineno: A line number in the source file where the warning was
|
||||
issued.
|
||||
@param file: A file to write the warning message to. If L{None},
|
||||
write to L{sys.stderr}.
|
||||
@param line: A line of source code to include with the warning message.
|
||||
If L{None}, attempt to read the line from C{filename} and
|
||||
C{lineno}.
|
||||
"""
|
||||
if file is None:
|
||||
self._log.warn(
|
||||
"{filename}:{lineno}: {category}: {warning}",
|
||||
warning=message,
|
||||
category=qual(category),
|
||||
filename=filename,
|
||||
lineno=lineno,
|
||||
)
|
||||
else:
|
||||
self._oldshowwarning(message, category, filename, lineno, file, line)
|
||||
|
||||
|
||||
globalLogPublisher = LogPublisher()
|
||||
globalLogBeginner = LogBeginner(globalLogPublisher, sys.stderr, sys, warnings)
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Logger interfaces.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
|
||||
|
||||
from zope.interface import Interface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._logger import Logger
|
||||
|
||||
|
||||
LogEvent = Dict[str, Any]
|
||||
LogTrace = List[Tuple["Logger", "ILogObserver"]]
|
||||
|
||||
|
||||
class ILogObserver(Interface):
|
||||
"""
|
||||
An observer which can handle log events.
|
||||
|
||||
Unlike most interfaces within Twisted, an L{ILogObserver} I{must be
|
||||
thread-safe}. Log observers may be called indiscriminately from many
|
||||
different threads, as any thread may wish to log a message at any time.
|
||||
"""
|
||||
|
||||
def __call__(event: LogEvent) -> None:
|
||||
"""
|
||||
Log an event.
|
||||
|
||||
@param event: A dictionary with arbitrary keys as defined by the
|
||||
application emitting logging events, as well as keys added by the
|
||||
logging system. The logging system reserves the right to set any
|
||||
key beginning with the prefix C{"log_"}; applications should not
|
||||
use any key so named. Currently, the following keys are used by
|
||||
the logging system in some way, if they are present (they are all
|
||||
optional):
|
||||
|
||||
- C{"log_format"}: a PEP-3101-style format string which draws
|
||||
upon the keys in the event as its values, used to format the
|
||||
event for human consumption.
|
||||
|
||||
- C{"log_flattened"}: a dictionary mapping keys derived from
|
||||
the names and format values used in the C{"log_format"}
|
||||
string to their values. This is used to preserve some
|
||||
structured information for use with
|
||||
L{twisted.logger.extractField}.
|
||||
|
||||
- C{"log_trace"}: A L{list} designed to capture information
|
||||
about which L{LogPublisher}s have observed the event.
|
||||
|
||||
- C{"log_level"}: a L{log level
|
||||
<twisted.logger.LogLevel>} constant, indicating the
|
||||
importance of and audience for this event.
|
||||
|
||||
- C{"log_namespace"}: a namespace for the emitter of the event,
|
||||
given as a L{str}.
|
||||
|
||||
- C{"log_system"}: a string indicating the network event or
|
||||
method call which resulted in the message being logged.
|
||||
"""
|
||||
187
backend/lib/python3.12/site-packages/twisted/logger/_io.py
Normal file
187
backend/lib/python3.12/site-packages/twisted/logger/_io.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_io -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
File-like object that logs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import AnyStr, Iterable, Optional
|
||||
|
||||
from constantly import NamedConstant
|
||||
from incremental import Version
|
||||
|
||||
from twisted.python.deprecate import deprecatedProperty
|
||||
from ._levels import LogLevel
|
||||
from ._logger import Logger
|
||||
|
||||
|
||||
class LoggingFile:
|
||||
"""
|
||||
File-like object that turns C{write()} calls into logging events.
|
||||
|
||||
Note that because event formats are L{str}, C{bytes} received via C{write()}
|
||||
are converted to C{str}, which is the opposite of what C{file} does.
|
||||
|
||||
@ivar softspace: Attribute to make this class more file-like under Python 2;
|
||||
value is zero or one. Do not use.
|
||||
"""
|
||||
|
||||
_softspace = 0
|
||||
|
||||
@deprecatedProperty(Version("Twisted", 21, 2, 0))
|
||||
def softspace(self):
|
||||
return self._softspace
|
||||
|
||||
@softspace.setter # type: ignore[no-redef]
|
||||
def softspace(self, value):
|
||||
self._softspace = value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logger: Logger,
|
||||
level: NamedConstant = LogLevel.info,
|
||||
encoding: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
@param logger: the logger to log through.
|
||||
@param level: the log level to emit events with.
|
||||
@param encoding: The encoding to expect when receiving bytes via
|
||||
C{write()}. If L{None}, use C{sys.getdefaultencoding()}.
|
||||
"""
|
||||
self.level = level
|
||||
self.log = logger
|
||||
|
||||
if encoding is None:
|
||||
self._encoding = sys.getdefaultencoding()
|
||||
else:
|
||||
self._encoding = encoding
|
||||
|
||||
self._buffer = ""
|
||||
self._closed = False
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
"""
|
||||
Read-only property. Is the file closed?
|
||||
|
||||
@return: true if closed, otherwise false.
|
||||
"""
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
"""
|
||||
Read-only property. File encoding.
|
||||
|
||||
@return: an encoding.
|
||||
"""
|
||||
return self._encoding
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
"""
|
||||
Read-only property. File mode.
|
||||
|
||||
@return: "w"
|
||||
"""
|
||||
return "w"
|
||||
|
||||
@property
|
||||
def newlines(self) -> None:
|
||||
"""
|
||||
Read-only property. Types of newlines encountered.
|
||||
|
||||
@return: L{None}
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""
|
||||
The name of this file; a repr-style string giving information about its
|
||||
namespace.
|
||||
|
||||
@return: A file name.
|
||||
"""
|
||||
return "<{} {}#{}>".format(
|
||||
self.__class__.__name__,
|
||||
self.log.namespace,
|
||||
self.level.name,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close this file so it can no longer be written to.
|
||||
"""
|
||||
self._closed = True
|
||||
|
||||
def flush(self) -> None:
|
||||
"""
|
||||
No-op; this file does not buffer.
|
||||
"""
|
||||
pass
|
||||
|
||||
def fileno(self) -> int:
|
||||
"""
|
||||
Returns an invalid file descriptor, since this is not backed by an FD.
|
||||
|
||||
@return: C{-1}
|
||||
"""
|
||||
return -1
|
||||
|
||||
def isatty(self) -> bool:
|
||||
"""
|
||||
A L{LoggingFile} is not a TTY.
|
||||
|
||||
@return: C{False}
|
||||
"""
|
||||
return False
|
||||
|
||||
def write(self, message: AnyStr) -> None:
|
||||
"""
|
||||
Log the given message.
|
||||
|
||||
@param message: The message to write.
|
||||
"""
|
||||
if self._closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
|
||||
if isinstance(message, bytes):
|
||||
text = message.decode(self._encoding)
|
||||
else:
|
||||
text = message
|
||||
|
||||
lines = (self._buffer + text).split("\n")
|
||||
self._buffer = lines[-1]
|
||||
lines = lines[0:-1]
|
||||
|
||||
for line in lines:
|
||||
self.log.emit(self.level, format="{log_io}", log_io=line)
|
||||
|
||||
def writelines(self, lines: Iterable[AnyStr]) -> None:
|
||||
"""
|
||||
Log each of the given lines as a separate message.
|
||||
|
||||
@param lines: Data to write.
|
||||
"""
|
||||
for line in lines:
|
||||
self.write(line)
|
||||
|
||||
def _unsupported(self, *args: object) -> None:
|
||||
"""
|
||||
Template for unsupported operations.
|
||||
|
||||
@param args: Arguments.
|
||||
"""
|
||||
raise OSError("unsupported operation")
|
||||
|
||||
read = _unsupported
|
||||
next = _unsupported
|
||||
readline = _unsupported
|
||||
readlines = _unsupported
|
||||
xreadlines = _unsupported
|
||||
seek = _unsupported
|
||||
tell = _unsupported
|
||||
truncate = _unsupported
|
||||
285
backend/lib/python3.12/site-packages/twisted/logger/_json.py
Normal file
285
backend/lib/python3.12/site-packages/twisted/logger/_json.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_json -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tools for saving and loading log events in a structured format.
|
||||
"""
|
||||
|
||||
from json import dumps, loads
|
||||
from typing import IO, Any, AnyStr, Dict, Iterable, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from constantly import NamedConstant
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from ._file import FileLogObserver
|
||||
from ._flatten import flattenEvent
|
||||
from ._interfaces import LogEvent
|
||||
from ._levels import LogLevel
|
||||
from ._logger import Logger
|
||||
|
||||
log = Logger()
|
||||
|
||||
|
||||
JSONDict = Dict[str, Any]
|
||||
|
||||
|
||||
def failureAsJSON(failure: Failure) -> JSONDict:
|
||||
"""
|
||||
Convert a failure to a JSON-serializable data structure.
|
||||
|
||||
@param failure: A failure to serialize.
|
||||
|
||||
@return: a mapping of strings to ... stuff, mostly reminiscent of
|
||||
L{Failure.__getstate__}
|
||||
"""
|
||||
return dict(
|
||||
failure.__getstate__(),
|
||||
type=dict(
|
||||
__module__=failure.type.__module__,
|
||||
__name__=failure.type.__name__,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def failureFromJSON(failureDict: JSONDict) -> Failure:
|
||||
"""
|
||||
Load a L{Failure} from a dictionary deserialized from JSON.
|
||||
|
||||
@param failureDict: a JSON-deserialized object like one previously returned
|
||||
by L{failureAsJSON}.
|
||||
|
||||
@return: L{Failure}
|
||||
"""
|
||||
f = Failure.__new__(Failure)
|
||||
typeInfo = failureDict["type"]
|
||||
failureDict["type"] = type(typeInfo["__name__"], (), typeInfo)
|
||||
f.__setstate__(failureDict)
|
||||
return f
|
||||
|
||||
|
||||
classInfo = [
|
||||
(
|
||||
lambda level: (
|
||||
isinstance(level, NamedConstant)
|
||||
and getattr(LogLevel, level.name, None) is level
|
||||
),
|
||||
UUID("02E59486-F24D-46AD-8224-3ACDF2A5732A"),
|
||||
lambda level: dict(name=level.name),
|
||||
lambda level: getattr(LogLevel, level["name"], None),
|
||||
),
|
||||
(
|
||||
lambda o: isinstance(o, Failure),
|
||||
UUID("E76887E2-20ED-49BF-A8F8-BA25CC586F2D"),
|
||||
failureAsJSON,
|
||||
failureFromJSON,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
uuidToLoader = {uuid: loader for (predicate, uuid, saver, loader) in classInfo}
|
||||
|
||||
|
||||
def objectLoadHook(aDict: JSONDict) -> object:
|
||||
"""
|
||||
Dictionary-to-object-translation hook for certain value types used within
|
||||
the logging system.
|
||||
|
||||
@see: the C{object_hook} parameter to L{json.load}
|
||||
|
||||
@param aDict: A dictionary loaded from a JSON object.
|
||||
|
||||
@return: C{aDict} itself, or the object represented by C{aDict}
|
||||
"""
|
||||
if "__class_uuid__" in aDict:
|
||||
return uuidToLoader[UUID(aDict["__class_uuid__"])](aDict)
|
||||
return aDict
|
||||
|
||||
|
||||
def objectSaveHook(pythonObject: object) -> JSONDict:
|
||||
"""
|
||||
Object-to-serializable hook for certain value types used within the logging
|
||||
system.
|
||||
|
||||
@see: the C{default} parameter to L{json.dump}
|
||||
|
||||
@param pythonObject: Any object.
|
||||
|
||||
@return: If the object is one of the special types the logging system
|
||||
supports, a specially-formatted dictionary; otherwise, a marker
|
||||
dictionary indicating that it could not be serialized.
|
||||
"""
|
||||
for predicate, uuid, saver, loader in classInfo:
|
||||
if predicate(pythonObject):
|
||||
result = saver(pythonObject)
|
||||
result["__class_uuid__"] = str(uuid)
|
||||
return result
|
||||
return {"unpersistable": True}
|
||||
|
||||
|
||||
def eventAsJSON(event: LogEvent) -> str:
|
||||
"""
|
||||
Encode an event as JSON, flattening it if necessary to preserve as much
|
||||
structure as possible.
|
||||
|
||||
Not all structure from the log event will be preserved when it is
|
||||
serialized.
|
||||
|
||||
@param event: A log event dictionary.
|
||||
|
||||
@return: A string of the serialized JSON; note that this will contain no
|
||||
newline characters, and may thus safely be stored in a line-delimited
|
||||
file.
|
||||
"""
|
||||
|
||||
def default(unencodable: object) -> Union[JSONDict, str]:
|
||||
"""
|
||||
Serialize an object not otherwise serializable by L{dumps}.
|
||||
|
||||
@param unencodable: An unencodable object.
|
||||
|
||||
@return: C{unencodable}, serialized
|
||||
"""
|
||||
if isinstance(unencodable, bytes):
|
||||
return unencodable.decode("charmap")
|
||||
return objectSaveHook(unencodable)
|
||||
|
||||
flattenEvent(event)
|
||||
return dumps(event, default=default, skipkeys=True)
|
||||
|
||||
|
||||
def eventFromJSON(eventText: str) -> JSONDict:
|
||||
"""
|
||||
Decode a log event from JSON.
|
||||
|
||||
@param eventText: The output of a previous call to L{eventAsJSON}
|
||||
|
||||
@return: A reconstructed version of the log event.
|
||||
"""
|
||||
return cast(JSONDict, loads(eventText, object_hook=objectLoadHook))
|
||||
|
||||
|
||||
def jsonFileLogObserver(
|
||||
outFile: IO[Any], recordSeparator: str = "\x1e"
|
||||
) -> FileLogObserver:
|
||||
"""
|
||||
Create a L{FileLogObserver} that emits JSON-serialized events to a
|
||||
specified (writable) file-like object.
|
||||
|
||||
Events are written in the following form::
|
||||
|
||||
RS + JSON + NL
|
||||
|
||||
C{JSON} is the serialized event, which is JSON text. C{NL} is a newline
|
||||
(C{"\\n"}). C{RS} is a record separator. By default, this is a single
|
||||
RS character (C{"\\x1e"}), which makes the default output conform to the
|
||||
IETF draft document "draft-ietf-json-text-sequence-13".
|
||||
|
||||
@param outFile: A file-like object. Ideally one should be passed which
|
||||
accepts L{str} data. Otherwise, UTF-8 L{bytes} will be used.
|
||||
@param recordSeparator: The record separator to use.
|
||||
|
||||
@return: A file log observer.
|
||||
"""
|
||||
return FileLogObserver(
|
||||
outFile, lambda event: f"{recordSeparator}{eventAsJSON(event)}\n"
|
||||
)
|
||||
|
||||
|
||||
def eventsFromJSONLogFile(
|
||||
inFile: IO[Any],
|
||||
recordSeparator: Optional[str] = None,
|
||||
bufferSize: int = 4096,
|
||||
) -> Iterable[LogEvent]:
|
||||
"""
|
||||
Load events from a file previously saved with L{jsonFileLogObserver}.
|
||||
Event records that are truncated or otherwise unreadable are ignored.
|
||||
|
||||
@param inFile: A (readable) file-like object. Data read from C{inFile}
|
||||
should be L{str} or UTF-8 L{bytes}.
|
||||
@param recordSeparator: The expected record separator.
|
||||
If L{None}, attempt to automatically detect the record separator from
|
||||
one of C{"\\x1e"} or C{""}.
|
||||
@param bufferSize: The size of the read buffer used while reading from
|
||||
C{inFile}.
|
||||
|
||||
@return: Log events as read from C{inFile}.
|
||||
"""
|
||||
|
||||
def asBytes(s: AnyStr) -> bytes:
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
else:
|
||||
return s.encode("utf-8")
|
||||
|
||||
def eventFromBytearray(record: bytearray) -> Optional[LogEvent]:
|
||||
try:
|
||||
text = bytes(record).decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
log.error(
|
||||
"Unable to decode UTF-8 for JSON record: {record!r}",
|
||||
record=bytes(record),
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
return eventFromJSON(text)
|
||||
except ValueError:
|
||||
log.error("Unable to read JSON record: {record!r}", record=bytes(record))
|
||||
return None
|
||||
|
||||
if recordSeparator is None:
|
||||
first = asBytes(inFile.read(1))
|
||||
|
||||
if first == b"\x1e":
|
||||
# This looks json-text-sequence compliant.
|
||||
recordSeparatorBytes = first
|
||||
else:
|
||||
# Default to simpler newline-separated stream, which does not use
|
||||
# a record separator.
|
||||
recordSeparatorBytes = b""
|
||||
|
||||
else:
|
||||
recordSeparatorBytes = asBytes(recordSeparator)
|
||||
first = b""
|
||||
|
||||
if recordSeparatorBytes == b"":
|
||||
recordSeparatorBytes = b"\n" # Split on newlines below
|
||||
|
||||
eventFromRecord = eventFromBytearray
|
||||
|
||||
else:
|
||||
|
||||
def eventFromRecord(record: bytearray) -> Optional[LogEvent]:
|
||||
if record[-1] == ord("\n"):
|
||||
return eventFromBytearray(record)
|
||||
else:
|
||||
log.error(
|
||||
"Unable to read truncated JSON record: {record!r}",
|
||||
record=bytes(record),
|
||||
)
|
||||
return None
|
||||
|
||||
buffer = bytearray(first)
|
||||
|
||||
while True:
|
||||
newData = inFile.read(bufferSize)
|
||||
|
||||
if not newData:
|
||||
if len(buffer) > 0:
|
||||
event = eventFromRecord(buffer)
|
||||
if event is not None:
|
||||
yield event
|
||||
break
|
||||
|
||||
buffer += asBytes(newData)
|
||||
records = buffer.split(recordSeparatorBytes)
|
||||
|
||||
for record in records[:-1]:
|
||||
if len(record) > 0:
|
||||
event = eventFromRecord(record)
|
||||
if event is not None:
|
||||
yield event
|
||||
|
||||
buffer = records[-1]
|
||||
147
backend/lib/python3.12/site-packages/twisted/logger/_legacy.py
Normal file
147
backend/lib/python3.12/site-packages/twisted/logger/_legacy.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_legacy -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Integration with L{twisted.python.log}.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from ._format import formatEvent
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
from ._levels import LogLevel
|
||||
from ._stdlib import StringifiableFromEvent, fromStdlibLogLevelMapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from twisted.python.log import ILogObserver as ILegacyLogObserver
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class LegacyLogObserverWrapper:
|
||||
"""
|
||||
L{ILogObserver} that wraps a L{twisted.python.log.ILogObserver}.
|
||||
|
||||
Received (new-style) events are modified prior to forwarding to
|
||||
the legacy observer to ensure compatibility with observers that
|
||||
expect legacy events.
|
||||
"""
|
||||
|
||||
def __init__(self, legacyObserver: "ILegacyLogObserver") -> None:
|
||||
"""
|
||||
@param legacyObserver: a legacy observer to which this observer will
|
||||
forward events.
|
||||
"""
|
||||
self.legacyObserver = legacyObserver
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "{self.__class__.__name__}({self.legacyObserver})".format(self=self)
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
"""
|
||||
Forward events to the legacy observer after editing them to
|
||||
ensure compatibility.
|
||||
|
||||
@param event: an event
|
||||
"""
|
||||
|
||||
# The "message" key is required by textFromEventDict()
|
||||
if "message" not in event:
|
||||
event["message"] = ()
|
||||
|
||||
if "time" not in event:
|
||||
event["time"] = event["log_time"]
|
||||
|
||||
if "system" not in event:
|
||||
event["system"] = event.get("log_system", "-")
|
||||
|
||||
# Format new style -> old style
|
||||
if "format" not in event and event.get("log_format", None) is not None:
|
||||
# Create an object that implements __str__() in order to defer the
|
||||
# work of formatting until it's needed by a legacy log observer.
|
||||
event["format"] = "%(log_legacy)s"
|
||||
event["log_legacy"] = StringifiableFromEvent(event.copy())
|
||||
|
||||
# In the old-style system, the 'message' key always holds a tuple
|
||||
# of messages. If we find the 'message' key here to not be a
|
||||
# tuple, it has been passed as new-style parameter. We drop it
|
||||
# here because we render it using the old-style 'format' key,
|
||||
# which otherwise doesn't get precedence, and the original event
|
||||
# has been copied above.
|
||||
if not isinstance(event["message"], tuple):
|
||||
event["message"] = ()
|
||||
|
||||
# From log.failure() -> isError blah blah
|
||||
if "log_failure" in event:
|
||||
if "failure" not in event:
|
||||
event["failure"] = event["log_failure"]
|
||||
if "isError" not in event:
|
||||
event["isError"] = 1
|
||||
if "why" not in event:
|
||||
event["why"] = formatEvent(event)
|
||||
elif "isError" not in event:
|
||||
if event["log_level"] in (LogLevel.error, LogLevel.critical):
|
||||
event["isError"] = 1
|
||||
else:
|
||||
event["isError"] = 0
|
||||
|
||||
self.legacyObserver(event)
|
||||
|
||||
|
||||
def publishToNewObserver(
|
||||
observer: ILogObserver,
|
||||
eventDict: Dict[str, Any],
|
||||
textFromEventDict: Callable[[Dict[str, Any]], Optional[str]],
|
||||
) -> None:
|
||||
"""
|
||||
Publish an old-style (L{twisted.python.log}) event to a new-style
|
||||
(L{twisted.logger}) observer.
|
||||
|
||||
@note: It's possible that a new-style event was sent to a
|
||||
L{LegacyLogObserverWrapper}, and may now be getting sent back to a
|
||||
new-style observer. In this case, it's already a new-style event,
|
||||
adapted to also look like an old-style event, and we don't need to
|
||||
tweak it again to be a new-style event, hence this checks for
|
||||
already-defined new-style keys.
|
||||
|
||||
@param observer: A new-style observer to handle this event.
|
||||
@param eventDict: An L{old-style <twisted.python.log>}, log event.
|
||||
@param textFromEventDict: callable that can format an old-style event as a
|
||||
string. Passed here rather than imported to avoid circular dependency.
|
||||
"""
|
||||
|
||||
if "log_time" not in eventDict:
|
||||
eventDict["log_time"] = eventDict["time"]
|
||||
|
||||
if "log_format" not in eventDict:
|
||||
text = textFromEventDict(eventDict)
|
||||
if text is not None:
|
||||
eventDict["log_text"] = text
|
||||
eventDict["log_format"] = "{log_text}"
|
||||
|
||||
if "log_level" not in eventDict:
|
||||
if "logLevel" in eventDict:
|
||||
try:
|
||||
level = fromStdlibLogLevelMapping[eventDict["logLevel"]]
|
||||
except KeyError:
|
||||
level = None
|
||||
elif "isError" in eventDict:
|
||||
if eventDict["isError"]:
|
||||
level = LogLevel.critical
|
||||
else:
|
||||
level = LogLevel.info
|
||||
else:
|
||||
level = LogLevel.info
|
||||
|
||||
if level is not None:
|
||||
eventDict["log_level"] = level
|
||||
|
||||
if "log_namespace" not in eventDict:
|
||||
eventDict["log_namespace"] = "log_legacy"
|
||||
|
||||
if "log_system" not in eventDict and "system" in eventDict:
|
||||
eventDict["log_system"] = eventDict["system"]
|
||||
|
||||
observer(eventDict)
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_levels -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Log levels.
|
||||
"""
|
||||
|
||||
from constantly import NamedConstant, Names
|
||||
|
||||
|
||||
class InvalidLogLevelError(Exception):
|
||||
"""
|
||||
Someone tried to use a L{LogLevel} that is unknown to the logging system.
|
||||
"""
|
||||
|
||||
def __init__(self, level: NamedConstant) -> None:
|
||||
"""
|
||||
@param level: A log level from L{LogLevel}.
|
||||
"""
|
||||
super().__init__(str(level))
|
||||
self.level = level
|
||||
|
||||
|
||||
class LogLevel(Names):
|
||||
"""
|
||||
Constants describing log levels.
|
||||
|
||||
@cvar debug: Debugging events: Information of use to a developer of the
|
||||
software, not generally of interest to someone running the software
|
||||
unless they are attempting to diagnose a software issue.
|
||||
|
||||
@cvar info: Informational events: Routine information about the status of
|
||||
an application, such as incoming connections, startup of a subsystem,
|
||||
etc.
|
||||
|
||||
@cvar warn: Warning events: Events that may require greater attention than
|
||||
informational events but are not a systemic failure condition, such as
|
||||
authorization failures, bad data from a network client, etc. Such
|
||||
events are of potential interest to system administrators, and should
|
||||
ideally be phrased in such a way, or documented, so as to indicate an
|
||||
action that an administrator might take to mitigate the warning.
|
||||
|
||||
@cvar error: Error conditions: Events indicating a systemic failure, such
|
||||
as programming errors in the form of unhandled exceptions, loss of
|
||||
connectivity to an external system without which no useful work can
|
||||
proceed, such as a database or API endpoint, or resource exhaustion.
|
||||
Similarly to warnings, errors that are related to operational
|
||||
parameters may be actionable to system administrators and should
|
||||
provide references to resources which an administrator might use to
|
||||
resolve them.
|
||||
|
||||
@cvar critical: Critical failures: Errors indicating systemic failure (ie.
|
||||
service outage), data corruption, imminent data loss, etc. which must
|
||||
be handled immediately. This includes errors unanticipated by the
|
||||
software, such as unhandled exceptions, wherein the cause and
|
||||
consequences are unknown.
|
||||
"""
|
||||
|
||||
debug = NamedConstant()
|
||||
info = NamedConstant()
|
||||
warn = NamedConstant()
|
||||
error = NamedConstant()
|
||||
critical = NamedConstant()
|
||||
|
||||
@classmethod
|
||||
def levelWithName(cls, name: str) -> NamedConstant:
|
||||
"""
|
||||
Get the log level with the given name.
|
||||
|
||||
@param name: The name of a log level.
|
||||
|
||||
@return: The L{LogLevel} with the specified C{name}.
|
||||
|
||||
@raise InvalidLogLevelError: if the C{name} does not name a valid log
|
||||
level.
|
||||
"""
|
||||
try:
|
||||
return cls.lookupByName(name)
|
||||
except ValueError:
|
||||
raise InvalidLogLevelError(name)
|
||||
455
backend/lib/python3.12/site-packages/twisted/logger/_logger.py
Normal file
455
backend/lib/python3.12/site-packages/twisted/logger/_logger.py
Normal file
@@ -0,0 +1,455 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_logger -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Logger class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from time import time
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, ContextManager, Optional, Protocol, cast
|
||||
|
||||
from twisted.python.compat import currentframe
|
||||
from twisted.python.failure import Failure
|
||||
from ._interfaces import ILogObserver, LogTrace
|
||||
from ._levels import InvalidLogLevelError, LogLevel
|
||||
|
||||
|
||||
class Operation(Protocol):
|
||||
"""
|
||||
An L{Operation} represents the success (or lack thereof) of code performed
|
||||
within the body of the L{Logger.failureHandler} context manager.
|
||||
"""
|
||||
|
||||
@property
|
||||
def succeeded(self) -> bool:
|
||||
"""
|
||||
Did the operation succeed? C{True} iff the code completed without
|
||||
raising an exception; C{False} while the code is running and C{False}
|
||||
if it raises an exception.
|
||||
"""
|
||||
|
||||
@property
|
||||
def failure(self) -> Failure | None:
|
||||
"""
|
||||
Did the operation raise an exception? If so, this L{Failure} is that
|
||||
exception.
|
||||
"""
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
"""
|
||||
Did the operation fail? C{True} iff the code raised an exception;
|
||||
C{False} while the code is running and C{False} if it completed without
|
||||
error.
|
||||
"""
|
||||
|
||||
|
||||
class _FailCtxMgr:
|
||||
succeeded: bool = False
|
||||
failure: Failure | None = None
|
||||
|
||||
def __init__(self, fail: Callable[[Failure], None]) -> None:
|
||||
self._fail = fail
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
return self.failure is not None
|
||||
|
||||
def __enter__(self) -> Operation:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
/,
|
||||
) -> bool:
|
||||
if exc_type is not None:
|
||||
failure = Failure()
|
||||
self.failure = failure
|
||||
self._fail(failure)
|
||||
else:
|
||||
self.succeeded = True
|
||||
return True
|
||||
|
||||
|
||||
class _FastFailCtxMgr:
|
||||
def __init__(self, fail: Callable[[Failure], None]) -> None:
|
||||
self._fail = fail
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
/,
|
||||
) -> bool:
|
||||
if exc_type is not None:
|
||||
failure = Failure()
|
||||
self.failure = failure
|
||||
self._fail(failure)
|
||||
return True
|
||||
|
||||
|
||||
class Logger:
|
||||
"""
|
||||
A L{Logger} emits log messages to an observer. You should instantiate it
|
||||
as a class or module attribute, as documented in L{this module's
|
||||
documentation <twisted.logger>}.
|
||||
|
||||
@ivar namespace: the namespace for this logger
|
||||
@ivar source: The object which is emitting events via this logger
|
||||
@ivar observer: The observer that this logger will send events to.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _namespaceFromCallingContext() -> str:
|
||||
"""
|
||||
Derive a namespace from the module containing the caller's caller.
|
||||
|
||||
@return: the fully qualified python name of a module.
|
||||
"""
|
||||
try:
|
||||
return cast(str, currentframe(2).f_globals["__name__"])
|
||||
except KeyError:
|
||||
return "<unknown>"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: Optional[str] = None,
|
||||
source: Optional[object] = None,
|
||||
observer: Optional["ILogObserver"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
@param namespace: The namespace for this logger. Uses a dotted
|
||||
notation, as used by python modules. If not L{None}, then the name
|
||||
of the module of the caller is used.
|
||||
@param source: The object which is emitting events via this
|
||||
logger; this is automatically set on instances of a class
|
||||
if this L{Logger} is an attribute of that class.
|
||||
@param observer: The observer that this logger will send events to.
|
||||
If L{None}, use the L{global log publisher <globalLogPublisher>}.
|
||||
"""
|
||||
if namespace is None:
|
||||
namespace = self._namespaceFromCallingContext()
|
||||
|
||||
self.namespace = namespace
|
||||
self.source = source
|
||||
|
||||
if observer is None:
|
||||
from ._global import globalLogPublisher
|
||||
|
||||
self.observer: ILogObserver = globalLogPublisher
|
||||
else:
|
||||
self.observer = observer
|
||||
|
||||
def __get__(self, instance: object, owner: Optional[type] = None) -> "Logger":
|
||||
"""
|
||||
When used as a descriptor, i.e.::
|
||||
|
||||
# File: athing.py
|
||||
class Something:
|
||||
log = Logger()
|
||||
def hello(self):
|
||||
self.log.info("Hello")
|
||||
|
||||
a L{Logger}'s namespace will be set to the name of the class it is
|
||||
declared on. In the above example, the namespace would be
|
||||
C{athing.Something}.
|
||||
|
||||
Additionally, its source will be set to the actual object referring to
|
||||
the L{Logger}. In the above example, C{Something.log.source} would be
|
||||
C{Something}, and C{Something().log.source} would be an instance of
|
||||
C{Something}.
|
||||
"""
|
||||
assert owner is not None
|
||||
|
||||
if instance is None:
|
||||
source: Any = owner
|
||||
else:
|
||||
source = instance
|
||||
|
||||
return self.__class__(
|
||||
".".join([owner.__module__, owner.__name__]),
|
||||
source,
|
||||
observer=self.observer,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.namespace!r}>"
|
||||
|
||||
def emit(
|
||||
self, level: LogLevel, format: Optional[str] = None, **kwargs: object
|
||||
) -> None:
|
||||
"""
|
||||
Emit a log event to all log observers at the given level.
|
||||
|
||||
@param level: a L{LogLevel}
|
||||
@param format: a message format using new-style (PEP 3101)
|
||||
formatting. The logging event (which is a L{dict}) is
|
||||
used to render this format string.
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
"""
|
||||
if level not in LogLevel.iterconstants():
|
||||
self.failure(
|
||||
"Got invalid log level {invalidLevel!r} in {logger}.emit().",
|
||||
Failure(InvalidLogLevelError(level)),
|
||||
invalidLevel=level,
|
||||
logger=self,
|
||||
)
|
||||
return
|
||||
|
||||
event = kwargs
|
||||
event.update(
|
||||
log_logger=self,
|
||||
log_level=level,
|
||||
log_namespace=self.namespace,
|
||||
log_source=self.source,
|
||||
log_format=format,
|
||||
log_time=time(),
|
||||
)
|
||||
|
||||
if "log_trace" in event:
|
||||
cast(LogTrace, event["log_trace"]).append((self, self.observer))
|
||||
|
||||
self.observer(event)
|
||||
|
||||
def failure(
|
||||
self,
|
||||
format: str,
|
||||
failure: Optional[Failure] = None,
|
||||
level: LogLevel = LogLevel.critical,
|
||||
**kwargs: object,
|
||||
) -> None:
|
||||
"""
|
||||
Log a failure and emit a traceback.
|
||||
|
||||
For example::
|
||||
|
||||
try:
|
||||
frob(knob)
|
||||
except Exception:
|
||||
log.failure("While frobbing {knob}", knob=knob)
|
||||
|
||||
or::
|
||||
|
||||
d = deferredFrob(knob)
|
||||
d.addErrback(lambda f: log.failure("While frobbing {knob}",
|
||||
f, knob=knob))
|
||||
|
||||
This method is meant to capture unexpected exceptions in code; an
|
||||
exception that is caught and handled somehow should be logged, if
|
||||
appropriate, via L{Logger.error} instead. If some unknown exception
|
||||
occurs and your code doesn't know how to handle it, as in the above
|
||||
example, then this method provides a means to describe the failure.
|
||||
This is done at L{LogLevel.critical} by default, since no corrective
|
||||
guidance can be offered to an user/administrator, and the impact of the
|
||||
condition is unknown.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param failure: a L{Failure} to log. If L{None}, a L{Failure} is
|
||||
created from the exception in flight.
|
||||
|
||||
@param level: a L{LogLevel} to use.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
|
||||
@see: L{Logger.failureHandler}
|
||||
|
||||
@see: L{Logger.failuresHandled}
|
||||
"""
|
||||
if failure is None:
|
||||
failure = Failure()
|
||||
|
||||
self.emit(level, format, log_failure=failure, **kwargs)
|
||||
|
||||
def debug(self, format: Optional[str] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
Emit a log event at log level L{LogLevel.debug}.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
"""
|
||||
self.emit(LogLevel.debug, format, **kwargs)
|
||||
|
||||
def info(self, format: Optional[str] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
Emit a log event at log level L{LogLevel.info}.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
"""
|
||||
self.emit(LogLevel.info, format, **kwargs)
|
||||
|
||||
def warn(self, format: Optional[str] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
Emit a log event at log level L{LogLevel.warn}.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
"""
|
||||
self.emit(LogLevel.warn, format, **kwargs)
|
||||
|
||||
def error(self, format: Optional[str] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
Emit a log event at log level L{LogLevel.error}.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
"""
|
||||
self.emit(LogLevel.error, format, **kwargs)
|
||||
|
||||
def critical(self, format: Optional[str] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
Emit a log event at log level L{LogLevel.critical}.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event.
|
||||
Note that values which are later mutated may result in
|
||||
non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
"""
|
||||
self.emit(LogLevel.critical, format, **kwargs)
|
||||
|
||||
def failuresHandled(
|
||||
self, format: str, level: LogLevel = LogLevel.critical, **kwargs: object
|
||||
) -> ContextManager[Operation]:
|
||||
"""
|
||||
Run some application code, logging a failure and emitting a traceback
|
||||
in the event that any of it fails, but continuing on. For example::
|
||||
|
||||
log = Logger(...)
|
||||
|
||||
def frameworkCode() -> None:
|
||||
with log.failuresHandled("While frobbing {knob}:", knob=knob) as op:
|
||||
frob(knob)
|
||||
if op.succeeded:
|
||||
log.info("frobbed {knob} successfully", knob=knob)
|
||||
|
||||
This method is meant to capture unexpected exceptions from application
|
||||
code; an exception that is caught and handled somehow should be logged,
|
||||
if appropriate, via L{Logger.error} instead. If some unknown exception
|
||||
occurs and your code doesn't know how to handle it, as in the above
|
||||
example, then this method provides a means to describe the failure.
|
||||
This is done at L{LogLevel.critical} by default, since no corrective
|
||||
guidance can be offered to an user/administrator, and the impact of the
|
||||
condition is unknown.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param level: a L{LogLevel} to use.
|
||||
|
||||
@param kwargs: additional key/value pairs to include in the event, if
|
||||
it is emitted. Note that values which are later mutated may result
|
||||
in non-deterministic behavior from observers that schedule work for
|
||||
later execution.
|
||||
|
||||
@see: L{Logger.failure}
|
||||
@see: L{Logger.failureHandler}
|
||||
|
||||
@return: A context manager which yields an L{Operation} which will have
|
||||
either its C{succeeded} or C{failed} attribute set to C{True} upon
|
||||
completion of the code within the code within the C{with} block.
|
||||
"""
|
||||
return _FailCtxMgr(lambda f: self.failure(format, f, level, **kwargs))
|
||||
|
||||
def failureHandler(
|
||||
self,
|
||||
staticMessage: str,
|
||||
level: LogLevel = LogLevel.critical,
|
||||
) -> ContextManager[None]:
|
||||
"""
|
||||
For performance-sensitive frameworks that needs to handle potential
|
||||
failures from frequently-called application code, and do not need to
|
||||
include detailed structured information about the failure nor inspect
|
||||
the result of the operation, this method returns a context manager that
|
||||
will log exceptions and continue, that can be shared across multiple
|
||||
invocations. It should be instantiated at module scope to avoid
|
||||
additional object creations.
|
||||
|
||||
For example::
|
||||
|
||||
log = Logger(...)
|
||||
ignoringFrobErrors = log.failureHandler("while frobbing:")
|
||||
|
||||
def hotLoop() -> None:
|
||||
with ignoringFrobErrors:
|
||||
frob()
|
||||
|
||||
This method is meant to capture unexpected exceptions from application
|
||||
code; an exception that is caught and handled somehow should be logged,
|
||||
if appropriate, via L{Logger.error} instead. If some unknown exception
|
||||
occurs and your code doesn't know how to handle it, as in the above
|
||||
example, then this method provides a means to describe the failure in
|
||||
nerd-speak. This is done at L{LogLevel.critical} by default, since no
|
||||
corrective guidance can be offered to an user/administrator, and the
|
||||
impact of the condition is unknown.
|
||||
|
||||
@param format: a message format using new-style (PEP 3101) formatting.
|
||||
The logging event (which is a L{dict}) is used to render this
|
||||
format string.
|
||||
|
||||
@param level: a L{LogLevel} to use.
|
||||
|
||||
@see: L{Logger.failure}
|
||||
|
||||
@return: A context manager which does not return a value, but will
|
||||
always exit from exceptions.
|
||||
"""
|
||||
return _FastFailCtxMgr(lambda f: self.failure(staticMessage, f, level))
|
||||
|
||||
|
||||
_log = Logger()
|
||||
|
||||
|
||||
def _loggerFor(obj: object) -> Logger:
|
||||
"""
|
||||
Get a L{Logger} instance attached to the given class.
|
||||
"""
|
||||
return _log.__get__(obj, obj.__class__)
|
||||
112
backend/lib/python3.12/site-packages/twisted/logger/_observer.py
Normal file
112
backend/lib/python3.12/site-packages/twisted/logger/_observer.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_observer -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Basic log observers.
|
||||
"""
|
||||
|
||||
from typing import Callable, Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
from ._logger import Logger
|
||||
|
||||
OBSERVER_DISABLED = (
|
||||
"Temporarily disabling observer {observer} due to exception: {log_failure}"
|
||||
)
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class LogPublisher:
|
||||
"""
|
||||
I{ILogObserver} that fans out events to other observers.
|
||||
|
||||
Keeps track of a set of L{ILogObserver} objects and forwards
|
||||
events to each.
|
||||
"""
|
||||
|
||||
def __init__(self, *observers: ILogObserver) -> None:
|
||||
self._observers = list(observers)
|
||||
self.log = Logger(observer=self)
|
||||
|
||||
def addObserver(self, observer: ILogObserver) -> None:
|
||||
"""
|
||||
Registers an observer with this publisher.
|
||||
|
||||
@param observer: An L{ILogObserver} to add.
|
||||
"""
|
||||
if not callable(observer):
|
||||
raise TypeError(f"Observer is not callable: {observer!r}")
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
|
||||
def removeObserver(self, observer: ILogObserver) -> None:
|
||||
"""
|
||||
Unregisters an observer with this publisher.
|
||||
|
||||
@param observer: An L{ILogObserver} to remove.
|
||||
"""
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
"""
|
||||
Forward events to contained observers.
|
||||
"""
|
||||
if "log_trace" not in event:
|
||||
trace: Optional[Callable[[ILogObserver], None]] = None
|
||||
|
||||
else:
|
||||
|
||||
def trace(observer: ILogObserver) -> None:
|
||||
"""
|
||||
Add tracing information for an observer.
|
||||
|
||||
@param observer: an observer being forwarded to
|
||||
"""
|
||||
event["log_trace"].append((self, observer))
|
||||
|
||||
brokenObservers = []
|
||||
|
||||
for observer in self._observers:
|
||||
if trace is not None:
|
||||
trace(observer)
|
||||
|
||||
try:
|
||||
observer(event)
|
||||
except Exception:
|
||||
brokenObservers.append((observer, Failure()))
|
||||
|
||||
for brokenObserver, failure in brokenObservers:
|
||||
errorLogger = self._errorLoggerForObserver(brokenObserver)
|
||||
errorLogger.failure(
|
||||
OBSERVER_DISABLED,
|
||||
failure=failure,
|
||||
observer=brokenObserver,
|
||||
)
|
||||
|
||||
def _errorLoggerForObserver(self, observer: ILogObserver) -> Logger:
|
||||
"""
|
||||
Create an error-logger based on this logger, which does not contain the
|
||||
given bad observer.
|
||||
|
||||
@param observer: The observer which previously had an error.
|
||||
|
||||
@return: A L{Logger} without the given observer.
|
||||
"""
|
||||
errorPublisher = LogPublisher(
|
||||
*(obs for obs in self._observers if obs is not observer)
|
||||
)
|
||||
return Logger(observer=errorPublisher)
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def bitbucketLogObserver(event: LogEvent) -> None:
|
||||
"""
|
||||
I{ILogObserver} that does nothing with the events it sees.
|
||||
"""
|
||||
131
backend/lib/python3.12/site-packages/twisted/logger/_stdlib.py
Normal file
131
backend/lib/python3.12/site-packages/twisted/logger/_stdlib.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_stdlib -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Integration with Python standard library logging.
|
||||
"""
|
||||
|
||||
import logging as stdlibLogging
|
||||
from typing import Mapping, Tuple
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from constantly import NamedConstant
|
||||
|
||||
from twisted.python.compat import currentframe
|
||||
from ._format import formatEvent
|
||||
from ._interfaces import ILogObserver, LogEvent
|
||||
from ._levels import LogLevel
|
||||
|
||||
# Mappings to Python's logging module
|
||||
toStdlibLogLevelMapping: Mapping[NamedConstant, int] = {
|
||||
LogLevel.debug: stdlibLogging.DEBUG,
|
||||
LogLevel.info: stdlibLogging.INFO,
|
||||
LogLevel.warn: stdlibLogging.WARNING,
|
||||
LogLevel.error: stdlibLogging.ERROR,
|
||||
LogLevel.critical: stdlibLogging.CRITICAL,
|
||||
}
|
||||
|
||||
|
||||
def _reverseLogLevelMapping() -> Mapping[int, NamedConstant]:
|
||||
"""
|
||||
Reverse the above mapping, adding both the numerical keys used above and
|
||||
the corresponding string keys also used by python logging.
|
||||
@return: the reversed mapping
|
||||
"""
|
||||
mapping = {}
|
||||
for logLevel, pyLogLevel in toStdlibLogLevelMapping.items():
|
||||
mapping[pyLogLevel] = logLevel
|
||||
mapping[stdlibLogging.getLevelName(pyLogLevel)] = logLevel
|
||||
return mapping
|
||||
|
||||
|
||||
fromStdlibLogLevelMapping = _reverseLogLevelMapping()
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class STDLibLogObserver:
|
||||
"""
|
||||
Log observer that writes to the python standard library's C{logging}
|
||||
module.
|
||||
|
||||
@note: Warning: specific logging configurations (example: network) can lead
|
||||
to this observer blocking. Nothing is done here to prevent that, so be
|
||||
sure to not to configure the standard library logging module to block
|
||||
when used in conjunction with this module: code within Twisted, such as
|
||||
twisted.web, assumes that logging does not block.
|
||||
|
||||
@cvar defaultStackDepth: This is the default number of frames that it takes
|
||||
to get from L{STDLibLogObserver} through the logging module, plus one;
|
||||
in other words, the number of frames if you were to call a
|
||||
L{STDLibLogObserver} directly. This is useful to use as an offset for
|
||||
the C{stackDepth} parameter to C{__init__}, to add frames for other
|
||||
publishers.
|
||||
"""
|
||||
|
||||
defaultStackDepth = 4
|
||||
|
||||
def __init__(
|
||||
self, name: str = "twisted", stackDepth: int = defaultStackDepth
|
||||
) -> None:
|
||||
"""
|
||||
@param name: logger identifier.
|
||||
@param stackDepth: The depth of the stack to investigate for caller
|
||||
metadata.
|
||||
"""
|
||||
self.logger = stdlibLogging.getLogger(name)
|
||||
self.logger.findCaller = self._findCaller # type: ignore[assignment]
|
||||
self.stackDepth = stackDepth
|
||||
|
||||
def _findCaller(
|
||||
self, stackInfo: bool = False, stackLevel: int = 1
|
||||
) -> Tuple[str, int, str, None]:
|
||||
"""
|
||||
Based on the stack depth passed to this L{STDLibLogObserver}, identify
|
||||
the calling function.
|
||||
|
||||
@param stackInfo: Whether or not to construct stack information.
|
||||
(Currently ignored.)
|
||||
@param stackLevel: The number of stack frames to skip when determining
|
||||
the caller (currently ignored; use stackDepth on the instance).
|
||||
|
||||
@return: Depending on Python version, either a 3-tuple of (filename,
|
||||
lineno, name) or a 4-tuple of that plus stack information.
|
||||
"""
|
||||
f = currentframe(self.stackDepth)
|
||||
co = f.f_code
|
||||
extra = (None,)
|
||||
return (co.co_filename, f.f_lineno, co.co_name) + extra
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
"""
|
||||
Format an event and bridge it to Python logging.
|
||||
"""
|
||||
level = event.get("log_level", LogLevel.info)
|
||||
failure = event.get("log_failure")
|
||||
if failure is None:
|
||||
excInfo = None
|
||||
else:
|
||||
excInfo = (failure.type, failure.value, failure.getTracebackObject())
|
||||
stdlibLevel = toStdlibLogLevelMapping.get(level, stdlibLogging.INFO)
|
||||
self.logger.log(stdlibLevel, StringifiableFromEvent(event), exc_info=excInfo)
|
||||
|
||||
|
||||
class StringifiableFromEvent:
|
||||
"""
|
||||
An object that implements C{__str__()} in order to defer the work of
|
||||
formatting until it's converted into a C{str}.
|
||||
"""
|
||||
|
||||
def __init__(self, event: LogEvent) -> None:
|
||||
"""
|
||||
@param event: An event.
|
||||
"""
|
||||
self.event = event
|
||||
|
||||
def __str__(self) -> str:
|
||||
return formatEvent(self.event)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return str(self).encode("utf-8")
|
||||
51
backend/lib/python3.12/site-packages/twisted/logger/_util.py
Normal file
51
backend/lib/python3.12/site-packages/twisted/logger/_util.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- test-case-name: twisted.logger.test.test_util -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Logging utilities.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from ._interfaces import LogTrace
|
||||
from ._logger import Logger
|
||||
|
||||
|
||||
def formatTrace(trace: LogTrace) -> str:
|
||||
"""
|
||||
Format a trace (that is, the contents of the C{log_trace} key of a log
|
||||
event) as a visual indication of the message's propagation through various
|
||||
observers.
|
||||
|
||||
@param trace: the contents of the C{log_trace} key from an event.
|
||||
|
||||
@return: A multi-line string with indentation and arrows indicating the
|
||||
flow of the message through various observers.
|
||||
"""
|
||||
|
||||
def formatWithName(obj: object) -> str:
|
||||
if hasattr(obj, "name"):
|
||||
return f"{obj} ({obj.name})"
|
||||
else:
|
||||
return f"{obj}"
|
||||
|
||||
result = []
|
||||
lineage: List[Logger] = []
|
||||
|
||||
for parent, child in trace:
|
||||
if not lineage or lineage[-1] is not parent:
|
||||
if parent in lineage:
|
||||
while lineage[-1] is not parent:
|
||||
lineage.pop()
|
||||
|
||||
else:
|
||||
if not lineage:
|
||||
result.append(f"{formatWithName(parent)}\n")
|
||||
|
||||
lineage.append(parent)
|
||||
|
||||
result.append(" " * len(lineage))
|
||||
result.append(f"-> {formatWithName(child)}\n")
|
||||
|
||||
return "".join(result)
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.logger.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Unit tests for L{twisted.logger}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._buffer}.
|
||||
"""
|
||||
|
||||
from typing import List, cast
|
||||
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._buffer import LimitedHistoryLogObserver
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
|
||||
|
||||
class LimitedHistoryLogObserverTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LimitedHistoryLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{LimitedHistoryLogObserver} provides L{ILogObserver}.
|
||||
"""
|
||||
observer = LimitedHistoryLogObserver(0)
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def test_order(self) -> None:
|
||||
"""
|
||||
L{LimitedHistoryLogObserver} saves history in the order it is received.
|
||||
"""
|
||||
size = 4
|
||||
events = [dict(n=n) for n in range(size // 2)]
|
||||
observer = LimitedHistoryLogObserver(size)
|
||||
|
||||
for event in events:
|
||||
observer(event)
|
||||
|
||||
outEvents: List[LogEvent] = []
|
||||
observer.replayTo(cast(ILogObserver, outEvents.append))
|
||||
self.assertEqual(events, outEvents)
|
||||
|
||||
def test_limit(self) -> None:
|
||||
"""
|
||||
When more events than a L{LimitedHistoryLogObserver}'s maximum size are
|
||||
buffered, older events will be dropped.
|
||||
"""
|
||||
size = 4
|
||||
events = [dict(n=n) for n in range(size * 2)]
|
||||
observer = LimitedHistoryLogObserver(size)
|
||||
|
||||
for event in events:
|
||||
observer(event)
|
||||
|
||||
outEvents: List[LogEvent] = []
|
||||
observer.replayTo(cast(ILogObserver, outEvents.append))
|
||||
self.assertEqual(events[-size:], outEvents)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._capture}.
|
||||
"""
|
||||
|
||||
from twisted.logger import Logger, LogLevel
|
||||
from twisted.trial.unittest import TestCase
|
||||
from .._capture import capturedLogs
|
||||
|
||||
|
||||
class LogCaptureTests(TestCase):
|
||||
"""
|
||||
Tests for L{LogCaptureTests}.
|
||||
"""
|
||||
|
||||
log = Logger()
|
||||
|
||||
def test_capture(self) -> None:
|
||||
"""
|
||||
Events logged within context are captured.
|
||||
"""
|
||||
foo = object()
|
||||
|
||||
with capturedLogs() as captured:
|
||||
self.log.debug("Capture this, please", foo=foo)
|
||||
self.log.info("Capture this too, please", foo=foo)
|
||||
|
||||
self.assertTrue(len(captured) == 2)
|
||||
self.assertEqual(captured[0]["log_format"], "Capture this, please")
|
||||
self.assertEqual(captured[0]["log_level"], LogLevel.debug)
|
||||
self.assertEqual(captured[0]["foo"], foo)
|
||||
self.assertEqual(captured[1]["log_format"], "Capture this too, please")
|
||||
self.assertEqual(captured[1]["log_level"], LogLevel.info)
|
||||
self.assertEqual(captured[1]["foo"], foo)
|
||||
@@ -0,0 +1,184 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._file}.
|
||||
"""
|
||||
|
||||
from io import StringIO
|
||||
from types import TracebackType
|
||||
from typing import IO, Any, AnyStr, Optional, Type, cast
|
||||
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase
|
||||
from .._file import FileLogObserver, textFileLogObserver
|
||||
from .._interfaces import ILogObserver
|
||||
|
||||
|
||||
class FileLogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{FileLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{FileLogObserver} is an L{ILogObserver}.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = FileLogObserver(fileHandle, lambda e: str(e))
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def test_observeWrites(self) -> None:
|
||||
"""
|
||||
L{FileLogObserver} writes to the given file when it observes events.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = FileLogObserver(fileHandle, lambda e: str(e))
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.getvalue(), str(event))
|
||||
|
||||
def _test_observeWrites(self, what: Optional[str], count: int) -> None:
|
||||
"""
|
||||
Verify that observer performs an expected number of writes when the
|
||||
formatter returns a given value.
|
||||
|
||||
@param what: the value for the formatter to return.
|
||||
@param count: the expected number of writes.
|
||||
"""
|
||||
with DummyFile() as fileHandle:
|
||||
observer = FileLogObserver(cast(IO[Any], fileHandle), lambda e: what)
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.writes, count)
|
||||
|
||||
def test_observeWritesNone(self) -> None:
|
||||
"""
|
||||
L{FileLogObserver} does not write to the given file when it observes
|
||||
events and C{formatEvent} returns L{None}.
|
||||
"""
|
||||
self._test_observeWrites(None, 0)
|
||||
|
||||
def test_observeWritesEmpty(self) -> None:
|
||||
"""
|
||||
L{FileLogObserver} does not write to the given file when it observes
|
||||
events and C{formatEvent} returns C{""}.
|
||||
"""
|
||||
self._test_observeWrites("", 0)
|
||||
|
||||
def test_observeFlushes(self) -> None:
|
||||
"""
|
||||
L{FileLogObserver} calles C{flush()} on the output file when it
|
||||
observes an event.
|
||||
"""
|
||||
with DummyFile() as fileHandle:
|
||||
observer = FileLogObserver(cast(IO[Any], fileHandle), lambda e: str(e))
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.flushes, 1)
|
||||
|
||||
|
||||
class TextFileLogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{textFileLogObserver}.
|
||||
"""
|
||||
|
||||
def test_returnsFileLogObserver(self) -> None:
|
||||
"""
|
||||
L{textFileLogObserver} returns a L{FileLogObserver}.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
self.assertIsInstance(observer, FileLogObserver)
|
||||
|
||||
def test_outFile(self) -> None:
|
||||
"""
|
||||
Returned L{FileLogObserver} has the correct outFile.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
self.assertIs(observer._outFile, fileHandle)
|
||||
|
||||
def test_timeFormat(self) -> None:
|
||||
"""
|
||||
Returned L{FileLogObserver} has the correct outFile.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle, timeFormat="%f")
|
||||
observer(dict(log_format="XYZZY", log_time=112345.6))
|
||||
self.assertEqual(fileHandle.getvalue(), "600000 [-#-] XYZZY\n")
|
||||
|
||||
def test_observeFailure(self) -> None:
|
||||
"""
|
||||
If the C{"log_failure"} key exists in an event, the observer appends
|
||||
the failure's traceback to the output.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
failure = Failure()
|
||||
|
||||
event = dict(log_failure=failure)
|
||||
observer(event)
|
||||
output = fileHandle.getvalue()
|
||||
self.assertTrue(
|
||||
output.split("\n")[1].startswith("\tTraceback "), msg=repr(output)
|
||||
)
|
||||
|
||||
def test_observeFailureThatRaisesInGetTraceback(self) -> None:
|
||||
"""
|
||||
If the C{"log_failure"} key exists in an event, and contains an object
|
||||
that raises when you call its C{getTraceback()}, then the observer
|
||||
appends a message noting the problem, instead of raising.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = textFileLogObserver(fileHandle)
|
||||
event = dict(log_failure=object()) # object has no getTraceback()
|
||||
observer(event)
|
||||
output = fileHandle.getvalue()
|
||||
expected = "(UNABLE TO OBTAIN TRACEBACK FROM EVENT)"
|
||||
self.assertIn(expected, output)
|
||||
|
||||
|
||||
class DummyFile:
|
||||
"""
|
||||
File that counts writes and flushes.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.writes = 0
|
||||
self.flushes = 0
|
||||
|
||||
def write(self, data: AnyStr) -> None:
|
||||
"""
|
||||
Write data.
|
||||
|
||||
@param data: data
|
||||
"""
|
||||
self.writes += 1
|
||||
|
||||
def flush(self) -> None:
|
||||
"""
|
||||
Flush buffers.
|
||||
"""
|
||||
self.flushes += 1
|
||||
|
||||
def __enter__(self) -> "DummyFile":
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
pass
|
||||
@@ -0,0 +1,393 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._filter}.
|
||||
"""
|
||||
|
||||
from typing import Iterable, List, Tuple, Union, cast
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from constantly import NamedConstant
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._filter import (
|
||||
FilteringLogObserver,
|
||||
ILogFilterPredicate,
|
||||
LogLevelFilterPredicate,
|
||||
PredicateResult,
|
||||
)
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._levels import InvalidLogLevelError, LogLevel
|
||||
from .._observer import LogPublisher, bitbucketLogObserver
|
||||
|
||||
|
||||
class FilteringLogObserverTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{FilteringLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{FilteringLogObserver} is an L{ILogObserver}.
|
||||
"""
|
||||
observer = FilteringLogObserver(cast(ILogObserver, lambda e: None), ())
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def filterWith(
|
||||
self, filters: Iterable[str], other: bool = False
|
||||
) -> Union[List[int], Tuple[List[int], List[int]]]:
|
||||
"""
|
||||
Apply a set of pre-defined filters on a known set of events and return
|
||||
the filtered list of event numbers.
|
||||
|
||||
The pre-defined events are four events with a C{count} attribute set to
|
||||
C{0}, C{1}, C{2}, and C{3}.
|
||||
|
||||
@param filters: names of the filters to apply.
|
||||
Options are:
|
||||
- C{"twoMinus"} (count <=2),
|
||||
- C{"twoPlus"} (count >= 2),
|
||||
- C{"notTwo"} (count != 2),
|
||||
- C{"no"} (False).
|
||||
@param other: Whether to return a list of filtered events as well.
|
||||
|
||||
@return: event numbers or 2-tuple of lists of event numbers.
|
||||
"""
|
||||
events: List[LogEvent] = [
|
||||
dict(count=0),
|
||||
dict(count=1),
|
||||
dict(count=2),
|
||||
dict(count=3),
|
||||
]
|
||||
|
||||
class Filters:
|
||||
@staticmethod
|
||||
def twoMinus(event: LogEvent) -> NamedConstant:
|
||||
"""
|
||||
count <= 2
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: L{PredicateResult.yes} if C{event["count"] <= 2},
|
||||
otherwise L{PredicateResult.maybe}.
|
||||
"""
|
||||
if event["count"] <= 2:
|
||||
return PredicateResult.yes
|
||||
return PredicateResult.maybe
|
||||
|
||||
@staticmethod
|
||||
def twoPlus(event: LogEvent) -> NamedConstant:
|
||||
"""
|
||||
count >= 2
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: L{PredicateResult.yes} if C{event["count"] >= 2},
|
||||
otherwise L{PredicateResult.maybe}.
|
||||
"""
|
||||
if event["count"] >= 2:
|
||||
return PredicateResult.yes
|
||||
return PredicateResult.maybe
|
||||
|
||||
@staticmethod
|
||||
def notTwo(event: LogEvent) -> NamedConstant:
|
||||
"""
|
||||
count != 2
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: L{PredicateResult.yes} if C{event["count"] != 2},
|
||||
otherwise L{PredicateResult.maybe}.
|
||||
"""
|
||||
if event["count"] == 2:
|
||||
return PredicateResult.no
|
||||
return PredicateResult.maybe
|
||||
|
||||
@staticmethod
|
||||
def no(event: LogEvent) -> NamedConstant:
|
||||
"""
|
||||
No way, man.
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: L{PredicateResult.no}
|
||||
"""
|
||||
return PredicateResult.no
|
||||
|
||||
@staticmethod
|
||||
def bogus(event: LogEvent) -> NamedConstant:
|
||||
"""
|
||||
Bogus result.
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: something other than a valid predicate result.
|
||||
"""
|
||||
return None
|
||||
|
||||
predicates = (getattr(Filters, f) for f in filters)
|
||||
eventsSeen: List[LogEvent] = []
|
||||
eventsNotSeen: List[LogEvent] = []
|
||||
trackingObserver = cast(ILogObserver, eventsSeen.append)
|
||||
|
||||
if other:
|
||||
negativeObserver = cast(ILogObserver, eventsNotSeen.append)
|
||||
else:
|
||||
negativeObserver = bitbucketLogObserver
|
||||
|
||||
filteringObserver = FilteringLogObserver(
|
||||
trackingObserver, predicates, negativeObserver
|
||||
)
|
||||
|
||||
for e in events:
|
||||
filteringObserver(e)
|
||||
|
||||
if other:
|
||||
return (
|
||||
[cast(int, e["count"]) for e in eventsSeen],
|
||||
[cast(int, e["count"]) for e in eventsNotSeen],
|
||||
)
|
||||
else:
|
||||
return [cast(int, e["count"]) for e in eventsSeen]
|
||||
|
||||
def test_shouldLogEventNoFilters(self) -> None:
|
||||
"""
|
||||
No filters: all events come through.
|
||||
"""
|
||||
self.assertEqual(self.filterWith([]), [0, 1, 2, 3])
|
||||
|
||||
def test_shouldLogEventNoFilter(self) -> None:
|
||||
"""
|
||||
Filter with negative predicate result.
|
||||
"""
|
||||
self.assertEqual(self.filterWith(["notTwo"]), [0, 1, 3])
|
||||
|
||||
def test_shouldLogEventOtherObserver(self) -> None:
|
||||
"""
|
||||
Filtered results get sent to the other observer, if passed.
|
||||
"""
|
||||
self.assertEqual(self.filterWith(["notTwo"], True), ([0, 1, 3], [2]))
|
||||
|
||||
def test_shouldLogEventYesFilter(self) -> None:
|
||||
"""
|
||||
Filter with positive predicate result.
|
||||
"""
|
||||
self.assertEqual(self.filterWith(["twoPlus"]), [0, 1, 2, 3])
|
||||
|
||||
def test_shouldLogEventYesNoFilter(self) -> None:
|
||||
"""
|
||||
Series of filters with positive and negative predicate results.
|
||||
"""
|
||||
self.assertEqual(self.filterWith(["twoPlus", "no"]), [2, 3])
|
||||
|
||||
def test_shouldLogEventYesYesNoFilter(self) -> None:
|
||||
"""
|
||||
Series of filters with positive, positive and negative predicate
|
||||
results.
|
||||
"""
|
||||
self.assertEqual(self.filterWith(["twoPlus", "twoMinus", "no"]), [0, 1, 2, 3])
|
||||
|
||||
def test_shouldLogEventBadPredicateResult(self) -> None:
|
||||
"""
|
||||
Filter with invalid predicate result.
|
||||
"""
|
||||
self.assertRaises(TypeError, self.filterWith, ["bogus"])
|
||||
|
||||
def test_call(self) -> None:
|
||||
"""
|
||||
Test filtering results from each predicate type.
|
||||
"""
|
||||
e: LogEvent = dict(obj=object())
|
||||
|
||||
def callWithPredicateResult(result: NamedConstant) -> List[LogEvent]:
|
||||
seen: List[LogEvent] = []
|
||||
observer = FilteringLogObserver(
|
||||
cast(ILogObserver, lambda e: seen.append(e)),
|
||||
(cast(ILogFilterPredicate, lambda e: result),),
|
||||
)
|
||||
observer(e)
|
||||
return seen
|
||||
|
||||
self.assertIn(e, callWithPredicateResult(PredicateResult.yes))
|
||||
self.assertIn(e, callWithPredicateResult(PredicateResult.maybe))
|
||||
self.assertNotIn(e, callWithPredicateResult(PredicateResult.no))
|
||||
|
||||
def test_trace(self) -> None:
|
||||
"""
|
||||
Tracing keeps track of forwarding through the filtering observer.
|
||||
"""
|
||||
event: LogEvent = dict(log_trace=[])
|
||||
|
||||
oYes = cast(ILogObserver, lambda e: None)
|
||||
oNo = cast(ILogObserver, lambda e: None)
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def testObserver(e: LogEvent) -> None:
|
||||
self.assertIs(e, event)
|
||||
self.assertEqual(
|
||||
event["log_trace"],
|
||||
[
|
||||
(publisher, yesFilter),
|
||||
(yesFilter, oYes),
|
||||
(publisher, noFilter),
|
||||
# ... noFilter doesn't call oNo
|
||||
(publisher, oTest),
|
||||
],
|
||||
)
|
||||
|
||||
oTest = testObserver
|
||||
|
||||
yesFilter = FilteringLogObserver(
|
||||
oYes, (cast(ILogFilterPredicate, lambda e: PredicateResult.yes),)
|
||||
)
|
||||
noFilter = FilteringLogObserver(
|
||||
oNo, (cast(ILogFilterPredicate, lambda e: PredicateResult.no),)
|
||||
)
|
||||
|
||||
publisher = LogPublisher(yesFilter, noFilter, testObserver)
|
||||
publisher(event)
|
||||
|
||||
|
||||
class LogLevelFilterPredicateTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LogLevelFilterPredicate}.
|
||||
"""
|
||||
|
||||
def test_defaultLogLevel(self) -> None:
|
||||
"""
|
||||
Default log level is used.
|
||||
"""
|
||||
predicate = LogLevelFilterPredicate()
|
||||
|
||||
# Test using both "" and None as default namespace, because None was the
|
||||
# documented default value in the past.
|
||||
|
||||
for default in ("", cast(str, None)):
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace(default), predicate.defaultLogLevel
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("rocker.cool.namespace"),
|
||||
predicate.defaultLogLevel,
|
||||
)
|
||||
|
||||
def test_setLogLevel(self) -> None:
|
||||
"""
|
||||
Setting and retrieving log levels.
|
||||
"""
|
||||
predicate = LogLevelFilterPredicate()
|
||||
|
||||
# Test using both "" and None as default namespace, because None was the
|
||||
# documented default value in the past.
|
||||
|
||||
for default in ("", cast(str, None)):
|
||||
predicate.setLogLevelForNamespace(default, LogLevel.error)
|
||||
predicate.setLogLevelForNamespace("twext.web2", LogLevel.debug)
|
||||
predicate.setLogLevelForNamespace("twext.web2.dav", LogLevel.warn)
|
||||
|
||||
self.assertEqual(predicate.logLevelForNamespace(""), LogLevel.error)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace(cast(str, None)), LogLevel.error
|
||||
)
|
||||
self.assertEqual(predicate.logLevelForNamespace("twisted"), LogLevel.error)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2"), LogLevel.debug
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2.dav"), LogLevel.warn
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2.dav.test"), LogLevel.warn
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2.dav.test1.test2"),
|
||||
LogLevel.warn,
|
||||
)
|
||||
|
||||
def test_setInvalidLogLevel(self) -> None:
|
||||
"""
|
||||
Can't pass invalid log levels to C{setLogLevelForNamespace()}.
|
||||
"""
|
||||
predicate = LogLevelFilterPredicate()
|
||||
|
||||
self.assertRaises(
|
||||
InvalidLogLevelError,
|
||||
predicate.setLogLevelForNamespace,
|
||||
"twext.web2",
|
||||
object(),
|
||||
)
|
||||
|
||||
# Level must be a constant, not the name of a constant
|
||||
self.assertRaises(
|
||||
InvalidLogLevelError,
|
||||
predicate.setLogLevelForNamespace,
|
||||
"twext.web2",
|
||||
"debug",
|
||||
)
|
||||
|
||||
def test_clearLogLevels(self) -> None:
|
||||
"""
|
||||
Clearing log levels.
|
||||
"""
|
||||
predicate = LogLevelFilterPredicate()
|
||||
|
||||
predicate.setLogLevelForNamespace("twext.web2", LogLevel.debug)
|
||||
predicate.setLogLevelForNamespace("twext.web2.dav", LogLevel.error)
|
||||
|
||||
predicate.clearLogLevels()
|
||||
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twisted"), predicate.defaultLogLevel
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2"), predicate.defaultLogLevel
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2.dav"), predicate.defaultLogLevel
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2.dav.test"),
|
||||
predicate.defaultLogLevel,
|
||||
)
|
||||
self.assertEqual(
|
||||
predicate.logLevelForNamespace("twext.web2.dav.test1.test2"),
|
||||
predicate.defaultLogLevel,
|
||||
)
|
||||
|
||||
def test_filtering(self) -> None:
|
||||
"""
|
||||
Events are filtered based on log level/namespace.
|
||||
"""
|
||||
predicate = LogLevelFilterPredicate()
|
||||
|
||||
predicate.setLogLevelForNamespace("", LogLevel.error)
|
||||
predicate.setLogLevelForNamespace("twext.web2", LogLevel.debug)
|
||||
predicate.setLogLevelForNamespace("twext.web2.dav", LogLevel.warn)
|
||||
|
||||
def checkPredicate(
|
||||
namespace: str, level: NamedConstant, expectedResult: NamedConstant
|
||||
) -> None:
|
||||
event: LogEvent = dict(log_namespace=namespace, log_level=level)
|
||||
self.assertEqual(expectedResult, predicate(event))
|
||||
|
||||
checkPredicate("", LogLevel.debug, PredicateResult.no)
|
||||
checkPredicate(cast(str, None), LogLevel.debug, PredicateResult.no)
|
||||
checkPredicate("", LogLevel.error, PredicateResult.no)
|
||||
checkPredicate(cast(str, None), LogLevel.error, PredicateResult.no)
|
||||
|
||||
checkPredicate("twext.web2", LogLevel.debug, PredicateResult.maybe)
|
||||
checkPredicate("twext.web2", LogLevel.error, PredicateResult.maybe)
|
||||
|
||||
checkPredicate("twext.web2.dav", LogLevel.debug, PredicateResult.no)
|
||||
checkPredicate("twext.web2.dav", LogLevel.error, PredicateResult.maybe)
|
||||
|
||||
checkPredicate("", LogLevel.critical, PredicateResult.no)
|
||||
checkPredicate(cast(str, None), LogLevel.critical, PredicateResult.no)
|
||||
checkPredicate("twext.web2", None, PredicateResult.no)
|
||||
@@ -0,0 +1,326 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._format}.
|
||||
"""
|
||||
|
||||
import json
|
||||
from itertools import count
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
try:
|
||||
from time import tzset
|
||||
except ImportError:
|
||||
tzset = None # type: ignore[assignment]
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._flatten import KeyFlattener, aFormatter, extractField, flattenEvent
|
||||
from .._format import formatEvent
|
||||
from .._interfaces import LogEvent
|
||||
|
||||
|
||||
class FlatFormattingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for flattened event formatting functions.
|
||||
"""
|
||||
|
||||
def test_formatFlatEvent(self) -> None:
|
||||
"""
|
||||
L{flattenEvent} will "flatten" an event so that, if scrubbed of all but
|
||||
serializable objects, it will preserve all necessary data to be
|
||||
formatted once serialized. When presented with an event thusly
|
||||
flattened, L{formatEvent} will produce the same output.
|
||||
"""
|
||||
counter = count()
|
||||
|
||||
class Ephemeral:
|
||||
attribute = "value"
|
||||
|
||||
event1 = dict(
|
||||
log_format=(
|
||||
"callable: {callme()} "
|
||||
"attribute: {object.attribute} "
|
||||
"numrepr: {number!r} "
|
||||
"numstr: {number!s} "
|
||||
"strrepr: {string!r} "
|
||||
"unistr: {unistr!s}"
|
||||
),
|
||||
callme=lambda: next(counter),
|
||||
object=Ephemeral(),
|
||||
number=7,
|
||||
string="hello",
|
||||
unistr="ö",
|
||||
)
|
||||
|
||||
flattenEvent(event1)
|
||||
|
||||
event2 = dict(event1)
|
||||
del event2["callme"]
|
||||
del event2["object"]
|
||||
event3 = json.loads(json.dumps(event2))
|
||||
self.assertEqual(
|
||||
formatEvent(event3),
|
||||
(
|
||||
"callable: 0 "
|
||||
"attribute: value "
|
||||
"numrepr: 7 "
|
||||
"numstr: 7 "
|
||||
"strrepr: 'hello' "
|
||||
"unistr: ö"
|
||||
),
|
||||
)
|
||||
|
||||
def test_formatFlatEventBadFormat(self) -> None:
|
||||
"""
|
||||
If the format string is invalid, an error is produced.
|
||||
"""
|
||||
event1 = dict(
|
||||
log_format=("strrepr: {string!X}"),
|
||||
string="hello",
|
||||
)
|
||||
|
||||
flattenEvent(event1)
|
||||
event2 = json.loads(json.dumps(event1))
|
||||
|
||||
self.assertTrue(formatEvent(event2).startswith("Unable to format event"))
|
||||
|
||||
def test_formatFlatEventWithMutatedFields(self) -> None:
|
||||
"""
|
||||
L{formatEvent} will prefer the stored C{str()} or C{repr()} value for
|
||||
an object, in case the other version.
|
||||
"""
|
||||
|
||||
class Unpersistable:
|
||||
"""
|
||||
Unpersitable object.
|
||||
"""
|
||||
|
||||
destructed = False
|
||||
|
||||
def selfDestruct(self) -> None:
|
||||
"""
|
||||
Self destruct.
|
||||
"""
|
||||
self.destructed = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.destructed:
|
||||
return "post-serialization garbage"
|
||||
else:
|
||||
return "un-persistable"
|
||||
|
||||
up = Unpersistable()
|
||||
event1 = dict(log_format="unpersistable: {unpersistable}", unpersistable=up)
|
||||
|
||||
flattenEvent(event1)
|
||||
up.selfDestruct()
|
||||
|
||||
self.assertEqual(formatEvent(event1), "unpersistable: un-persistable")
|
||||
|
||||
def test_keyFlattening(self) -> None:
|
||||
"""
|
||||
Test that L{KeyFlattener.flatKey} returns the expected keys for format
|
||||
fields.
|
||||
"""
|
||||
|
||||
def keyFromFormat(format: str) -> str:
|
||||
for (
|
||||
literalText,
|
||||
fieldName,
|
||||
formatSpec,
|
||||
conversion,
|
||||
) in aFormatter.parse(format):
|
||||
assert fieldName is not None
|
||||
return KeyFlattener().flatKey(fieldName, formatSpec, conversion)
|
||||
assert False, "Unable to derive key from format: {format}"
|
||||
|
||||
# No name
|
||||
try:
|
||||
self.assertEqual(keyFromFormat("{}"), "!:")
|
||||
except ValueError:
|
||||
# In python 2.6, an empty field name causes Formatter.parse to
|
||||
# raise ValueError.
|
||||
# In Python 2.7, it's allowed, so this exception is unexpected.
|
||||
raise
|
||||
|
||||
# Just a name
|
||||
self.assertEqual(keyFromFormat("{foo}"), "foo!:")
|
||||
|
||||
# Add conversion
|
||||
self.assertEqual(keyFromFormat("{foo!s}"), "foo!s:")
|
||||
self.assertEqual(keyFromFormat("{foo!r}"), "foo!r:")
|
||||
|
||||
# Add format spec
|
||||
self.assertEqual(keyFromFormat("{foo:%s}"), "foo!:%s")
|
||||
self.assertEqual(keyFromFormat("{foo:!}"), "foo!:!")
|
||||
self.assertEqual(keyFromFormat("{foo::}"), "foo!::")
|
||||
|
||||
# Both
|
||||
self.assertEqual(keyFromFormat("{foo!s:%s}"), "foo!s:%s")
|
||||
self.assertEqual(keyFromFormat("{foo!s:!}"), "foo!s:!")
|
||||
self.assertEqual(keyFromFormat("{foo!s::}"), "foo!s::")
|
||||
|
||||
sameFlattener = KeyFlattener()
|
||||
(
|
||||
(
|
||||
literalText,
|
||||
fieldName,
|
||||
formatSpec,
|
||||
conversion,
|
||||
),
|
||||
) = aFormatter.parse("{x}")
|
||||
assert fieldName is not None
|
||||
|
||||
self.assertEqual(
|
||||
sameFlattener.flatKey(fieldName, formatSpec, conversion), "x!:"
|
||||
)
|
||||
self.assertEqual(
|
||||
sameFlattener.flatKey(fieldName, formatSpec, conversion), "x!:/2"
|
||||
)
|
||||
|
||||
def _test_formatFlatEvent_fieldNamesSame(
|
||||
self, event: Optional[LogEvent] = None
|
||||
) -> LogEvent:
|
||||
"""
|
||||
The same format field used twice in one event is rendered twice.
|
||||
|
||||
@param event: An event to flatten. If L{None}, create a new event.
|
||||
@return: C{event} or the event created.
|
||||
"""
|
||||
if event is None:
|
||||
counter = count()
|
||||
|
||||
class CountStr:
|
||||
"""
|
||||
Hack
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(next(counter))
|
||||
|
||||
event = dict(
|
||||
log_format="{x} {x}",
|
||||
x=CountStr(),
|
||||
)
|
||||
|
||||
flattenEvent(event)
|
||||
self.assertEqual(formatEvent(event), "0 1")
|
||||
|
||||
return event
|
||||
|
||||
def test_formatFlatEventFieldNamesSame(self) -> None:
|
||||
"""
|
||||
The same format field used twice in one event is rendered twice.
|
||||
"""
|
||||
self._test_formatFlatEvent_fieldNamesSame()
|
||||
|
||||
def test_formatFlatEventFieldNamesSameAgain(self) -> None:
|
||||
"""
|
||||
The same event flattened twice gives the same (already rendered)
|
||||
result.
|
||||
"""
|
||||
event = self._test_formatFlatEvent_fieldNamesSame()
|
||||
self._test_formatFlatEvent_fieldNamesSame(event)
|
||||
|
||||
def test_formatEventFlatTrailingText(self) -> None:
|
||||
"""
|
||||
L{formatEvent} will handle a flattened event with tailing text after
|
||||
a replacement field.
|
||||
"""
|
||||
event = dict(
|
||||
log_format="test {x} trailing",
|
||||
x="value",
|
||||
)
|
||||
flattenEvent(event)
|
||||
|
||||
result = formatEvent(event)
|
||||
|
||||
self.assertEqual(result, "test value trailing")
|
||||
|
||||
def test_extractField(
|
||||
self, flattenFirst: Callable[[LogEvent], LogEvent] = lambda x: x
|
||||
) -> None:
|
||||
"""
|
||||
L{extractField} will extract a field used in the format string.
|
||||
|
||||
@param flattenFirst: callable to flatten an event
|
||||
"""
|
||||
|
||||
class ObjectWithRepr:
|
||||
def __repr__(self) -> str:
|
||||
return "repr"
|
||||
|
||||
class Something:
|
||||
def __init__(self) -> None:
|
||||
self.number = 7
|
||||
self.object = ObjectWithRepr()
|
||||
|
||||
def __getstate__(self) -> None:
|
||||
raise NotImplementedError("Just in case.")
|
||||
|
||||
event = dict(
|
||||
log_format="{something.number} {something.object}",
|
||||
something=Something(),
|
||||
)
|
||||
|
||||
flattened = flattenFirst(event)
|
||||
|
||||
def extract(field: str) -> Any:
|
||||
return extractField(field, flattened)
|
||||
|
||||
self.assertEqual(extract("something.number"), 7)
|
||||
self.assertEqual(extract("something.number!s"), "7")
|
||||
self.assertEqual(extract("something.object!s"), "repr")
|
||||
|
||||
def test_extractFieldFlattenFirst(self) -> None:
|
||||
"""
|
||||
L{extractField} behaves identically if the event is explicitly
|
||||
flattened first.
|
||||
"""
|
||||
|
||||
def flattened(event: LogEvent) -> LogEvent:
|
||||
flattenEvent(event)
|
||||
return event
|
||||
|
||||
self.test_extractField(flattened)
|
||||
|
||||
def test_flattenEventWithoutFormat(self) -> None:
|
||||
"""
|
||||
L{flattenEvent} will do nothing to an event with no format string.
|
||||
"""
|
||||
inputEvent = {"a": "b", "c": 1}
|
||||
flattenEvent(inputEvent)
|
||||
self.assertEqual(inputEvent, {"a": "b", "c": 1})
|
||||
|
||||
def test_flattenEventWithInertFormat(self) -> None:
|
||||
"""
|
||||
L{flattenEvent} will do nothing to an event with a format string that
|
||||
contains no format fields.
|
||||
"""
|
||||
inputEvent = {"a": "b", "c": 1, "log_format": "simple message"}
|
||||
flattenEvent(inputEvent)
|
||||
self.assertEqual(
|
||||
inputEvent,
|
||||
{
|
||||
"a": "b",
|
||||
"c": 1,
|
||||
"log_format": "simple message",
|
||||
},
|
||||
)
|
||||
|
||||
def test_flattenEventWithNoneFormat(self) -> None:
|
||||
"""
|
||||
L{flattenEvent} will do nothing to an event with log_format set to
|
||||
None.
|
||||
"""
|
||||
inputEvent = {"a": "b", "c": 1, "log_format": None}
|
||||
flattenEvent(inputEvent)
|
||||
self.assertEqual(
|
||||
inputEvent,
|
||||
{
|
||||
"a": "b",
|
||||
"c": 1,
|
||||
"log_format": None,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,732 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._format}.
|
||||
"""
|
||||
|
||||
from typing import Any, AnyStr, Dict, Optional, cast
|
||||
|
||||
try:
|
||||
from time import tzset
|
||||
|
||||
# We should upgrade to a version of pyflakes that does not require this.
|
||||
tzset
|
||||
except ImportError:
|
||||
tzset = None # type: ignore[assignment]
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.test.test_tzhelper import addTZCleanup, mktime, setTZ
|
||||
from twisted.trial import unittest
|
||||
from twisted.trial.unittest import SkipTest
|
||||
from .._format import (
|
||||
eventAsText,
|
||||
formatEvent,
|
||||
formatEventAsClassicLogText,
|
||||
formatTime,
|
||||
formatUnformattableEvent,
|
||||
formatWithCall,
|
||||
)
|
||||
from .._interfaces import LogEvent
|
||||
from .._levels import LogLevel
|
||||
|
||||
|
||||
class FormattingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for basic event formatting functions.
|
||||
"""
|
||||
|
||||
def format(self, logFormat: AnyStr, **event: object) -> str:
|
||||
"""
|
||||
Create a Twisted log event dictionary from C{event} with the given
|
||||
C{logFormat} format string, format it with L{formatEvent}, ensure that
|
||||
its type is L{str}, and return its result.
|
||||
"""
|
||||
event["log_format"] = logFormat
|
||||
result = formatEvent(event)
|
||||
self.assertIs(type(result), str)
|
||||
return result
|
||||
|
||||
def test_formatEvent(self) -> None:
|
||||
"""
|
||||
L{formatEvent} will format an event according to several rules:
|
||||
|
||||
- A string with no formatting instructions will be passed straight
|
||||
through.
|
||||
|
||||
- PEP 3101 strings will be formatted using the keys and values of
|
||||
the event as named fields.
|
||||
|
||||
- PEP 3101 keys ending with C{()} will be treated as instructions
|
||||
to call that key (which ought to be a callable) before
|
||||
formatting.
|
||||
|
||||
L{formatEvent} will always return L{str}, and if given bytes, will
|
||||
always treat its format string as UTF-8 encoded.
|
||||
"""
|
||||
self.assertEqual("", self.format(b""))
|
||||
self.assertEqual("", self.format(""))
|
||||
self.assertEqual("abc", self.format("{x}", x="abc"))
|
||||
self.assertEqual(
|
||||
"no, yes.",
|
||||
self.format(
|
||||
"{not_called}, {called()}.", not_called="no", called=lambda: "yes"
|
||||
),
|
||||
)
|
||||
self.assertEqual("S\xe1nchez", self.format(b"S\xc3\xa1nchez"))
|
||||
self.assertIn("Unable to format event", self.format(b"S\xe1nchez"))
|
||||
maybeResult = self.format(b"S{a!s}nchez", a=b"\xe1")
|
||||
self.assertIn("Sb'\\xe1'nchez", maybeResult)
|
||||
|
||||
xe1 = str(repr(b"\xe1"))
|
||||
self.assertIn("S" + xe1 + "nchez", self.format(b"S{a!r}nchez", a=b"\xe1"))
|
||||
|
||||
def test_formatMethod(self) -> None:
|
||||
"""
|
||||
L{formatEvent} will format PEP 3101 keys containing C{.}s ending with
|
||||
C{()} as methods.
|
||||
"""
|
||||
|
||||
class World:
|
||||
def where(self) -> str:
|
||||
return "world"
|
||||
|
||||
self.assertEqual(
|
||||
"hello world", self.format("hello {what.where()}", what=World())
|
||||
)
|
||||
|
||||
def test_formatAttributeSubscript(self) -> None:
|
||||
"""
|
||||
L{formatEvent} will format subscripts of attributes per PEP 3101.
|
||||
"""
|
||||
|
||||
class Example(object):
|
||||
config: Dict[str, str] = dict(foo="bar", baz="qux")
|
||||
|
||||
self.assertEqual(
|
||||
"bar qux",
|
||||
self.format(
|
||||
"{example.config[foo]} {example.config[baz]}",
|
||||
example=Example(),
|
||||
),
|
||||
)
|
||||
|
||||
def test_formatEventNoFormat(self) -> None:
|
||||
"""
|
||||
Formatting an event with no format.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
result = formatEvent(event)
|
||||
|
||||
self.assertEqual("", result)
|
||||
|
||||
def test_formatEventWeirdFormat(self) -> None:
|
||||
"""
|
||||
Formatting an event with a bogus format.
|
||||
"""
|
||||
event = dict(log_format=object(), foo=1, bar=2)
|
||||
result = formatEvent(event)
|
||||
|
||||
self.assertIn("Log format must be str", result)
|
||||
self.assertIn(repr(event), result)
|
||||
|
||||
def test_formatUnformattableEvent(self) -> None:
|
||||
"""
|
||||
Formatting an event that's just plain out to get us.
|
||||
"""
|
||||
event = dict(log_format="{evil()}", evil=lambda: 1 / 0)
|
||||
result = formatEvent(event)
|
||||
|
||||
self.assertIn("Unable to format event", result)
|
||||
self.assertIn(repr(event), result)
|
||||
|
||||
def test_formatUnformattableEventWithUnformattableKey(self) -> None:
|
||||
"""
|
||||
Formatting an unformattable event that has an unformattable key.
|
||||
"""
|
||||
event: LogEvent = {
|
||||
"log_format": "{evil()}",
|
||||
"evil": lambda: 1 / 0,
|
||||
cast(str, Unformattable()): "gurk",
|
||||
}
|
||||
result = formatEvent(event)
|
||||
self.assertIn("MESSAGE LOST: unformattable object logged:", result)
|
||||
self.assertIn("Recoverable data:", result)
|
||||
self.assertIn("Exception during formatting:", result)
|
||||
|
||||
def test_formatUnformattableEventWithUnformattableValue(self) -> None:
|
||||
"""
|
||||
Formatting an unformattable event that has an unformattable value.
|
||||
"""
|
||||
event = dict(
|
||||
log_format="{evil()}",
|
||||
evil=lambda: 1 / 0,
|
||||
gurk=Unformattable(),
|
||||
)
|
||||
result = formatEvent(event)
|
||||
self.assertIn("MESSAGE LOST: unformattable object logged:", result)
|
||||
self.assertIn("Recoverable data:", result)
|
||||
self.assertIn("Exception during formatting:", result)
|
||||
|
||||
def test_formatUnformattableEventWithUnformattableErrorOMGWillItStop(self) -> None:
|
||||
"""
|
||||
Formatting an unformattable event that has an unformattable value.
|
||||
"""
|
||||
event = dict(
|
||||
log_format="{evil()}",
|
||||
evil=lambda: 1 / 0,
|
||||
recoverable="okay",
|
||||
)
|
||||
# Call formatUnformattableEvent() directly with a bogus exception.
|
||||
result = formatUnformattableEvent(event, cast(BaseException, Unformattable()))
|
||||
self.assertIn("MESSAGE LOST: unformattable object logged:", result)
|
||||
self.assertIn(repr("recoverable") + " = " + repr("okay"), result)
|
||||
|
||||
|
||||
class TimeFormattingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for time formatting functions.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
addTZCleanup(self)
|
||||
|
||||
def test_formatTimeWithDefaultFormat(self) -> None:
|
||||
"""
|
||||
Default time stamp format is RFC 3339 and offset respects the timezone
|
||||
as set by the standard C{TZ} environment variable and L{tzset} API.
|
||||
"""
|
||||
if tzset is None:
|
||||
raise SkipTest("Platform cannot change timezone; unable to verify offsets.")
|
||||
|
||||
def testForTimeZone(
|
||||
name: str, expectedDST: Optional[str], expectedSTD: str
|
||||
) -> None:
|
||||
setTZ(name)
|
||||
|
||||
localSTD = mktime((2007, 1, 31, 0, 0, 0, 2, 31, 0))
|
||||
self.assertEqual(formatTime(localSTD), expectedSTD)
|
||||
|
||||
if expectedDST:
|
||||
localDST = mktime((2006, 6, 30, 0, 0, 0, 4, 181, 1))
|
||||
self.assertEqual(formatTime(localDST), expectedDST)
|
||||
|
||||
# UTC
|
||||
testForTimeZone(
|
||||
"UTC+00",
|
||||
None,
|
||||
"2007-01-31T00:00:00+0000",
|
||||
)
|
||||
|
||||
# West of UTC
|
||||
testForTimeZone(
|
||||
"EST+05EDT,M4.1.0,M10.5.0",
|
||||
"2006-06-30T00:00:00-0400",
|
||||
"2007-01-31T00:00:00-0500",
|
||||
)
|
||||
|
||||
# East of UTC
|
||||
testForTimeZone(
|
||||
"CEST-01CEDT,M4.1.0,M10.5.0",
|
||||
"2006-06-30T00:00:00+0200",
|
||||
"2007-01-31T00:00:00+0100",
|
||||
)
|
||||
|
||||
# No DST
|
||||
testForTimeZone(
|
||||
"CST+06",
|
||||
None,
|
||||
"2007-01-31T00:00:00-0600",
|
||||
)
|
||||
|
||||
def test_formatTimeWithNoTime(self) -> None:
|
||||
"""
|
||||
If C{when} argument is L{None}, we get the default output.
|
||||
"""
|
||||
self.assertEqual(formatTime(None), "-")
|
||||
self.assertEqual(formatTime(None, default="!"), "!")
|
||||
|
||||
def test_formatTimeWithNoFormat(self) -> None:
|
||||
"""
|
||||
If C{timeFormat} argument is L{None}, we get the default output.
|
||||
"""
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
self.assertEqual(formatTime(t, timeFormat=None), "-")
|
||||
self.assertEqual(formatTime(t, timeFormat=None, default="!"), "!")
|
||||
|
||||
def test_formatTimeWithAlternateTimeFormat(self) -> None:
|
||||
"""
|
||||
Alternate time format in output.
|
||||
"""
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
self.assertEqual(formatTime(t, timeFormat="%Y/%W"), "2013/38")
|
||||
|
||||
def test_formatTimePercentF(self) -> None:
|
||||
"""
|
||||
"%f" supported in time format.
|
||||
"""
|
||||
self.assertEqual(formatTime(1000000.23456, timeFormat="%f"), "234560")
|
||||
|
||||
|
||||
class ClassicLogFormattingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for classic text log event formatting functions.
|
||||
"""
|
||||
|
||||
def test_formatTimeDefault(self) -> None:
|
||||
"""
|
||||
Time is first field. Default time stamp format is RFC 3339 and offset
|
||||
respects the timezone as set by the standard C{TZ} environment variable
|
||||
and L{tzset} API.
|
||||
"""
|
||||
if tzset is None:
|
||||
raise SkipTest("Platform cannot change timezone; unable to verify offsets.")
|
||||
|
||||
addTZCleanup(self)
|
||||
setTZ("UTC+00")
|
||||
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
event = dict(log_format="XYZZY", log_time=t)
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"2013-09-24T11:40:47+0000 [-\x23-] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatTimeCustom(self) -> None:
|
||||
"""
|
||||
Time is first field. Custom formatting function is an optional
|
||||
argument.
|
||||
"""
|
||||
|
||||
def formatTime(t: Optional[float]) -> str:
|
||||
return f"__{t}__"
|
||||
|
||||
event = dict(log_format="XYZZY", log_time=12345)
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event, formatTime=formatTime),
|
||||
"__12345__ [-\x23-] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatNamespace(self) -> None:
|
||||
"""
|
||||
Namespace is first part of second field.
|
||||
"""
|
||||
event = dict(log_format="XYZZY", log_namespace="my.namespace")
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"- [my.namespace\x23-] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatLevel(self) -> None:
|
||||
"""
|
||||
Level is second part of second field.
|
||||
"""
|
||||
event = dict(log_format="XYZZY", log_level=LogLevel.warn)
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"- [-\x23warn] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatSystem(self) -> None:
|
||||
"""
|
||||
System is second field.
|
||||
"""
|
||||
event = dict(log_format="XYZZY", log_system="S.Y.S.T.E.M.")
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"- [S.Y.S.T.E.M.] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatSystemRulz(self) -> None:
|
||||
"""
|
||||
System is not supplanted by namespace and level.
|
||||
"""
|
||||
event = dict(
|
||||
log_format="XYZZY",
|
||||
log_namespace="my.namespace",
|
||||
log_level=LogLevel.warn,
|
||||
log_system="S.Y.S.T.E.M.",
|
||||
)
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"- [S.Y.S.T.E.M.] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatSystemUnformattable(self) -> None:
|
||||
"""
|
||||
System is not supplanted by namespace and level.
|
||||
"""
|
||||
event = dict(log_format="XYZZY", log_system=Unformattable())
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"- [UNFORMATTABLE] XYZZY\n",
|
||||
)
|
||||
|
||||
def test_formatFormat(self) -> None:
|
||||
"""
|
||||
Formatted event is last field.
|
||||
"""
|
||||
event = dict(log_format="id:{id}", id="123")
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
"- [-\x23-] id:123\n",
|
||||
)
|
||||
|
||||
def test_formatNoFormat(self) -> None:
|
||||
"""
|
||||
No format string.
|
||||
"""
|
||||
event = dict(id="123")
|
||||
self.assertIs(formatEventAsClassicLogText(event), None)
|
||||
|
||||
def test_formatEmptyFormat(self) -> None:
|
||||
"""
|
||||
Empty format string.
|
||||
"""
|
||||
event = dict(log_format="", id="123")
|
||||
self.assertIs(formatEventAsClassicLogText(event), None)
|
||||
|
||||
def test_formatFormatMultiLine(self) -> None:
|
||||
"""
|
||||
If the formatted event has newlines, indent additional lines.
|
||||
"""
|
||||
event = dict(log_format='XYZZY\nA hollow voice says:\n"Plugh"')
|
||||
self.assertEqual(
|
||||
formatEventAsClassicLogText(event),
|
||||
'- [-\x23-] XYZZY\n\tA hollow voice says:\n\t"Plugh"\n',
|
||||
)
|
||||
|
||||
|
||||
class FormatFieldTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for format field functions.
|
||||
"""
|
||||
|
||||
def test_formatWithCall(self) -> None:
|
||||
"""
|
||||
L{formatWithCall} is an extended version of L{str.format} that
|
||||
will interpret a set of parentheses "C{()}" at the end of a format key
|
||||
to mean that the format key ought to be I{called} rather than
|
||||
stringified.
|
||||
"""
|
||||
self.assertEqual(
|
||||
formatWithCall(
|
||||
"Hello, {world}. {callme()}.",
|
||||
dict(world="earth", callme=lambda: "maybe"),
|
||||
),
|
||||
"Hello, earth. maybe.",
|
||||
)
|
||||
self.assertEqual(
|
||||
formatWithCall("Hello, {repr()!r}.", dict(repr=lambda: "repr")),
|
||||
"Hello, 'repr'.",
|
||||
)
|
||||
|
||||
|
||||
class Unformattable:
|
||||
"""
|
||||
An object that raises an exception from C{__repr__}.
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(1 / 0)
|
||||
|
||||
|
||||
class CapturedError(Exception):
|
||||
"""
|
||||
A captured error for use in format tests.
|
||||
"""
|
||||
|
||||
|
||||
class EventAsTextTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{eventAsText}, all of which ensure that the
|
||||
returned type is UTF-8 decoded text.
|
||||
"""
|
||||
|
||||
def test_eventWithTraceback(self) -> None:
|
||||
"""
|
||||
An event with a C{log_failure} key will have a traceback appended.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
event: LogEvent = {"log_format": "This is a test log message"}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIn(str(f.getTraceback()), eventText)
|
||||
self.assertIn("This is a test log message", eventText)
|
||||
|
||||
def test_formatEmptyEventWithTraceback(self) -> None:
|
||||
"""
|
||||
An event with an empty C{log_format} key appends a traceback from
|
||||
the accompanying failure.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
event: LogEvent = {"log_format": ""}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIn(str(f.getTraceback()), eventText)
|
||||
self.assertIn("This is a fake error", eventText)
|
||||
|
||||
def test_formatUnformattableWithTraceback(self) -> None:
|
||||
"""
|
||||
An event with an unformattable value in the C{log_format} key still
|
||||
has a traceback appended.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
event = {
|
||||
"log_format": "{evil()}",
|
||||
"evil": lambda: 1 / 0,
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIsInstance(eventText, str)
|
||||
self.assertIn(str(f.getTraceback()), eventText)
|
||||
self.assertIn("This is a fake error", eventText)
|
||||
|
||||
def test_formatUnformattableErrorWithTraceback(self) -> None:
|
||||
"""
|
||||
An event with an unformattable value in the C{log_format} key, that
|
||||
throws an exception when __repr__ is invoked still has a traceback
|
||||
appended.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
event: LogEvent = {
|
||||
"log_format": "{evil()}",
|
||||
"evil": lambda: 1 / 0,
|
||||
cast(str, Unformattable()): "gurk",
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIsInstance(eventText, str)
|
||||
self.assertIn("MESSAGE LOST", eventText)
|
||||
self.assertIn(str(f.getTraceback()), eventText)
|
||||
self.assertIn("This is a fake error", eventText)
|
||||
|
||||
def test_formatEventUnformattableTraceback(self) -> None:
|
||||
"""
|
||||
If a traceback cannot be appended, a message indicating this is true
|
||||
is appended.
|
||||
"""
|
||||
event: LogEvent = {"log_format": ""}
|
||||
event["log_failure"] = object()
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIsInstance(eventText, str)
|
||||
self.assertIn("(UNABLE TO OBTAIN TRACEBACK FROM EVENT)", eventText)
|
||||
|
||||
def test_formatEventNonCritical(self) -> None:
|
||||
"""
|
||||
An event with no C{log_failure} key will not have a traceback appended.
|
||||
"""
|
||||
event: LogEvent = {"log_format": "This is a test log message"}
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIsInstance(eventText, str)
|
||||
self.assertIn("This is a test log message", eventText)
|
||||
|
||||
def test_formatTracebackMultibyte(self) -> None:
|
||||
"""
|
||||
An exception message with multibyte characters is properly handled.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("€")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
event: LogEvent = {"log_format": "This is a test log message"}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIn("€", eventText)
|
||||
self.assertIn("Traceback", eventText)
|
||||
|
||||
def test_formatTracebackHandlesUTF8DecodeFailure(self) -> None:
|
||||
"""
|
||||
An error raised attempting to decode the UTF still produces a
|
||||
valid log message.
|
||||
"""
|
||||
try:
|
||||
# 'test' in utf-16
|
||||
raise CapturedError(b"\xff\xfet\x00e\x00s\x00t\x00")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
event: LogEvent = {"log_format": "This is a test log message"}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(event, includeTimestamp=True, includeSystem=False)
|
||||
self.assertIn("Traceback", eventText)
|
||||
self.assertIn(r'CapturedError(b"\xff\xfet\x00e\x00s\x00t\x00")', eventText)
|
||||
|
||||
def test_eventAsTextSystemOnly(self) -> None:
|
||||
"""
|
||||
If includeSystem is specified as the only option no timestamp or
|
||||
traceback are printed.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
event: LogEvent = {
|
||||
"log_format": "ABCD",
|
||||
"log_system": "fake_system",
|
||||
"log_time": t,
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(
|
||||
event,
|
||||
includeTimestamp=False,
|
||||
includeTraceback=False,
|
||||
includeSystem=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
eventText,
|
||||
"[fake_system] ABCD",
|
||||
)
|
||||
|
||||
def test_eventAsTextTimestampOnly(self) -> None:
|
||||
"""
|
||||
If includeTimestamp is specified as the only option no system or
|
||||
traceback are printed.
|
||||
"""
|
||||
if tzset is None:
|
||||
raise SkipTest("Platform cannot change timezone; unable to verify offsets.")
|
||||
|
||||
addTZCleanup(self)
|
||||
setTZ("UTC+00")
|
||||
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
event: LogEvent = {
|
||||
"log_format": "ABCD",
|
||||
"log_system": "fake_system",
|
||||
"log_time": t,
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(
|
||||
event,
|
||||
includeTimestamp=True,
|
||||
includeTraceback=False,
|
||||
includeSystem=False,
|
||||
)
|
||||
self.assertEqual(
|
||||
eventText,
|
||||
"2013-09-24T11:40:47+0000 ABCD",
|
||||
)
|
||||
|
||||
def test_eventAsTextSystemMissing(self) -> None:
|
||||
"""
|
||||
If includeSystem is specified with a missing system [-#-]
|
||||
is used.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
event: LogEvent = {
|
||||
"log_format": "ABCD",
|
||||
"log_time": t,
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(
|
||||
event,
|
||||
includeTimestamp=False,
|
||||
includeTraceback=False,
|
||||
includeSystem=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
eventText,
|
||||
"[-\x23-] ABCD",
|
||||
)
|
||||
|
||||
def test_eventAsTextSystemMissingNamespaceAndLevel(self) -> None:
|
||||
"""
|
||||
If includeSystem is specified with a missing system but
|
||||
namespace and level are present they are used.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
event: LogEvent = {
|
||||
"log_format": "ABCD",
|
||||
"log_time": t,
|
||||
"log_level": LogLevel.info,
|
||||
"log_namespace": "test",
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(
|
||||
event,
|
||||
includeTimestamp=False,
|
||||
includeTraceback=False,
|
||||
includeSystem=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
eventText,
|
||||
"[test\x23info] ABCD",
|
||||
)
|
||||
|
||||
def test_eventAsTextSystemMissingLevelOnly(self) -> None:
|
||||
"""
|
||||
If includeSystem is specified with a missing system but
|
||||
level is present, level is included.
|
||||
"""
|
||||
try:
|
||||
raise CapturedError("This is a fake error")
|
||||
except CapturedError:
|
||||
f = Failure()
|
||||
|
||||
t = mktime((2013, 9, 24, 11, 40, 47, 1, 267, -1))
|
||||
event: LogEvent = {
|
||||
"log_format": "ABCD",
|
||||
"log_time": t,
|
||||
"log_level": LogLevel.info,
|
||||
}
|
||||
event["log_failure"] = f
|
||||
eventText = eventAsText(
|
||||
event,
|
||||
includeTimestamp=False,
|
||||
includeTraceback=False,
|
||||
includeSystem=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
eventText,
|
||||
"[-\x23info] ABCD",
|
||||
)
|
||||
|
||||
def test_eventAsTextTypeAsInput(self) -> None:
|
||||
"""
|
||||
C{eventAsText} can handle formats that have classes or types as input.
|
||||
"""
|
||||
|
||||
def getText(logFormat: str, **event: Any) -> str:
|
||||
return eventAsText(
|
||||
{"log_format": logFormat, **event},
|
||||
includeTimestamp=False,
|
||||
includeTraceback=False,
|
||||
includeSystem=False,
|
||||
)
|
||||
|
||||
self.assertEqual(str(int), getText("{c}", c=int))
|
||||
self.assertEqual(str(RuntimeError), getText("{c}", c=RuntimeError))
|
||||
self.assertEqual("str", getText("{c.__name__}", c=str))
|
||||
@@ -0,0 +1,352 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._global}.
|
||||
"""
|
||||
|
||||
import io
|
||||
from typing import IO, Any, List, Optional, TextIO, Tuple, Type, cast
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import unittest
|
||||
from .._file import textFileLogObserver
|
||||
from .._global import MORE_THAN_ONCE_WARNING, LogBeginner
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._levels import LogLevel
|
||||
from .._logger import Logger
|
||||
from .._observer import LogPublisher
|
||||
from ..test.test_stdlib import nextLine
|
||||
|
||||
|
||||
def compareEvents(
|
||||
test: unittest.TestCase,
|
||||
actualEvents: List[LogEvent],
|
||||
expectedEvents: List[LogEvent],
|
||||
) -> None:
|
||||
"""
|
||||
Compare two sequences of log events, examining only the the keys which are
|
||||
present in both.
|
||||
|
||||
@param test: a test case doing the comparison
|
||||
@param actualEvents: A list of log events that were emitted by a logger.
|
||||
@param expectedEvents: A list of log events that were expected by a test.
|
||||
"""
|
||||
if len(actualEvents) != len(expectedEvents):
|
||||
test.assertEqual(actualEvents, expectedEvents)
|
||||
allMergedKeys = set()
|
||||
|
||||
for event in expectedEvents:
|
||||
allMergedKeys |= set(event.keys())
|
||||
|
||||
def simplify(event: LogEvent) -> LogEvent:
|
||||
copy = event.copy()
|
||||
for key in event.keys():
|
||||
if key not in allMergedKeys:
|
||||
copy.pop(key)
|
||||
return copy
|
||||
|
||||
simplifiedActual = [simplify(event) for event in actualEvents]
|
||||
test.assertEqual(simplifiedActual, expectedEvents)
|
||||
|
||||
|
||||
class LogBeginnerTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LogBeginner}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.publisher = LogPublisher()
|
||||
self.errorStream = io.StringIO()
|
||||
|
||||
class NotSys:
|
||||
stdout = object()
|
||||
stderr = object()
|
||||
|
||||
class NotWarnings:
|
||||
def __init__(self) -> None:
|
||||
self.warnings: List[
|
||||
Tuple[
|
||||
str, Type[Warning], str, int, Optional[IO[Any]], Optional[int]
|
||||
]
|
||||
] = []
|
||||
|
||||
def showwarning(
|
||||
self,
|
||||
message: str,
|
||||
category: Type[Warning],
|
||||
filename: str,
|
||||
lineno: int,
|
||||
file: Optional[IO[Any]] = None,
|
||||
line: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Emulate warnings.showwarning.
|
||||
|
||||
@param message: A warning message to emit.
|
||||
@param category: A warning category to associate with
|
||||
C{message}.
|
||||
@param filename: A file name for the source code file issuing
|
||||
the warning.
|
||||
@param lineno: A line number in the source file where the
|
||||
warning was issued.
|
||||
@param file: A file to write the warning message to. If
|
||||
L{None}, write to L{sys.stderr}.
|
||||
@param line: A line of source code to include with the warning
|
||||
message. If L{None}, attempt to read the line from
|
||||
C{filename} and C{lineno}.
|
||||
"""
|
||||
self.warnings.append((message, category, filename, lineno, file, line))
|
||||
|
||||
self.sysModule = NotSys()
|
||||
self.warningsModule = NotWarnings()
|
||||
self.beginner = LogBeginner(
|
||||
self.publisher, self.errorStream, self.sysModule, self.warningsModule
|
||||
)
|
||||
|
||||
def test_beginLoggingToAddObservers(self) -> None:
|
||||
"""
|
||||
Test that C{beginLoggingTo()} adds observers.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
|
||||
events1: List[LogEvent] = []
|
||||
events2: List[LogEvent] = []
|
||||
|
||||
o1 = cast(ILogObserver, lambda e: events1.append(e))
|
||||
o2 = cast(ILogObserver, lambda e: events2.append(e))
|
||||
|
||||
self.beginner.beginLoggingTo((o1, o2))
|
||||
self.publisher(event)
|
||||
|
||||
self.assertEqual([event], events1)
|
||||
self.assertEqual([event], events2)
|
||||
|
||||
def test_beginLoggingToBufferedEvents(self) -> None:
|
||||
"""
|
||||
Test that events are buffered until C{beginLoggingTo()} is
|
||||
called.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
|
||||
events1: List[LogEvent] = []
|
||||
events2: List[LogEvent] = []
|
||||
|
||||
o1 = cast(ILogObserver, lambda e: events1.append(e))
|
||||
o2 = cast(ILogObserver, lambda e: events2.append(e))
|
||||
|
||||
self.publisher(event) # Before beginLoggingTo; this is buffered
|
||||
self.beginner.beginLoggingTo((o1, o2))
|
||||
|
||||
self.assertEqual([event], events1)
|
||||
self.assertEqual([event], events2)
|
||||
|
||||
def _bufferLimitTest(self, limit: int, beginner: LogBeginner) -> None:
|
||||
"""
|
||||
Verify that when more than C{limit} events are logged to L{LogBeginner},
|
||||
only the last C{limit} are replayed by L{LogBeginner.beginLoggingTo}.
|
||||
|
||||
@param limit: The maximum number of events the log beginner should
|
||||
buffer.
|
||||
@param beginner: The L{LogBeginner} against which to verify.
|
||||
|
||||
@raise: C{self.failureException} if the wrong events are replayed by
|
||||
C{beginner}.
|
||||
"""
|
||||
for count in range(limit + 1):
|
||||
self.publisher(dict(count=count))
|
||||
events: List[LogEvent] = []
|
||||
beginner.beginLoggingTo([cast(ILogObserver, events.append)])
|
||||
self.assertEqual(
|
||||
list(range(1, limit + 1)),
|
||||
list(event["count"] for event in events),
|
||||
)
|
||||
|
||||
def test_defaultBufferLimit(self) -> None:
|
||||
"""
|
||||
Up to C{LogBeginner._DEFAULT_BUFFER_SIZE} log events are buffered for
|
||||
replay by L{LogBeginner.beginLoggingTo}.
|
||||
"""
|
||||
limit = LogBeginner._DEFAULT_BUFFER_SIZE
|
||||
self._bufferLimitTest(limit, self.beginner)
|
||||
|
||||
def test_overrideBufferLimit(self) -> None:
|
||||
"""
|
||||
The size of the L{LogBeginner} event buffer can be overridden with the
|
||||
C{initialBufferSize} initilizer argument.
|
||||
"""
|
||||
limit = 3
|
||||
beginner = LogBeginner(
|
||||
self.publisher,
|
||||
self.errorStream,
|
||||
self.sysModule,
|
||||
self.warningsModule,
|
||||
initialBufferSize=limit,
|
||||
)
|
||||
self._bufferLimitTest(limit, beginner)
|
||||
|
||||
def test_beginLoggingToTwice(self) -> None:
|
||||
"""
|
||||
When invoked twice, L{LogBeginner.beginLoggingTo} will emit a log
|
||||
message warning the user that they previously began logging, and add
|
||||
the new log observers.
|
||||
"""
|
||||
events1: List[LogEvent] = []
|
||||
events2: List[LogEvent] = []
|
||||
fileHandle = io.StringIO()
|
||||
textObserver = textFileLogObserver(fileHandle)
|
||||
self.publisher(dict(event="prebuffer"))
|
||||
firstFilename, firstLine = nextLine()
|
||||
self.beginner.beginLoggingTo([cast(ILogObserver, events1.append), textObserver])
|
||||
self.publisher(dict(event="postbuffer"))
|
||||
secondFilename, secondLine = nextLine()
|
||||
self.beginner.beginLoggingTo([cast(ILogObserver, events2.append), textObserver])
|
||||
self.publisher(dict(event="postwarn"))
|
||||
warning = dict(
|
||||
log_format=MORE_THAN_ONCE_WARNING,
|
||||
log_level=LogLevel.warn,
|
||||
fileNow=secondFilename,
|
||||
lineNow=secondLine,
|
||||
fileThen=firstFilename,
|
||||
lineThen=firstLine,
|
||||
)
|
||||
|
||||
self.maxDiff = None
|
||||
compareEvents(
|
||||
self,
|
||||
events1,
|
||||
[
|
||||
dict(event="prebuffer"),
|
||||
dict(event="postbuffer"),
|
||||
warning,
|
||||
dict(event="postwarn"),
|
||||
],
|
||||
)
|
||||
compareEvents(self, events2, [warning, dict(event="postwarn")])
|
||||
|
||||
output = fileHandle.getvalue()
|
||||
self.assertIn(f"<{firstFilename}:{firstLine}>", output)
|
||||
self.assertIn(f"<{secondFilename}:{secondLine}>", output)
|
||||
|
||||
def test_criticalLogging(self) -> None:
|
||||
"""
|
||||
Critical messages will be written as text to the error stream.
|
||||
"""
|
||||
log = Logger(observer=self.publisher)
|
||||
log.info("ignore this")
|
||||
log.critical("a critical {message}", message="message")
|
||||
self.assertEqual(self.errorStream.getvalue(), "a critical message\n")
|
||||
|
||||
def test_criticalLoggingStops(self) -> None:
|
||||
"""
|
||||
Once logging has begun with C{beginLoggingTo}, critical messages are no
|
||||
longer written to the output stream.
|
||||
"""
|
||||
log = Logger(observer=self.publisher)
|
||||
self.beginner.beginLoggingTo(())
|
||||
log.critical("another critical message")
|
||||
self.assertEqual(self.errorStream.getvalue(), "")
|
||||
|
||||
def test_beginLoggingToRedirectStandardIO(self) -> None:
|
||||
"""
|
||||
L{LogBeginner.beginLoggingTo} will re-direct the standard output and
|
||||
error streams by setting the C{stdio} and C{stderr} attributes on its
|
||||
sys module object.
|
||||
"""
|
||||
events: List[LogEvent] = []
|
||||
self.beginner.beginLoggingTo([cast(ILogObserver, events.append)])
|
||||
print("Hello, world.", file=cast(TextIO, self.sysModule.stdout))
|
||||
compareEvents(
|
||||
self, events, [dict(log_namespace="stdout", log_io="Hello, world.")]
|
||||
)
|
||||
del events[:]
|
||||
print("Error, world.", file=cast(TextIO, self.sysModule.stderr))
|
||||
compareEvents(
|
||||
self, events, [dict(log_namespace="stderr", log_io="Error, world.")]
|
||||
)
|
||||
|
||||
def test_beginLoggingToDontRedirect(self) -> None:
|
||||
"""
|
||||
L{LogBeginner.beginLoggingTo} will leave the existing stdout/stderr in
|
||||
place if it has been told not to replace them.
|
||||
"""
|
||||
oldOut = self.sysModule.stdout
|
||||
oldErr = self.sysModule.stderr
|
||||
self.beginner.beginLoggingTo((), redirectStandardIO=False)
|
||||
self.assertIs(self.sysModule.stdout, oldOut)
|
||||
self.assertIs(self.sysModule.stderr, oldErr)
|
||||
|
||||
def test_beginLoggingToPreservesEncoding(self) -> None:
|
||||
"""
|
||||
When L{LogBeginner.beginLoggingTo} redirects stdout/stderr streams, the
|
||||
replacement streams will preserve the encoding of the replaced streams,
|
||||
to minimally disrupt any application relying on a specific encoding.
|
||||
"""
|
||||
|
||||
weird = io.TextIOWrapper(io.BytesIO(), "shift-JIS")
|
||||
weirderr = io.TextIOWrapper(io.BytesIO(), "big5")
|
||||
|
||||
self.sysModule.stdout = weird
|
||||
self.sysModule.stderr = weirderr
|
||||
|
||||
events: List[LogEvent] = []
|
||||
self.beginner.beginLoggingTo([cast(ILogObserver, events.append)])
|
||||
stdout = cast(TextIO, self.sysModule.stdout)
|
||||
stderr = cast(TextIO, self.sysModule.stderr)
|
||||
self.assertEqual(stdout.encoding, "shift-JIS")
|
||||
self.assertEqual(stderr.encoding, "big5")
|
||||
|
||||
stdout.write(b"\x97\x9B\n") # type: ignore[call-overload]
|
||||
stderr.write(b"\xBC\xFC\n") # type: ignore[call-overload]
|
||||
compareEvents(self, events, [dict(log_io="\u674e"), dict(log_io="\u7469")])
|
||||
|
||||
def test_warningsModule(self) -> None:
|
||||
"""
|
||||
L{LogBeginner.beginLoggingTo} will redirect the warnings of its
|
||||
warnings module into the logging system.
|
||||
"""
|
||||
self.warningsModule.showwarning("a message", DeprecationWarning, __file__, 1)
|
||||
events: List[LogEvent] = []
|
||||
self.beginner.beginLoggingTo([cast(ILogObserver, events.append)])
|
||||
self.warningsModule.showwarning(
|
||||
"another message", DeprecationWarning, __file__, 2
|
||||
)
|
||||
f = io.StringIO()
|
||||
self.warningsModule.showwarning(
|
||||
"yet another", DeprecationWarning, __file__, 3, file=f
|
||||
)
|
||||
self.assertEqual(
|
||||
self.warningsModule.warnings,
|
||||
[
|
||||
("a message", DeprecationWarning, __file__, 1, None, None),
|
||||
("yet another", DeprecationWarning, __file__, 3, f, None),
|
||||
],
|
||||
)
|
||||
compareEvents(
|
||||
self,
|
||||
events,
|
||||
[
|
||||
dict(
|
||||
warning="another message",
|
||||
category=(
|
||||
DeprecationWarning.__module__
|
||||
+ "."
|
||||
+ DeprecationWarning.__name__
|
||||
),
|
||||
filename=__file__,
|
||||
lineno=2,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_failuresAppendTracebacks(self) -> None:
|
||||
"""
|
||||
The string resulting from a logged failure contains a traceback.
|
||||
"""
|
||||
f = Failure(Exception("this is not the behavior you are looking for"))
|
||||
log = Logger(observer=self.publisher)
|
||||
log.failure("a failure", failure=f)
|
||||
msg = self.errorStream.getvalue()
|
||||
self.assertIn("a failure", msg)
|
||||
self.assertIn("this is not the behavior you are looking for", msg)
|
||||
self.assertIn("Traceback", msg)
|
||||
@@ -0,0 +1,289 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._io}.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from constantly import NamedConstant
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._io import LoggingFile
|
||||
from .._levels import LogLevel
|
||||
from .._logger import Logger
|
||||
from .._observer import LogPublisher
|
||||
|
||||
|
||||
@implementer(ILogObserver)
|
||||
class TestLoggingFile(LoggingFile):
|
||||
"""
|
||||
L{LoggingFile} that is also an observer which captures events and messages.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logger: Logger,
|
||||
level: NamedConstant = LogLevel.info,
|
||||
encoding: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(logger=logger, level=level, encoding=encoding)
|
||||
self.events: List[LogEvent] = []
|
||||
self.messages: List[str] = []
|
||||
|
||||
def __call__(self, event: LogEvent) -> None:
|
||||
self.events.append(event)
|
||||
if "log_io" in event:
|
||||
self.messages.append(event["log_io"])
|
||||
|
||||
|
||||
class LoggingFileTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LoggingFile}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Create a logger for test L{LoggingFile} instances to use.
|
||||
"""
|
||||
self.publisher = LogPublisher()
|
||||
self.logger = Logger(observer=self.publisher)
|
||||
|
||||
def test_softspace(self) -> None:
|
||||
"""
|
||||
L{LoggingFile.softspace} is 0.
|
||||
"""
|
||||
self.assertEqual(LoggingFile(self.logger).softspace, 0)
|
||||
|
||||
warningsShown = self.flushWarnings([self.test_softspace])
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
self.assertEqual(warningsShown[0]["category"], DeprecationWarning)
|
||||
deprecatedClass = "twisted.logger._io.LoggingFile.softspace"
|
||||
self.assertEqual(
|
||||
warningsShown[0]["message"],
|
||||
"%s was deprecated in Twisted 21.2.0" % (deprecatedClass),
|
||||
)
|
||||
|
||||
def test_readOnlyAttributes(self) -> None:
|
||||
"""
|
||||
Some L{LoggingFile} attributes are read-only.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
|
||||
self.assertRaises(AttributeError, setattr, f, "closed", True)
|
||||
self.assertRaises(AttributeError, setattr, f, "encoding", "utf-8")
|
||||
self.assertRaises(AttributeError, setattr, f, "mode", "r")
|
||||
self.assertRaises(AttributeError, setattr, f, "newlines", ["\n"])
|
||||
self.assertRaises(AttributeError, setattr, f, "name", "foo")
|
||||
|
||||
def test_unsupportedMethods(self) -> None:
|
||||
"""
|
||||
Some L{LoggingFile} methods are unsupported.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
|
||||
self.assertRaises(IOError, f.read)
|
||||
self.assertRaises(IOError, f.next)
|
||||
self.assertRaises(IOError, f.readline)
|
||||
self.assertRaises(IOError, f.readlines)
|
||||
self.assertRaises(IOError, f.xreadlines)
|
||||
self.assertRaises(IOError, f.seek)
|
||||
self.assertRaises(IOError, f.tell)
|
||||
self.assertRaises(IOError, f.truncate)
|
||||
|
||||
def test_level(self) -> None:
|
||||
"""
|
||||
Default level is L{LogLevel.info} if not set.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertEqual(f.level, LogLevel.info)
|
||||
|
||||
f = LoggingFile(self.logger, level=LogLevel.error)
|
||||
self.assertEqual(f.level, LogLevel.error)
|
||||
|
||||
def test_encoding(self) -> None:
|
||||
"""
|
||||
Default encoding is C{sys.getdefaultencoding()} if not set.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertEqual(f.encoding, sys.getdefaultencoding())
|
||||
|
||||
f = LoggingFile(self.logger, encoding="utf-8")
|
||||
self.assertEqual(f.encoding, "utf-8")
|
||||
|
||||
def test_mode(self) -> None:
|
||||
"""
|
||||
Reported mode is C{"w"}.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertEqual(f.mode, "w")
|
||||
|
||||
def test_newlines(self) -> None:
|
||||
"""
|
||||
The C{newlines} attribute is L{None}.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertIsNone(f.newlines)
|
||||
|
||||
def test_name(self) -> None:
|
||||
"""
|
||||
The C{name} attribute is fixed.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertEqual(f.name, "<LoggingFile twisted.logger.test.test_io#info>")
|
||||
|
||||
def test_close(self) -> None:
|
||||
"""
|
||||
L{LoggingFile.close} closes the file.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
f.close()
|
||||
|
||||
self.assertTrue(f.closed)
|
||||
self.assertRaises(ValueError, f.write, "Hello")
|
||||
|
||||
def test_flush(self) -> None:
|
||||
"""
|
||||
L{LoggingFile.flush} does nothing.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
f.flush()
|
||||
|
||||
def test_fileno(self) -> None:
|
||||
"""
|
||||
L{LoggingFile.fileno} returns C{-1}.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertEqual(f.fileno(), -1)
|
||||
|
||||
def test_isatty(self) -> None:
|
||||
"""
|
||||
L{LoggingFile.isatty} returns C{False}.
|
||||
"""
|
||||
f = LoggingFile(self.logger)
|
||||
self.assertFalse(f.isatty())
|
||||
|
||||
def test_writeBuffering(self) -> None:
|
||||
"""
|
||||
Writing buffers correctly.
|
||||
"""
|
||||
f = self.observedFile()
|
||||
f.write("Hello")
|
||||
self.assertEqual(f.messages, [])
|
||||
f.write(", world!\n")
|
||||
self.assertEqual(f.messages, ["Hello, world!"])
|
||||
f.write("It's nice to meet you.\n\nIndeed.")
|
||||
self.assertEqual(
|
||||
f.messages,
|
||||
[
|
||||
"Hello, world!",
|
||||
"It's nice to meet you.",
|
||||
"",
|
||||
],
|
||||
)
|
||||
|
||||
def test_writeBytesDecoded(self) -> None:
|
||||
"""
|
||||
Bytes are decoded to text.
|
||||
"""
|
||||
f = self.observedFile(encoding="utf-8")
|
||||
f.write(b"Hello, Mr. S\xc3\xa1nchez\n")
|
||||
self.assertEqual(f.messages, ["Hello, Mr. S\xe1nchez"])
|
||||
|
||||
def test_writeUnicode(self) -> None:
|
||||
"""
|
||||
Unicode is unmodified.
|
||||
"""
|
||||
f = self.observedFile(encoding="utf-8")
|
||||
f.write("Hello, Mr. S\xe1nchez\n")
|
||||
self.assertEqual(f.messages, ["Hello, Mr. S\xe1nchez"])
|
||||
|
||||
def test_writeLevel(self) -> None:
|
||||
"""
|
||||
Log level is emitted properly.
|
||||
"""
|
||||
f = self.observedFile()
|
||||
f.write("Hello\n")
|
||||
self.assertEqual(len(f.events), 1)
|
||||
self.assertEqual(f.events[0]["log_level"], LogLevel.info)
|
||||
|
||||
f = self.observedFile(level=LogLevel.error)
|
||||
f.write("Hello\n")
|
||||
self.assertEqual(len(f.events), 1)
|
||||
self.assertEqual(f.events[0]["log_level"], LogLevel.error)
|
||||
|
||||
def test_writeFormat(self) -> None:
|
||||
"""
|
||||
Log format is C{"{message}"}.
|
||||
"""
|
||||
f = self.observedFile()
|
||||
f.write("Hello\n")
|
||||
self.assertEqual(len(f.events), 1)
|
||||
self.assertEqual(f.events[0]["log_format"], "{log_io}")
|
||||
|
||||
def test_writelinesBuffering(self) -> None:
|
||||
"""
|
||||
C{writelines} does not add newlines.
|
||||
"""
|
||||
# Note this is different behavior than t.p.log.StdioOnnaStick.
|
||||
f = self.observedFile()
|
||||
f.writelines(("Hello", ", ", ""))
|
||||
self.assertEqual(f.messages, [])
|
||||
f.writelines(("world!\n",))
|
||||
self.assertEqual(f.messages, ["Hello, world!"])
|
||||
f.writelines(("It's nice to meet you.\n\n", "Indeed."))
|
||||
self.assertEqual(
|
||||
f.messages,
|
||||
[
|
||||
"Hello, world!",
|
||||
"It's nice to meet you.",
|
||||
"",
|
||||
],
|
||||
)
|
||||
|
||||
def test_print(self) -> None:
|
||||
"""
|
||||
L{LoggingFile} can replace L{sys.stdout}.
|
||||
"""
|
||||
f = self.observedFile()
|
||||
self.patch(sys, "stdout", f)
|
||||
|
||||
print("Hello,", end=" ")
|
||||
print("world.")
|
||||
|
||||
self.assertEqual(f.messages, ["Hello, world."])
|
||||
|
||||
def observedFile(
|
||||
self,
|
||||
level: NamedConstant = LogLevel.info,
|
||||
encoding: Optional[str] = None,
|
||||
) -> TestLoggingFile:
|
||||
"""
|
||||
Construct a L{LoggingFile} with a built-in observer.
|
||||
|
||||
@param level: C{level} argument to L{LoggingFile}
|
||||
@param encoding: C{encoding} argument to L{LoggingFile}
|
||||
|
||||
@return: a L{TestLoggingFile} with an observer that appends received
|
||||
events into the file's C{events} attribute (a L{list}) and
|
||||
event messages into the file's C{messages} attribute (a L{list}).
|
||||
"""
|
||||
# Logger takes an observer argument, for which we want to use the
|
||||
# TestLoggingFile we will create, but that takes the Logger as an
|
||||
# argument, so we'll use an array to indirectly reference the
|
||||
# TestLoggingFile.
|
||||
loggingFiles: List[TestLoggingFile] = []
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
loggingFiles[0](event)
|
||||
|
||||
log = Logger(observer=observer)
|
||||
loggingFiles.append(TestLoggingFile(logger=log, level=level, encoding=encoding))
|
||||
|
||||
return loggingFiles[0]
|
||||
@@ -0,0 +1,485 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.logger._json}.
|
||||
"""
|
||||
|
||||
from io import BytesIO, StringIO
|
||||
from typing import IO, Any, List, Optional, Sequence, cast
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase
|
||||
from .._flatten import extractField
|
||||
from .._format import formatEvent
|
||||
from .._global import globalLogPublisher
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._json import (
|
||||
eventAsJSON,
|
||||
eventFromJSON,
|
||||
eventsFromJSONLogFile,
|
||||
jsonFileLogObserver,
|
||||
log as jsonLog,
|
||||
)
|
||||
from .._levels import LogLevel
|
||||
from .._logger import Logger
|
||||
from .._observer import LogPublisher
|
||||
|
||||
|
||||
def savedJSONInvariants(testCase: TestCase, savedJSON: str) -> str:
|
||||
"""
|
||||
Assert a few things about the result of L{eventAsJSON}, then return it.
|
||||
|
||||
@param testCase: The L{TestCase} with which to perform the assertions.
|
||||
@param savedJSON: The result of L{eventAsJSON}.
|
||||
|
||||
@return: C{savedJSON}
|
||||
|
||||
@raise AssertionError: If any of the preconditions fail.
|
||||
"""
|
||||
testCase.assertIsInstance(savedJSON, str)
|
||||
testCase.assertEqual(savedJSON.count("\n"), 0)
|
||||
return savedJSON
|
||||
|
||||
|
||||
class SaveLoadTests(TestCase):
|
||||
"""
|
||||
Tests for loading and saving log events.
|
||||
"""
|
||||
|
||||
def savedEventJSON(self, event: LogEvent) -> str:
|
||||
"""
|
||||
Serialize some an events, assert some things about it, and return the
|
||||
JSON.
|
||||
|
||||
@param event: An event.
|
||||
|
||||
@return: JSON.
|
||||
"""
|
||||
return savedJSONInvariants(self, eventAsJSON(event))
|
||||
|
||||
def test_simpleSaveLoad(self) -> None:
|
||||
"""
|
||||
Saving and loading an empty dictionary results in an empty dictionary.
|
||||
"""
|
||||
self.assertEqual(eventFromJSON(self.savedEventJSON({})), {})
|
||||
|
||||
def test_saveLoad(self) -> None:
|
||||
"""
|
||||
Saving and loading a dictionary with some simple values in it results
|
||||
in those same simple values in the output; according to JSON's rules,
|
||||
though, all dictionary keys must be L{str} and any non-L{str}
|
||||
keys will be converted.
|
||||
"""
|
||||
self.assertEqual(
|
||||
eventFromJSON(self.savedEventJSON({1: 2, "3": "4"})), # type: ignore[dict-item]
|
||||
{"1": 2, "3": "4"},
|
||||
)
|
||||
|
||||
def test_saveUnPersistable(self) -> None:
|
||||
"""
|
||||
Saving and loading an object which cannot be represented in JSON will
|
||||
result in a placeholder.
|
||||
"""
|
||||
self.assertEqual(
|
||||
eventFromJSON(self.savedEventJSON({"1": 2, "3": object()})),
|
||||
{"1": 2, "3": {"unpersistable": True}},
|
||||
)
|
||||
|
||||
def test_saveNonASCII(self) -> None:
|
||||
"""
|
||||
Non-ASCII keys and values can be saved and loaded.
|
||||
"""
|
||||
self.assertEqual(
|
||||
eventFromJSON(self.savedEventJSON({"\u1234": "\u4321", "3": object()})),
|
||||
{"\u1234": "\u4321", "3": {"unpersistable": True}},
|
||||
)
|
||||
|
||||
def test_saveBytes(self) -> None:
|
||||
"""
|
||||
Any L{bytes} objects will be saved as if they are latin-1 so they can
|
||||
be faithfully re-loaded.
|
||||
"""
|
||||
inputEvent = {"hello": bytes(range(255))}
|
||||
# On Python 3, bytes keys will be skipped by the JSON encoder. Not
|
||||
# much we can do about that. Let's make sure that we don't get an
|
||||
# error, though.
|
||||
inputEvent.update({b"skipped": "okay"}) # type: ignore[dict-item]
|
||||
self.assertEqual(
|
||||
eventFromJSON(self.savedEventJSON(inputEvent)),
|
||||
{"hello": bytes(range(255)).decode("charmap")},
|
||||
)
|
||||
|
||||
def test_saveUnPersistableThenFormat(self) -> None:
|
||||
"""
|
||||
Saving and loading an object which cannot be represented in JSON, but
|
||||
has a string representation which I{can} be saved as JSON, will result
|
||||
in the same string formatting; any extractable fields will retain their
|
||||
data types.
|
||||
"""
|
||||
|
||||
class Reprable:
|
||||
def __init__(self, value: object) -> None:
|
||||
self.value = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "reprable"
|
||||
|
||||
inputEvent = {"log_format": "{object} {object.value}", "object": Reprable(7)}
|
||||
outputEvent = eventFromJSON(self.savedEventJSON(inputEvent))
|
||||
self.assertEqual(formatEvent(outputEvent), "reprable 7")
|
||||
|
||||
def test_extractingFieldsPostLoad(self) -> None:
|
||||
"""
|
||||
L{extractField} can extract fields from an object that's been saved and
|
||||
loaded from JSON.
|
||||
"""
|
||||
|
||||
class Obj:
|
||||
def __init__(self) -> None:
|
||||
self.value = 345
|
||||
|
||||
inputEvent = dict(log_format="{object.value}", object=Obj())
|
||||
loadedEvent = eventFromJSON(self.savedEventJSON(inputEvent))
|
||||
self.assertEqual(extractField("object.value", loadedEvent), 345)
|
||||
|
||||
# The behavior of extractField is consistent between pre-persistence
|
||||
# and post-persistence events, although looking up the key directly
|
||||
# won't be:
|
||||
self.assertRaises(KeyError, extractField, "object", loadedEvent)
|
||||
self.assertRaises(KeyError, extractField, "object", inputEvent)
|
||||
|
||||
def test_failureStructurePreserved(self) -> None:
|
||||
"""
|
||||
Round-tripping a failure through L{eventAsJSON} preserves its class and
|
||||
structure.
|
||||
"""
|
||||
events: List[LogEvent] = []
|
||||
log = Logger(observer=cast(ILogObserver, events.append))
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
f = Failure()
|
||||
log.failure("a message about failure", f)
|
||||
self.assertEqual(len(events), 1)
|
||||
loaded = eventFromJSON(self.savedEventJSON(events[0]))["log_failure"]
|
||||
self.assertIsInstance(loaded, Failure)
|
||||
self.assertTrue(loaded.check(ZeroDivisionError))
|
||||
self.assertIsInstance(loaded.getTraceback(), str)
|
||||
|
||||
def test_saveLoadLevel(self) -> None:
|
||||
"""
|
||||
It's important that the C{log_level} key remain a
|
||||
L{constantly.NamedConstant} object.
|
||||
"""
|
||||
inputEvent = dict(log_level=LogLevel.warn)
|
||||
loadedEvent = eventFromJSON(self.savedEventJSON(inputEvent))
|
||||
self.assertIs(loadedEvent["log_level"], LogLevel.warn)
|
||||
|
||||
def test_saveLoadUnknownLevel(self) -> None:
|
||||
"""
|
||||
If a saved bit of JSON (let's say, from a future version of Twisted)
|
||||
were to persist a different log_level, it will resolve as None.
|
||||
"""
|
||||
loadedEvent = eventFromJSON(
|
||||
'{"log_level": {"name": "other", '
|
||||
'"__class_uuid__": "02E59486-F24D-46AD-8224-3ACDF2A5732A"}}'
|
||||
)
|
||||
self.assertEqual(loadedEvent, dict(log_level=None))
|
||||
|
||||
|
||||
class FileLogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{jsonFileLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
A L{FileLogObserver} returned by L{jsonFileLogObserver} is an
|
||||
L{ILogObserver}.
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = jsonFileLogObserver(fileHandle)
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def assertObserverWritesJSON(self, recordSeparator: str = "\x1e") -> None:
|
||||
"""
|
||||
Asserts that an observer created by L{jsonFileLogObserver} with the
|
||||
given arguments writes events serialized as JSON text, using the given
|
||||
record separator.
|
||||
|
||||
@param recordSeparator: C{recordSeparator} argument to
|
||||
L{jsonFileLogObserver}
|
||||
"""
|
||||
with StringIO() as fileHandle:
|
||||
observer = jsonFileLogObserver(fileHandle, recordSeparator)
|
||||
event = dict(x=1)
|
||||
observer(event)
|
||||
self.assertEqual(fileHandle.getvalue(), f'{recordSeparator}{{"x": 1}}\n')
|
||||
|
||||
def test_observeWritesDefaultRecordSeparator(self) -> None:
|
||||
"""
|
||||
A L{FileLogObserver} created by L{jsonFileLogObserver} writes events
|
||||
serialzed as JSON text to a file when it observes events.
|
||||
By default, the record separator is C{"\\x1e"}.
|
||||
"""
|
||||
self.assertObserverWritesJSON()
|
||||
|
||||
def test_observeWritesEmptyRecordSeparator(self) -> None:
|
||||
"""
|
||||
A L{FileLogObserver} created by L{jsonFileLogObserver} writes events
|
||||
serialzed as JSON text to a file when it observes events.
|
||||
This test sets the record separator to C{""}.
|
||||
"""
|
||||
self.assertObserverWritesJSON(recordSeparator="")
|
||||
|
||||
def test_failureFormatting(self) -> None:
|
||||
"""
|
||||
A L{FileLogObserver} created by L{jsonFileLogObserver} writes failures
|
||||
serialized as JSON text to a file when it observes events.
|
||||
"""
|
||||
io = StringIO()
|
||||
publisher = LogPublisher()
|
||||
logged: List[LogEvent] = []
|
||||
publisher.addObserver(cast(ILogObserver, logged.append))
|
||||
publisher.addObserver(jsonFileLogObserver(io))
|
||||
logger = Logger(observer=publisher)
|
||||
try:
|
||||
1 / 0
|
||||
except BaseException:
|
||||
logger.failure("failed as expected")
|
||||
reader = StringIO(io.getvalue())
|
||||
deserialized = list(eventsFromJSONLogFile(reader))
|
||||
|
||||
def checkEvents(logEvents: Sequence[LogEvent]) -> None:
|
||||
self.assertEqual(len(logEvents), 1)
|
||||
[failureEvent] = logEvents
|
||||
self.assertIn("log_failure", failureEvent)
|
||||
failureObject = failureEvent["log_failure"]
|
||||
self.assertIsInstance(failureObject, Failure)
|
||||
tracebackObject = failureObject.getTracebackObject()
|
||||
self.assertEqual(
|
||||
tracebackObject.tb_frame.f_code.co_filename.rstrip("co"),
|
||||
__file__.rstrip("co"),
|
||||
)
|
||||
|
||||
checkEvents(logged)
|
||||
checkEvents(deserialized)
|
||||
|
||||
|
||||
class LogFileReaderTests(TestCase):
|
||||
"""
|
||||
Tests for L{eventsFromJSONLogFile}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.errorEvents: List[LogEvent] = []
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
if event["log_namespace"] == jsonLog.namespace and "record" in event:
|
||||
self.errorEvents.append(event)
|
||||
|
||||
self.logObserver = observer
|
||||
|
||||
globalLogPublisher.addObserver(observer)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
globalLogPublisher.removeObserver(self.logObserver)
|
||||
|
||||
def _readEvents(
|
||||
self,
|
||||
inFile: IO[Any],
|
||||
recordSeparator: Optional[str] = None,
|
||||
bufferSize: int = 4096,
|
||||
) -> None:
|
||||
"""
|
||||
Test that L{eventsFromJSONLogFile} reads two pre-defined events from a
|
||||
file: C{{"x": 1}} and C{{"y": 2}}.
|
||||
|
||||
@param inFile: C{inFile} argument to L{eventsFromJSONLogFile}
|
||||
@param recordSeparator: C{recordSeparator} argument to
|
||||
L{eventsFromJSONLogFile}
|
||||
@param bufferSize: C{bufferSize} argument to L{eventsFromJSONLogFile}
|
||||
"""
|
||||
events = iter(eventsFromJSONLogFile(inFile, recordSeparator, bufferSize))
|
||||
|
||||
self.assertEqual(next(events), {"x": 1})
|
||||
self.assertEqual(next(events), {"y": 2})
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
|
||||
def test_readEventsAutoWithRecordSeparator(self) -> None:
|
||||
"""
|
||||
L{eventsFromJSONLogFile} reads events from a file and automatically
|
||||
detects use of C{"\\x1e"} as the record separator.
|
||||
"""
|
||||
with StringIO('\x1e{"x": 1}\n' '\x1e{"y": 2}\n') as fileHandle:
|
||||
self._readEvents(fileHandle)
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readEventsAutoEmptyRecordSeparator(self) -> None:
|
||||
"""
|
||||
L{eventsFromJSONLogFile} reads events from a file and automatically
|
||||
detects use of C{""} as the record separator.
|
||||
"""
|
||||
with StringIO('{"x": 1}\n' '{"y": 2}\n') as fileHandle:
|
||||
self._readEvents(fileHandle)
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readEventsExplicitRecordSeparator(self) -> None:
|
||||
"""
|
||||
L{eventsFromJSONLogFile} reads events from a file and is told to use
|
||||
a specific record separator.
|
||||
"""
|
||||
# Use "\x08" (backspace)... because that seems weird enough.
|
||||
with StringIO('\x08{"x": 1}\n' '\x08{"y": 2}\n') as fileHandle:
|
||||
self._readEvents(fileHandle, recordSeparator="\x08")
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readEventsPartialBuffer(self) -> None:
|
||||
"""
|
||||
L{eventsFromJSONLogFile} handles buffering a partial event.
|
||||
"""
|
||||
with StringIO('\x1e{"x": 1}\n' '\x1e{"y": 2}\n') as fileHandle:
|
||||
# Use a buffer size smaller than the event text.
|
||||
self._readEvents(fileHandle, bufferSize=1)
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readTruncated(self) -> None:
|
||||
"""
|
||||
If the JSON text for a record is truncated, skip it.
|
||||
"""
|
||||
with StringIO('\x1e{"x": 1' '\x1e{"y": 2}\n') as fileHandle:
|
||||
events = iter(eventsFromJSONLogFile(fileHandle))
|
||||
|
||||
self.assertEqual(next(events), {"y": 2})
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
|
||||
# We should have logged the lost record
|
||||
self.assertEqual(len(self.errorEvents), 1)
|
||||
self.assertEqual(
|
||||
self.errorEvents[0]["log_format"],
|
||||
"Unable to read truncated JSON record: {record!r}",
|
||||
)
|
||||
self.assertEqual(self.errorEvents[0]["record"], b'{"x": 1')
|
||||
|
||||
def test_readUnicode(self) -> None:
|
||||
"""
|
||||
If the file being read from vends L{str}, strings decode from JSON
|
||||
as-is.
|
||||
"""
|
||||
# The Euro currency sign is "\u20ac"
|
||||
with StringIO('\x1e{"currency": "\u20ac"}\n') as fileHandle:
|
||||
events = iter(eventsFromJSONLogFile(fileHandle))
|
||||
|
||||
self.assertEqual(next(events), {"currency": "\u20ac"})
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readUTF8Bytes(self) -> None:
|
||||
"""
|
||||
If the file being read from vends L{bytes}, strings decode from JSON as
|
||||
UTF-8.
|
||||
"""
|
||||
# The Euro currency sign is b"\xe2\x82\xac" in UTF-8
|
||||
with BytesIO(b'\x1e{"currency": "\xe2\x82\xac"}\n') as fileHandle:
|
||||
events = iter(eventsFromJSONLogFile(fileHandle))
|
||||
|
||||
# The Euro currency sign is "\u20ac"
|
||||
self.assertEqual(next(events), {"currency": "\u20ac"})
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readTruncatedUTF8Bytes(self) -> None:
|
||||
"""
|
||||
If the JSON text for a record is truncated in the middle of a two-byte
|
||||
Unicode codepoint, we don't want to see a codec exception and the
|
||||
stream is read properly when the additional data arrives.
|
||||
"""
|
||||
# The Euro currency sign is "\u20ac" and encodes in UTF-8 as three
|
||||
# bytes: b"\xe2\x82\xac".
|
||||
with BytesIO(b'\x1e{"x": "\xe2\x82\xac"}\n') as fileHandle:
|
||||
events = iter(eventsFromJSONLogFile(fileHandle, bufferSize=8))
|
||||
|
||||
self.assertEqual(next(events), {"x": "\u20ac"}) # Got text
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
|
||||
def test_readInvalidUTF8Bytes(self) -> None:
|
||||
"""
|
||||
If the JSON text for a record contains invalid UTF-8 text, ignore that
|
||||
record.
|
||||
"""
|
||||
# The string b"\xe2\xac" is bogus
|
||||
with BytesIO(b'\x1e{"x": "\xe2\xac"}\n' b'\x1e{"y": 2}\n') as fileHandle:
|
||||
events = iter(eventsFromJSONLogFile(fileHandle))
|
||||
|
||||
self.assertEqual(next(events), {"y": 2})
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
|
||||
# We should have logged the lost record
|
||||
self.assertEqual(len(self.errorEvents), 1)
|
||||
self.assertEqual(
|
||||
self.errorEvents[0]["log_format"],
|
||||
"Unable to decode UTF-8 for JSON record: {record!r}",
|
||||
)
|
||||
self.assertEqual(self.errorEvents[0]["record"], b'{"x": "\xe2\xac"}\n')
|
||||
|
||||
def test_readInvalidJSON(self) -> None:
|
||||
"""
|
||||
If the JSON text for a record is invalid, skip it.
|
||||
"""
|
||||
with StringIO('\x1e{"x": }\n' '\x1e{"y": 2}\n') as fileHandle:
|
||||
events = iter(eventsFromJSONLogFile(fileHandle))
|
||||
|
||||
self.assertEqual(next(events), {"y": 2})
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
|
||||
# We should have logged the lost record
|
||||
self.assertEqual(len(self.errorEvents), 1)
|
||||
self.assertEqual(
|
||||
self.errorEvents[0]["log_format"],
|
||||
"Unable to read JSON record: {record!r}",
|
||||
)
|
||||
self.assertEqual(self.errorEvents[0]["record"], b'{"x": }\n')
|
||||
|
||||
def test_readUnseparated(self) -> None:
|
||||
"""
|
||||
Multiple events without a record separator are skipped.
|
||||
"""
|
||||
with StringIO('\x1e{"x": 1}\n' '{"y": 2}\n') as fileHandle:
|
||||
events = eventsFromJSONLogFile(fileHandle)
|
||||
|
||||
self.assertRaises(StopIteration, next, events) # No more events
|
||||
|
||||
# We should have logged the lost record
|
||||
self.assertEqual(len(self.errorEvents), 1)
|
||||
self.assertEqual(
|
||||
self.errorEvents[0]["log_format"],
|
||||
"Unable to read JSON record: {record!r}",
|
||||
)
|
||||
self.assertEqual(self.errorEvents[0]["record"], b'{"x": 1}\n{"y": 2}\n')
|
||||
|
||||
def test_roundTrip(self) -> None:
|
||||
"""
|
||||
Data written by a L{FileLogObserver} returned by L{jsonFileLogObserver}
|
||||
and read by L{eventsFromJSONLogFile} is reconstructed properly.
|
||||
"""
|
||||
event = dict(x=1)
|
||||
|
||||
with StringIO() as fileHandle:
|
||||
observer = jsonFileLogObserver(fileHandle)
|
||||
observer(event)
|
||||
|
||||
fileHandle.seek(0)
|
||||
events = eventsFromJSONLogFile(fileHandle)
|
||||
|
||||
self.assertEqual(tuple(events), (event,))
|
||||
self.assertEqual(len(self.errorEvents), 0)
|
||||
@@ -0,0 +1,413 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._legacy}.
|
||||
"""
|
||||
|
||||
import logging as py_logging
|
||||
from time import time
|
||||
from typing import List, cast
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python import context, log as legacyLog
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import unittest
|
||||
from .._format import formatEvent
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._legacy import LegacyLogObserverWrapper, publishToNewObserver
|
||||
from .._levels import LogLevel
|
||||
|
||||
|
||||
class LegacyLogObserverWrapperTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LegacyLogObserverWrapper}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{LegacyLogObserverWrapper} is an L{ILogObserver}.
|
||||
"""
|
||||
legacyObserver = cast(legacyLog.ILogObserver, lambda e: None)
|
||||
observer = LegacyLogObserverWrapper(legacyObserver)
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
L{LegacyLogObserverWrapper} returns the expected string.
|
||||
"""
|
||||
|
||||
@implementer(legacyLog.ILogObserver)
|
||||
class LegacyObserver:
|
||||
def __repr__(self) -> str:
|
||||
return "<Legacy Observer>"
|
||||
|
||||
def __call__(self, eventDict: legacyLog.EventDict) -> None:
|
||||
return
|
||||
|
||||
observer = LegacyLogObserverWrapper(LegacyObserver())
|
||||
|
||||
self.assertEqual(repr(observer), "LegacyLogObserverWrapper(<Legacy Observer>)")
|
||||
|
||||
def observe(self, event: LogEvent) -> LogEvent:
|
||||
"""
|
||||
Send an event to a wrapped legacy observer and capture the event as
|
||||
seen by that observer.
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: the event as observed by the legacy wrapper
|
||||
"""
|
||||
events: List[LogEvent] = []
|
||||
|
||||
legacyObserver = cast(legacyLog.ILogObserver, lambda e: events.append(e))
|
||||
observer = LegacyLogObserverWrapper(legacyObserver)
|
||||
observer(event)
|
||||
self.assertEqual(len(events), 1)
|
||||
|
||||
return events[0]
|
||||
|
||||
def forwardAndVerify(self, event: LogEvent) -> LogEvent:
|
||||
"""
|
||||
Send an event to a wrapped legacy observer and verify that its data is
|
||||
preserved.
|
||||
|
||||
@param event: an event
|
||||
|
||||
@return: the event as observed by the legacy wrapper
|
||||
"""
|
||||
# Make sure keys that are expected by the logging system are present
|
||||
event.setdefault("log_time", time())
|
||||
event.setdefault("log_system", "-")
|
||||
event.setdefault("log_level", LogLevel.info)
|
||||
|
||||
# Send a copy: don't mutate me, bro
|
||||
observed = self.observe(dict(event))
|
||||
|
||||
# Don't expect modifications
|
||||
for key, value in event.items():
|
||||
self.assertIn(key, observed)
|
||||
|
||||
return observed
|
||||
|
||||
def test_forward(self) -> None:
|
||||
"""
|
||||
Basic forwarding: event keys as observed by a legacy observer are the
|
||||
same.
|
||||
"""
|
||||
self.forwardAndVerify(dict(foo=1, bar=2))
|
||||
|
||||
def test_time(self) -> None:
|
||||
"""
|
||||
The new-style C{"log_time"} key is copied to the old-style C{"time"}
|
||||
key.
|
||||
"""
|
||||
stamp = time()
|
||||
event = self.forwardAndVerify(dict(log_time=stamp))
|
||||
self.assertEqual(event["time"], stamp)
|
||||
|
||||
def test_timeAlreadySet(self) -> None:
|
||||
"""
|
||||
The new-style C{"log_time"} key does not step on a pre-existing
|
||||
old-style C{"time"} key.
|
||||
"""
|
||||
stamp = time()
|
||||
event = self.forwardAndVerify(dict(log_time=stamp + 1, time=stamp))
|
||||
self.assertEqual(event["time"], stamp)
|
||||
|
||||
def test_system(self) -> None:
|
||||
"""
|
||||
The new-style C{"log_system"} key is copied to the old-style
|
||||
C{"system"} key.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict(log_system="foo"))
|
||||
self.assertEqual(event["system"], "foo")
|
||||
|
||||
def test_systemAlreadySet(self) -> None:
|
||||
"""
|
||||
The new-style C{"log_system"} key does not step on a pre-existing
|
||||
old-style C{"system"} key.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict(log_system="foo", system="bar"))
|
||||
self.assertEqual(event["system"], "bar")
|
||||
|
||||
def test_noSystem(self) -> None:
|
||||
"""
|
||||
If the new-style C{"log_system"} key is absent, the old-style
|
||||
C{"system"} key is set to C{"-"}.
|
||||
"""
|
||||
# Don't use forwardAndVerify(), since that sets log_system.
|
||||
event = dict(log_time=time(), log_level=LogLevel.info)
|
||||
observed = self.observe(dict(event))
|
||||
self.assertEqual(observed["system"], "-")
|
||||
|
||||
def test_levelNotChange(self) -> None:
|
||||
"""
|
||||
If explicitly set, the C{isError} key will be preserved when forwarding
|
||||
from a new-style logging emitter to a legacy logging observer,
|
||||
regardless of log level.
|
||||
"""
|
||||
self.forwardAndVerify(dict(log_level=LogLevel.info, isError=1))
|
||||
self.forwardAndVerify(dict(log_level=LogLevel.warn, isError=1))
|
||||
self.forwardAndVerify(dict(log_level=LogLevel.error, isError=0))
|
||||
self.forwardAndVerify(dict(log_level=LogLevel.critical, isError=0))
|
||||
|
||||
def test_pythonLogLevelNotSet(self) -> None:
|
||||
"""
|
||||
The new-style C{"log_level"} key is not translated to the old-style
|
||||
C{"logLevel"} key.
|
||||
|
||||
Events are forwarded from the old module from to new module and are
|
||||
then seen by old-style observers.
|
||||
We don't want to add unexpected keys to old-style events.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict(log_level=LogLevel.info))
|
||||
self.assertNotIn("logLevel", event)
|
||||
|
||||
def test_stringPythonLogLevel(self) -> None:
|
||||
"""
|
||||
If a stdlib log level was provided as a string (eg. C{"WARNING"}) in
|
||||
the legacy "logLevel" key, it does not get converted to a number.
|
||||
The documentation suggested that numerical values should be used but
|
||||
this was not a requirement.
|
||||
"""
|
||||
event = self.forwardAndVerify(
|
||||
dict(
|
||||
logLevel="WARNING", # py_logging.WARNING is 30
|
||||
)
|
||||
)
|
||||
self.assertEqual(event["logLevel"], "WARNING")
|
||||
|
||||
def test_message(self) -> None:
|
||||
"""
|
||||
The old-style C{"message"} key is added, even if no new-style
|
||||
C{"log_format"} is given, as it is required, but may be empty.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict())
|
||||
self.assertEqual(event["message"], ()) # "message" is a tuple
|
||||
|
||||
def test_messageAlreadySet(self) -> None:
|
||||
"""
|
||||
The old-style C{"message"} key is not modified if it already exists.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict(message=("foo", "bar")))
|
||||
self.assertEqual(event["message"], ("foo", "bar"))
|
||||
|
||||
def test_format(self) -> None:
|
||||
"""
|
||||
Formatting is translated such that text is rendered correctly, even
|
||||
though old-style logging doesn't use PEP 3101 formatting.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict(log_format="Hello, {who}!", who="world"))
|
||||
self.assertEqual(legacyLog.textFromEventDict(event), "Hello, world!")
|
||||
|
||||
def test_formatMessage(self) -> None:
|
||||
"""
|
||||
Using the message key, which is special in old-style, works for
|
||||
new-style formatting.
|
||||
"""
|
||||
event = self.forwardAndVerify(
|
||||
dict(log_format="Hello, {message}!", message="world")
|
||||
)
|
||||
self.assertEqual(legacyLog.textFromEventDict(event), "Hello, world!")
|
||||
|
||||
def test_formatAlreadySet(self) -> None:
|
||||
"""
|
||||
Formatting is not altered if the old-style C{"format"} key already
|
||||
exists.
|
||||
"""
|
||||
event = self.forwardAndVerify(dict(log_format="Hello!", format="Howdy!"))
|
||||
self.assertEqual(legacyLog.textFromEventDict(event), "Howdy!")
|
||||
|
||||
def eventWithFailure(self, **values: object) -> LogEvent:
|
||||
"""
|
||||
Create a new-style event with a captured failure.
|
||||
|
||||
@param values: Additional values to include in the event.
|
||||
|
||||
@return: the new event
|
||||
"""
|
||||
failure = Failure(RuntimeError("nyargh!"))
|
||||
return self.forwardAndVerify(
|
||||
dict(log_failure=failure, log_format="oopsie...", **values)
|
||||
)
|
||||
|
||||
def test_failure(self) -> None:
|
||||
"""
|
||||
Captured failures in the new style set the old-style C{"failure"},
|
||||
C{"isError"}, and C{"why"} keys.
|
||||
"""
|
||||
event = self.eventWithFailure()
|
||||
self.assertIs(event["failure"], event["log_failure"])
|
||||
self.assertTrue(event["isError"])
|
||||
self.assertEqual(event["why"], "oopsie...")
|
||||
|
||||
def test_failureAlreadySet(self) -> None:
|
||||
"""
|
||||
Captured failures in the new style do not step on a pre-existing
|
||||
old-style C{"failure"} key.
|
||||
"""
|
||||
failure = Failure(RuntimeError("Weak salsa!"))
|
||||
event = self.eventWithFailure(failure=failure)
|
||||
self.assertIs(event["failure"], failure)
|
||||
|
||||
def test_isErrorAlreadySet(self) -> None:
|
||||
"""
|
||||
Captured failures in the new style do not step on a pre-existing
|
||||
old-style C{"isError"} key.
|
||||
"""
|
||||
event = self.eventWithFailure(isError=0)
|
||||
self.assertEqual(event["isError"], 0)
|
||||
|
||||
def test_whyAlreadySet(self) -> None:
|
||||
"""
|
||||
Captured failures in the new style do not step on a pre-existing
|
||||
old-style C{"failure"} key.
|
||||
"""
|
||||
event = self.eventWithFailure(why="blah")
|
||||
self.assertEqual(event["why"], "blah")
|
||||
|
||||
|
||||
class PublishToNewObserverTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{publishToNewObserver}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.events: List[LogEvent] = []
|
||||
self.observer = cast(ILogObserver, self.events.append)
|
||||
|
||||
def legacyEvent(self, *message: str, **values: object) -> legacyLog.EventDict:
|
||||
"""
|
||||
Return a basic old-style event as would be created by L{legacyLog.msg}.
|
||||
|
||||
@param message: a message event value in the legacy event format
|
||||
@param values: additional event values in the legacy event format
|
||||
|
||||
@return: a legacy event
|
||||
"""
|
||||
event = (context.get(legacyLog.ILogContext) or {}).copy()
|
||||
event.update(values)
|
||||
event["message"] = message
|
||||
event["time"] = time()
|
||||
if "isError" not in event:
|
||||
event["isError"] = 0
|
||||
return event
|
||||
|
||||
def test_observed(self) -> None:
|
||||
"""
|
||||
The observer is called exactly once.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer, self.legacyEvent(), legacyLog.textFromEventDict
|
||||
)
|
||||
self.assertEqual(len(self.events), 1)
|
||||
|
||||
def test_time(self) -> None:
|
||||
"""
|
||||
The old-style C{"time"} key is copied to the new-style C{"log_time"}
|
||||
key.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer, self.legacyEvent(), legacyLog.textFromEventDict
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_time"], self.events[0]["time"])
|
||||
|
||||
def test_message(self) -> None:
|
||||
"""
|
||||
A published old-style event should format as text in the same way as
|
||||
the given C{textFromEventDict} callable would format it.
|
||||
"""
|
||||
|
||||
def textFromEventDict(event: LogEvent) -> str:
|
||||
return "".join(reversed(" ".join(event["message"])))
|
||||
|
||||
event = self.legacyEvent("Hello,", "world!")
|
||||
text = textFromEventDict(event)
|
||||
|
||||
publishToNewObserver(self.observer, event, textFromEventDict)
|
||||
self.assertEqual(formatEvent(self.events[0]), text)
|
||||
|
||||
def test_defaultLogLevel(self) -> None:
|
||||
"""
|
||||
Published event should have log level of L{LogLevel.info}.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer, self.legacyEvent(), legacyLog.textFromEventDict
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_level"], LogLevel.info)
|
||||
|
||||
def test_isError(self) -> None:
|
||||
"""
|
||||
If C{"isError"} is set to C{1} (true) on the legacy event, the
|
||||
C{"log_level"} key should get set to L{LogLevel.critical}.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer, self.legacyEvent(isError=1), legacyLog.textFromEventDict
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_level"], LogLevel.critical)
|
||||
|
||||
def test_stdlibLogLevel(self) -> None:
|
||||
"""
|
||||
If the old-style C{"logLevel"} key is set to a standard library logging
|
||||
level, using a predefined (L{int}) constant, the new-style
|
||||
C{"log_level"} key should get set to the corresponding log level.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer,
|
||||
self.legacyEvent(logLevel=py_logging.WARNING),
|
||||
legacyLog.textFromEventDict,
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_level"], LogLevel.warn)
|
||||
|
||||
def test_stdlibLogLevelWithString(self) -> None:
|
||||
"""
|
||||
If the old-style C{"logLevel"} key is set to a standard library logging
|
||||
level, using a string value, the new-style C{"log_level"} key should
|
||||
get set to the corresponding log level.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer,
|
||||
self.legacyEvent(logLevel="WARNING"),
|
||||
legacyLog.textFromEventDict,
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_level"], LogLevel.warn)
|
||||
|
||||
def test_stdlibLogLevelWithGarbage(self) -> None:
|
||||
"""
|
||||
If the old-style C{"logLevel"} key is set to a standard library logging
|
||||
level, using an unknown value, the new-style C{"log_level"} key should
|
||||
not get set.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer,
|
||||
self.legacyEvent(logLevel="Foo!!!!!"),
|
||||
legacyLog.textFromEventDict,
|
||||
)
|
||||
self.assertNotIn("log_level", self.events[0])
|
||||
|
||||
def test_defaultNamespace(self) -> None:
|
||||
"""
|
||||
Published event should have a namespace of C{"log_legacy"} to indicate
|
||||
that it was forwarded from legacy logging.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer, self.legacyEvent(), legacyLog.textFromEventDict
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_namespace"], "log_legacy")
|
||||
|
||||
def test_system(self) -> None:
|
||||
"""
|
||||
The old-style C{"system"} key is copied to the new-style
|
||||
C{"log_system"} key.
|
||||
"""
|
||||
publishToNewObserver(
|
||||
self.observer, self.legacyEvent(), legacyLog.textFromEventDict
|
||||
)
|
||||
self.assertEqual(self.events[0]["log_system"], self.events[0]["system"])
|
||||
@@ -0,0 +1,34 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._levels}.
|
||||
"""
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._levels import InvalidLogLevelError, LogLevel
|
||||
|
||||
|
||||
class LogLevelTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LogLevel}.
|
||||
"""
|
||||
|
||||
def test_levelWithName(self) -> None:
|
||||
"""
|
||||
Look up log level by name.
|
||||
"""
|
||||
for level in LogLevel.iterconstants():
|
||||
self.assertIs(LogLevel.levelWithName(level.name), level)
|
||||
|
||||
def test_levelWithInvalidName(self) -> None:
|
||||
"""
|
||||
You can't make up log level names.
|
||||
"""
|
||||
bogus = "*bogus*"
|
||||
try:
|
||||
LogLevel.levelWithName(bogus)
|
||||
except InvalidLogLevelError as e:
|
||||
self.assertIs(e.level, bogus)
|
||||
else:
|
||||
self.fail("Expected InvalidLogLevelError.")
|
||||
@@ -0,0 +1,354 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._logger}.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Type, cast
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from constantly import NamedConstant
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import unittest
|
||||
from .._format import formatEvent
|
||||
from .._global import globalLogPublisher
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._levels import InvalidLogLevelError, LogLevel
|
||||
from .._logger import Logger
|
||||
|
||||
|
||||
class TestLogger(Logger):
|
||||
"""
|
||||
L{Logger} with an overridden C{emit} method that keeps track of received
|
||||
events.
|
||||
"""
|
||||
|
||||
def emit(
|
||||
self, level: NamedConstant, format: Optional[str] = None, **kwargs: object
|
||||
) -> None:
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
self.event = event
|
||||
|
||||
globalLogPublisher.addObserver(observer)
|
||||
try:
|
||||
Logger.emit(self, level, format, **kwargs)
|
||||
finally:
|
||||
globalLogPublisher.removeObserver(observer)
|
||||
|
||||
self.emitted = {
|
||||
"level": level,
|
||||
"format": format,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
|
||||
class LogComposedObject:
|
||||
"""
|
||||
A regular object, with a logger attached.
|
||||
"""
|
||||
|
||||
log = TestLogger()
|
||||
|
||||
def __init__(self, state: Optional[str] = None) -> None:
|
||||
self.state = state
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<LogComposedObject {self.state}>"
|
||||
|
||||
|
||||
class LoggerTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{Logger}.
|
||||
"""
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
repr() on Logger
|
||||
"""
|
||||
namespace = "bleargh"
|
||||
log = Logger(namespace)
|
||||
self.assertEqual(repr(log), f"<Logger {repr(namespace)}>")
|
||||
|
||||
def test_namespaceDefault(self) -> None:
|
||||
"""
|
||||
Default namespace is module name.
|
||||
"""
|
||||
log = Logger()
|
||||
self.assertEqual(log.namespace, __name__)
|
||||
|
||||
def test_namespaceOMGItsTooHard(self) -> None:
|
||||
"""
|
||||
Default namespace is C{"<unknown>"} when a logger is created from a
|
||||
context in which is can't be determined automatically and no namespace
|
||||
was specified.
|
||||
"""
|
||||
result: List[Logger] = []
|
||||
exec(
|
||||
"result.append(Logger())",
|
||||
dict(Logger=Logger),
|
||||
locals(),
|
||||
)
|
||||
self.assertEqual(result[0].namespace, "<unknown>")
|
||||
|
||||
def test_namespaceAttribute(self) -> None:
|
||||
"""
|
||||
Default namespace for classes using L{Logger} as a descriptor is the
|
||||
class name they were retrieved from.
|
||||
"""
|
||||
obj = LogComposedObject()
|
||||
|
||||
expectedNamespace = "{}.{}".format(
|
||||
obj.__module__,
|
||||
obj.__class__.__name__,
|
||||
)
|
||||
|
||||
self.assertEqual(cast(TestLogger, obj.log).namespace, expectedNamespace)
|
||||
self.assertEqual(
|
||||
cast(Type[TestLogger], LogComposedObject.log).namespace, expectedNamespace
|
||||
)
|
||||
self.assertIs(
|
||||
cast(Type[TestLogger], LogComposedObject.log).source, LogComposedObject
|
||||
)
|
||||
self.assertIs(cast(TestLogger, obj.log).source, obj)
|
||||
self.assertIsNone(Logger().source)
|
||||
|
||||
def test_descriptorObserver(self) -> None:
|
||||
"""
|
||||
When used as a descriptor, the observer is propagated.
|
||||
"""
|
||||
observed: List[LogEvent] = []
|
||||
|
||||
class MyObject:
|
||||
log = Logger(observer=cast(ILogObserver, observed.append))
|
||||
|
||||
MyObject.log.info("hello")
|
||||
self.assertEqual(len(observed), 1)
|
||||
self.assertEqual(observed[0]["log_format"], "hello")
|
||||
|
||||
def test_sourceAvailableForFormatting(self) -> None:
|
||||
"""
|
||||
On instances that have a L{Logger} class attribute, the C{log_source}
|
||||
key is available to format strings.
|
||||
"""
|
||||
obj = LogComposedObject("hello")
|
||||
log = cast(TestLogger, obj.log)
|
||||
log.error("Hello, {log_source}.")
|
||||
|
||||
self.assertIn("log_source", log.event)
|
||||
self.assertEqual(log.event["log_source"], obj)
|
||||
|
||||
stuff = formatEvent(log.event)
|
||||
self.assertIn("Hello, <LogComposedObject hello>.", stuff)
|
||||
|
||||
def test_basicLogger(self) -> None:
|
||||
"""
|
||||
Test that log levels and messages are emitted correctly for
|
||||
Logger.
|
||||
"""
|
||||
log = TestLogger()
|
||||
|
||||
for level in LogLevel.iterconstants():
|
||||
format = "This is a {level_name} message"
|
||||
message = format.format(level_name=level.name)
|
||||
|
||||
logMethod = getattr(log, level.name)
|
||||
logMethod(format, junk=message, level_name=level.name)
|
||||
|
||||
# Ensure that test_emit got called with expected arguments
|
||||
self.assertEqual(log.emitted["level"], level)
|
||||
self.assertEqual(log.emitted["format"], format)
|
||||
self.assertEqual(log.emitted["kwargs"]["junk"], message)
|
||||
|
||||
self.assertTrue(hasattr(log, "event"), "No event observed.")
|
||||
|
||||
self.assertEqual(log.event["log_format"], format)
|
||||
self.assertEqual(log.event["log_level"], level)
|
||||
self.assertEqual(log.event["log_namespace"], __name__)
|
||||
self.assertIsNone(log.event["log_source"])
|
||||
self.assertEqual(log.event["junk"], message)
|
||||
|
||||
self.assertEqual(formatEvent(log.event), message)
|
||||
|
||||
def test_sourceOnClass(self) -> None:
|
||||
"""
|
||||
C{log_source} event key refers to the class.
|
||||
"""
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
self.assertEqual(event["log_source"], Thingo)
|
||||
|
||||
class Thingo:
|
||||
log = TestLogger(observer=observer)
|
||||
|
||||
cast(TestLogger, Thingo.log).info()
|
||||
|
||||
def test_sourceOnInstance(self) -> None:
|
||||
"""
|
||||
C{log_source} event key refers to the instance.
|
||||
"""
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
self.assertEqual(event["log_source"], thingo)
|
||||
|
||||
class Thingo:
|
||||
log = TestLogger(observer=observer)
|
||||
|
||||
thingo = Thingo()
|
||||
cast(TestLogger, thingo.log).info()
|
||||
|
||||
def test_sourceUnbound(self) -> None:
|
||||
"""
|
||||
C{log_source} event key is L{None}.
|
||||
"""
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
self.assertIsNone(event["log_source"])
|
||||
|
||||
log = TestLogger(observer=observer)
|
||||
log.info()
|
||||
|
||||
def test_defaultFailure(self) -> None:
|
||||
"""
|
||||
Test that log.failure() emits the right data.
|
||||
"""
|
||||
log = TestLogger()
|
||||
try:
|
||||
raise RuntimeError("baloney!")
|
||||
except RuntimeError:
|
||||
log.failure("Whoops")
|
||||
|
||||
errors = self.flushLoggedErrors(RuntimeError)
|
||||
self.assertEqual(len(errors), 1)
|
||||
|
||||
self.assertEqual(log.emitted["level"], LogLevel.critical)
|
||||
self.assertEqual(log.emitted["format"], "Whoops")
|
||||
|
||||
def test_conflictingKwargs(self) -> None:
|
||||
"""
|
||||
Make sure that kwargs conflicting with args don't pass through.
|
||||
"""
|
||||
log = TestLogger()
|
||||
|
||||
log.warn(
|
||||
"*",
|
||||
log_format="#",
|
||||
log_level=LogLevel.error,
|
||||
log_namespace="*namespace*",
|
||||
log_source="*source*",
|
||||
)
|
||||
|
||||
self.assertEqual(log.event["log_format"], "*")
|
||||
self.assertEqual(log.event["log_level"], LogLevel.warn)
|
||||
self.assertEqual(log.event["log_namespace"], log.namespace)
|
||||
self.assertIsNone(log.event["log_source"])
|
||||
|
||||
def test_logInvalidLogLevel(self) -> None:
|
||||
"""
|
||||
Test passing in a bogus log level to C{emit()}.
|
||||
"""
|
||||
log = TestLogger()
|
||||
|
||||
log.emit("*bogus*")
|
||||
|
||||
errors = self.flushLoggedErrors(InvalidLogLevelError)
|
||||
self.assertEqual(len(errors), 1)
|
||||
|
||||
def test_trace(self) -> None:
|
||||
"""
|
||||
Tracing keeps track of forwarding to the publisher.
|
||||
"""
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def publisher(event: LogEvent) -> None:
|
||||
observer(event)
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
self.assertEqual(event["log_trace"], [(log, publisher)])
|
||||
|
||||
log = TestLogger(observer=publisher)
|
||||
log.info("Hello.", log_trace=[])
|
||||
|
||||
def test_failuresHandled(self) -> None:
|
||||
"""
|
||||
The L{Logger.failuresHandled} context manager catches any
|
||||
L{BaseException} and converts it into a logged L{Failure}.
|
||||
"""
|
||||
events = []
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def logged(event: LogEvent) -> None:
|
||||
events.append(event)
|
||||
|
||||
log = TestLogger(observer=logged)
|
||||
reprd = 0
|
||||
|
||||
class Reprable:
|
||||
def __repr__(self) -> str:
|
||||
nonlocal reprd
|
||||
reprd += 1
|
||||
return f"<repr {reprd}>"
|
||||
|
||||
with log.failuresHandled(
|
||||
"while testing failure handling for {value}", value=Reprable()
|
||||
) as operation:
|
||||
1 / 0
|
||||
self.assertEqual(operation.succeeded, False)
|
||||
self.assertEqual(operation.failed, True)
|
||||
self.assertEqual(len(events), 1)
|
||||
[logged] = events
|
||||
events[:] = []
|
||||
f: Failure = logged["log_failure"]
|
||||
self.assertEqual(reprd, 0)
|
||||
self.assertEqual(
|
||||
formatEvent(logged), "while testing failure handling for <repr 1>"
|
||||
)
|
||||
self.assertEqual(reprd, 1)
|
||||
self.assertEqual(f.type, ZeroDivisionError)
|
||||
with log.failuresHandled("succeeding for {value}", value=Reprable()) as op2:
|
||||
self.assertEqual(op2.succeeded, False)
|
||||
self.assertEqual(op2.failed, False)
|
||||
self.assertEqual(reprd, 1)
|
||||
self.assertEqual(op2.succeeded, True)
|
||||
self.assertEqual(op2.failed, False)
|
||||
|
||||
def test_failureHandler(self) -> None:
|
||||
"""
|
||||
The L{Logger.failureHandler} context manager can safely be shared
|
||||
amongst multiple invocations and converts L{BaseException} into logged
|
||||
L{Failure}s.
|
||||
"""
|
||||
events = []
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def logged(event: LogEvent) -> None:
|
||||
events.append(event)
|
||||
|
||||
log = TestLogger(observer=logged)
|
||||
|
||||
failureHandler = log.failureHandler("hello")
|
||||
success = False
|
||||
with failureHandler as fh:
|
||||
success = True
|
||||
self.assertIs(fh, None)
|
||||
self.assertEqual(success, True)
|
||||
self.assertEqual(events, [])
|
||||
success = False
|
||||
|
||||
def raisebase() -> None:
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
with failureHandler as fh:
|
||||
raisebase()
|
||||
|
||||
self.assertEqual(len(events), 1)
|
||||
[logged] = events
|
||||
f = logged["log_failure"]
|
||||
self.assertEqual(f.type, KeyboardInterrupt)
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._observer}.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, cast
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._logger import Logger
|
||||
from .._observer import LogPublisher
|
||||
|
||||
|
||||
class LogPublisherTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{LogPublisher}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{LogPublisher} is an L{ILogObserver}.
|
||||
"""
|
||||
publisher = LogPublisher()
|
||||
try:
|
||||
verifyObject(ILogObserver, publisher)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def test_observers(self) -> None:
|
||||
"""
|
||||
L{LogPublisher.observers} returns the observers.
|
||||
"""
|
||||
o1 = cast(ILogObserver, lambda e: None)
|
||||
o2 = cast(ILogObserver, lambda e: None)
|
||||
|
||||
publisher = LogPublisher(o1, o2)
|
||||
self.assertEqual({o1, o2}, set(publisher._observers))
|
||||
|
||||
def test_addObserver(self) -> None:
|
||||
"""
|
||||
L{LogPublisher.addObserver} adds an observer.
|
||||
"""
|
||||
o1 = cast(ILogObserver, lambda e: None)
|
||||
o2 = cast(ILogObserver, lambda e: None)
|
||||
o3 = cast(ILogObserver, lambda e: None)
|
||||
|
||||
publisher = LogPublisher(o1, o2)
|
||||
publisher.addObserver(o3)
|
||||
self.assertEqual({o1, o2, o3}, set(publisher._observers))
|
||||
|
||||
def test_addObserverNotCallable(self) -> None:
|
||||
"""
|
||||
L{LogPublisher.addObserver} refuses to add an observer that's
|
||||
not callable.
|
||||
"""
|
||||
publisher = LogPublisher()
|
||||
self.assertRaises(TypeError, publisher.addObserver, object())
|
||||
|
||||
def test_removeObserver(self) -> None:
|
||||
"""
|
||||
L{LogPublisher.removeObserver} removes an observer.
|
||||
"""
|
||||
o1 = cast(ILogObserver, lambda e: None)
|
||||
o2 = cast(ILogObserver, lambda e: None)
|
||||
o3 = cast(ILogObserver, lambda e: None)
|
||||
|
||||
publisher = LogPublisher(o1, o2, o3)
|
||||
publisher.removeObserver(o2)
|
||||
self.assertEqual({o1, o3}, set(publisher._observers))
|
||||
|
||||
def test_removeObserverNotRegistered(self) -> None:
|
||||
"""
|
||||
L{LogPublisher.removeObserver} removes an observer that is not
|
||||
registered.
|
||||
"""
|
||||
o1 = cast(ILogObserver, lambda e: None)
|
||||
o2 = cast(ILogObserver, lambda e: None)
|
||||
o3 = cast(ILogObserver, lambda e: None)
|
||||
|
||||
publisher = LogPublisher(o1, o2)
|
||||
publisher.removeObserver(o3)
|
||||
self.assertEqual({o1, o2}, set(publisher._observers))
|
||||
|
||||
def test_fanOut(self) -> None:
|
||||
"""
|
||||
L{LogPublisher} calls its observers.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
|
||||
events1: List[LogEvent] = []
|
||||
events2: List[LogEvent] = []
|
||||
events3: List[LogEvent] = []
|
||||
|
||||
o1 = cast(ILogObserver, events1.append)
|
||||
o2 = cast(ILogObserver, events2.append)
|
||||
o3 = cast(ILogObserver, events3.append)
|
||||
|
||||
publisher = LogPublisher(o1, o2, o3)
|
||||
publisher(event)
|
||||
self.assertIn(event, events1)
|
||||
self.assertIn(event, events2)
|
||||
self.assertIn(event, events3)
|
||||
|
||||
def test_observerRaises(self) -> None:
|
||||
"""
|
||||
Observer raises an exception during fan out: a failure is logged, but
|
||||
not re-raised. Life goes on.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
exception = RuntimeError("ARGH! EVIL DEATH!")
|
||||
|
||||
events: List[LogEvent] = []
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
shouldRaise = not events
|
||||
events.append(event)
|
||||
if shouldRaise:
|
||||
raise exception
|
||||
|
||||
collector: List[LogEvent] = []
|
||||
|
||||
publisher = LogPublisher(observer, cast(ILogObserver, collector.append))
|
||||
publisher(event)
|
||||
|
||||
# Verify that the observer saw my event
|
||||
self.assertIn(event, events)
|
||||
|
||||
# Verify that the observer raised my exception
|
||||
errors = [e["log_failure"] for e in collector if "log_failure" in e]
|
||||
self.assertEqual(len(errors), 1)
|
||||
self.assertIs(errors[0].value, exception)
|
||||
# Make sure the exceptional observer does not receive its own error.
|
||||
self.assertEqual(len(events), 1)
|
||||
|
||||
def test_observerRaisesAndLoggerHatesMe(self) -> None:
|
||||
"""
|
||||
Observer raises an exception during fan out and the publisher's Logger
|
||||
pukes when the failure is reported. The exception does not propagate
|
||||
back to the caller.
|
||||
"""
|
||||
event = dict(foo=1, bar=2)
|
||||
exception = RuntimeError("ARGH! EVIL DEATH!")
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def observer(event: LogEvent) -> None:
|
||||
raise RuntimeError("Sad panda")
|
||||
|
||||
class GurkLogger(Logger):
|
||||
def failure(self, *args: object, **kwargs: object) -> None:
|
||||
raise exception
|
||||
|
||||
publisher = LogPublisher(observer)
|
||||
publisher.log = GurkLogger()
|
||||
publisher(event)
|
||||
|
||||
# Here, the lack of an exception thus far is a success, of sorts
|
||||
|
||||
def test_trace(self) -> None:
|
||||
"""
|
||||
Tracing keeps track of forwarding to observers.
|
||||
"""
|
||||
event = dict(foo=1, bar=2, log_trace=[])
|
||||
|
||||
traces: Dict[int, Tuple[Tuple[Logger, ILogObserver]]] = {}
|
||||
|
||||
# Copy trace to a tuple; otherwise, both observers will store the same
|
||||
# mutable list, and we won't be able to see o1's view distinctly.
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o1(e: LogEvent) -> None:
|
||||
traces.setdefault(
|
||||
1, cast(Tuple[Tuple[Logger, ILogObserver]], tuple(e["log_trace"]))
|
||||
)
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o2(e: LogEvent) -> None:
|
||||
traces.setdefault(
|
||||
2, cast(Tuple[Tuple[Logger, ILogObserver]], tuple(e["log_trace"]))
|
||||
)
|
||||
|
||||
publisher = LogPublisher(o1, o2)
|
||||
publisher(event)
|
||||
|
||||
self.assertEqual(traces[1], ((publisher, o1),))
|
||||
self.assertEqual(traces[2], ((publisher, o1), (publisher, o2)))
|
||||
@@ -0,0 +1,293 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._format}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging as py_logging
|
||||
import sys
|
||||
from inspect import getsourcefile
|
||||
from io import BytesIO, TextIOWrapper
|
||||
from logging import Formatter, LogRecord, StreamHandler, getLogger
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from zope.interface.exceptions import BrokenMethodImplementation
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python.compat import currentframe
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import unittest
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._levels import LogLevel
|
||||
from .._stdlib import STDLibLogObserver
|
||||
|
||||
|
||||
def nextLine() -> Tuple[Optional[str], int]:
|
||||
"""
|
||||
Retrive the file name and line number immediately after where this function
|
||||
is called.
|
||||
|
||||
@return: the file name and line number
|
||||
"""
|
||||
caller = currentframe(1)
|
||||
return (
|
||||
getsourcefile(sys.modules[caller.f_globals["__name__"]]),
|
||||
caller.f_lineno + 1,
|
||||
)
|
||||
|
||||
|
||||
class StdlibLoggingContainer:
|
||||
"""
|
||||
Continer for a test configuration of stdlib logging objects.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.rootLogger = getLogger("")
|
||||
|
||||
self.originalLevel = self.rootLogger.getEffectiveLevel()
|
||||
self.rootLogger.setLevel(py_logging.DEBUG)
|
||||
|
||||
self.bufferedHandler = BufferedHandler()
|
||||
self.rootLogger.addHandler(self.bufferedHandler)
|
||||
|
||||
self.streamHandler, self.output = handlerAndBytesIO()
|
||||
self.rootLogger.addHandler(self.streamHandler)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the logger.
|
||||
"""
|
||||
self.rootLogger.setLevel(self.originalLevel)
|
||||
self.rootLogger.removeHandler(self.bufferedHandler)
|
||||
self.rootLogger.removeHandler(self.streamHandler)
|
||||
self.streamHandler.close()
|
||||
self.output.close()
|
||||
|
||||
def outputAsText(self) -> str:
|
||||
"""
|
||||
Get the output to the underlying stream as text.
|
||||
|
||||
@return: the output text
|
||||
"""
|
||||
return self.output.getvalue().decode("utf-8")
|
||||
|
||||
|
||||
class STDLibLogObserverTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{STDLibLogObserver}.
|
||||
"""
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{STDLibLogObserver} is an L{ILogObserver}.
|
||||
"""
|
||||
observer = STDLibLogObserver()
|
||||
try:
|
||||
verifyObject(ILogObserver, observer)
|
||||
except BrokenMethodImplementation as e:
|
||||
self.fail(e)
|
||||
|
||||
def py_logger(self) -> StdlibLoggingContainer:
|
||||
"""
|
||||
Create a logging object we can use to test with.
|
||||
|
||||
@return: a stdlib-style logger
|
||||
"""
|
||||
logger = StdlibLoggingContainer()
|
||||
self.addCleanup(logger.close)
|
||||
return logger
|
||||
|
||||
def logEvent(self, *events: LogEvent) -> Tuple[List[LogRecord], str]:
|
||||
"""
|
||||
Send one or more events to Python's logging module, and capture the
|
||||
emitted L{LogRecord}s and output stream as a string.
|
||||
|
||||
@param events: events
|
||||
|
||||
@return: a tuple: (records, output)
|
||||
"""
|
||||
pl = self.py_logger()
|
||||
observer = STDLibLogObserver(
|
||||
# Add 1 to default stack depth to skip *this* frame, since
|
||||
# tests will want to know about their own frames.
|
||||
stackDepth=STDLibLogObserver.defaultStackDepth
|
||||
+ 1
|
||||
)
|
||||
|
||||
for event in events:
|
||||
observer(event)
|
||||
|
||||
return pl.bufferedHandler.records, pl.outputAsText()
|
||||
|
||||
def test_name(self) -> None:
|
||||
"""
|
||||
Logger name.
|
||||
"""
|
||||
records, output = self.logEvent({})
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0].name, "twisted")
|
||||
|
||||
def test_levels(self) -> None:
|
||||
"""
|
||||
Log levels.
|
||||
"""
|
||||
levelMapping = {
|
||||
None: py_logging.INFO, # Default
|
||||
LogLevel.debug: py_logging.DEBUG,
|
||||
LogLevel.info: py_logging.INFO,
|
||||
LogLevel.warn: py_logging.WARNING,
|
||||
LogLevel.error: py_logging.ERROR,
|
||||
LogLevel.critical: py_logging.CRITICAL,
|
||||
}
|
||||
|
||||
# Build a set of events for each log level
|
||||
events = []
|
||||
for level, pyLevel in levelMapping.items():
|
||||
event = {}
|
||||
|
||||
# Set the log level on the event, except for default
|
||||
if level is not None:
|
||||
event["log_level"] = level
|
||||
|
||||
# Remember the Python log level we expect to see for this
|
||||
# event (as an int)
|
||||
event["py_levelno"] = int(pyLevel)
|
||||
|
||||
events.append(event)
|
||||
|
||||
records, output = self.logEvent(*events)
|
||||
self.assertEqual(len(records), len(levelMapping))
|
||||
|
||||
# Check that each event has the correct level
|
||||
for i in range(len(records)):
|
||||
self.assertEqual(records[i].levelno, events[i]["py_levelno"])
|
||||
|
||||
def test_callerInfo(self) -> None:
|
||||
"""
|
||||
C{pathname}, C{lineno}, C{exc_info}, C{func} is set properly on
|
||||
records.
|
||||
"""
|
||||
filename, logLine = nextLine()
|
||||
records, output = self.logEvent({})
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0].pathname, filename)
|
||||
self.assertEqual(records[0].lineno, logLine)
|
||||
self.assertIsNone(records[0].exc_info)
|
||||
|
||||
# Attribute "func" is missing from record, which is weird because it's
|
||||
# documented.
|
||||
# self.assertEqual(records[0].func, "test_callerInfo")
|
||||
|
||||
def test_basicFormat(self) -> None:
|
||||
"""
|
||||
Basic formattable event passes the format along correctly.
|
||||
"""
|
||||
event = dict(log_format="Hello, {who}!", who="dude")
|
||||
records, output = self.logEvent(event)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(str(records[0].msg), "Hello, dude!")
|
||||
self.assertEqual(records[0].args, ())
|
||||
|
||||
def test_basicFormatRendered(self) -> None:
|
||||
"""
|
||||
Basic formattable event renders correctly.
|
||||
"""
|
||||
event = dict(log_format="Hello, {who}!", who="dude")
|
||||
records, output = self.logEvent(event)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertTrue(output.endswith(":Hello, dude!\n"), repr(output))
|
||||
|
||||
def test_noFormat(self) -> None:
|
||||
"""
|
||||
Event with no format.
|
||||
"""
|
||||
records, output = self.logEvent({})
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(str(records[0].msg), "")
|
||||
|
||||
def test_failure(self) -> None:
|
||||
"""
|
||||
An event with a failure logs the failure details as well.
|
||||
"""
|
||||
|
||||
def failing_func() -> None:
|
||||
1 / 0
|
||||
|
||||
try:
|
||||
failing_func()
|
||||
except ZeroDivisionError:
|
||||
failure = Failure()
|
||||
|
||||
event = dict(log_format="Hi mom", who="me", log_failure=failure)
|
||||
records, output = self.logEvent(event)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertIn("Hi mom", output)
|
||||
self.assertIn("in failing_func", output)
|
||||
self.assertIn("ZeroDivisionError", output)
|
||||
|
||||
def test_cleanedFailure(self) -> None:
|
||||
"""
|
||||
A cleaned Failure object has a fake traceback object; make sure that
|
||||
logging such a failure still results in the exception details being
|
||||
logged.
|
||||
"""
|
||||
|
||||
def failing_func() -> None:
|
||||
1 / 0
|
||||
|
||||
try:
|
||||
failing_func()
|
||||
except ZeroDivisionError:
|
||||
failure = Failure()
|
||||
failure.cleanFailure()
|
||||
|
||||
event = dict(log_format="Hi mom", who="me", log_failure=failure)
|
||||
records, output = self.logEvent(event)
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertIn("Hi mom", output)
|
||||
self.assertIn("in failing_func", output)
|
||||
self.assertIn("ZeroDivisionError", output)
|
||||
|
||||
|
||||
def handlerAndBytesIO() -> tuple[StreamHandler[TextIOWrapper], BytesIO]:
|
||||
"""
|
||||
Construct a 2-tuple of C{(StreamHandler, BytesIO)} for testing interaction
|
||||
with the 'logging' module.
|
||||
|
||||
@return: handler and io object
|
||||
"""
|
||||
output = BytesIO()
|
||||
template = py_logging.BASIC_FORMAT
|
||||
stream = TextIOWrapper(output, encoding="utf-8", newline="\n")
|
||||
formatter = Formatter(template)
|
||||
handler = StreamHandler(stream)
|
||||
handler.setFormatter(formatter)
|
||||
return handler, output
|
||||
|
||||
|
||||
class BufferedHandler(py_logging.Handler):
|
||||
"""
|
||||
A L{py_logging.Handler} that remembers all logged records in a list.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize this L{BufferedHandler}.
|
||||
"""
|
||||
py_logging.Handler.__init__(self)
|
||||
self.records: List[LogRecord] = []
|
||||
|
||||
def emit(self, record: LogRecord) -> None:
|
||||
"""
|
||||
Remember the record.
|
||||
"""
|
||||
self.records.append(record)
|
||||
@@ -0,0 +1,139 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{twisted.logger._util}.
|
||||
"""
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.trial import unittest
|
||||
from .._interfaces import ILogObserver, LogEvent
|
||||
from .._observer import LogPublisher
|
||||
from .._util import formatTrace
|
||||
|
||||
|
||||
class UtilTests(unittest.TestCase):
|
||||
"""
|
||||
Utility tests.
|
||||
"""
|
||||
|
||||
def test_trace(self) -> None:
|
||||
"""
|
||||
Tracing keeps track of forwarding done by the publisher.
|
||||
"""
|
||||
publisher = LogPublisher()
|
||||
|
||||
event: LogEvent = dict(log_trace=[])
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o1(e: LogEvent) -> None:
|
||||
pass
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o2(e: LogEvent) -> None:
|
||||
self.assertIs(e, event)
|
||||
self.assertEqual(
|
||||
e["log_trace"],
|
||||
[
|
||||
(publisher, o1),
|
||||
(publisher, o2),
|
||||
# Event hasn't been sent to o3 yet
|
||||
],
|
||||
)
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o3(e: LogEvent) -> None:
|
||||
self.assertIs(e, event)
|
||||
self.assertEqual(
|
||||
e["log_trace"],
|
||||
[
|
||||
(publisher, o1),
|
||||
(publisher, o2),
|
||||
(publisher, o3),
|
||||
],
|
||||
)
|
||||
|
||||
publisher.addObserver(o1)
|
||||
publisher.addObserver(o2)
|
||||
publisher.addObserver(o3)
|
||||
publisher(event)
|
||||
|
||||
def test_formatTrace(self) -> None:
|
||||
"""
|
||||
Format trace as string.
|
||||
"""
|
||||
event: LogEvent = dict(log_trace=[])
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o1(e: LogEvent) -> None:
|
||||
pass
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o2(e: LogEvent) -> None:
|
||||
pass
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o3(e: LogEvent) -> None:
|
||||
pass
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o4(e: LogEvent) -> None:
|
||||
pass
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def o5(e: LogEvent) -> None:
|
||||
pass
|
||||
|
||||
o1.name = "root/o1"
|
||||
o2.name = "root/p1/o2"
|
||||
o3.name = "root/p1/o3"
|
||||
o4.name = "root/p1/p2/o4"
|
||||
o5.name = "root/o5"
|
||||
|
||||
expectedTrace: str # populated below
|
||||
|
||||
@implementer(ILogObserver)
|
||||
def testObserver(e: LogEvent) -> None:
|
||||
self.assertIs(e, event)
|
||||
trace = formatTrace(e["log_trace"])
|
||||
self.assertEqual(trace, expectedTrace)
|
||||
|
||||
oTest = testObserver
|
||||
|
||||
p2 = LogPublisher(o4)
|
||||
p1 = LogPublisher(o2, o3, p2)
|
||||
|
||||
p2.name = "root/p1/p2/" # type: ignore[attr-defined]
|
||||
p1.name = "root/p1/" # type: ignore[attr-defined]
|
||||
|
||||
root = LogPublisher(o1, p1, o5, oTest)
|
||||
root.name = "root/" # type: ignore[attr-defined]
|
||||
expectedTraceTemplate = (
|
||||
"{root} ({root.name})\n"
|
||||
" -> {o1} ({o1.name})\n"
|
||||
" -> {p1} ({p1.name})\n"
|
||||
" -> {o2} ({o2.name})\n"
|
||||
" -> {o3} ({o3.name})\n"
|
||||
" -> {p2} ({p2.name})\n"
|
||||
" -> {o4} ({o4.name})\n"
|
||||
" -> {o5} ({o5.name})\n"
|
||||
" -> {oTest}\n"
|
||||
)
|
||||
|
||||
# Mypy rightly complains that we're pulling some Shenanigans with all
|
||||
# these 'name' attributes above (LogPublisher does not have any such
|
||||
# attribute, neither does FunctionType) so we split up the 'format'
|
||||
# call so it can't see the attributes being formatted.
|
||||
expectedTrace = expectedTraceTemplate.format(
|
||||
root=root,
|
||||
o1=o1,
|
||||
o2=o2,
|
||||
o3=o3,
|
||||
o4=o4,
|
||||
o5=o5,
|
||||
p1=p1,
|
||||
p2=p2,
|
||||
oTest=oTest,
|
||||
)
|
||||
root(event)
|
||||
Reference in New Issue
Block a user