blob: 00d6e6d5b2f1af009012e7daf9376a70a52ecc69 [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
Edward Lemurdd5051f2018-08-08 00:56:41 +00006from __future__ import print_function
7
Edward Lemur6f812e12018-07-31 22:45:57 +00008import contextlib
Edward Lemur32e3d1e2018-07-12 00:54:05 +00009import functools
10import json
11import os
12import subprocess
13import sys
14import tempfile
15import threading
16import time
17import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000018
19try:
20 import urllib2 as urllib
21except ImportError: # For Py3 compatibility
22 import urllib.request as urllib
Edward Lemur32e3d1e2018-07-12 00:54:05 +000023
24import detect_host_arch
25import gclient_utils
26import metrics_utils
27
28
29DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
30CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
31UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
32
Edward Lemur32e3d1e2018-07-12 00:54:05 +000033DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0'
34DEFAULT_COUNTDOWN = 10
35
36INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000037 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Tomasz Ĺšniatowskibc56d8c2018-08-28 17:56:25 +000038 'be created.'
Edward Lemurdd5051f2018-08-08 00:56:41 +000039)
40PERMISSION_DENIED_WARNING = (
41 'Could not write the metrics collection config:\n\t%s\n'
42 'Metrics collection will be disabled.'
Edward Lemur32e3d1e2018-07-12 00:54:05 +000043)
44
45
46class _Config(object):
47 def __init__(self):
48 self._initialized = False
49 self._config = {}
50
51 def _ensure_initialized(self):
52 if self._initialized:
53 return
54
55 try:
56 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
57 except (IOError, ValueError):
58 config = {}
59
60 self._config = config.copy()
61
62 if 'is-googler' not in self._config:
63 # /should-upload is only accessible from Google IPs, so we only need to
64 # check if we can reach the page. An external developer would get access
65 # denied.
66 try:
Raul Tambreb946b232019-03-26 14:48:46 +000067 req = urllib.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000068 self._config['is-googler'] = req.getcode() == 200
Raul Tambreb946b232019-03-26 14:48:46 +000069 except (urllib.URLError, urllib.HTTPError):
Edward Lemur32e3d1e2018-07-12 00:54:05 +000070 self._config['is-googler'] = False
71
72 # Make sure the config variables we need are present, and initialize them to
73 # safe values otherwise.
74 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
75 self._config.setdefault('opt-in', None)
Edward Lemur48836262018-10-18 02:08:06 +000076 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000077
78 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +000079 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000080 self._write_config()
81
82 self._initialized = True
83
84 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +000085 try:
86 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
87 except IOError as e:
88 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
89 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +000090
91 @property
Edward Lemur48836262018-10-18 02:08:06 +000092 def version(self):
93 self._ensure_initialized()
94 return self._config['version']
95
96 @property
Edward Lemur32e3d1e2018-07-12 00:54:05 +000097 def is_googler(self):
98 self._ensure_initialized()
99 return self._config['is-googler']
100
101 @property
102 def opted_in(self):
103 self._ensure_initialized()
104 return self._config['opt-in']
105
106 @opted_in.setter
107 def opted_in(self, value):
108 self._ensure_initialized()
109 self._config['opt-in'] = value
Edward Lemur48836262018-10-18 02:08:06 +0000110 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000111 self._write_config()
112
113 @property
114 def countdown(self):
115 self._ensure_initialized()
116 return self._config['countdown']
117
Edward Lemur48836262018-10-18 02:08:06 +0000118 @property
119 def should_collect_metrics(self):
120 # Don't collect the metrics unless the user is a googler, the user has opted
121 # in, or the countdown has expired.
122 if not self.is_googler:
123 return False
124 if self.opted_in is False:
125 return False
126 if self.opted_in is None and self.countdown > 0:
127 return False
128 return True
129
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000130 def decrease_countdown(self):
131 self._ensure_initialized()
132 if self.countdown == 0:
133 return
134 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000135 if self.countdown == 0:
136 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000137 self._write_config()
138
Edward Lemur48836262018-10-18 02:08:06 +0000139 def reset_config(self):
140 # Only reset countdown if we're already collecting metrics.
141 if self.should_collect_metrics:
142 self._ensure_initialized()
143 self._config['countdown'] = DEFAULT_COUNTDOWN
144 self._config['opt-in'] = None
145
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000146
147class MetricsCollector(object):
148 def __init__(self):
149 self._metrics_lock = threading.Lock()
150 self._reported_metrics = {}
151 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000152 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000153 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000154
155 @property
156 def config(self):
157 return self._config
158
Edward Lemur3298e7b2018-07-17 18:21:27 +0000159 @property
160 def collecting_metrics(self):
161 return self._collecting_metrics
162
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000163 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000164 if self._collect_custom_metrics:
165 with self._metrics_lock:
166 self._reported_metrics[name] = value
167
Edward Lemur149834e2018-10-22 19:15:13 +0000168 def add_repeated(self, name, value):
169 if self._collect_custom_metrics:
170 with self._metrics_lock:
171 self._reported_metrics.setdefault(name, []).append(value)
172
Edward Lemur6f812e12018-07-31 22:45:57 +0000173 @contextlib.contextmanager
174 def pause_metrics_collection(self):
175 collect_custom_metrics = self._collect_custom_metrics
176 self._collect_custom_metrics = False
177 try:
178 yield
179 finally:
180 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000181
182 def _upload_metrics_data(self):
183 """Upload the metrics data to the AppEngine app."""
184 # We invoke a subprocess, and use stdin.write instead of communicate(),
185 # so that we are able to return immediately, leaving the upload running in
186 # the background.
187 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
188 p.stdin.write(json.dumps(self._reported_metrics))
189
190 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000191 # If we're already collecting metrics, just execute the function.
192 # e.g. git-cl split invokes git-cl upload several times to upload each
193 # splitted CL.
194 if self.collecting_metrics:
195 # Don't collect metrics for this function.
196 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
197 with self.pause_metrics_collection():
198 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000199
200 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000201 self.add('command', command_name)
202 try:
203 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000204 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000205 exception = None
206 # pylint: disable=bare-except
207 except:
208 exception = sys.exc_info()
209 finally:
210 self.add('execution_time', time.time() - start)
211
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000212 exit_code = metrics_utils.return_code_from_exception(exception)
213 self.add('exit_code', exit_code)
214
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000215 # Add metrics regarding environment information.
216 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
217 self.add('python_version', metrics_utils.get_python_version())
218 self.add('host_os', gclient_utils.GetMacWinOrLinux())
219 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000220
221 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
222 if depot_tools_age is not None:
223 self.add('depot_tools_age', depot_tools_age)
224
225 git_version = metrics_utils.get_git_version()
226 if git_version:
227 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000228
229 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000230 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000231 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000232 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000233
234 def collect_metrics(self, command_name):
235 """A decorator used to collect metrics over the life of a function.
236
237 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000238 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000239 """
240 def _decorator(func):
241 # Do this first so we don't have to read, and possibly create a config
242 # file.
243 if DISABLE_METRICS_COLLECTION:
244 return func
Edward Lemur48836262018-10-18 02:08:06 +0000245 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000246 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000247 # Otherwise, collect the metrics.
248 # Needed to preserve the __name__ and __doc__ attributes of func.
249 @functools.wraps(func)
250 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000251 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000252 return _inner
253 return _decorator
254
Edward Lemur6f812e12018-07-31 22:45:57 +0000255 @contextlib.contextmanager
256 def print_notice_and_exit(self):
257 """A context manager used to print the notice and terminate execution.
258
259 This decorator executes the function and prints the monitoring notice if
260 necessary. If an exception is raised, we will catch it, and print it before
261 printing the metrics collection notice.
262 This will call sys.exit() with an appropriate exit code to ensure the notice
263 is the last thing printed."""
264 # Needed to preserve the __name__ and __doc__ attributes of func.
265 try:
266 yield
267 exception = None
268 # pylint: disable=bare-except
269 except:
270 exception = sys.exc_info()
271
272 # Print the exception before the metrics notice, so that the notice is
273 # clearly visible even if gclient fails.
274 if exception:
275 if isinstance(exception[1], KeyboardInterrupt):
276 sys.stderr.write('Interrupted\n')
277 elif not isinstance(exception[1], SystemExit):
278 traceback.print_exception(*exception)
279
Edward Lemur48836262018-10-18 02:08:06 +0000280 # Check if the version has changed
281 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
282 and self.config.opted_in is not False
283 and self.config.version != metrics_utils.CURRENT_VERSION):
284 metrics_utils.print_version_change(self.config.version)
285 self.config.reset_config()
286
Edward Lemur6f812e12018-07-31 22:45:57 +0000287 # Print the notice
288 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
289 and self.config.opted_in is None):
290 metrics_utils.print_notice(self.config.countdown)
291 self.config.decrease_countdown()
292
293 sys.exit(metrics_utils.return_code_from_exception(exception))
294
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000295
296collector = MetricsCollector()