blob: df366a4013e7fde9cab35596d50182862590b765 [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
Edward Lemur32e3d1e2018-07-12 00:54:05 +000012import sys
13import tempfile
14import threading
15import time
16import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000017
18try:
19 import urllib2 as urllib
20except ImportError: # For Py3 compatibility
21 import urllib.request as urllib
Edward Lemur32e3d1e2018-07-12 00:54:05 +000022
23import detect_host_arch
24import gclient_utils
25import metrics_utils
Edward Lesmese1a9c8d2020-04-15 02:54:55 +000026import subprocess2
Edward Lemur32e3d1e2018-07-12 00:54:05 +000027
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):
Edward Lesmes1e59a242021-04-30 18:38:25 +0000120 # DEPOT_TOOLS_REPORT_BUILD is set on bots to report metrics.
121 if os.getenv(metrics_utils.DEPOT_TOOLS_REPORT_BUILD):
122 return True
123 # Don't report metrics if user is not a Googler.
Edward Lemur48836262018-10-18 02:08:06 +0000124 if not self.is_googler:
125 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000126 # Don't report metrics if user has opted out.
Edward Lemur48836262018-10-18 02:08:06 +0000127 if self.opted_in is False:
128 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000129 # Don't report metrics if countdown hasn't reached 0.
Edward Lemur48836262018-10-18 02:08:06 +0000130 if self.opted_in is None and self.countdown > 0:
131 return False
132 return True
133
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000134 def decrease_countdown(self):
135 self._ensure_initialized()
136 if self.countdown == 0:
137 return
138 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000139 if self.countdown == 0:
140 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000141 self._write_config()
142
Edward Lemur48836262018-10-18 02:08:06 +0000143 def reset_config(self):
144 # Only reset countdown if we're already collecting metrics.
145 if self.should_collect_metrics:
146 self._ensure_initialized()
147 self._config['countdown'] = DEFAULT_COUNTDOWN
148 self._config['opt-in'] = None
149
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000150
151class MetricsCollector(object):
152 def __init__(self):
153 self._metrics_lock = threading.Lock()
154 self._reported_metrics = {}
155 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000156 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000157 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000158
159 @property
160 def config(self):
161 return self._config
162
Edward Lemur3298e7b2018-07-17 18:21:27 +0000163 @property
164 def collecting_metrics(self):
165 return self._collecting_metrics
166
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000167 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000168 if self._collect_custom_metrics:
169 with self._metrics_lock:
170 self._reported_metrics[name] = value
171
Edward Lemur149834e2018-10-22 19:15:13 +0000172 def add_repeated(self, name, value):
173 if self._collect_custom_metrics:
174 with self._metrics_lock:
175 self._reported_metrics.setdefault(name, []).append(value)
176
Edward Lemur6f812e12018-07-31 22:45:57 +0000177 @contextlib.contextmanager
178 def pause_metrics_collection(self):
179 collect_custom_metrics = self._collect_custom_metrics
180 self._collect_custom_metrics = False
181 try:
182 yield
183 finally:
184 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000185
186 def _upload_metrics_data(self):
187 """Upload the metrics data to the AppEngine app."""
188 # We invoke a subprocess, and use stdin.write instead of communicate(),
189 # so that we are able to return immediately, leaving the upload running in
190 # the background.
Edward Lesmese1a9c8d2020-04-15 02:54:55 +0000191 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
Edward Lemur73065b22019-07-22 20:12:01 +0000192 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000193
194 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000195 # If we're already collecting metrics, just execute the function.
196 # e.g. git-cl split invokes git-cl upload several times to upload each
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000197 # split CL.
Edward Lemur6f812e12018-07-31 22:45:57 +0000198 if self.collecting_metrics:
199 # Don't collect metrics for this function.
200 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
201 with self.pause_metrics_collection():
202 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000203
204 self._collecting_metrics = True
Edward Lemur18df41e2019-04-26 00:42:04 +0000205 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000206 self.add('command', command_name)
207 try:
208 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000209 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000210 exception = None
211 # pylint: disable=bare-except
212 except:
213 exception = sys.exc_info()
214 finally:
215 self.add('execution_time', time.time() - start)
216
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000217 exit_code = metrics_utils.return_code_from_exception(exception)
218 self.add('exit_code', exit_code)
219
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000220 # Add metrics regarding environment information.
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000221 self.add('timestamp', int(time.time()))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000222 self.add('python_version', metrics_utils.get_python_version())
Milad Fa52fdd1f2020-09-15 21:24:46 +0000223 self.add('host_os', gclient_utils.GetMacWinAixOrLinux())
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000224 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000225
226 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
227 if depot_tools_age is not None:
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000228 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000229
230 git_version = metrics_utils.get_git_version()
231 if git_version:
232 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000233
Edward Lesmes1e59a242021-04-30 18:38:25 +0000234 bot_metrics = metrics_utils.get_bot_metrics()
235 if bot_metrics:
236 self.add('bot_metrics', bot_metrics)
237
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000238 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000239 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000240 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000241 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000242
243 def collect_metrics(self, command_name):
244 """A decorator used to collect metrics over the life of a function.
245
246 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000247 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000248 """
249 def _decorator(func):
250 # Do this first so we don't have to read, and possibly create a config
251 # file.
252 if DISABLE_METRICS_COLLECTION:
253 return func
Edward Lemur48836262018-10-18 02:08:06 +0000254 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000255 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000256 # Otherwise, collect the metrics.
257 # Needed to preserve the __name__ and __doc__ attributes of func.
258 @functools.wraps(func)
259 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000260 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000261 return _inner
262 return _decorator
263
Edward Lemur6f812e12018-07-31 22:45:57 +0000264 @contextlib.contextmanager
265 def print_notice_and_exit(self):
266 """A context manager used to print the notice and terminate execution.
267
268 This decorator executes the function and prints the monitoring notice if
269 necessary. If an exception is raised, we will catch it, and print it before
270 printing the metrics collection notice.
271 This will call sys.exit() with an appropriate exit code to ensure the notice
272 is the last thing printed."""
273 # Needed to preserve the __name__ and __doc__ attributes of func.
274 try:
275 yield
276 exception = None
277 # pylint: disable=bare-except
278 except:
279 exception = sys.exc_info()
280
281 # Print the exception before the metrics notice, so that the notice is
282 # clearly visible even if gclient fails.
283 if exception:
284 if isinstance(exception[1], KeyboardInterrupt):
285 sys.stderr.write('Interrupted\n')
286 elif not isinstance(exception[1], SystemExit):
287 traceback.print_exception(*exception)
288
Edward Lemur48836262018-10-18 02:08:06 +0000289 # Check if the version has changed
290 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
291 and self.config.opted_in is not False
292 and self.config.version != metrics_utils.CURRENT_VERSION):
293 metrics_utils.print_version_change(self.config.version)
294 self.config.reset_config()
295
Edward Lemur6f812e12018-07-31 22:45:57 +0000296 # Print the notice
297 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
298 and self.config.opted_in is None):
299 metrics_utils.print_notice(self.config.countdown)
300 self.config.decrease_countdown()
301
302 sys.exit(metrics_utils.return_code_from_exception(exception))
303
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000304
305collector = MetricsCollector()