blob: 71bcb6d62ddb0f368a0ae67ff7bffd9dc16182f5 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env python3
Edward Lemur32e3d1e2018-07-12 00:54:05 +00002# 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
Edward Lemur32e3d1e2018-07-12 00:54:05 +000013import threading
14import time
15import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000016
17try:
18 import urllib2 as urllib
19except ImportError: # For Py3 compatibility
20 import urllib.request as urllib
Edward Lemur32e3d1e2018-07-12 00:54:05 +000021
22import detect_host_arch
23import gclient_utils
24import metrics_utils
Edward Lesmese1a9c8d2020-04-15 02:54:55 +000025import subprocess2
Edward Lemur32e3d1e2018-07-12 00:54:05 +000026
27
28DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
29CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
30UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
31
Edward Lemur32e3d1e2018-07-12 00:54:05 +000032DEFAULT_COUNTDOWN = 10
33
Joanna Wang92d7df82023-02-01 18:12:02 +000034# TODO(b/265929888): Remove this variable when dogfood is over.
35DEPOT_TOOLS_ENV = ['DOGFOOD_STACKED_CHANGES']
36
Edward Lemur32e3d1e2018-07-12 00:54:05 +000037INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000038 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Tomasz Ĺšniatowskibc56d8c2018-08-28 17:56:25 +000039 'be created.'
Edward Lemurdd5051f2018-08-08 00:56:41 +000040)
41PERMISSION_DENIED_WARNING = (
42 'Could not write the metrics collection config:\n\t%s\n'
43 'Metrics collection will be disabled.'
Edward Lemur32e3d1e2018-07-12 00:54:05 +000044)
45
46
47class _Config(object):
48 def __init__(self):
49 self._initialized = False
50 self._config = {}
51
52 def _ensure_initialized(self):
53 if self._initialized:
54 return
55
Edward Lesmes9c349062021-05-06 20:02:39 +000056 # Metrics collection is disabled, so don't collect any metrics.
57 if not metrics_utils.COLLECT_METRICS:
58 self._config = {
59 'is-googler': False,
60 'countdown': 0,
61 'opt-in': False,
62 'version': metrics_utils.CURRENT_VERSION,
63 }
64 self._initialized = True
65 return
66
Josip Sokcevic4de5dea2022-03-23 21:15:14 +000067 # We are running on a bot. Ignore config and collect metrics.
Edward Lesmes9c349062021-05-06 20:02:39 +000068 if metrics_utils.REPORT_BUILD:
69 self._config = {
70 'is-googler': True,
71 'countdown': 0,
72 'opt-in': True,
73 'version': metrics_utils.CURRENT_VERSION,
74 }
75 self._initialized = True
76 return
77
Edward Lemur32e3d1e2018-07-12 00:54:05 +000078 try:
79 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
80 except (IOError, ValueError):
81 config = {}
82
83 self._config = config.copy()
84
85 if 'is-googler' not in self._config:
86 # /should-upload is only accessible from Google IPs, so we only need to
87 # check if we can reach the page. An external developer would get access
88 # denied.
89 try:
Raul Tambreb946b232019-03-26 14:48:46 +000090 req = urllib.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000091 self._config['is-googler'] = req.getcode() == 200
Raul Tambreb946b232019-03-26 14:48:46 +000092 except (urllib.URLError, urllib.HTTPError):
Edward Lemur32e3d1e2018-07-12 00:54:05 +000093 self._config['is-googler'] = False
94
95 # Make sure the config variables we need are present, and initialize them to
96 # safe values otherwise.
97 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
98 self._config.setdefault('opt-in', None)
Edward Lemur48836262018-10-18 02:08:06 +000099 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000100
101 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +0000102 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000103 self._write_config()
104
105 self._initialized = True
106
107 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +0000108 try:
109 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
110 except IOError as e:
111 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
112 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000113
114 @property
Edward Lemur48836262018-10-18 02:08:06 +0000115 def version(self):
116 self._ensure_initialized()
117 return self._config['version']
118
119 @property
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000120 def is_googler(self):
121 self._ensure_initialized()
122 return self._config['is-googler']
123
124 @property
125 def opted_in(self):
126 self._ensure_initialized()
127 return self._config['opt-in']
128
129 @opted_in.setter
130 def opted_in(self, value):
131 self._ensure_initialized()
132 self._config['opt-in'] = value
Edward Lemur48836262018-10-18 02:08:06 +0000133 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000134 self._write_config()
135
136 @property
137 def countdown(self):
138 self._ensure_initialized()
139 return self._config['countdown']
140
Edward Lemur48836262018-10-18 02:08:06 +0000141 @property
142 def should_collect_metrics(self):
Edward Lesmes1e59a242021-04-30 18:38:25 +0000143 # Don't report metrics if user is not a Googler.
Edward Lemur48836262018-10-18 02:08:06 +0000144 if not self.is_googler:
145 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000146 # Don't report metrics if user has opted out.
Edward Lemur48836262018-10-18 02:08:06 +0000147 if self.opted_in is False:
148 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000149 # Don't report metrics if countdown hasn't reached 0.
Edward Lemur48836262018-10-18 02:08:06 +0000150 if self.opted_in is None and self.countdown > 0:
151 return False
152 return True
153
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000154 def decrease_countdown(self):
155 self._ensure_initialized()
156 if self.countdown == 0:
157 return
158 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000159 if self.countdown == 0:
160 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000161 self._write_config()
162
Edward Lemur48836262018-10-18 02:08:06 +0000163 def reset_config(self):
164 # Only reset countdown if we're already collecting metrics.
165 if self.should_collect_metrics:
166 self._ensure_initialized()
167 self._config['countdown'] = DEFAULT_COUNTDOWN
168 self._config['opt-in'] = None
169
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000170
171class MetricsCollector(object):
172 def __init__(self):
173 self._metrics_lock = threading.Lock()
174 self._reported_metrics = {}
175 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000176 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000177 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000178
179 @property
180 def config(self):
181 return self._config
182
Edward Lemur3298e7b2018-07-17 18:21:27 +0000183 @property
184 def collecting_metrics(self):
185 return self._collecting_metrics
186
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000187 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000188 if self._collect_custom_metrics:
189 with self._metrics_lock:
190 self._reported_metrics[name] = value
191
Edward Lemur149834e2018-10-22 19:15:13 +0000192 def add_repeated(self, name, value):
193 if self._collect_custom_metrics:
194 with self._metrics_lock:
195 self._reported_metrics.setdefault(name, []).append(value)
196
Edward Lemur6f812e12018-07-31 22:45:57 +0000197 @contextlib.contextmanager
198 def pause_metrics_collection(self):
199 collect_custom_metrics = self._collect_custom_metrics
200 self._collect_custom_metrics = False
201 try:
202 yield
203 finally:
204 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000205
206 def _upload_metrics_data(self):
207 """Upload the metrics data to the AppEngine app."""
Edward Lesmes73faeea2021-05-07 17:12:20 +0000208 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000209 # We invoke a subprocess, and use stdin.write instead of communicate(),
210 # so that we are able to return immediately, leaving the upload running in
211 # the background.
Edward Lemur73065b22019-07-22 20:12:01 +0000212 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
Edward Lesmes73faeea2021-05-07 17:12:20 +0000213 # ... but if we're running on a bot, wait until upload has completed.
214 if metrics_utils.REPORT_BUILD:
215 p.communicate()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000216
217 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000218 # If we're already collecting metrics, just execute the function.
219 # e.g. git-cl split invokes git-cl upload several times to upload each
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000220 # split CL.
Edward Lemur6f812e12018-07-31 22:45:57 +0000221 if self.collecting_metrics:
222 # Don't collect metrics for this function.
223 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
224 with self.pause_metrics_collection():
225 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000226
227 self._collecting_metrics = True
Edward Lemur18df41e2019-04-26 00:42:04 +0000228 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000229 self.add('command', command_name)
Joanna Wang5ed21de2023-02-02 21:17:41 +0000230 for env in DEPOT_TOOLS_ENV:
231 if env in os.environ:
Joanna Wang79499cf2023-02-02 22:07:02 +0000232 self.add_repeated('env_vars', {
Joanna Wang5ed21de2023-02-02 21:17:41 +0000233 'name': env,
234 'value': os.environ.get(env)
235 })
236
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000237 try:
238 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000239 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000240 exception = None
241 # pylint: disable=bare-except
242 except:
243 exception = sys.exc_info()
244 finally:
245 self.add('execution_time', time.time() - start)
246
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000247 exit_code = metrics_utils.return_code_from_exception(exception)
248 self.add('exit_code', exit_code)
249
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000250 # Add metrics regarding environment information.
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000251 self.add('timestamp', int(time.time()))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000252 self.add('python_version', metrics_utils.get_python_version())
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000253 self.add('host_os', gclient_utils.GetOperatingSystem())
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000254 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000255
256 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
257 if depot_tools_age is not None:
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000258 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000259
260 git_version = metrics_utils.get_git_version()
261 if git_version:
262 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000263
Edward Lesmes1e59a242021-04-30 18:38:25 +0000264 bot_metrics = metrics_utils.get_bot_metrics()
265 if bot_metrics:
266 self.add('bot_metrics', bot_metrics)
267
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000268 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000269 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000270 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000271 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000272
273 def collect_metrics(self, command_name):
274 """A decorator used to collect metrics over the life of a function.
275
276 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000277 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000278 """
279 def _decorator(func):
Edward Lemur48836262018-10-18 02:08:06 +0000280 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000281 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000282 # Needed to preserve the __name__ and __doc__ attributes of func.
283 @functools.wraps(func)
284 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000285 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000286 return _inner
287 return _decorator
288
Edward Lemur6f812e12018-07-31 22:45:57 +0000289 @contextlib.contextmanager
290 def print_notice_and_exit(self):
291 """A context manager used to print the notice and terminate execution.
292
293 This decorator executes the function and prints the monitoring notice if
294 necessary. If an exception is raised, we will catch it, and print it before
295 printing the metrics collection notice.
296 This will call sys.exit() with an appropriate exit code to ensure the notice
297 is the last thing printed."""
298 # Needed to preserve the __name__ and __doc__ attributes of func.
299 try:
300 yield
301 exception = None
302 # pylint: disable=bare-except
303 except:
304 exception = sys.exc_info()
305
306 # Print the exception before the metrics notice, so that the notice is
307 # clearly visible even if gclient fails.
308 if exception:
309 if isinstance(exception[1], KeyboardInterrupt):
310 sys.stderr.write('Interrupted\n')
311 elif not isinstance(exception[1], SystemExit):
312 traceback.print_exception(*exception)
313
Edward Lemur48836262018-10-18 02:08:06 +0000314 # Check if the version has changed
Edward Lesmes9c349062021-05-06 20:02:39 +0000315 if (self.config.is_googler
Edward Lemur48836262018-10-18 02:08:06 +0000316 and self.config.opted_in is not False
317 and self.config.version != metrics_utils.CURRENT_VERSION):
318 metrics_utils.print_version_change(self.config.version)
319 self.config.reset_config()
320
Edward Lemur6f812e12018-07-31 22:45:57 +0000321 # Print the notice
Edward Lesmes9c349062021-05-06 20:02:39 +0000322 if self.config.is_googler and self.config.opted_in is None:
Edward Lemur6f812e12018-07-31 22:45:57 +0000323 metrics_utils.print_notice(self.config.countdown)
324 self.config.decrease_countdown()
325
326 sys.exit(metrics_utils.return_code_from_exception(exception))
327
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000328
329collector = MetricsCollector()