blob: de8d8bc4f6976cb6dcd8d9a26cf9a270b79d8096 [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 Wang5ed21de2023-02-02 21:17:41 +0000231 for env in DEPOT_TOOLS_ENV:
232 if env in os.environ:
Joanna Wang79499cf2023-02-02 22:07:02 +0000233 self.add_repeated('env_vars', {
Joanna Wang5ed21de2023-02-02 21:17:41 +0000234 'name': env,
235 'value': os.environ.get(env)
236 })
237
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000238 try:
239 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000240 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000241 exception = None
242 # pylint: disable=bare-except
243 except:
244 exception = sys.exc_info()
245 finally:
246 self.add('execution_time', time.time() - start)
247
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000248 exit_code = metrics_utils.return_code_from_exception(exception)
249 self.add('exit_code', exit_code)
250
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000251 # Add metrics regarding environment information.
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000252 self.add('timestamp', int(time.time()))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000253 self.add('python_version', metrics_utils.get_python_version())
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000254 self.add('host_os', gclient_utils.GetOperatingSystem())
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000255 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000256
257 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
258 if depot_tools_age is not None:
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000259 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000260
261 git_version = metrics_utils.get_git_version()
262 if git_version:
263 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000264
Edward Lesmes1e59a242021-04-30 18:38:25 +0000265 bot_metrics = metrics_utils.get_bot_metrics()
266 if bot_metrics:
267 self.add('bot_metrics', bot_metrics)
268
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000269 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000270 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000271 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000272 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000273
274 def collect_metrics(self, command_name):
275 """A decorator used to collect metrics over the life of a function.
276
277 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000278 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000279 """
280 def _decorator(func):
Edward Lemur48836262018-10-18 02:08:06 +0000281 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000282 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000283 # Needed to preserve the __name__ and __doc__ attributes of func.
284 @functools.wraps(func)
285 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000286 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000287 return _inner
288 return _decorator
289
Edward Lemur6f812e12018-07-31 22:45:57 +0000290 @contextlib.contextmanager
291 def print_notice_and_exit(self):
292 """A context manager used to print the notice and terminate execution.
293
294 This decorator executes the function and prints the monitoring notice if
295 necessary. If an exception is raised, we will catch it, and print it before
296 printing the metrics collection notice.
297 This will call sys.exit() with an appropriate exit code to ensure the notice
298 is the last thing printed."""
299 # Needed to preserve the __name__ and __doc__ attributes of func.
300 try:
301 yield
302 exception = None
303 # pylint: disable=bare-except
304 except:
305 exception = sys.exc_info()
306
307 # Print the exception before the metrics notice, so that the notice is
308 # clearly visible even if gclient fails.
309 if exception:
310 if isinstance(exception[1], KeyboardInterrupt):
311 sys.stderr.write('Interrupted\n')
312 elif not isinstance(exception[1], SystemExit):
313 traceback.print_exception(*exception)
314
Edward Lemur48836262018-10-18 02:08:06 +0000315 # Check if the version has changed
Edward Lesmes9c349062021-05-06 20:02:39 +0000316 if (self.config.is_googler
Edward Lemur48836262018-10-18 02:08:06 +0000317 and self.config.opted_in is not False
318 and self.config.version != metrics_utils.CURRENT_VERSION):
319 metrics_utils.print_version_change(self.config.version)
320 self.config.reset_config()
321
Edward Lemur6f812e12018-07-31 22:45:57 +0000322 # Print the notice
Edward Lesmes9c349062021-05-06 20:02:39 +0000323 if self.config.is_googler and self.config.opted_in is None:
Edward Lemur6f812e12018-07-31 22:45:57 +0000324 metrics_utils.print_notice(self.config.countdown)
325 self.config.decrease_countdown()
326
327 sys.exit(metrics_utils.return_code_from_exception(exception))
328
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000329
330collector = MetricsCollector()