blob: 5645f28587f0f267c2e579133b9a64bfd39c645d [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 Lemur6f812e12018-07-31 22:45:57 +00006import contextlib
Edward Lemur32e3d1e2018-07-12 00:54:05 +00007import functools
8import json
9import os
Edward Lemur32e3d1e2018-07-12 00:54:05 +000010import sys
Edward Lemur32e3d1e2018-07-12 00:54:05 +000011import threading
12import time
13import traceback
Gavin Mak5f955df2023-08-30 15:39:13 +000014import urllib.request
Edward Lemur32e3d1e2018-07-12 00:54:05 +000015
16import detect_host_arch
17import gclient_utils
18import metrics_utils
Edward Lesmese1a9c8d2020-04-15 02:54:55 +000019import subprocess2
Edward Lemur32e3d1e2018-07-12 00:54:05 +000020
Edward Lemur32e3d1e2018-07-12 00:54:05 +000021DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
22CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
23UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
24
Edward Lemur32e3d1e2018-07-12 00:54:05 +000025DEFAULT_COUNTDOWN = 10
26
Joanna Wang92d7df82023-02-01 18:12:02 +000027# TODO(b/265929888): Remove this variable when dogfood is over.
28DEPOT_TOOLS_ENV = ['DOGFOOD_STACKED_CHANGES']
29
Edward Lemur32e3d1e2018-07-12 00:54:05 +000030INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000031 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Mike Frysinger124bb8e2023-09-06 05:48:55 +000032 'be created.')
Edward Lemurdd5051f2018-08-08 00:56:41 +000033PERMISSION_DENIED_WARNING = (
34 'Could not write the metrics collection config:\n\t%s\n'
Mike Frysinger124bb8e2023-09-06 05:48:55 +000035 'Metrics collection will be disabled.')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000036
37
38class _Config(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000039 def __init__(self):
40 self._initialized = False
41 self._config = {}
Edward Lemur32e3d1e2018-07-12 00:54:05 +000042
Mike Frysinger124bb8e2023-09-06 05:48:55 +000043 def _ensure_initialized(self):
44 if self._initialized:
45 return
Edward Lemur32e3d1e2018-07-12 00:54:05 +000046
Mike Frysinger124bb8e2023-09-06 05:48:55 +000047 # Metrics collection is disabled, so don't collect any metrics.
48 if not metrics_utils.COLLECT_METRICS:
49 self._config = {
50 'is-googler': False,
51 'countdown': 0,
52 'opt-in': False,
53 'version': metrics_utils.CURRENT_VERSION,
54 }
55 self._initialized = True
56 return
Edward Lesmes9c349062021-05-06 20:02:39 +000057
Mike Frysinger124bb8e2023-09-06 05:48:55 +000058 # We are running on a bot. Ignore config and collect metrics.
59 if metrics_utils.REPORT_BUILD:
60 self._config = {
61 'is-googler': True,
62 'countdown': 0,
63 'opt-in': True,
64 'version': metrics_utils.CURRENT_VERSION,
65 }
66 self._initialized = True
67 return
Edward Lesmes9c349062021-05-06 20:02:39 +000068
Mike Frysinger124bb8e2023-09-06 05:48:55 +000069 try:
70 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
71 except (IOError, ValueError):
72 config = {}
Edward Lemur32e3d1e2018-07-12 00:54:05 +000073
Mike Frysinger124bb8e2023-09-06 05:48:55 +000074 self._config = config.copy()
Edward Lemur32e3d1e2018-07-12 00:54:05 +000075
Mike Frysinger124bb8e2023-09-06 05:48:55 +000076 if 'is-googler' not in self._config:
77 # /should-upload is only accessible from Google IPs, so we only need
78 # to check if we can reach the page. An external developer would get
79 # access denied.
80 try:
81 req = urllib.request.urlopen(metrics_utils.APP_URL +
82 '/should-upload')
83 self._config['is-googler'] = req.getcode() == 200
84 except (urllib.request.URLError, urllib.request.HTTPError):
85 self._config['is-googler'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +000086
Mike Frysinger124bb8e2023-09-06 05:48:55 +000087 # Make sure the config variables we need are present, and initialize
88 # them to safe values otherwise.
89 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
90 self._config.setdefault('opt-in', None)
91 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000092
Mike Frysinger124bb8e2023-09-06 05:48:55 +000093 if config != self._config:
94 print(INVALID_CONFIG_WARNING, file=sys.stderr)
95 self._write_config()
Edward Lemur32e3d1e2018-07-12 00:54:05 +000096
Mike Frysinger124bb8e2023-09-06 05:48:55 +000097 self._initialized = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +000098
Mike Frysinger124bb8e2023-09-06 05:48:55 +000099 def _write_config(self):
100 try:
101 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
102 except IOError as e:
103 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
104 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000105
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000106 @property
107 def version(self):
108 self._ensure_initialized()
109 return self._config['version']
Edward Lemur48836262018-10-18 02:08:06 +0000110
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000111 @property
112 def is_googler(self):
113 self._ensure_initialized()
114 return self._config['is-googler']
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000115
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000116 @property
117 def opted_in(self):
118 self._ensure_initialized()
119 return self._config['opt-in']
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000120
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000121 @opted_in.setter
122 def opted_in(self, value):
123 self._ensure_initialized()
124 self._config['opt-in'] = value
125 self._config['version'] = metrics_utils.CURRENT_VERSION
126 self._write_config()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000127
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000128 @property
129 def countdown(self):
130 self._ensure_initialized()
131 return self._config['countdown']
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000132
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000133 @property
134 def should_collect_metrics(self):
135 # Don't report metrics if user is not a Googler.
136 if not self.is_googler:
137 return False
138 # Don't report metrics if user has opted out.
139 if self.opted_in is False:
140 return False
141 # Don't report metrics if countdown hasn't reached 0.
142 if self.opted_in is None and self.countdown > 0:
143 return False
144 return True
Edward Lemur48836262018-10-18 02:08:06 +0000145
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000146 def decrease_countdown(self):
147 self._ensure_initialized()
148 if self.countdown == 0:
149 return
150 self._config['countdown'] -= 1
151 if self.countdown == 0:
152 self._config['version'] = metrics_utils.CURRENT_VERSION
153 self._write_config()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000154
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000155 def reset_config(self):
156 # Only reset countdown if we're already collecting metrics.
157 if self.should_collect_metrics:
158 self._ensure_initialized()
159 self._config['countdown'] = DEFAULT_COUNTDOWN
160 self._config['opt-in'] = None
Edward Lemur48836262018-10-18 02:08:06 +0000161
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000162
163class MetricsCollector(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000164 def __init__(self):
165 self._metrics_lock = threading.Lock()
166 self._reported_metrics = {}
167 self._config = _Config()
168 self._collecting_metrics = False
169 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000170
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000171 @property
172 def config(self):
173 return self._config
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000174
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 @property
176 def collecting_metrics(self):
177 return self._collecting_metrics
Edward Lemur3298e7b2018-07-17 18:21:27 +0000178
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 def add(self, name, value):
180 if self._collect_custom_metrics:
181 with self._metrics_lock:
182 self._reported_metrics[name] = value
Edward Lemur6f812e12018-07-31 22:45:57 +0000183
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000184 def add_repeated(self, name, value):
185 if self._collect_custom_metrics:
186 with self._metrics_lock:
187 self._reported_metrics.setdefault(name, []).append(value)
Edward Lemur149834e2018-10-22 19:15:13 +0000188
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000189 @contextlib.contextmanager
190 def pause_metrics_collection(self):
191 collect_custom_metrics = self._collect_custom_metrics
192 self._collect_custom_metrics = False
193 try:
194 yield
195 finally:
196 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000197
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000198 def _upload_metrics_data(self):
199 """Upload the metrics data to the AppEngine app."""
200 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT],
201 stdin=subprocess2.PIPE)
202 # We invoke a subprocess, and use stdin.write instead of communicate(),
203 # so that we are able to return immediately, leaving the upload running
204 # in the background.
205 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
206 # ... but if we're running on a bot, wait until upload has completed.
207 if metrics_utils.REPORT_BUILD:
208 p.communicate()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000209
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000210 def _collect_metrics(self, func, command_name, *args, **kwargs):
211 # If we're already collecting metrics, just execute the function.
212 # e.g. git-cl split invokes git-cl upload several times to upload each
213 # split CL.
214 if self.collecting_metrics:
215 # Don't collect metrics for this function.
216 # e.g. Don't record the arguments git-cl split passes to git-cl
217 # upload.
218 with self.pause_metrics_collection():
219 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000220
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000221 self._collecting_metrics = True
222 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
223 self.add('command', command_name)
224 for env in DEPOT_TOOLS_ENV:
225 if env in os.environ:
226 self.add_repeated('env_vars', {
227 'name': env,
228 'value': os.environ.get(env)
229 })
Joanna Wang5ed21de2023-02-02 21:17:41 +0000230
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000231 try:
232 start = time.time()
233 result = func(*args, **kwargs)
234 exception = None
235 # pylint: disable=bare-except
236 except:
237 exception = sys.exc_info()
238 finally:
239 self.add('execution_time', time.time() - start)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000240
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 exit_code = metrics_utils.return_code_from_exception(exception)
242 self.add('exit_code', exit_code)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000243
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000244 # Add metrics regarding environment information.
245 self.add('timestamp', int(time.time()))
246 self.add('python_version', metrics_utils.get_python_version())
247 self.add('host_os', gclient_utils.GetOperatingSystem())
248 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000249
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000250 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
251 if depot_tools_age is not None:
252 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000253
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000254 git_version = metrics_utils.get_git_version()
255 if git_version:
256 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000257
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000258 bot_metrics = metrics_utils.get_bot_metrics()
259 if bot_metrics:
260 self.add('bot_metrics', bot_metrics)
Edward Lesmes1e59a242021-04-30 18:38:25 +0000261
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000262 self._upload_metrics_data()
263 if exception:
264 gclient_utils.reraise(exception[0], exception[1], exception[2])
265 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000266
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000267 def collect_metrics(self, command_name):
268 """A decorator used to collect metrics over the life of a function.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000269
270 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000271 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000272 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 def _decorator(func):
274 if not self.config.should_collect_metrics:
275 return func
276 # Needed to preserve the __name__ and __doc__ attributes of func.
277 @functools.wraps(func)
278 def _inner(*args, **kwargs):
279 return self._collect_metrics(func, command_name, *args,
280 **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000281
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000282 return _inner
283
284 return _decorator
285
286 @contextlib.contextmanager
287 def print_notice_and_exit(self):
288 """A context manager used to print the notice and terminate execution.
Edward Lemur6f812e12018-07-31 22:45:57 +0000289
290 This decorator executes the function and prints the monitoring notice if
291 necessary. If an exception is raised, we will catch it, and print it before
292 printing the metrics collection notice.
293 This will call sys.exit() with an appropriate exit code to ensure the notice
294 is the last thing printed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000295 # Needed to preserve the __name__ and __doc__ attributes of func.
296 try:
297 yield
298 exception = None
299 # pylint: disable=bare-except
300 except:
301 exception = sys.exc_info()
Edward Lemur6f812e12018-07-31 22:45:57 +0000302
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000303 # Print the exception before the metrics notice, so that the notice is
304 # clearly visible even if gclient fails.
305 if exception:
306 if isinstance(exception[1], KeyboardInterrupt):
307 sys.stderr.write('Interrupted\n')
308 elif not isinstance(exception[1], SystemExit):
309 traceback.print_exception(*exception)
Edward Lemur6f812e12018-07-31 22:45:57 +0000310
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000311 # Check if the version has changed
312 if (self.config.is_googler and self.config.opted_in is not False
313 and self.config.version != metrics_utils.CURRENT_VERSION):
314 metrics_utils.print_version_change(self.config.version)
315 self.config.reset_config()
Edward Lemur48836262018-10-18 02:08:06 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 # Print the notice
318 if self.config.is_googler and self.config.opted_in is None:
319 metrics_utils.print_notice(self.config.countdown)
320 self.config.decrease_countdown()
Edward Lemur6f812e12018-07-31 22:45:57 +0000321
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000322 sys.exit(metrics_utils.return_code_from_exception(exception))
Edward Lemur6f812e12018-07-31 22:45:57 +0000323
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000324
325collector = MetricsCollector()