mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 15:31:08 -05:00
okay fine
This commit is contained in:
136
.venv/lib/python3.12/site-packages/twisted/logger/__init__.py
Normal file
136
.venv/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
|
||||
54
.venv/lib/python3.12/site-packages/twisted/logger/_buffer.py
Normal file
54
.venv/lib/python3.12/site-packages/twisted/logger/_buffer.py
Normal file
@@ -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
.venv/lib/python3.12/site-packages/twisted/logger/_file.py
Normal file
77
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_filter.py
Normal file
211
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_flatten.py
Normal file
175
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_format.py
Normal file
421
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_global.py
Normal file
226
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_io.py
Normal file
187
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_json.py
Normal file
285
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_legacy.py
Normal file
147
.venv/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)
|
||||
81
.venv/lib/python3.12/site-packages/twisted/logger/_levels.py
Normal file
81
.venv/lib/python3.12/site-packages/twisted/logger/_levels.py
Normal file
@@ -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
.venv/lib/python3.12/site-packages/twisted/logger/_logger.py
Normal file
455
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_observer.py
Normal file
112
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_stdlib.py
Normal file
131
.venv/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
.venv/lib/python3.12/site-packages/twisted/logger/_util.py
Normal file
51
.venv/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}.
|
||||
"""
|
||||
@@ -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