1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4""" The module to create and manage ExceptionOccurrence records. """ 5 6import os 7import json 8import traceback 9 10from abc import ABC, abstractmethod 11from typing import List 12 13from google.protobuf import any_pb2 14from google.protobuf.json_format import MessageToDict 15 16from lib.proto.exception_occurrences_pb2 import ExceptionOccurrence 17from lib.proto.exception_occurrences_pb2 import ExceptionOccurrences 18 19# This is used as the key when being uploaded to ResultDB via result_sink 20# and shouldn't be changed 21EXCEPTION_OCCURRENCES_KEY = 'exception_occurrences' 22 23# This is used as the key when being uploaded to ResultDB via rdb 24# and shouldn't be changed 25EXCEPTION_OCCURRENCES_FILENAME = f'{EXCEPTION_OCCURRENCES_KEY}.jsonpb' 26 27_records: List[ExceptionOccurrence] = [] 28 29 30class Formatter(ABC): 31 32 @abstractmethod 33 def format_name(self, exc: Exception) -> str: 34 """Format the exception name.""" 35 36 @abstractmethod 37 def format_stacktrace(self, exc: Exception) -> List[str]: 38 """Format the exception stacktrace.""" 39 40 41class _Formatter(Formatter): 42 43 def format_name(self, exc: Exception) -> str: 44 exc_name = type(exc).__qualname__ 45 exc_module = type(exc).__module__ 46 if exc_module not in ('__main__', 'builtins'): 47 exc_name = '%s.%s' % (exc_module, exc_name) 48 return exc_name 49 50 def format_stacktrace(self, exc: Exception) -> List[str]: 51 return traceback.format_exception(type(exc), exc, exc.__traceback__) 52 53 54# Default formatter 55_default_formatter = _Formatter() 56 57def _record_time(exc: ExceptionOccurrence): 58 exc.occurred_time.GetCurrentTime() 59 60def register(exc: Exception, 61 formatter: Formatter = _default_formatter) -> ExceptionOccurrence: 62 """Create and register an ExceptionOccurrence record.""" 63 ret = ExceptionOccurrence(name=formatter.format_name(exc), 64 stacktrace=formatter.format_stacktrace(exc)) 65 _record_time(ret) 66 _records.append(ret) 67 return ret 68 69 70def size() -> int: 71 """Get the current size of registered ExceptionOccurrence records.""" 72 return len(_records) 73 74 75def clear() -> None: 76 """Clear all the registered ExceptionOccurrence records.""" 77 _records.clear() 78 79 80def clear_stacktrace() -> None: 81 """Clear the stacktrace from all the records while keeping the records. 82 83 This can be called to reduce the size of the overall records and avoid 84 the size issue when uploaded to other services, e.g. RDB. 85 """ 86 for record in _records: 87 record.ClearField('stacktrace') 88 89 90def to_dict() -> dict: 91 """Convert all the registered ExceptionOccurrence records to an dict. 92 93 The records are wrapped in protobuf Any message before exported as dict 94 so that an additional key "@type" is included. 95 """ 96 occurrences = ExceptionOccurrences() 97 occurrences.datapoints.extend(_records) 98 any_msg = any_pb2.Any() 99 any_msg.Pack(occurrences) 100 return MessageToDict(any_msg, preserving_proto_field_name=True) 101 102 103def to_json() -> str: 104 """Convert all the registered ExceptionOccurrence records to a json str.""" 105 return json.dumps(to_dict(), sort_keys=True, indent=2) 106 107 108def dump(dir_path: str) -> None: 109 """Dumps the records into |EXCEPTION_OCCURRENCES_FILENAME| in the |path|.""" 110 os.makedirs(dir_path, exist_ok=True) 111 with open(os.path.join(dir_path, EXCEPTION_OCCURRENCES_FILENAME), 112 'w', 113 encoding='utf-8') as wf: 114 wf.write(to_json()) 115