host_name info is useful in several use cases including 1) to distinguish the concurrent tool invocations from the same user. 2) to understand the potential different performance from different hosts (e.g. local workstation VS cloudtop). 3) required to integrate the tool event log with the EPR system. Test: atest tool_event_logger_test Bug: 348482213 Change-Id: I98422b3f8ccec73d57fcdcdedac91780c448dc47
234 lines
6.2 KiB
Python
234 lines
6.2 KiB
Python
# Copyright 2024, The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
|
|
import argparse
|
|
import datetime
|
|
import getpass
|
|
import logging
|
|
import os
|
|
import platform
|
|
import sys
|
|
import tempfile
|
|
import uuid
|
|
|
|
from atest.metrics import clearcut_client
|
|
from atest.proto import clientanalytics_pb2
|
|
from proto import tool_event_pb2
|
|
|
|
LOG_SOURCE = 2395
|
|
|
|
|
|
class ToolEventLogger:
|
|
"""Logs tool events to Sawmill through Clearcut."""
|
|
|
|
def __init__(
|
|
self,
|
|
tool_tag: str,
|
|
invocation_id: str,
|
|
user_name: str,
|
|
host_name: str,
|
|
source_root: str,
|
|
platform_version: str,
|
|
python_version: str,
|
|
client: clearcut_client.Clearcut,
|
|
):
|
|
self.tool_tag = tool_tag
|
|
self.invocation_id = invocation_id
|
|
self.user_name = user_name
|
|
self.host_name = host_name
|
|
self.source_root = source_root
|
|
self.platform_version = platform_version
|
|
self.python_version = python_version
|
|
self._clearcut_client = client
|
|
|
|
@classmethod
|
|
def create(cls, tool_tag: str):
|
|
return ToolEventLogger(
|
|
tool_tag=tool_tag,
|
|
invocation_id=str(uuid.uuid4()),
|
|
user_name=getpass.getuser(),
|
|
host_name=platform.node(),
|
|
source_root=os.environ.get('ANDROID_BUILD_TOP', ''),
|
|
platform_version=platform.platform(),
|
|
python_version=platform.python_version(),
|
|
client=clearcut_client.Clearcut(LOG_SOURCE),
|
|
)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
self.flush()
|
|
|
|
def log_invocation_started(self, event_time: datetime, command_args: str):
|
|
"""Creates an event log with invocation started info."""
|
|
event = self._create_tool_event()
|
|
event.invocation_started.CopyFrom(
|
|
tool_event_pb2.ToolEvent.InvocationStarted(
|
|
command_args=command_args,
|
|
os=f'{self.platform_version}:{self.python_version}',
|
|
)
|
|
)
|
|
|
|
logging.debug('Log invocation_started: %s', event)
|
|
self._log_clearcut_event(event, event_time)
|
|
|
|
def log_invocation_stopped(
|
|
self,
|
|
event_time: datetime,
|
|
exit_code: int,
|
|
exit_log: str,
|
|
):
|
|
"""Creates an event log with invocation stopped info."""
|
|
event = self._create_tool_event()
|
|
event.invocation_stopped.CopyFrom(
|
|
tool_event_pb2.ToolEvent.InvocationStopped(
|
|
exit_code=exit_code,
|
|
exit_log=exit_log,
|
|
)
|
|
)
|
|
|
|
logging.debug('Log invocation_stopped: %s', event)
|
|
self._log_clearcut_event(event, event_time)
|
|
|
|
def flush(self):
|
|
"""Sends all batched events to Clearcut."""
|
|
logging.debug('Sending events to Clearcut.')
|
|
self._clearcut_client.flush_events()
|
|
|
|
def _create_tool_event(self):
|
|
return tool_event_pb2.ToolEvent(
|
|
tool_tag=self.tool_tag,
|
|
invocation_id=self.invocation_id,
|
|
user_name=self.user_name,
|
|
host_name=self.host_name,
|
|
source_root=self.source_root,
|
|
)
|
|
|
|
def _log_clearcut_event(
|
|
self, tool_event: tool_event_pb2.ToolEvent, event_time: datetime
|
|
):
|
|
log_event = clientanalytics_pb2.LogEvent(
|
|
event_time_ms=int(event_time.timestamp() * 1000),
|
|
source_extension=tool_event.SerializeToString(),
|
|
)
|
|
self._clearcut_client.log(log_event)
|
|
|
|
|
|
class ArgumentParserWithLogging(argparse.ArgumentParser):
|
|
|
|
def error(self, message):
|
|
logging.error('Failed to parse args with error: %s', message)
|
|
super().error(message)
|
|
|
|
|
|
def create_arg_parser():
|
|
"""Creates an instance of the default ToolEventLogger arg parser."""
|
|
|
|
parser = ArgumentParserWithLogging(
|
|
description='Build and upload logs for Android dev tools',
|
|
add_help=True,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--tool_tag',
|
|
type=str,
|
|
required=True,
|
|
help='Name of the tool.',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--start_timestamp',
|
|
type=lambda ts: datetime.datetime.fromtimestamp(float(ts)),
|
|
required=True,
|
|
help=(
|
|
'Timestamp when the tool starts. The timestamp should have the format'
|
|
'%s.%N which represents the seconds elapses since epoch.'
|
|
),
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--end_timestamp',
|
|
type=lambda ts: datetime.datetime.fromtimestamp(float(ts)),
|
|
required=True,
|
|
help=(
|
|
'Timestamp when the tool exits. The timestamp should have the format'
|
|
'%s.%N which represents the seconds elapses since epoch.'
|
|
),
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--tool_args',
|
|
type=str,
|
|
help='Parameters that are passed to the tool.',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--exit_code',
|
|
type=int,
|
|
required=True,
|
|
help='Tool exit code.',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--exit_log',
|
|
type=str,
|
|
help='Logs when tool exits.',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--dry_run',
|
|
action='store_true',
|
|
help='Dry run the tool event logger if set.',
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def configure_logging():
|
|
root_logging_dir = tempfile.mkdtemp(prefix='tool_event_logger_')
|
|
|
|
log_fmt = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s'
|
|
date_fmt = '%Y-%m-%d %H:%M:%S'
|
|
_, log_path = tempfile.mkstemp(dir=root_logging_dir, suffix='.log')
|
|
|
|
logging.basicConfig(
|
|
filename=log_path, level=logging.DEBUG, format=log_fmt, datefmt=date_fmt
|
|
)
|
|
|
|
|
|
def main(argv: list[str]):
|
|
args = create_arg_parser().parse_args(argv[1:])
|
|
|
|
if args.dry_run:
|
|
logging.debug('This is a dry run.')
|
|
return
|
|
|
|
try:
|
|
with ToolEventLogger.create(args.tool_tag) as logger:
|
|
logger.log_invocation_started(args.start_timestamp, args.tool_args)
|
|
logger.log_invocation_stopped(
|
|
args.end_timestamp, args.exit_code, args.exit_log
|
|
)
|
|
except Exception as e:
|
|
logging.error('Log failed with unexpected error: %s', e)
|
|
raise
|
|
|
|
|
|
if __name__ == '__main__':
|
|
configure_logging()
|
|
main(sys.argv)
|