blob: 65a9696011dbd46d4397b4b7b14cbc4da8e0c1bc [file] [log] [blame]
Zhuoyao Zhang7b11b712024-04-24 22:58:20 +00001# 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)