blob: 3ccbfc9a112b414693e37db411af8fad55ff7149 [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
Gavin Mak5f955df2023-08-30 15:39:13 +000016import urllib.request
Edward Lemur32e3d1e2018-07-12 00:54:05 +000017
18import detect_host_arch
19import gclient_utils
20import metrics_utils
Edward Lesmese1a9c8d2020-04-15 02:54:55 +000021import subprocess2
Edward Lemur32e3d1e2018-07-12 00:54:05 +000022
Edward Lemur32e3d1e2018-07-12 00:54:05 +000023DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
24CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
25UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
26
Edward Lemur32e3d1e2018-07-12 00:54:05 +000027DEFAULT_COUNTDOWN = 10
28
Joanna Wang92d7df82023-02-01 18:12:02 +000029# TODO(b/265929888): Remove this variable when dogfood is over.
30DEPOT_TOOLS_ENV = ['DOGFOOD_STACKED_CHANGES']
31
Edward Lemur32e3d1e2018-07-12 00:54:05 +000032INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000033 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Mike Frysinger124bb8e2023-09-06 05:48:55 +000034 'be created.')
Edward Lemurdd5051f2018-08-08 00:56:41 +000035PERMISSION_DENIED_WARNING = (
36 'Could not write the metrics collection config:\n\t%s\n'
Mike Frysinger124bb8e2023-09-06 05:48:55 +000037 'Metrics collection will be disabled.')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000038
39
40class _Config(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000041 def __init__(self):
42 self._initialized = False
43 self._config = {}
Edward Lemur32e3d1e2018-07-12 00:54:05 +000044
Mike Frysinger124bb8e2023-09-06 05:48:55 +000045 def _ensure_initialized(self):
46 if self._initialized:
47 return
Edward Lemur32e3d1e2018-07-12 00:54:05 +000048
Mike Frysinger124bb8e2023-09-06 05:48:55 +000049 # Metrics collection is disabled, so don't collect any metrics.
50 if not metrics_utils.COLLECT_METRICS:
51 self._config = {
52 'is-googler': False,
53 'countdown': 0,
54 'opt-in': False,
55 'version': metrics_utils.CURRENT_VERSION,
56 }
57 self._initialized = True
58 return
Edward Lesmes9c349062021-05-06 20:02:39 +000059
Mike Frysinger124bb8e2023-09-06 05:48:55 +000060 # We are running on a bot. Ignore config and collect metrics.
61 if metrics_utils.REPORT_BUILD:
62 self._config = {
63 'is-googler': True,
64 'countdown': 0,
65 'opt-in': True,
66 'version': metrics_utils.CURRENT_VERSION,
67 }
68 self._initialized = True
69 return
Edward Lesmes9c349062021-05-06 20:02:39 +000070
Mike Frysinger124bb8e2023-09-06 05:48:55 +000071 try:
72 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
73 except (IOError, ValueError):
74 config = {}
Edward Lemur32e3d1e2018-07-12 00:54:05 +000075
Mike Frysinger124bb8e2023-09-06 05:48:55 +000076 self._config = config.copy()
Edward Lemur32e3d1e2018-07-12 00:54:05 +000077
Mike Frysinger124bb8e2023-09-06 05:48:55 +000078 if 'is-googler' not in self._config:
79 # /should-upload is only accessible from Google IPs, so we only need
80 # to check if we can reach the page. An external developer would get
81 # access denied.
82 try:
83 req = urllib.request.urlopen(metrics_utils.APP_URL +
84 '/should-upload')
85 self._config['is-googler'] = req.getcode() == 200
86 except (urllib.request.URLError, urllib.request.HTTPError):
87 self._config['is-googler'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +000088
Mike Frysinger124bb8e2023-09-06 05:48:55 +000089 # Make sure the config variables we need are present, and initialize
90 # them to safe values otherwise.
91 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
92 self._config.setdefault('opt-in', None)
93 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000094
Mike Frysinger124bb8e2023-09-06 05:48:55 +000095 if config != self._config:
96 print(INVALID_CONFIG_WARNING, file=sys.stderr)
97 self._write_config()
Edward Lemur32e3d1e2018-07-12 00:54:05 +000098
Mike Frysinger124bb8e2023-09-06 05:48:55 +000099 self._initialized = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000100
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000101 def _write_config(self):
102 try:
103 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
104 except IOError as e:
105 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
106 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000107
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000108 @property
109 def version(self):
110 self._ensure_initialized()
111 return self._config['version']
Edward Lemur48836262018-10-18 02:08:06 +0000112
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000113 @property
114 def is_googler(self):
115 self._ensure_initialized()
116 return self._config['is-googler']
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000117
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000118 @property
119 def opted_in(self):
120 self._ensure_initialized()
121 return self._config['opt-in']
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000122
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000123 @opted_in.setter
124 def opted_in(self, value):
125 self._ensure_initialized()
126 self._config['opt-in'] = value
127 self._config['version'] = metrics_utils.CURRENT_VERSION
128 self._write_config()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000129
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000130 @property
131 def countdown(self):
132 self._ensure_initialized()
133 return self._config['countdown']
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000134
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 @property
136 def should_collect_metrics(self):
137 # Don't report metrics if user is not a Googler.
138 if not self.is_googler:
139 return False
140 # Don't report metrics if user has opted out.
141 if self.opted_in is False:
142 return False
143 # Don't report metrics if countdown hasn't reached 0.
144 if self.opted_in is None and self.countdown > 0:
145 return False
146 return True
Edward Lemur48836262018-10-18 02:08:06 +0000147
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000148 def decrease_countdown(self):
149 self._ensure_initialized()
150 if self.countdown == 0:
151 return
152 self._config['countdown'] -= 1
153 if self.countdown == 0:
154 self._config['version'] = metrics_utils.CURRENT_VERSION
155 self._write_config()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000156
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000157 def reset_config(self):
158 # Only reset countdown if we're already collecting metrics.
159 if self.should_collect_metrics:
160 self._ensure_initialized()
161 self._config['countdown'] = DEFAULT_COUNTDOWN
162 self._config['opt-in'] = None
Edward Lemur48836262018-10-18 02:08:06 +0000163
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000164
165class MetricsCollector(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000166 def __init__(self):
167 self._metrics_lock = threading.Lock()
168 self._reported_metrics = {}
169 self._config = _Config()
170 self._collecting_metrics = False
171 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000172
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000173 @property
174 def config(self):
175 return self._config
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000176
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000177 @property
178 def collecting_metrics(self):
179 return self._collecting_metrics
Edward Lemur3298e7b2018-07-17 18:21:27 +0000180
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000181 def add(self, name, value):
182 if self._collect_custom_metrics:
183 with self._metrics_lock:
184 self._reported_metrics[name] = value
Edward Lemur6f812e12018-07-31 22:45:57 +0000185
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000186 def add_repeated(self, name, value):
187 if self._collect_custom_metrics:
188 with self._metrics_lock:
189 self._reported_metrics.setdefault(name, []).append(value)
Edward Lemur149834e2018-10-22 19:15:13 +0000190
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000191 @contextlib.contextmanager
192 def pause_metrics_collection(self):
193 collect_custom_metrics = self._collect_custom_metrics
194 self._collect_custom_metrics = False
195 try:
196 yield
197 finally:
198 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000199
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000200 def _upload_metrics_data(self):
201 """Upload the metrics data to the AppEngine app."""
202 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT],
203 stdin=subprocess2.PIPE)
204 # We invoke a subprocess, and use stdin.write instead of communicate(),
205 # so that we are able to return immediately, leaving the upload running
206 # in the background.
207 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
208 # ... but if we're running on a bot, wait until upload has completed.
209 if metrics_utils.REPORT_BUILD:
210 p.communicate()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000211
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000212 def _collect_metrics(self, func, command_name, *args, **kwargs):
213 # If we're already collecting metrics, just execute the function.
214 # e.g. git-cl split invokes git-cl upload several times to upload each
215 # split CL.
216 if self.collecting_metrics:
217 # Don't collect metrics for this function.
218 # e.g. Don't record the arguments git-cl split passes to git-cl
219 # upload.
220 with self.pause_metrics_collection():
221 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000222
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000223 self._collecting_metrics = True
224 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
225 self.add('command', command_name)
226 for env in DEPOT_TOOLS_ENV:
227 if env in os.environ:
228 self.add_repeated('env_vars', {
229 'name': env,
230 'value': os.environ.get(env)
231 })
Joanna Wang5ed21de2023-02-02 21:17:41 +0000232
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000233 try:
234 start = time.time()
235 result = func(*args, **kwargs)
236 exception = None
237 # pylint: disable=bare-except
238 except:
239 exception = sys.exc_info()
240 finally:
241 self.add('execution_time', time.time() - start)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000242
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 exit_code = metrics_utils.return_code_from_exception(exception)
244 self.add('exit_code', exit_code)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000245
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000246 # Add metrics regarding environment information.
247 self.add('timestamp', int(time.time()))
248 self.add('python_version', metrics_utils.get_python_version())
249 self.add('host_os', gclient_utils.GetOperatingSystem())
250 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000251
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
253 if depot_tools_age is not None:
254 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000255
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000256 git_version = metrics_utils.get_git_version()
257 if git_version:
258 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000259
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000260 bot_metrics = metrics_utils.get_bot_metrics()
261 if bot_metrics:
262 self.add('bot_metrics', bot_metrics)
Edward Lesmes1e59a242021-04-30 18:38:25 +0000263
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000264 self._upload_metrics_data()
265 if exception:
266 gclient_utils.reraise(exception[0], exception[1], exception[2])
267 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000268
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000269 def collect_metrics(self, command_name):
270 """A decorator used to collect metrics over the life of a function.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000271
272 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000273 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000274 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000275 def _decorator(func):
276 if not self.config.should_collect_metrics:
277 return func
278 # Needed to preserve the __name__ and __doc__ attributes of func.
279 @functools.wraps(func)
280 def _inner(*args, **kwargs):
281 return self._collect_metrics(func, command_name, *args,
282 **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000283
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 return _inner
285
286 return _decorator
287
288 @contextlib.contextmanager
289 def print_notice_and_exit(self):
290 """A context manager used to print the notice and terminate execution.
Edward Lemur6f812e12018-07-31 22:45:57 +0000291
292 This decorator executes the function and prints the monitoring notice if
293 necessary. If an exception is raised, we will catch it, and print it before
294 printing the metrics collection notice.
295 This will call sys.exit() with an appropriate exit code to ensure the notice
296 is the last thing printed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000297 # Needed to preserve the __name__ and __doc__ attributes of func.
298 try:
299 yield
300 exception = None
301 # pylint: disable=bare-except
302 except:
303 exception = sys.exc_info()
Edward Lemur6f812e12018-07-31 22:45:57 +0000304
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000305 # Print the exception before the metrics notice, so that the notice is
306 # clearly visible even if gclient fails.
307 if exception:
308 if isinstance(exception[1], KeyboardInterrupt):
309 sys.stderr.write('Interrupted\n')
310 elif not isinstance(exception[1], SystemExit):
311 traceback.print_exception(*exception)
Edward Lemur6f812e12018-07-31 22:45:57 +0000312
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000313 # Check if the version has changed
314 if (self.config.is_googler and self.config.opted_in is not False
315 and self.config.version != metrics_utils.CURRENT_VERSION):
316 metrics_utils.print_version_change(self.config.version)
317 self.config.reset_config()
Edward Lemur48836262018-10-18 02:08:06 +0000318
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000319 # Print the notice
320 if self.config.is_googler and self.config.opted_in is None:
321 metrics_utils.print_notice(self.config.countdown)
322 self.config.decrease_countdown()
Edward Lemur6f812e12018-07-31 22:45:57 +0000323
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000324 sys.exit(metrics_utils.return_code_from_exception(exception))
Edward Lemur6f812e12018-07-31 22:45:57 +0000325
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000326
327collector = MetricsCollector()