source: trunk/src/allmydata/util/eliotutil.py

Last change on this file was 1539c8dd, checked in by Itamar Turner-Trauring <itamar@…>, at 2024-08-13T15:23:14Z

Remove usage of no longer available attrs API

  • Property mode set to 100644
File size: 9.1 KB
Line 
1"""
2Tools aimed at the interaction between Tahoe-LAFS implementation and Eliot.
3
4Ported to Python 3.
5"""
6
7__all__ = [
8    "MemoryLogger",
9    "inline_callbacks",
10    "eliot_logging_service",
11    "opt_eliot_destination",
12    "opt_help_eliot_destinations",
13    "validateInstanceOf",
14    "validateSetMembership",
15    "capture_logging",
16]
17
18from sys import (
19    stdout,
20)
21from functools import wraps
22from logging import (
23    INFO,
24    Handler,
25    getLogger,
26)
27from json import loads
28
29from six import ensure_text
30from zope.interface import (
31    implementer,
32)
33
34import attr
35from attr.validators import optional
36from twisted.internet import reactor
37from eliot import (
38    ILogger,
39    Message,
40    FileDestination,
41    write_traceback,
42    start_action,
43)
44from eliot.testing import (
45    MemoryLogger,
46    capture_logging,
47)
48
49from eliot._validation import (
50    ValidationError,
51)
52from eliot.twisted import (
53    DeferredContext,
54    inline_callbacks,
55)
56from eliot.logwriter import ThreadedWriter
57from twisted.python.usage import (
58    UsageError,
59)
60from twisted.python.filepath import (
61    FilePath,
62)
63from twisted.python.logfile import (
64    LogFile,
65)
66from twisted.logger import (
67    ILogObserver,
68    eventAsJSON,
69    globalLogPublisher,
70)
71from twisted.internet.defer import (
72    maybeDeferred,
73)
74from twisted.application.service import MultiService
75
76from .attrs_provides import provides
77from .jsonbytes import AnyBytesJSONEncoder
78
79
80def validateInstanceOf(t):
81    """
82    Return an Eliot validator that requires values to be instances of ``t``.
83    """
84    def validator(v):
85        if not isinstance(v, t):
86            raise ValidationError("{} not an instance of {}".format(v, t))
87    return validator
88
89def validateSetMembership(s):
90    """
91    Return an Eliot validator that requires values to be elements of ``s``.
92    """
93    def validator(v):
94        if v not in s:
95            raise ValidationError("{} not in {}".format(v, s))
96    return validator
97
98def eliot_logging_service(reactor, destinations):
99    """
100    Parse the given Eliot destination descriptions and return an ``IService``
101    which will add them when started and remove them when stopped.
102
103    See ``--help-eliot-destinations`` for details about supported
104    destinations.
105    """
106    return _EliotLogging(destinations=list(
107        get_destination(reactor)
108        for get_destination
109        in destinations
110    ))
111
112
113# An Options-based argument parser for configuring Eliot logging.  Set this as
114# a same-named attribute on your Options subclass.
115def opt_eliot_destination(self, description):
116    """
117    Add an Eliot logging destination.  May be given more than once.
118    """
119    try:
120        destination = _parse_destination_description(description)
121    except Exception as e:
122        raise UsageError(str(e))
123    else:
124        self.setdefault("destinations", []).append(destination)
125
126
127def opt_help_eliot_destinations(self):
128    """
129    Emit usage information for --eliot-destination.
130    """
131    print(
132        "Available destinations:\n"
133        # Might want to generate this from some metadata someday but we just
134        # have one hard-coded destination type now, it's easier to hard-code
135        # the help.
136        "\tfile:<path>[,rotate_length=<bytes>][,max_rotated_files=<count>]\n"
137        "\tSensible defaults are supplied for rotate_length and max_rotated_files\n"
138        "\tif they are not given.\n",
139        file=self.stdout,
140    )
141    raise SystemExit(0)
142
143
144class _EliotLogging(MultiService):
145    """
146    A service which adds stdout as an Eliot destination while it is running.
147    """
148    def __init__(self, destinations):
149        """
150        :param list destinations: The Eliot destinations which will is added by this
151            service.
152        """
153        MultiService.__init__(self)
154        for destination in destinations:
155            service = ThreadedWriter(destination, reactor)
156            service.setServiceParent(self)
157
158    def startService(self):
159        self.stdlib_cleanup = _stdlib_logging_to_eliot_configuration(getLogger())
160        self.twisted_observer = _TwistedLoggerToEliotObserver()
161        globalLogPublisher.addObserver(self.twisted_observer)
162        return MultiService.startService(self)
163
164
165    def stopService(self):
166        globalLogPublisher.removeObserver(self.twisted_observer)
167        self.stdlib_cleanup()
168        return MultiService.stopService(self)
169
170
171@implementer(ILogObserver)
172@attr.s(frozen=True)
173class _TwistedLoggerToEliotObserver(object):
174    """
175    An ``ILogObserver`` which re-publishes events as Eliot messages.
176    """
177    logger = attr.ib(default=None, validator=optional(provides(ILogger)))
178
179    def _observe(self, event):
180        flattened = loads(eventAsJSON(event))
181        # We get a timestamp from Eliot.
182        flattened.pop(u"log_time")
183        # This is never serializable anyway.  "Legacy" log events (from
184        # twisted.python.log) don't have this so make it optional.
185        flattened.pop(u"log_logger", None)
186
187        Message.new(
188            message_type=u"eliot:twisted",
189            **flattened
190        ).write(self.logger)
191
192
193    # The actual ILogObserver interface uses this.
194    __call__ = _observe
195
196
197class _StdlibLoggingToEliotHandler(Handler):
198    def __init__(self, logger=None):
199        Handler.__init__(self)
200        self.logger = logger
201
202    def emit(self, record):
203        Message.new(
204            message_type=u"eliot:stdlib",
205            log_level=record.levelname,
206            logger=record.name,
207            message=record.getMessage()
208        ).write(self.logger)
209
210        if record.exc_info:
211            write_traceback(
212                logger=self.logger,
213                exc_info=record.exc_info,
214            )
215
216
217def _stdlib_logging_to_eliot_configuration(stdlib_logger, eliot_logger=None):
218    """
219    Add a handler to ``stdlib_logger`` which will relay events to
220    ``eliot_logger`` (or the default Eliot logger if ``eliot_logger`` is
221    ``None``).
222    """
223    handler = _StdlibLoggingToEliotHandler(eliot_logger)
224    handler.set_name(u"eliot")
225    handler.setLevel(INFO)
226    stdlib_logger.addHandler(handler)
227    return lambda: stdlib_logger.removeHandler(handler)
228
229
230class _DestinationParser(object):
231    def parse(self, description):
232        description = ensure_text(description)
233
234        try:
235            kind, args = description.split(u":", 1)
236        except ValueError:
237            raise ValueError(
238                u"Eliot destination description must be formatted like "
239                u"<kind>:<args>."
240            )
241        try:
242            parser = getattr(self, u"_parse_{}".format(kind))
243        except AttributeError:
244            raise ValueError(
245                u"Unknown destination description: {}".format(description)
246            )
247        else:
248            return parser(kind, args)
249
250    def _get_arg(self, arg_name, default, arg_list):
251        return dict(
252            arg.split(u"=", 1)
253            for arg
254            in arg_list
255        ).get(
256            arg_name,
257            default,
258        )
259
260    def _parse_file(self, kind, arg_text):
261        # Reserve the possibility of an escape character in the future.  \ is
262        # the standard choice but it's the path separator on Windows which
263        # pretty much ruins it in this context.  Most other symbols already
264        # have some shell-assigned meaning which makes them treacherous to use
265        # in a CLI interface.  Eliminating all such dangerous symbols leaves
266        # approximately @.
267        if u"@" in arg_text:
268            raise ValueError(
269                u"Unsupported escape character (@) in destination text ({!r}).".format(arg_text),
270            )
271        arg_list = arg_text.split(u",")
272        path_name = arg_list.pop(0)
273        if path_name == "-":
274            get_file = lambda: stdout
275        else:
276            path = FilePath(path_name)
277            rotate_length = int(self._get_arg(
278                u"rotate_length",
279                1024 * 1024 * 1024,
280                arg_list,
281            ))
282            max_rotated_files = int(self._get_arg(
283                u"max_rotated_files",
284                10,
285                arg_list,
286            ))
287            def get_file():
288                path.parent().makedirs(ignoreExistingDirectory=True)
289                return LogFile(
290                    path.basename(),
291                    path.dirname(),
292                    rotateLength=rotate_length,
293                    maxRotatedFiles=max_rotated_files,
294                )
295        return lambda reactor: FileDestination(get_file(), encoder=AnyBytesJSONEncoder)
296
297
298_parse_destination_description = _DestinationParser().parse
299
300def log_call_deferred(action_type):
301    """
302    Like ``eliot.log_call`` but for functions which return ``Deferred``.
303    """
304    def decorate_log_call_deferred(f):
305        @wraps(f)
306        def logged_f(*a, **kw):
307            # Use the action's context method to avoid ending the action when
308            # the `with` block ends.
309            with start_action(action_type=action_type).context():
310                # Use addActionFinish so that the action finishes when the
311                # Deferred fires.
312                d = maybeDeferred(f, *a, **kw)
313                return DeferredContext(d).addActionFinish()
314        return logged_f
315    return decorate_log_call_deferred
Note: See TracBrowser for help on using the repository browser.