From 3bbbe5b98d398ea627a9229f9b5637ed5797b00f Mon Sep 17 00:00:00 2001 From: Grant Orndorff Date: Tue, 14 Nov 2023 15:56:22 -0500 Subject: [PATCH] logs: use less heavy structure for journald logging --- features/unattached_commands.feature | 2 +- uaclient/log.py | 51 ++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature index 397ae48d7f..e8c64eb8fb 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 ee185f3340..a398ce6fc0 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: @@ -104,7 +141,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)