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 <achilleas@koutsou.net>
This commit is contained in:
Achilleas Koutsou 2021-10-05 17:25:09 +02:00 committed by Ondřej Budai
parent 439f7f08c7
commit 512933ddd4

View file

@ -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"""