blob: b249d91c56a208a5d8d96fa4a916d16b96389c03 [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,
Zhuoyao Zhangd0675882024-06-21 20:47:57 +000041 host_name: str,
Zhuoyao Zhang7b11b712024-04-24 22:58:20 +000042 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
Zhuoyao Zhangd0675882024-06-21 20:47:57 +000050 self.host_name = host_name
Zhuoyao Zhang7b11b712024-04-24 22:58:20 +000051 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(),
Zhuoyao Zhangd0675882024-06-21 20:47:57 +000062 host_name=platform.node(),
Zhuoyao Zhang7b11b712024-04-24 22:58:20 +000063 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,
Zhuoyao Zhangd0675882024-06-21 20:47:57 +0000116 host_name=self.host_name,
Zhuoyao Zhang7b11b712024-04-24 22:58:20 +0000117 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)