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