From 512933ddd4ad32cd29b30d8e053149b110f45c4d Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 5 Oct 2021 17:25:09 +0200 Subject: [PATCH] monitor: classes for new progress monitor Foundation for new monitor type that will emit a JSON object for each log message. The following classes are defined: - LogLine: The top-level object that can be serialised into a single object containing a message and associated metadata. - Context: Contextual information for a log line message. Describes the origin of the message and the current pipeline and stage. Automatically deduplicates this information using a hash/ID: keeps a history of IDs and omits the context when the context is not new. - Progress: Information on the progress of the build. The object is recursive: can contain a sub-progress for nested progress reporting (pipelines > stages > ...). Signed-off-by: Achilleas Koutsou --- osbuild/monitor.py | 148 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/osbuild/monitor.py b/osbuild/monitor.py index 9741a415..272a2748 100644 --- a/osbuild/monitor.py +++ b/osbuild/monitor.py @@ -10,16 +10,162 @@ are called on the monitor object at certain events. Consult the import abc import datetime +import hashlib import json import os import sys import time -from typing import Dict +from typing import Dict, Optional, Set import osbuild from osbuild.util.term import fmt as vt +class Context: + """Context for a single log line. Automatically calculates hash/id when read.""" + + def __init__(self, + origin: Optional[str] = None, + pipeline: Optional[osbuild.Pipeline] = None, + stage: Optional[osbuild.Stage] = None): + self._origin = origin + self._pipeline_name = pipeline.name if pipeline else None + self._pipeline_id = pipeline.id if pipeline else None + self._stage_name = stage.name if stage else None + self._stage_id = stage.id if stage else None + self._id = None + self._id_history: Set[str] = set() + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, origin: str): + self._id = None + self._origin = origin + + @property + def pipeline_name(self): + return self._pipeline_name + + @property + def pipeline_id(self): + return self._pipeline_id + + def pipeline(self, pipeline: osbuild.Pipeline): + self._id = None + self._pipeline_name = pipeline.name + self._pipeline_id = pipeline.id + + @property + def stage_name(self): + return self._stage_name + + @property + def stage_id(self): + return self._stage_id + + def stage(self, stage: osbuild.Stage): + self._id = None + self._stage_name = stage.name + self._stage_id = stage.id + + @property + def id(self): + if self._id is None: + self._id = hashlib.sha256(json.dumps(self._dict()).encode()).hexdigest() + return self._id + + def _dict(self): + return { + "origin": self._origin, + "pipeline": { + "name": self._pipeline_name, + "id": self._pipeline_id, + "stage": { + "name": self._stage_name, + "id": self._stage_id, + }, + }, + } + + def as_dict(self): + d = self._dict() + ctxid = self.id + if ctxid in self._id_history: + return {"id": self.id} + d["id"] = self.id + self._id_history.add(self.id) + return d + + +class Progress: + def __init__(self, name: str, total: int, unit: Optional[str] = None): + self.name = name + self.total = total + self.unit = unit + self.done = None + self._sub_progress: Optional[Progress] = None + + def incr(self, depth=0): + if depth > 0: + self._sub_progress.incr(depth - 1) + else: + if self.done is None: + self.done = 0 + else: + self.done += 1 + if self._sub_progress: + self._sub_progress.reset() + + def reset(self): + self.done = None + if self._sub_progress: + self._sub_progress.reset() + + def sub_progress(self, prog: "Progress"): + self._sub_progress = prog + + def as_dict(self): + d = { + "name": self.name, + "total": self.total, + "done": self.done, + "unit": self.unit, + } + if self._sub_progress: + d["progress"] = self._sub_progress.as_dict() + return d + + +class LogLine: + """A single JSON serializable log line + + Create a single log line with a given message, error message, context, and progress objects. + All arguments are optional. A timestamp is added to the dictionary when calling the 'as_dict()' method. + """ + + def __init__(self, *, + message: Optional[str] = None, + error: Optional[str] = None, + context: Optional[Context] = None, + progress: Optional[Progress] = None): + self.message = message + self.error = error + self.context = context + self.progress = progress + + def as_dict(self): + return { + "message": self.message, + "error": self.error, + "context": self.context.as_dict(), + "progress": self.progress.as_dict(), + "timestamp": time.time(), + } + + class TextWriter: """Helper class for writing text to file descriptors"""