1# Copyright 2018, 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"""Metrics base class.""" 16 17from __future__ import print_function 18 19import getpass 20import logging 21import random 22import socket 23import subprocess 24import time 25import uuid 26 27from atest import constants 28from atest.metrics import clearcut_client 29from atest.proto import clientanalytics_pb2 30from atest.proto import external_user_log_pb2 31from atest.proto import internal_user_log_pb2 32 33INTERNAL_USER = 0 34EXTERNAL_USER = 1 35 36ATEST_EVENTS = { 37 INTERNAL_USER: internal_user_log_pb2.AtestLogEventInternal, 38 EXTERNAL_USER: external_user_log_pb2.AtestLogEventExternal, 39} 40# log source 41ATEST_LOG_SOURCE = {INTERNAL_USER: 971, EXTERNAL_USER: 934} 42 43 44def get_user_email(): 45 """Get user mail in git config. 46 47 Returns: 48 user's email. 49 """ 50 try: 51 output = subprocess.check_output( 52 ['git', 'config', '--get', 'user.email'], universal_newlines=True 53 ) 54 return output.strip() if output else '' 55 except OSError: 56 # OSError can be raised when running atest_unittests on a host 57 # without git being set up. 58 logging.debug( 59 'Unable to determine if this is an external run, git is not found.' 60 ) 61 except subprocess.CalledProcessError: 62 logging.debug( 63 'Unable to determine if this is an external run, email ' 64 'is not found in git config.' 65 ) 66 return '' 67 68 69def get_user_type(): 70 """Get user type. 71 72 Determine the internal user by passing at least one check: 73 - whose git mail domain is from google 74 - whose hostname is from google 75 Otherwise is external user. 76 77 Returns: 78 INTERNAL_USER if user is internal, EXTERNAL_USER otherwise. 79 """ 80 email = get_user_email() 81 if email.endswith(constants.INTERNAL_EMAIL): 82 return INTERNAL_USER 83 84 try: 85 hostname = socket.getfqdn() 86 if hostname and any((x in hostname) for x in constants.INTERNAL_HOSTNAME): 87 return INTERNAL_USER 88 except IOError: 89 logging.debug( 90 'Unable to determine if this is an external run, hostname is not found.' 91 ) 92 return EXTERNAL_USER 93 94 95def get_user_key(): 96 """Get user key 97 98 Returns: 99 A UUID.uuid5 keyed on the user's email 100 """ 101 key = uuid.uuid5(uuid.NAMESPACE_DNS, get_user_email()) 102 return key 103 104 105class MetricsBase: 106 """Class for separating allowed fields and sending metric.""" 107 108 _run_id = str(uuid.uuid4()) 109 user_type = get_user_type() 110 if user_type == INTERNAL_USER: 111 _user_key = getpass.getuser() 112 else: 113 try: 114 # pylint: disable=protected-access 115 _user_key = str(get_user_key()) 116 # pylint: disable=broad-except 117 except Exception: 118 _user_key = str(uuid.UUID(int=0)) 119 120 _log_source = ATEST_LOG_SOURCE[user_type] 121 cc = clearcut_client.Clearcut(_log_source) 122 tool_name = None 123 sub_tool_name = '' 124 125 def __new__(cls, **kwargs): 126 """Send metric event to clearcut. 127 128 Args: 129 cls: this class object. 130 **kwargs: A dict of named arguments. 131 132 Returns: 133 A Clearcut instance. 134 """ 135 # pylint: disable=no-member 136 if not cls.tool_name: 137 logging.debug('There is no tool_name, and metrics stops sending.') 138 return None 139 allowed = ( 140 {constants.EXTERNAL} 141 if cls.user_type == EXTERNAL_USER 142 else {constants.EXTERNAL, constants.INTERNAL} 143 ) 144 fields = [ 145 k 146 for k, v in vars(cls).items() 147 if not k.startswith('_') and v in allowed 148 ] 149 fields_and_values = {} 150 for field in fields: 151 if field in kwargs: 152 fields_and_values[field] = kwargs.pop(field) 153 params = { 154 'user_key': cls._user_key, 155 'run_id': cls._run_id, 156 'user_type': cls.user_type, 157 'tool_name': cls.tool_name, 158 'sub_tool_name': cls.sub_tool_name, 159 cls._EVENT_NAME: fields_and_values, 160 } 161 log_event = cls._build_full_event(ATEST_EVENTS[cls.user_type](**params)) 162 cls.cc.log(log_event) 163 return cls.cc 164 165 @classmethod 166 def get_run_id(cls) -> str: 167 """Returns the unique run id set for the current invocation.""" 168 return cls._run_id 169 170 @classmethod 171 def _build_full_event(cls, atest_event): 172 """This is all protobuf building you can ignore. 173 174 Args: 175 cls: this class object. 176 atest_event: A client_pb2.AtestLogEvent instance. 177 178 Returns: 179 A clientanalytics_pb2.LogEvent instance. 180 """ 181 log_event = clientanalytics_pb2.LogEvent() 182 log_event.event_time_ms = int((time.time() - random.randint(1, 600)) * 1000) 183 log_event.source_extension = atest_event.SerializeToString() 184 return log_event 185