diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature index 2186a52b52..b9d4029df1 100644 --- a/features/unattached_commands.feature +++ b/features/unattached_commands.feature @@ -389,7 +389,7 @@ Feature: Command behaviour when unattached """ Then stdout matches regexp: """ - "WARNING", "ubuntupro.apt", "fail", \d+, "Failed to fetch ESM Apt Cache item: https://esm.ubuntu.com/apps/ubuntu/dists/-apps-security/InRelease", {}] + WARNING|ubuntupro.apt|fail|\d+|Failed to fetch ESM Apt Cache item: https://esm.ubuntu.com/apps/ubuntu/dists/-apps-security/InRelease|{} """ When I run `ls /var/crash/` with sudo Then stdout does not contain substring: diff --git a/uaclient/log.py b/uaclient/log.py index a9e2ce2d82..37429f95d8 100644 --- a/uaclient/log.py +++ b/uaclient/log.py @@ -1,9 +1,10 @@ +import abc import json import logging import os import pathlib from collections import OrderedDict -from typing import Any, Dict, List, Union # noqa: F401 +from typing import Any, Dict, List, Tuple, Union # noqa: F401 from uaclient import defaults, system, util @@ -16,9 +17,10 @@ def filter(self, record: logging.LogRecord): return True -class JsonArrayFormatter(logging.Formatter): - """Json Array Formatter for our logging mechanism - Custom made for Pro logging needs +class BaseProFormatter(logging.Formatter, metaclass=abc.ABCMeta): + """Formatter class that collects all the fields we care about in the + correct order and calls a subclass defined formatter function to turn + it into a string. """ default_time_format = "%Y-%m-%dT%H:%M:%S" @@ -30,7 +32,7 @@ class JsonArrayFormatter(logging.Formatter): "funcName", "lineno", "message", - ) + ) # type: Tuple[str, ...] def format(self, record: logging.LogRecord) -> str: record.message = record.getMessage() @@ -59,7 +61,42 @@ def format(self, record: logging.LogRecord) -> str: local_log_record[field] = value local_log_record["extra"] = extra_message_dict - return json.dumps(list(local_log_record.values())) + return self.subformatter(local_log_record) + + @abc.abstractmethod + def subformatter(self, d: Dict[str, Any]) -> str: + pass + + +class JsonArrayFormatter(BaseProFormatter): + def subformatter(self, d: Dict[str, Any]) -> str: + return json.dumps(list(d.values())) + + +class JournaldFormatter(BaseProFormatter): + """Logging to journald in json format is considered unideal + This formatter includes all the same fields, separated by `|` + characters to maintain structure + """ + + required_fields = ( + # no need for asctime because journald already has the time + "levelname", + "name", + "funcName", + "lineno", + "message", + ) + + def subformatter(self, d: Dict[str, Any]) -> str: + values = [] + for v in d.values(): + if isinstance(v, str): + values.append(v) + else: + # json format only for non string types + values.append(json.dumps(v)) + return "|".join(values) def get_user_log_file() -> str: @@ -92,7 +129,7 @@ def setup_journald_logging(): logger.setLevel(logging.INFO) logger.addFilter(RedactionFilter()) console_handler = logging.StreamHandler() - console_handler.setFormatter(JsonArrayFormatter()) + console_handler.setFormatter(JournaldFormatter()) console_handler.setLevel(logging.INFO) logger.addHandler(console_handler)