xref: /aosp_15_r20/build/make/tools/tool_event_logger/tool_event_logger.py (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1# Copyright 2024, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16import argparse
17import datetime
18import getpass
19import logging
20import os
21import platform
22import sys
23import tempfile
24import uuid
25
26from atest.metrics import clearcut_client
27from atest.proto import clientanalytics_pb2
28from proto import tool_event_pb2
29
30LOG_SOURCE = 2395
31
32
33class ToolEventLogger:
34  """Logs tool events to Sawmill through Clearcut."""
35
36  def __init__(
37      self,
38      tool_tag: str,
39      invocation_id: str,
40      user_name: str,
41      host_name: str,
42      source_root: str,
43      platform_version: str,
44      python_version: str,
45      client: clearcut_client.Clearcut,
46  ):
47    self.tool_tag = tool_tag
48    self.invocation_id = invocation_id
49    self.user_name = user_name
50    self.host_name = host_name
51    self.source_root = source_root
52    self.platform_version = platform_version
53    self.python_version = python_version
54    self._clearcut_client = client
55
56  @classmethod
57  def create(cls, tool_tag: str):
58    return ToolEventLogger(
59        tool_tag=tool_tag,
60        invocation_id=str(uuid.uuid4()),
61        user_name=getpass.getuser(),
62        host_name=platform.node(),
63        source_root=os.environ.get('ANDROID_BUILD_TOP', ''),
64        platform_version=platform.platform(),
65        python_version=platform.python_version(),
66        client=clearcut_client.Clearcut(LOG_SOURCE),
67    )
68
69  def __enter__(self):
70    return self
71
72  def __exit__(self, exc_type, exc_val, exc_tb):
73    self.flush()
74
75  def log_invocation_started(self, event_time: datetime, command_args: str):
76    """Creates an event log with invocation started info."""
77    event = self._create_tool_event()
78    event.invocation_started.CopyFrom(
79        tool_event_pb2.ToolEvent.InvocationStarted(
80            command_args=command_args,
81            os=f'{self.platform_version}:{self.python_version}',
82        )
83    )
84
85    logging.debug('Log invocation_started: %s', event)
86    self._log_clearcut_event(event, event_time)
87
88  def log_invocation_stopped(
89      self,
90      event_time: datetime,
91      exit_code: int,
92      exit_log: str,
93  ):
94    """Creates an event log with invocation stopped info."""
95    event = self._create_tool_event()
96    event.invocation_stopped.CopyFrom(
97        tool_event_pb2.ToolEvent.InvocationStopped(
98            exit_code=exit_code,
99            exit_log=exit_log,
100        )
101    )
102
103    logging.debug('Log invocation_stopped: %s', event)
104    self._log_clearcut_event(event, event_time)
105
106  def flush(self):
107    """Sends all batched events to Clearcut."""
108    logging.debug('Sending events to Clearcut.')
109    self._clearcut_client.flush_events()
110
111  def _create_tool_event(self):
112    return tool_event_pb2.ToolEvent(
113        tool_tag=self.tool_tag,
114        invocation_id=self.invocation_id,
115        user_name=self.user_name,
116        host_name=self.host_name,
117        source_root=self.source_root,
118    )
119
120  def _log_clearcut_event(
121      self, tool_event: tool_event_pb2.ToolEvent, event_time: datetime
122  ):
123    log_event = clientanalytics_pb2.LogEvent(
124        event_time_ms=int(event_time.timestamp() * 1000),
125        source_extension=tool_event.SerializeToString(),
126    )
127    self._clearcut_client.log(log_event)
128
129
130class ArgumentParserWithLogging(argparse.ArgumentParser):
131
132  def error(self, message):
133    logging.error('Failed to parse args with error: %s', message)
134    super().error(message)
135
136
137def create_arg_parser():
138  """Creates an instance of the default ToolEventLogger arg parser."""
139
140  parser = ArgumentParserWithLogging(
141      description='Build and upload logs for Android dev tools',
142      add_help=True,
143      formatter_class=argparse.RawDescriptionHelpFormatter,
144  )
145
146  parser.add_argument(
147      '--tool_tag',
148      type=str,
149      required=True,
150      help='Name of the tool.',
151  )
152
153  parser.add_argument(
154      '--start_timestamp',
155      type=lambda ts: datetime.datetime.fromtimestamp(float(ts)),
156      required=True,
157      help=(
158          'Timestamp when the tool starts. The timestamp should have the format'
159          '%s.%N which represents the seconds elapses since epoch.'
160      ),
161  )
162
163  parser.add_argument(
164      '--end_timestamp',
165      type=lambda ts: datetime.datetime.fromtimestamp(float(ts)),
166      required=True,
167      help=(
168          'Timestamp when the tool exits. The timestamp should have the format'
169          '%s.%N which represents the seconds elapses since epoch.'
170      ),
171  )
172
173  parser.add_argument(
174      '--tool_args',
175      type=str,
176      help='Parameters that are passed to the tool.',
177  )
178
179  parser.add_argument(
180      '--exit_code',
181      type=int,
182      required=True,
183      help='Tool exit code.',
184  )
185
186  parser.add_argument(
187      '--exit_log',
188      type=str,
189      help='Logs when tool exits.',
190  )
191
192  parser.add_argument(
193      '--dry_run',
194      action='store_true',
195      help='Dry run the tool event logger if set.',
196  )
197
198  return parser
199
200
201def configure_logging():
202  root_logging_dir = tempfile.mkdtemp(prefix='tool_event_logger_')
203
204  log_fmt = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s'
205  date_fmt = '%Y-%m-%d %H:%M:%S'
206  _, log_path = tempfile.mkstemp(dir=root_logging_dir, suffix='.log')
207
208  logging.basicConfig(
209      filename=log_path, level=logging.DEBUG, format=log_fmt, datefmt=date_fmt
210  )
211
212
213def main(argv: list[str]):
214  args = create_arg_parser().parse_args(argv[1:])
215
216  if args.dry_run:
217    logging.debug('This is a dry run.')
218    return
219
220  try:
221    with ToolEventLogger.create(args.tool_tag) as logger:
222      logger.log_invocation_started(args.start_timestamp, args.tool_args)
223      logger.log_invocation_stopped(
224          args.end_timestamp, args.exit_code, args.exit_log
225      )
226  except Exception as e:
227    logging.error('Log failed with unexpected error: %s', e)
228    raise
229
230
231if __name__ == '__main__':
232  configure_logging()
233  main(sys.argv)
234