|  | 
|  | 1 | +# Copyright 2020 Axis Communications AB. | 
|  | 2 | +# | 
|  | 3 | +# For a full list of individual contributors, please see the commit history. | 
|  | 4 | +# | 
|  | 5 | +# Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 6 | +# you may not use this file except in compliance with the License. | 
|  | 7 | +# You may obtain a copy of the License at | 
|  | 8 | +# | 
|  | 9 | +#     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 10 | +# | 
|  | 11 | +# Unless required by applicable law or agreed to in writing, software | 
|  | 12 | +# distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 14 | +# See the License for the specific language governing permissions and | 
|  | 15 | +# limitations under the License. | 
|  | 16 | +"""ETOS log formatter.""" | 
|  | 17 | +import datetime | 
|  | 18 | +import json | 
|  | 19 | +import logging | 
|  | 20 | +import sys | 
|  | 21 | +import traceback | 
|  | 22 | + | 
|  | 23 | + | 
|  | 24 | +# LogRecord fields that we exclude, typically because we offer | 
|  | 25 | +# something better. | 
|  | 26 | +_EXCLUDED_FIELDS = ( | 
|  | 27 | +    # Superfluous since we're saving the timestamp in '@timestamp'. | 
|  | 28 | +    "asctime", | 
|  | 29 | +    # The arguments to the format string can contain arbitrary objects | 
|  | 30 | +    # that can't be serialized to JSON. We could pass them to str(), | 
|  | 31 | +    # but that assumes that '%s' is used in the format string. | 
|  | 32 | +    "args", | 
|  | 33 | +    "msg", | 
|  | 34 | +    # Exception info is saved in exception.{type,message,stacktrace} | 
|  | 35 | +    # instead. | 
|  | 36 | +    "exc_info", | 
|  | 37 | +    "exc_text", | 
|  | 38 | +    "stack_info", | 
|  | 39 | +) | 
|  | 40 | + | 
|  | 41 | + | 
|  | 42 | +class EtosLogFormatter(logging.Formatter): | 
|  | 43 | +    """Logging formatter that produces a JSON object. | 
|  | 44 | +
 | 
|  | 45 | +    The resulting JSON object contains the logging.LogRecord fields | 
|  | 46 | +    untouched. The following exception from this rule exist: | 
|  | 47 | +
 | 
|  | 48 | +        - A '@timestamp' key with the log record's UTC timestamp in ISO8601 | 
|  | 49 | +          format, digestible by Logstash. | 
|  | 50 | +        - If an exception context exists (i.e. sys.exc_info() returns | 
|  | 51 | +          something) the resulting JSON object will contain an 'exception' | 
|  | 52 | +          key pointing to an object with the following keys: | 
|  | 53 | +
 | 
|  | 54 | +              - 'type': The fully-qualified type name (i.e. including | 
|  | 55 | +                 package and module). | 
|  | 56 | +              - 'message': The exception message. | 
|  | 57 | +              - 'stacktrace': The formatted stacktrace as a multiline string | 
|  | 58 | +                 (not ending in a newline character). | 
|  | 59 | +    """ | 
|  | 60 | + | 
|  | 61 | +    def format(self, record): | 
|  | 62 | +        """Serialize LogRecord data as JSON. | 
|  | 63 | +        Overrides the inherited behavior by ignoring any configured | 
|  | 64 | +        format string and storing attributes of the passed LogRecord | 
|  | 65 | +        object in a dictionary and serializing it to JSON. See class | 
|  | 66 | +        docstring for details. | 
|  | 67 | +        """ | 
|  | 68 | +        fields = { | 
|  | 69 | +            k: v | 
|  | 70 | +            for k, v in record.__dict__.items() | 
|  | 71 | +            if k not in _EXCLUDED_FIELDS and not k.startswith("_") | 
|  | 72 | +        } | 
|  | 73 | +        fields["@timestamp"] = self.formatTime(record) | 
|  | 74 | +        fields["message"] = record.getMessage() | 
|  | 75 | +        exc_type, exc_message, stack = sys.exc_info() | 
|  | 76 | +        if exc_type and exc_message and stack: | 
|  | 77 | +            fields["exception"] = { | 
|  | 78 | +                "type": "{}.{}".format(exc_type.__module__, exc_type.__name__), | 
|  | 79 | +                "message": str(exc_message), | 
|  | 80 | +                # format_tb() returns a list of newline-terminated strings. | 
|  | 81 | +                # Don't include the final newline. | 
|  | 82 | +                "stacktrace": "".join(traceback.format_tb(stack)).rstrip(), | 
|  | 83 | +            } | 
|  | 84 | +        # No need to append a newline character; that's up to the handler. | 
|  | 85 | +        return json.dumps(fields) | 
|  | 86 | + | 
|  | 87 | +    def formatTime(self, record, datefmt=None): | 
|  | 88 | +        """Formats the log record's timestamp in ISO8601 format, in UTC. | 
|  | 89 | +        Overrides the inherited behavior by always returning ISO8601 | 
|  | 90 | +        strings without allowing custom date formats. | 
|  | 91 | +        Raises: | 
|  | 92 | +            NotImplementedError: If the `datefmt` parameter is not None. | 
|  | 93 | +        """ | 
|  | 94 | +        if datefmt is not None: | 
|  | 95 | +            raise NotImplementedError("Only default time format is supported.") | 
|  | 96 | +        # Make Logstash's @timestamp parser happy by including a "T" | 
|  | 97 | +        # between the date and the time. Append 'Z' to make it clear | 
|  | 98 | +        # that the timestamp is UTC. | 
|  | 99 | +        return datetime.datetime.utcfromtimestamp(record.created).strftime( | 
|  | 100 | +            "%Y-%m-%dT%H:%M:%S.%fZ" | 
|  | 101 | +        ) | 
0 commit comments