aboutsummaryrefslogtreecommitdiff
path: root/cli/lib/metrics/metrics_util.py
blob: 23a3f556cfb6b0255c0cb1ec9cfb0cce3ae9a0dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#
# Copyright (C) 2016 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.
#

"""This module provides utilities for Brillo GA hits.

Example use:

  from metrics import metrics_util

  success = metrics_util.send_hit(hit)

  if not success:
    metrics_util.send_retries()
"""

import multiprocessing
import os
import platform

from core import user_config
from core import util
from metrics import hit_store
from metrics.data_types import hit
from metrics.data_types import meta_data


_BRILLO_APP_ID = 'UA-67119306-1'
_BRILLO_APP_NAME = 'bdk'
_BRILLO_CD_OS = 1
_BRILLO_CD_OS_VERSION = 2
_BRILLO_CD_CPU_CORES = 3
_BRILLO_CD_CPU_SPEED = 4
_BRILLO_CD_RAM = 5
_BRILLO_CD_BDK = 6
_BRILLO_CD_RESULT = 7
_BRILLO_CD_TYPE = 8
_BRILLO_PROTOCOL_VERSION = 1


def _get_os_version():
    """Gets the OS release and major revision"""
    return '.'.join(platform.release().split('.')[:2])


def _get_cpu_speed():
    """Gets the CPU speed of the first core."""
    speed = 'unknown'
    try:
        for line in open('/proc/cpuinfo'):
            if 'model name' in line:
                speed = line.split('@')[1].strip()
                break
    except IOError:
        pass
    return speed


def _get_ram():
    """Gets the amount of physical memory in the system in GB.

    This function rounds down to the nearest byte.
    """
    ram = 'unknown'
    try:
        ram = (os.sysconf('SC_PAGE_SIZE')
               * os.sysconf('SC_PHYS_PAGES') / (1024**3))
    except (AttributeError, ValueError):
        # AttributeError: os.sysconf is only defined on unix systems.
        # ValueError: configuration names are not recognized (shouldn't happen,
        #   but better safe than sorry).
        pass
    return ram


def get_meta_data():
    return meta_data.MetaData(_BRILLO_PROTOCOL_VERSION,
                              _BRILLO_APP_ID,
                              _BRILLO_APP_NAME,
                              util.GetBDKVersion(),
                              user_config.USER_CONFIG.uid or 0)


def get_custom_dimensions(result=None):
    cds = {}
    cds[_BRILLO_CD_OS] = platform.system()
    cds[_BRILLO_CD_OS_VERSION] = _get_os_version()
    cds[_BRILLO_CD_CPU_CORES] = multiprocessing.cpu_count()
    cds[_BRILLO_CD_CPU_SPEED] = _get_cpu_speed()
    cds[_BRILLO_CD_RAM] = _get_ram()
    cds[_BRILLO_CD_BDK] = util.GetBDKVersion()
    cds[_BRILLO_CD_TYPE] = _MetricsUtilState.get_command_type()
    if result is not None:
        cds[_BRILLO_CD_RESULT] = result
    return cds


def _requires_metrics_init_and_opt_in(func):
    """Decorator for checking whether a user has opted into metrics.

    This decorator provides an easy method for ensuring that the metrics library
    has been initialized and the user has opted in to metrics.

    Args:
        func: The function to run.  This function should have a return value of
              True on success, and False on failure.

    Returns:
        False if the user has not opted in to metrics, otherwise runs func() and
        returns its return value.
    """
    def wrapper(*args, **kwargs):
        if not _MetricsUtilState.initialized:
            initialize()
        if _MetricsUtilState.user_store.metrics_opt_in != '1':
            return False
        return func(*args, **kwargs)
    return wrapper


def initialize():
    _MetricsUtilState.initialize()


@_requires_metrics_init_and_opt_in
def send_hit(hit_obj):
    """Sends a hit, saving if it fails.

    Args:
        hit_obj: a metrics.data_types.hit.Hit object

    Returns:
        True if the Send succeeds, False otherwise.
    """
    return send_hit_fields(hit_obj.get_fields())


@_requires_metrics_init_and_opt_in
def send_hit_fields(hit_fields):
    """Sends a hit, saving if it fails.

    Args:
        hit_fields: A dictionary of { key : val } describing the hit.
    """
    result = False
    try:
        result = hit.Hit.send_fields(hit_fields)
    finally:
        if not result:
            hit_store.Backup().save(hit_fields)
    return result


@_requires_metrics_init_and_opt_in
def send_hit_and_retries(hit_obj):
    """Sends a hit, saving if it fails, retrying all others if it succeeds.

    The idea is sending probably fails due to a poor network connection. If
    this send succeeded, then others might too.

    Args:
      hit: a metrics.data_types.hit.Hit object

    Returns:
      True if the Send succeeds, False otherwise.
    """
    result = send_hit(hit_obj)
    if result:
        send_retries()
    return result


@_requires_metrics_init_and_opt_in
def send_retries():
    """Retries all previously failed sends.

    If they fail again, they will be re-saved for the *next* time SendRetries
    is called.
    """
    for hit_fields in hit_store.Backup().retrieve_all():
        send_hit_fields(hit_fields)


def set_command_type(command_type):
    """Set the command type for any metrics being sent."""
    _MetricsUtilState.set_command_type(command_type)


class _MetricsUtilState(object):
    """Metrics utility State.

    This class maintains some class variables used by metrics_util to keep track
    of initialization and configuration.
    """

    user_store = user_config.USER_CONFIG
    initialized = False
    _command_type = ''

    @classmethod
    def initialize(cls):
        if not cls.initialized:
            if not cls.user_store.complete():
                cls.user_store.initialize()
            cls.initialized = True

    @classmethod
    def get_command_type(cls):
        return cls._command_type

    @classmethod
    def set_command_type(cls, command_type):
        cls._command_type = command_type.parser.prog