diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py3/twisted/logger | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py3/twisted/logger')
17 files changed, 2602 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py3/twisted/logger/__init__.py b/contrib/python/Twisted/py3/twisted/logger/__init__.py new file mode 100644 index 0000000000..62f2f71f4e --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/__init__.py @@ -0,0 +1,135 @@ +# -*- 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", + "_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 + +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 diff --git a/contrib/python/Twisted/py3/twisted/logger/_buffer.py b/contrib/python/Twisted/py3/twisted/logger/_buffer.py new file mode 100644 index 0000000000..d5e514f18b --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_buffer.py @@ -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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_capture.py b/contrib/python/Twisted/py3/twisted/logger/_capture.py new file mode 100644 index 0000000000..9d3ce0e3ab --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_capture.py @@ -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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_file.py b/contrib/python/Twisted/py3/twisted/logger/_file.py new file mode 100644 index 0000000000..43ae32cd29 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_file.py @@ -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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_filter.py b/contrib/python/Twisted/py3/twisted/logger/_filter.py new file mode 100644 index 0000000000..fa4220ea3e --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_filter.py @@ -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 # type: ignore[import] + +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 diff --git a/contrib/python/Twisted/py3/twisted/logger/_flatten.py b/contrib/python/Twisted/py3/twisted/logger/_flatten.py new file mode 100644 index 0000000000..b79476aa24 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_flatten.py @@ -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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_format.py b/contrib/python/Twisted/py3/twisted/logger/_format.py new file mode 100644 index 0000000000..4bc06ec40c --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_format.py @@ -0,0 +1,373 @@ +# -*- test-case-name: twisted.logger.test.test_format -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tools for formatting logging events. +""" + +from datetime import datetime as DateTime +from typing import Any, Callable, Iterator, Mapping, Optional, Union, cast + +from constantly import NamedConstant # type: ignore[import] + +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" + + +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{"()"}. + """ + callit = key.endswith("()") + realKey = key[:-2] if callit else key + value = self._submapping[realKey] + if callit: + value = value() + return value + + +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, + ) diff --git a/contrib/python/Twisted/py3/twisted/logger/_global.py b/contrib/python/Twisted/py3/twisted/logger/_global.py new file mode 100644 index 0000000000..8ae89baf72 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_global.py @@ -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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_interfaces.py b/contrib/python/Twisted/py3/twisted/logger/_interfaces.py new file mode 100644 index 0000000000..496de1de54 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_interfaces.py @@ -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. + """ diff --git a/contrib/python/Twisted/py3/twisted/logger/_io.py b/contrib/python/Twisted/py3/twisted/logger/_io.py new file mode 100644 index 0000000000..7a49718db7 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_io.py @@ -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 # type: ignore[import] +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 diff --git a/contrib/python/Twisted/py3/twisted/logger/_json.py b/contrib/python/Twisted/py3/twisted/logger/_json.py new file mode 100644 index 0000000000..2ecdd43045 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_json.py @@ -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 # type: ignore[import] + +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.__dict__ = 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] diff --git a/contrib/python/Twisted/py3/twisted/logger/_legacy.py b/contrib/python/Twisted/py3/twisted/logger/_legacy.py new file mode 100644 index 0000000000..2847bc7a40 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_legacy.py @@ -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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_levels.py b/contrib/python/Twisted/py3/twisted/logger/_levels.py new file mode 100644 index 0000000000..800a549f88 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_levels.py @@ -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 # type: ignore[import] + + +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) diff --git a/contrib/python/Twisted/py3/twisted/logger/_logger.py b/contrib/python/Twisted/py3/twisted/logger/_logger.py new file mode 100644 index 0000000000..cc428d87af --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_logger.py @@ -0,0 +1,269 @@ +# -*- test-case-name: twisted.logger.test.test_logger -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Logger class. +""" + +from time import time +from typing import Any, Optional, cast + +from twisted.python.compat import currentframe +from twisted.python.failure import Failure +from ._interfaces import ILogObserver, LogTrace +from ._levels import InvalidLogLevelError, LogLevel + + +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 generally 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 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 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. + """ + 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) + + +_log = Logger() +_loggerFor = lambda obj: _log.__get__(obj, obj.__class__) diff --git a/contrib/python/Twisted/py3/twisted/logger/_observer.py b/contrib/python/Twisted/py3/twisted/logger/_observer.py new file mode 100644 index 0000000000..86f89c37b4 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_observer.py @@ -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. + """ diff --git a/contrib/python/Twisted/py3/twisted/logger/_stdlib.py b/contrib/python/Twisted/py3/twisted/logger/_stdlib.py new file mode 100644 index 0000000000..030b643883 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_stdlib.py @@ -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 # type: ignore[import] + +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") diff --git a/contrib/python/Twisted/py3/twisted/logger/_util.py b/contrib/python/Twisted/py3/twisted/logger/_util.py new file mode 100644 index 0000000000..e8f02ddd22 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/logger/_util.py @@ -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) |