• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      source_root: str,
42      platform_version: str,
43      python_version: str,
44      client: clearcut_client.Clearcut,
45  ):
46    self.tool_tag = tool_tag
47    self.invocation_id = invocation_id
48    self.user_name = user_name
49    self.source_root = source_root
50    self.platform_version = platform_version
51    self.python_version = python_version
52    self._clearcut_client = client
53
54  @classmethod
55  def create(cls, tool_tag: str):
56    return ToolEventLogger(
57        tool_tag=tool_tag,
58        invocation_id=str(uuid.uuid4()),
59        user_name=getpass.getuser(),
60        source_root=os.environ.get('ANDROID_BUILD_TOP', ''),
61        platform_version=platform.platform(),
62        python_version=platform.python_version(),
63        client=clearcut_client.Clearcut(LOG_SOURCE),
64    )
65
66  def __enter__(self):
67    return self
68
69  def __exit__(self, exc_type, exc_val, exc_tb):
70    self.flush()
71
72  def log_invocation_started(self, event_time: datetime, command_args: str):
73    """Creates an event log with invocation started info."""
74    event = self._create_tool_event()
75    event.invocation_started.CopyFrom(
76        tool_event_pb2.ToolEvent.InvocationStarted(
77            command_args=command_args,
78            os=f'{self.platform_version}:{self.python_version}',
79        )
80    )
81
82    logging.debug('Log invocation_started: %s', event)
83    self._log_clearcut_event(event, event_time)
84
85  def log_invocation_stopped(
86      self,
87      event_time: datetime,
88      exit_code: int,
89      exit_log: str,
90  ):
91    """Creates an event log with invocation stopped info."""
92    event = self._create_tool_event()
93    event.invocation_stopped.CopyFrom(
94        tool_event_pb2.ToolEvent.InvocationStopped(
95            exit_code=exit_code,
96            exit_log=exit_log,
97        )
98    )
99
100    logging.debug('Log invocation_stopped: %s', event)
101    self._log_clearcut_event(event, event_time)
102
103  def flush(self):
104    """Sends all batched events to Clearcut."""
105    logging.debug('Sending events to Clearcut.')
106    self._clearcut_client.flush_events()
107
108  def _create_tool_event(self):
109    return tool_event_pb2.ToolEvent(
110        tool_tag=self.tool_tag,
111        invocation_id=self.invocation_id,
112        user_name=self.user_name,
113        source_root=self.source_root,
114    )
115
116  def _log_clearcut_event(
117      self, tool_event: tool_event_pb2.ToolEvent, event_time: datetime
118  ):
119    log_event = clientanalytics_pb2.LogEvent(
120        event_time_ms=int(event_time.timestamp() * 1000),
121        source_extension=tool_event.SerializeToString(),
122    )
123    self._clearcut_client.log(log_event)
124
125
126class ArgumentParserWithLogging(argparse.ArgumentParser):
127
128  def error(self, message):
129    logging.error('Failed to parse args with error: %s', message)
130    super().error(message)
131
132
133def create_arg_parser():
134  """Creates an instance of the default ToolEventLogger arg parser."""
135
136  parser = ArgumentParserWithLogging(
137      description='Build and upload logs for Android dev tools',
138      add_help=True,
139      formatter_class=argparse.RawDescriptionHelpFormatter,
140  )
141
142  parser.add_argument(
143      '--tool_tag',
144      type=str,
145      required=True,
146      help='Name of the tool.',
147  )
148
149  parser.add_argument(
150      '--start_timestamp',
151      type=lambda ts: datetime.datetime.fromtimestamp(float(ts)),
152      required=True,
153      help=(
154          'Timestamp when the tool starts. The timestamp should have the format'
155          '%s.%N which represents the seconds elapses since epoch.'
156      ),
157  )
158
159  parser.add_argument(
160      '--end_timestamp',
161      type=lambda ts: datetime.datetime.fromtimestamp(float(ts)),
162      required=True,
163      help=(
164          'Timestamp when the tool exits. The timestamp should have the format'
165          '%s.%N which represents the seconds elapses since epoch.'
166      ),
167  )
168
169  parser.add_argument(
170      '--tool_args',
171      type=str,
172      help='Parameters that are passed to the tool.',
173  )
174
175  parser.add_argument(
176      '--exit_code',
177      type=int,
178      required=True,
179      help='Tool exit code.',
180  )
181
182  parser.add_argument(
183      '--exit_log',
184      type=str,
185      help='Logs when tool exits.',
186  )
187
188  parser.add_argument(
189      '--dry_run',
190      action='store_true',
191      help='Dry run the tool event logger if set.',
192  )
193
194  return parser
195
196
197def configure_logging():
198  root_logging_dir = tempfile.mkdtemp(prefix='tool_event_logger_')
199
200  log_fmt = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s'
201  date_fmt = '%Y-%m-%d %H:%M:%S'
202  _, log_path = tempfile.mkstemp(dir=root_logging_dir, suffix='.log')
203
204  logging.basicConfig(
205      filename=log_path, level=logging.DEBUG, format=log_fmt, datefmt=date_fmt
206  )
207
208
209def main(argv: list[str]):
210  args = create_arg_parser().parse_args(argv[1:])
211
212  if args.dry_run:
213    logging.debug('This is a dry run.')
214    return
215
216  try:
217    with ToolEventLogger.create(args.tool_tag) as logger:
218      logger.log_invocation_started(args.start_timestamp, args.tool_args)
219      logger.log_invocation_stopped(
220          args.end_timestamp, args.exit_code, args.exit_log
221      )
222  except Exception as e:
223    logging.error('Log failed with unexpected error: %s', e)
224    raise
225
226
227if __name__ == '__main__':
228  configure_logging()
229  main(sys.argv)
230