diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature index 0ee17a4f2f..e4b130bb2b 100644 --- a/features/unattached_commands.feature +++ b/features/unattached_commands.feature @@ -346,7 +346,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 dfd22bb58c..5ec6ce910f 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 from uaclient.config import UAConfig @@ -17,9 +18,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" @@ -31,7 +33,7 @@ class JsonArrayFormatter(logging.Formatter): "funcName", "lineno", "message", - ) + ) # type: Tuple[str, ...] def format(self, record: logging.LogRecord) -> str: record.message = record.getMessage() @@ -60,7 +62,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_or_root_log_file_path() -> str: @@ -103,7 +140,7 @@ def setup_journald_logging(): logger = logging.getLogger("ubuntupro") logger.setLevel(logging.INFO) console_handler = logging.StreamHandler() - console_handler.setFormatter(JsonArrayFormatter()) + console_handler.setFormatter(JournaldFormatter()) console_handler.setLevel(logging.INFO) console_handler.addFilter(RedactionFilter()) logger.addHandler(console_handler)