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