blob: 9d1b585a8bc053ab2ba3b340662767aa5ed528da [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
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 +000033DEFAULT_COUNTDOWN = 10
34
Joanna Wang92d7df82023-02-01 18:12:02 +000035# TODO(b/265929888): Remove this variable when dogfood is over.
36DEPOT_TOOLS_ENV = ['DOGFOOD_STACKED_CHANGES']
37
Edward Lemur32e3d1e2018-07-12 00:54:05 +000038INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000039 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Tomasz Ĺšniatowskibc56d8c2018-08-28 17:56:25 +000040 'be created.'
Edward Lemurdd5051f2018-08-08 00:56:41 +000041)
42PERMISSION_DENIED_WARNING = (
43 'Could not write the metrics collection config:\n\t%s\n'
44 'Metrics collection will be disabled.'
Edward Lemur32e3d1e2018-07-12 00:54:05 +000045)
46
47
48class _Config(object):
49 def __init__(self):
50 self._initialized = False
51 self._config = {}
52
53 def _ensure_initialized(self):
54 if self._initialized:
55 return
56
Edward Lesmes9c349062021-05-06 20:02:39 +000057 # Metrics collection is disabled, so don't collect any metrics.
58 if not metrics_utils.COLLECT_METRICS:
59 self._config = {
60 'is-googler': False,
61 'countdown': 0,
62 'opt-in': False,
63 'version': metrics_utils.CURRENT_VERSION,
64 }
65 self._initialized = True
66 return
67
Josip Sokcevic4de5dea2022-03-23 21:15:14 +000068 # We are running on a bot. Ignore config and collect metrics.
Edward Lesmes9c349062021-05-06 20:02:39 +000069 if metrics_utils.REPORT_BUILD:
70 self._config = {
71 'is-googler': True,
72 'countdown': 0,
73 'opt-in': True,
74 'version': metrics_utils.CURRENT_VERSION,
75 }
76 self._initialized = True
77 return
78
Edward Lemur32e3d1e2018-07-12 00:54:05 +000079 try:
80 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
81 except (IOError, ValueError):
82 config = {}
83
84 self._config = config.copy()
85
86 if 'is-googler' not in self._config:
87 # /should-upload is only accessible from Google IPs, so we only need to
88 # check if we can reach the page. An external developer would get access
89 # denied.
90 try:
Raul Tambreb946b232019-03-26 14:48:46 +000091 req = urllib.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000092 self._config['is-googler'] = req.getcode() == 200
Raul Tambreb946b232019-03-26 14:48:46 +000093 except (urllib.URLError, urllib.HTTPError):
Edward Lemur32e3d1e2018-07-12 00:54:05 +000094 self._config['is-googler'] = False
95
96 # Make sure the config variables we need are present, and initialize them to
97 # safe values otherwise.
98 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
99 self._config.setdefault('opt-in', None)
Edward Lemur48836262018-10-18 02:08:06 +0000100 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000101
102 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +0000103 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000104 self._write_config()
105
106 self._initialized = True
107
108 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +0000109 try:
110 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
111 except IOError as e:
112 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
113 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000114
115 @property
Edward Lemur48836262018-10-18 02:08:06 +0000116 def version(self):
117 self._ensure_initialized()
118 return self._config['version']
119
120 @property
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000121 def is_googler(self):
122 self._ensure_initialized()
123 return self._config['is-googler']
124
125 @property
126 def opted_in(self):
127 self._ensure_initialized()
128 return self._config['opt-in']
129
130 @opted_in.setter
131 def opted_in(self, value):
132 self._ensure_initialized()
133 self._config['opt-in'] = value
Edward Lemur48836262018-10-18 02:08:06 +0000134 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000135 self._write_config()
136
137 @property
138 def countdown(self):
139 self._ensure_initialized()
140 return self._config['countdown']
141
Edward Lemur48836262018-10-18 02:08:06 +0000142 @property
143 def should_collect_metrics(self):
Edward Lesmes1e59a242021-04-30 18:38:25 +0000144 # Don't report metrics if user is not a Googler.
Edward Lemur48836262018-10-18 02:08:06 +0000145 if not self.is_googler:
146 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000147 # Don't report metrics if user has opted out.
Edward Lemur48836262018-10-18 02:08:06 +0000148 if self.opted_in is False:
149 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000150 # Don't report metrics if countdown hasn't reached 0.
Edward Lemur48836262018-10-18 02:08:06 +0000151 if self.opted_in is None and self.countdown > 0:
152 return False
153 return True
154
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000155 def decrease_countdown(self):
156 self._ensure_initialized()
157 if self.countdown == 0:
158 return
159 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000160 if self.countdown == 0:
161 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000162 self._write_config()
163
Edward Lemur48836262018-10-18 02:08:06 +0000164 def reset_config(self):
165 # Only reset countdown if we're already collecting metrics.
166 if self.should_collect_metrics:
167 self._ensure_initialized()
168 self._config['countdown'] = DEFAULT_COUNTDOWN
169 self._config['opt-in'] = None
170
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000171
172class MetricsCollector(object):
173 def __init__(self):
174 self._metrics_lock = threading.Lock()
175 self._reported_metrics = {}
176 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000177 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000178 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000179
180 @property
181 def config(self):
182 return self._config
183
Edward Lemur3298e7b2018-07-17 18:21:27 +0000184 @property
185 def collecting_metrics(self):
186 return self._collecting_metrics
187
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000188 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000189 if self._collect_custom_metrics:
190 with self._metrics_lock:
191 self._reported_metrics[name] = value
192
Edward Lemur149834e2018-10-22 19:15:13 +0000193 def add_repeated(self, name, value):
194 if self._collect_custom_metrics:
195 with self._metrics_lock:
196 self._reported_metrics.setdefault(name, []).append(value)
197
Edward Lemur6f812e12018-07-31 22:45:57 +0000198 @contextlib.contextmanager
199 def pause_metrics_collection(self):
200 collect_custom_metrics = self._collect_custom_metrics
201 self._collect_custom_metrics = False
202 try:
203 yield
204 finally:
205 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000206
207 def _upload_metrics_data(self):
208 """Upload the metrics data to the AppEngine app."""
Edward Lesmes73faeea2021-05-07 17:12:20 +0000209 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000210 # We invoke a subprocess, and use stdin.write instead of communicate(),
211 # so that we are able to return immediately, leaving the upload running in
212 # the background.
Edward Lemur73065b22019-07-22 20:12:01 +0000213 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
Edward Lesmes73faeea2021-05-07 17:12:20 +0000214 # ... but if we're running on a bot, wait until upload has completed.
215 if metrics_utils.REPORT_BUILD:
216 p.communicate()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000217
218 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000219 # If we're already collecting metrics, just execute the function.
220 # e.g. git-cl split invokes git-cl upload several times to upload each
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000221 # split CL.
Edward Lemur6f812e12018-07-31 22:45:57 +0000222 if self.collecting_metrics:
223 # Don't collect metrics for this function.
224 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
225 with self.pause_metrics_collection():
226 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000227
228 self._collecting_metrics = True
Edward Lemur18df41e2019-04-26 00:42:04 +0000229 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000230 self.add('command', command_name)
Joanna Wang92d7df82023-02-01 18:12:02 +0000231 environment_variables = [
232 '%s=%s' % (env, os.environ.get(env)) for env in DEPOT_TOOLS_ENV
233 if env in os.environ
234 ]
235 if environment_variables:
236 self.add('env_variables', ','.join(environment_variables))
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()