blob: 714ebadac490326e326d2509de279d569c4d4e6f [file] [log] [blame]
Edward Lemur32e3d1e2018-07-12 00:54:05 +00001#!/usr/bin/env python
2# Copyright (c) 2018 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import functools
7import json
8import os
9import subprocess
10import sys
11import tempfile
12import threading
13import time
14import traceback
15import urllib2
16
17import detect_host_arch
18import gclient_utils
19import metrics_utils
20
21
22DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
23CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
24UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
25
26APP_URL = 'https://cit-cli-metrics.appspot.com'
27
28DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0'
29DEFAULT_COUNTDOWN = 10
30
31INVALID_CONFIG_WARNING = (
32 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one has '
33 'been created'
34)
35
36
37class _Config(object):
38 def __init__(self):
39 self._initialized = False
40 self._config = {}
41
42 def _ensure_initialized(self):
43 if self._initialized:
44 return
45
46 try:
47 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
48 except (IOError, ValueError):
49 config = {}
50
51 self._config = config.copy()
52
53 if 'is-googler' not in self._config:
54 # /should-upload is only accessible from Google IPs, so we only need to
55 # check if we can reach the page. An external developer would get access
56 # denied.
57 try:
58 req = urllib2.urlopen(APP_URL + '/should-upload')
59 self._config['is-googler'] = req.getcode() == 200
60 except (urllib2.URLError, urllib2.HTTPError):
61 self._config['is-googler'] = False
62
63 # Make sure the config variables we need are present, and initialize them to
64 # safe values otherwise.
65 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
66 self._config.setdefault('opt-in', None)
67
68 if config != self._config:
69 print INVALID_CONFIG_WARNING
70 self._write_config()
71
72 self._initialized = True
73
74 def _write_config(self):
75 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
76
77 @property
78 def is_googler(self):
79 self._ensure_initialized()
80 return self._config['is-googler']
81
82 @property
83 def opted_in(self):
84 self._ensure_initialized()
85 return self._config['opt-in']
86
87 @opted_in.setter
88 def opted_in(self, value):
89 self._ensure_initialized()
90 self._config['opt-in'] = value
91 self._write_config()
92
93 @property
94 def countdown(self):
95 self._ensure_initialized()
96 return self._config['countdown']
97
98 def decrease_countdown(self):
99 self._ensure_initialized()
100 if self.countdown == 0:
101 return
102 self._config['countdown'] -= 1
103 self._write_config()
104
105
106class MetricsCollector(object):
107 def __init__(self):
108 self._metrics_lock = threading.Lock()
109 self._reported_metrics = {}
110 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000111 self._collecting_metrics = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000112
113 @property
114 def config(self):
115 return self._config
116
Edward Lemur3298e7b2018-07-17 18:21:27 +0000117 @property
118 def collecting_metrics(self):
119 return self._collecting_metrics
120
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000121 def add(self, name, value):
Edward Lemur3298e7b2018-07-17 18:21:27 +0000122 if not self.collecting_metrics:
123 return
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000124 with self._metrics_lock:
125 self._reported_metrics[name] = value
126
127 def _upload_metrics_data(self):
128 """Upload the metrics data to the AppEngine app."""
129 # We invoke a subprocess, and use stdin.write instead of communicate(),
130 # so that we are able to return immediately, leaving the upload running in
131 # the background.
132 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
133 p.stdin.write(json.dumps(self._reported_metrics))
134
135 def _collect_metrics(self, func, command_name, *args, **kwargs):
136 self.add('command', command_name)
137 try:
138 start = time.time()
139 func(*args, **kwargs)
140 exception = None
141 # pylint: disable=bare-except
142 except:
143 exception = sys.exc_info()
144 finally:
145 self.add('execution_time', time.time() - start)
146
147 # Print the exception before the metrics notice, so that the notice is
148 # clearly visible even if gclient fails.
149 if exception and not isinstance(exception[1], SystemExit):
150 traceback.print_exception(*exception)
151
152 exit_code = metrics_utils.return_code_from_exception(exception)
153 self.add('exit_code', exit_code)
154
155 # Print the metrics notice only if the user has not explicitly opted in
156 # or out.
157 if self.config.opted_in is None:
158 metrics_utils.print_notice(self.config.countdown)
159
160 # Add metrics regarding environment information.
161 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
162 self.add('python_version', metrics_utils.get_python_version())
163 self.add('host_os', gclient_utils.GetMacWinOrLinux())
164 self.add('host_arch', detect_host_arch.HostArch())
165 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
166
167 self._upload_metrics_data()
168 sys.exit(exit_code)
169
170 def collect_metrics(self, command_name):
171 """A decorator used to collect metrics over the life of a function.
172
173 This decorator executes the function and collects metrics about the system
174 environment and the function performance. It also catches all the Exceptions
175 and invokes sys.exit once the function is done executing.
176 """
177 def _decorator(func):
178 # Do this first so we don't have to read, and possibly create a config
179 # file.
180 if DISABLE_METRICS_COLLECTION:
181 return func
182 # If the user has opted out or the user is not a googler, then there is no
183 # need to do anything.
184 if self.config.opted_in == False or not self.config.is_googler:
185 return func
186 # If the user hasn't opted in or out, and the countdown is not yet 0, just
187 # display the notice.
188 if self.config.opted_in == None and self.config.countdown > 0:
189 metrics_utils.print_notice(self.config.countdown)
190 self.config.decrease_countdown()
191 return func
192 # Otherwise, collect the metrics.
193 # Needed to preserve the __name__ and __doc__ attributes of func.
194 @functools.wraps(func)
195 def _inner(*args, **kwargs):
196 self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur3298e7b2018-07-17 18:21:27 +0000197 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000198 return _inner
199 return _decorator
200
201
202collector = MetricsCollector()