xref: /aosp_15_r20/external/mesa3d/bin/ci/structured_logger.py (revision 6104692788411f58d303aa86923a9ff6ecaded22)
1*61046927SAndroid Build Coastguard Worker"""
2*61046927SAndroid Build Coastguard WorkerA structured logging utility supporting multiple data formats such as CSV, JSON,
3*61046927SAndroid Build Coastguard Workerand YAML.
4*61046927SAndroid Build Coastguard Worker
5*61046927SAndroid Build Coastguard WorkerThe main purpose of this script, besides having relevant information available
6*61046927SAndroid Build Coastguard Workerin a condensed and deserialized.
7*61046927SAndroid Build Coastguard Worker
8*61046927SAndroid Build Coastguard WorkerThis script defines a protocol for different file handling strategies and provides
9*61046927SAndroid Build Coastguard Workerimplementations for CSV, JSON, and YAML formats. The main class, StructuredLogger,
10*61046927SAndroid Build Coastguard Workerallows for easy interaction with log data, enabling users to load, save, increment,
11*61046927SAndroid Build Coastguard Workerset, and append fields in the log. The script also includes context managers for
12*61046927SAndroid Build Coastguard Workerfile locking and editing log data to ensure data integrity and avoid race conditions.
13*61046927SAndroid Build Coastguard Worker"""
14*61046927SAndroid Build Coastguard Worker
15*61046927SAndroid Build Coastguard Workerimport json
16*61046927SAndroid Build Coastguard Workerimport os
17*61046927SAndroid Build Coastguard Workerfrom collections.abc import MutableMapping, MutableSequence
18*61046927SAndroid Build Coastguard Workerfrom contextlib import contextmanager
19*61046927SAndroid Build Coastguard Workerfrom datetime import datetime
20*61046927SAndroid Build Coastguard Workerfrom pathlib import Path
21*61046927SAndroid Build Coastguard Workerfrom typing import Any, Protocol
22*61046927SAndroid Build Coastguard Worker
23*61046927SAndroid Build Coastguard Workerimport fire
24*61046927SAndroid Build Coastguard Workerfrom filelock import FileLock
25*61046927SAndroid Build Coastguard Worker
26*61046927SAndroid Build Coastguard Workertry:
27*61046927SAndroid Build Coastguard Worker    import polars as pl
28*61046927SAndroid Build Coastguard Worker
29*61046927SAndroid Build Coastguard Worker    CSV_LIB_EXCEPTION = None
30*61046927SAndroid Build Coastguard Workerexcept ImportError as e:
31*61046927SAndroid Build Coastguard Worker    CSV_LIB_EXCEPTION: ImportError = e
32*61046927SAndroid Build Coastguard Worker
33*61046927SAndroid Build Coastguard Workertry:
34*61046927SAndroid Build Coastguard Worker    from ruamel.yaml import YAML
35*61046927SAndroid Build Coastguard Worker
36*61046927SAndroid Build Coastguard Worker    YAML_LIB_EXCEPTION = None
37*61046927SAndroid Build Coastguard Workerexcept ImportError as e:
38*61046927SAndroid Build Coastguard Worker    YAML_LIB_EXCEPTION: ImportError = e
39*61046927SAndroid Build Coastguard Worker
40*61046927SAndroid Build Coastguard Worker
41*61046927SAndroid Build Coastguard Workerclass ContainerProxy:
42*61046927SAndroid Build Coastguard Worker    """
43*61046927SAndroid Build Coastguard Worker    A proxy class that wraps a mutable container object (such as a dictionary or
44*61046927SAndroid Build Coastguard Worker    a list) and calls a provided save_callback function whenever the container
45*61046927SAndroid Build Coastguard Worker    or its contents
46*61046927SAndroid Build Coastguard Worker    are changed.
47*61046927SAndroid Build Coastguard Worker    """
48*61046927SAndroid Build Coastguard Worker    def __init__(self, container, save_callback):
49*61046927SAndroid Build Coastguard Worker        self.container = container
50*61046927SAndroid Build Coastguard Worker        self.save_callback = save_callback
51*61046927SAndroid Build Coastguard Worker
52*61046927SAndroid Build Coastguard Worker    def __getitem__(self, key):
53*61046927SAndroid Build Coastguard Worker        value = self.container[key]
54*61046927SAndroid Build Coastguard Worker        if isinstance(value, (MutableMapping, MutableSequence)):
55*61046927SAndroid Build Coastguard Worker            return ContainerProxy(value, self.save_callback)
56*61046927SAndroid Build Coastguard Worker        return value
57*61046927SAndroid Build Coastguard Worker
58*61046927SAndroid Build Coastguard Worker    def __setitem__(self, key, value):
59*61046927SAndroid Build Coastguard Worker        self.container[key] = value
60*61046927SAndroid Build Coastguard Worker        self.save_callback()
61*61046927SAndroid Build Coastguard Worker
62*61046927SAndroid Build Coastguard Worker    def __delitem__(self, key):
63*61046927SAndroid Build Coastguard Worker        del self.container[key]
64*61046927SAndroid Build Coastguard Worker        self.save_callback()
65*61046927SAndroid Build Coastguard Worker
66*61046927SAndroid Build Coastguard Worker    def __getattr__(self, name):
67*61046927SAndroid Build Coastguard Worker        attr = getattr(self.container, name)
68*61046927SAndroid Build Coastguard Worker
69*61046927SAndroid Build Coastguard Worker        if callable(attr):
70*61046927SAndroid Build Coastguard Worker            def wrapper(*args, **kwargs):
71*61046927SAndroid Build Coastguard Worker                result = attr(*args, **kwargs)
72*61046927SAndroid Build Coastguard Worker                self.save_callback()
73*61046927SAndroid Build Coastguard Worker                return result
74*61046927SAndroid Build Coastguard Worker
75*61046927SAndroid Build Coastguard Worker            return wrapper
76*61046927SAndroid Build Coastguard Worker        return attr
77*61046927SAndroid Build Coastguard Worker
78*61046927SAndroid Build Coastguard Worker    def __iter__(self):
79*61046927SAndroid Build Coastguard Worker        return iter(self.container)
80*61046927SAndroid Build Coastguard Worker
81*61046927SAndroid Build Coastguard Worker    def __len__(self):
82*61046927SAndroid Build Coastguard Worker        return len(self.container)
83*61046927SAndroid Build Coastguard Worker
84*61046927SAndroid Build Coastguard Worker    def __repr__(self):
85*61046927SAndroid Build Coastguard Worker        return repr(self.container)
86*61046927SAndroid Build Coastguard Worker
87*61046927SAndroid Build Coastguard Worker
88*61046927SAndroid Build Coastguard Workerclass AutoSaveDict(dict):
89*61046927SAndroid Build Coastguard Worker    """
90*61046927SAndroid Build Coastguard Worker    A subclass of the built-in dict class with additional functionality to
91*61046927SAndroid Build Coastguard Worker    automatically save changes to the dictionary. It maintains a timestamp of
92*61046927SAndroid Build Coastguard Worker    the last modification and automatically wraps nested mutable containers
93*61046927SAndroid Build Coastguard Worker    using ContainerProxy.
94*61046927SAndroid Build Coastguard Worker    """
95*61046927SAndroid Build Coastguard Worker    timestamp_key = "_timestamp"
96*61046927SAndroid Build Coastguard Worker
97*61046927SAndroid Build Coastguard Worker    def __init__(self, *args, save_callback, register_timestamp=True, **kwargs):
98*61046927SAndroid Build Coastguard Worker        self.save_callback = save_callback
99*61046927SAndroid Build Coastguard Worker        self.__register_timestamp = register_timestamp
100*61046927SAndroid Build Coastguard Worker        self.__heartbeat()
101*61046927SAndroid Build Coastguard Worker        super().__init__(*args, **kwargs)
102*61046927SAndroid Build Coastguard Worker        self.__wrap_dictionaries()
103*61046927SAndroid Build Coastguard Worker
104*61046927SAndroid Build Coastguard Worker    def __heartbeat(self):
105*61046927SAndroid Build Coastguard Worker        if self.__register_timestamp:
106*61046927SAndroid Build Coastguard Worker            self[AutoSaveDict.timestamp_key] = datetime.now().isoformat()
107*61046927SAndroid Build Coastguard Worker
108*61046927SAndroid Build Coastguard Worker    def __save(self):
109*61046927SAndroid Build Coastguard Worker        self.__heartbeat()
110*61046927SAndroid Build Coastguard Worker        self.save_callback()
111*61046927SAndroid Build Coastguard Worker
112*61046927SAndroid Build Coastguard Worker    def __wrap_dictionaries(self):
113*61046927SAndroid Build Coastguard Worker        for key, value in self.items():
114*61046927SAndroid Build Coastguard Worker            if isinstance(value, MutableMapping) and not isinstance(
115*61046927SAndroid Build Coastguard Worker                value, AutoSaveDict
116*61046927SAndroid Build Coastguard Worker            ):
117*61046927SAndroid Build Coastguard Worker                self[key] = AutoSaveDict(
118*61046927SAndroid Build Coastguard Worker                    value, save_callback=self.save_callback, register_timestamp=False
119*61046927SAndroid Build Coastguard Worker                )
120*61046927SAndroid Build Coastguard Worker
121*61046927SAndroid Build Coastguard Worker    def __setitem__(self, key, value):
122*61046927SAndroid Build Coastguard Worker        if isinstance(value, MutableMapping) and not isinstance(value, AutoSaveDict):
123*61046927SAndroid Build Coastguard Worker            value = AutoSaveDict(
124*61046927SAndroid Build Coastguard Worker                value, save_callback=self.save_callback, register_timestamp=False
125*61046927SAndroid Build Coastguard Worker            )
126*61046927SAndroid Build Coastguard Worker        super().__setitem__(key, value)
127*61046927SAndroid Build Coastguard Worker
128*61046927SAndroid Build Coastguard Worker        if self.__register_timestamp and key == AutoSaveDict.timestamp_key:
129*61046927SAndroid Build Coastguard Worker            return
130*61046927SAndroid Build Coastguard Worker        self.__save()
131*61046927SAndroid Build Coastguard Worker
132*61046927SAndroid Build Coastguard Worker    def __getitem__(self, key):
133*61046927SAndroid Build Coastguard Worker        value = super().__getitem__(key)
134*61046927SAndroid Build Coastguard Worker        if isinstance(value, (MutableMapping, MutableSequence)):
135*61046927SAndroid Build Coastguard Worker            return ContainerProxy(value, self.__save)
136*61046927SAndroid Build Coastguard Worker        return value
137*61046927SAndroid Build Coastguard Worker
138*61046927SAndroid Build Coastguard Worker    def __delitem__(self, key):
139*61046927SAndroid Build Coastguard Worker        super().__delitem__(key)
140*61046927SAndroid Build Coastguard Worker        self.__save()
141*61046927SAndroid Build Coastguard Worker
142*61046927SAndroid Build Coastguard Worker    def pop(self, *args, **kwargs):
143*61046927SAndroid Build Coastguard Worker        result = super().pop(*args, **kwargs)
144*61046927SAndroid Build Coastguard Worker        self.__save()
145*61046927SAndroid Build Coastguard Worker        return result
146*61046927SAndroid Build Coastguard Worker
147*61046927SAndroid Build Coastguard Worker    def update(self, *args, **kwargs):
148*61046927SAndroid Build Coastguard Worker        super().update(*args, **kwargs)
149*61046927SAndroid Build Coastguard Worker        self.__wrap_dictionaries()
150*61046927SAndroid Build Coastguard Worker        self.__save()
151*61046927SAndroid Build Coastguard Worker
152*61046927SAndroid Build Coastguard Worker
153*61046927SAndroid Build Coastguard Workerclass StructuredLoggerStrategy(Protocol):
154*61046927SAndroid Build Coastguard Worker    def load_data(self, file_path: Path) -> dict:
155*61046927SAndroid Build Coastguard Worker        pass
156*61046927SAndroid Build Coastguard Worker
157*61046927SAndroid Build Coastguard Worker    def save_data(self, file_path: Path, data: dict) -> None:
158*61046927SAndroid Build Coastguard Worker        pass
159*61046927SAndroid Build Coastguard Worker
160*61046927SAndroid Build Coastguard Worker
161*61046927SAndroid Build Coastguard Workerclass CSVStrategy:
162*61046927SAndroid Build Coastguard Worker    def __init__(self) -> None:
163*61046927SAndroid Build Coastguard Worker        if CSV_LIB_EXCEPTION:
164*61046927SAndroid Build Coastguard Worker            raise RuntimeError(
165*61046927SAndroid Build Coastguard Worker                "Can't parse CSV files. Missing library"
166*61046927SAndroid Build Coastguard Worker            ) from CSV_LIB_EXCEPTION
167*61046927SAndroid Build Coastguard Worker
168*61046927SAndroid Build Coastguard Worker    def load_data(self, file_path: Path) -> dict:
169*61046927SAndroid Build Coastguard Worker        dicts: list[dict[str, Any]] = pl.read_csv(
170*61046927SAndroid Build Coastguard Worker            file_path, try_parse_dates=True
171*61046927SAndroid Build Coastguard Worker        ).to_dicts()
172*61046927SAndroid Build Coastguard Worker        data = {}
173*61046927SAndroid Build Coastguard Worker        for d in dicts:
174*61046927SAndroid Build Coastguard Worker            for k, v in d.items():
175*61046927SAndroid Build Coastguard Worker                if k != AutoSaveDict.timestamp_key and k in data:
176*61046927SAndroid Build Coastguard Worker                    if isinstance(data[k], list):
177*61046927SAndroid Build Coastguard Worker                        data[k].append(v)
178*61046927SAndroid Build Coastguard Worker                        continue
179*61046927SAndroid Build Coastguard Worker                    data[k] = [data[k], v]
180*61046927SAndroid Build Coastguard Worker                else:
181*61046927SAndroid Build Coastguard Worker                    data[k] = v
182*61046927SAndroid Build Coastguard Worker        return data
183*61046927SAndroid Build Coastguard Worker
184*61046927SAndroid Build Coastguard Worker    def save_data(self, file_path: Path, data: dict) -> None:
185*61046927SAndroid Build Coastguard Worker        pl.DataFrame(data).write_csv(file_path)
186*61046927SAndroid Build Coastguard Worker
187*61046927SAndroid Build Coastguard Worker
188*61046927SAndroid Build Coastguard Workerclass JSONStrategy:
189*61046927SAndroid Build Coastguard Worker    def load_data(self, file_path: Path) -> dict:
190*61046927SAndroid Build Coastguard Worker        return json.loads(file_path.read_text())
191*61046927SAndroid Build Coastguard Worker
192*61046927SAndroid Build Coastguard Worker    def save_data(self, file_path: Path, data: dict) -> None:
193*61046927SAndroid Build Coastguard Worker        with open(file_path, "w") as f:
194*61046927SAndroid Build Coastguard Worker            json.dump(data, f, indent=2)
195*61046927SAndroid Build Coastguard Worker
196*61046927SAndroid Build Coastguard Worker
197*61046927SAndroid Build Coastguard Workerclass YAMLStrategy:
198*61046927SAndroid Build Coastguard Worker    def __init__(self):
199*61046927SAndroid Build Coastguard Worker        if YAML_LIB_EXCEPTION:
200*61046927SAndroid Build Coastguard Worker            raise RuntimeError(
201*61046927SAndroid Build Coastguard Worker                "Can't parse YAML files. Missing library"
202*61046927SAndroid Build Coastguard Worker            ) from YAML_LIB_EXCEPTION
203*61046927SAndroid Build Coastguard Worker        self.yaml = YAML()
204*61046927SAndroid Build Coastguard Worker        self.yaml.indent(sequence=4, offset=2)
205*61046927SAndroid Build Coastguard Worker        self.yaml.default_flow_style = False
206*61046927SAndroid Build Coastguard Worker        self.yaml.representer.add_representer(AutoSaveDict, self.represent_dict)
207*61046927SAndroid Build Coastguard Worker
208*61046927SAndroid Build Coastguard Worker    @classmethod
209*61046927SAndroid Build Coastguard Worker    def represent_dict(cls, dumper, data):
210*61046927SAndroid Build Coastguard Worker        return dumper.represent_mapping("tag:yaml.org,2002:map", data)
211*61046927SAndroid Build Coastguard Worker
212*61046927SAndroid Build Coastguard Worker    def load_data(self, file_path: Path) -> dict:
213*61046927SAndroid Build Coastguard Worker        return self.yaml.load(file_path.read_text())
214*61046927SAndroid Build Coastguard Worker
215*61046927SAndroid Build Coastguard Worker    def save_data(self, file_path: Path, data: dict) -> None:
216*61046927SAndroid Build Coastguard Worker        with open(file_path, "w") as f:
217*61046927SAndroid Build Coastguard Worker            self.yaml.dump(data, f)
218*61046927SAndroid Build Coastguard Worker
219*61046927SAndroid Build Coastguard Worker
220*61046927SAndroid Build Coastguard Workerclass StructuredLogger:
221*61046927SAndroid Build Coastguard Worker    def __init__(
222*61046927SAndroid Build Coastguard Worker        self, file_name: str, strategy: StructuredLoggerStrategy = None, truncate=False
223*61046927SAndroid Build Coastguard Worker    ):
224*61046927SAndroid Build Coastguard Worker        self.file_name: str = file_name
225*61046927SAndroid Build Coastguard Worker        self.file_path = Path(self.file_name)
226*61046927SAndroid Build Coastguard Worker        self._data: AutoSaveDict = AutoSaveDict(save_callback=self.save_data)
227*61046927SAndroid Build Coastguard Worker
228*61046927SAndroid Build Coastguard Worker        if strategy is None:
229*61046927SAndroid Build Coastguard Worker            self.strategy: StructuredLoggerStrategy = self.guess_strategy_from_file(
230*61046927SAndroid Build Coastguard Worker                self.file_path
231*61046927SAndroid Build Coastguard Worker            )
232*61046927SAndroid Build Coastguard Worker        else:
233*61046927SAndroid Build Coastguard Worker            self.strategy = strategy
234*61046927SAndroid Build Coastguard Worker
235*61046927SAndroid Build Coastguard Worker        if not self.file_path.exists():
236*61046927SAndroid Build Coastguard Worker            Path.mkdir(self.file_path.parent, exist_ok=True)
237*61046927SAndroid Build Coastguard Worker            self.save_data()
238*61046927SAndroid Build Coastguard Worker            return
239*61046927SAndroid Build Coastguard Worker
240*61046927SAndroid Build Coastguard Worker        if truncate:
241*61046927SAndroid Build Coastguard Worker            with self.get_lock():
242*61046927SAndroid Build Coastguard Worker                os.truncate(self.file_path, 0)
243*61046927SAndroid Build Coastguard Worker                self.save_data()
244*61046927SAndroid Build Coastguard Worker
245*61046927SAndroid Build Coastguard Worker    def load_data(self):
246*61046927SAndroid Build Coastguard Worker        self._data = self.strategy.load_data(self.file_path)
247*61046927SAndroid Build Coastguard Worker
248*61046927SAndroid Build Coastguard Worker    def save_data(self):
249*61046927SAndroid Build Coastguard Worker        self.strategy.save_data(self.file_path, self._data)
250*61046927SAndroid Build Coastguard Worker
251*61046927SAndroid Build Coastguard Worker    @property
252*61046927SAndroid Build Coastguard Worker    def data(self) -> AutoSaveDict:
253*61046927SAndroid Build Coastguard Worker        return self._data
254*61046927SAndroid Build Coastguard Worker
255*61046927SAndroid Build Coastguard Worker    @contextmanager
256*61046927SAndroid Build Coastguard Worker    def get_lock(self):
257*61046927SAndroid Build Coastguard Worker        with FileLock(f"{self.file_path}.lock", timeout=10):
258*61046927SAndroid Build Coastguard Worker            yield
259*61046927SAndroid Build Coastguard Worker
260*61046927SAndroid Build Coastguard Worker    @contextmanager
261*61046927SAndroid Build Coastguard Worker    def edit_context(self):
262*61046927SAndroid Build Coastguard Worker        """
263*61046927SAndroid Build Coastguard Worker        Context manager that ensures proper loading and saving of log data when
264*61046927SAndroid Build Coastguard Worker        performing multiple modifications.
265*61046927SAndroid Build Coastguard Worker        """
266*61046927SAndroid Build Coastguard Worker        with self.get_lock():
267*61046927SAndroid Build Coastguard Worker            try:
268*61046927SAndroid Build Coastguard Worker                self.load_data()
269*61046927SAndroid Build Coastguard Worker                yield
270*61046927SAndroid Build Coastguard Worker            finally:
271*61046927SAndroid Build Coastguard Worker                self.save_data()
272*61046927SAndroid Build Coastguard Worker
273*61046927SAndroid Build Coastguard Worker    @staticmethod
274*61046927SAndroid Build Coastguard Worker    def guess_strategy_from_file(file_path: Path) -> StructuredLoggerStrategy:
275*61046927SAndroid Build Coastguard Worker        file_extension = file_path.suffix.lower().lstrip(".")
276*61046927SAndroid Build Coastguard Worker        return StructuredLogger.get_strategy(file_extension)
277*61046927SAndroid Build Coastguard Worker
278*61046927SAndroid Build Coastguard Worker    @staticmethod
279*61046927SAndroid Build Coastguard Worker    def get_strategy(strategy_name: str) -> StructuredLoggerStrategy:
280*61046927SAndroid Build Coastguard Worker        strategies = {
281*61046927SAndroid Build Coastguard Worker            "csv": CSVStrategy,
282*61046927SAndroid Build Coastguard Worker            "json": JSONStrategy,
283*61046927SAndroid Build Coastguard Worker            "yaml": YAMLStrategy,
284*61046927SAndroid Build Coastguard Worker            "yml": YAMLStrategy,
285*61046927SAndroid Build Coastguard Worker        }
286*61046927SAndroid Build Coastguard Worker
287*61046927SAndroid Build Coastguard Worker        try:
288*61046927SAndroid Build Coastguard Worker            return strategies[strategy_name]()
289*61046927SAndroid Build Coastguard Worker        except KeyError as e:
290*61046927SAndroid Build Coastguard Worker            raise ValueError(f"Unknown strategy for: {strategy_name}") from e
291*61046927SAndroid Build Coastguard Worker
292*61046927SAndroid Build Coastguard Worker
293*61046927SAndroid Build Coastguard Workerif __name__ == "__main__":
294*61046927SAndroid Build Coastguard Worker    fire.Fire(StructuredLogger)
295