blob: 4641bf2d5f3a90a5fa2f785070c935e45d18c68e [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
23
24DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
25CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
26UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
27
Edward Lemur32e3d1e2018-07-12 00:54:05 +000028DEFAULT_COUNTDOWN = 10
29
Joanna Wang92d7df82023-02-01 18:12:02 +000030# TODO(b/265929888): Remove this variable when dogfood is over.
31DEPOT_TOOLS_ENV = ['DOGFOOD_STACKED_CHANGES']
32
Edward Lemur32e3d1e2018-07-12 00:54:05 +000033INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000034 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Tomasz Ĺšniatowskibc56d8c2018-08-28 17:56:25 +000035 'be created.'
Edward Lemurdd5051f2018-08-08 00:56:41 +000036)
37PERMISSION_DENIED_WARNING = (
38 'Could not write the metrics collection config:\n\t%s\n'
39 'Metrics collection will be disabled.'
Edward Lemur32e3d1e2018-07-12 00:54:05 +000040)
41
42
43class _Config(object):
44 def __init__(self):
45 self._initialized = False
46 self._config = {}
47
48 def _ensure_initialized(self):
49 if self._initialized:
50 return
51
Edward Lesmes9c349062021-05-06 20:02:39 +000052 # Metrics collection is disabled, so don't collect any metrics.
53 if not metrics_utils.COLLECT_METRICS:
54 self._config = {
55 'is-googler': False,
56 'countdown': 0,
57 'opt-in': False,
58 'version': metrics_utils.CURRENT_VERSION,
59 }
60 self._initialized = True
61 return
62
Josip Sokcevic4de5dea2022-03-23 21:15:14 +000063 # We are running on a bot. Ignore config and collect metrics.
Edward Lesmes9c349062021-05-06 20:02:39 +000064 if metrics_utils.REPORT_BUILD:
65 self._config = {
66 'is-googler': True,
67 'countdown': 0,
68 'opt-in': True,
69 'version': metrics_utils.CURRENT_VERSION,
70 }
71 self._initialized = True
72 return
73
Edward Lemur32e3d1e2018-07-12 00:54:05 +000074 try:
75 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
76 except (IOError, ValueError):
77 config = {}
78
79 self._config = config.copy()
80
81 if 'is-googler' not in self._config:
82 # /should-upload is only accessible from Google IPs, so we only need to
83 # check if we can reach the page. An external developer would get access
84 # denied.
85 try:
Gavin Mak5f955df2023-08-30 15:39:13 +000086 req = urllib.request.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000087 self._config['is-googler'] = req.getcode() == 200
Gavin Mak5f955df2023-08-30 15:39:13 +000088 except (urllib.request.URLError, urllib.request.HTTPError):
Edward Lemur32e3d1e2018-07-12 00:54:05 +000089 self._config['is-googler'] = False
90
91 # Make sure the config variables we need are present, and initialize them to
92 # safe values otherwise.
93 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
94 self._config.setdefault('opt-in', None)
Edward Lemur48836262018-10-18 02:08:06 +000095 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000096
97 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +000098 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000099 self._write_config()
100
101 self._initialized = True
102
103 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +0000104 try:
105 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
106 except IOError as e:
107 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
108 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000109
110 @property
Edward Lemur48836262018-10-18 02:08:06 +0000111 def version(self):
112 self._ensure_initialized()
113 return self._config['version']
114
115 @property
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000116 def is_googler(self):
117 self._ensure_initialized()
118 return self._config['is-googler']
119
120 @property
121 def opted_in(self):
122 self._ensure_initialized()
123 return self._config['opt-in']
124
125 @opted_in.setter
126 def opted_in(self, value):
127 self._ensure_initialized()
128 self._config['opt-in'] = value
Edward Lemur48836262018-10-18 02:08:06 +0000129 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000130 self._write_config()
131
132 @property
133 def countdown(self):
134 self._ensure_initialized()
135 return self._config['countdown']
136
Edward Lemur48836262018-10-18 02:08:06 +0000137 @property
138 def should_collect_metrics(self):
Edward Lesmes1e59a242021-04-30 18:38:25 +0000139 # Don't report metrics if user is not a Googler.
Edward Lemur48836262018-10-18 02:08:06 +0000140 if not self.is_googler:
141 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000142 # Don't report metrics if user has opted out.
Edward Lemur48836262018-10-18 02:08:06 +0000143 if self.opted_in is False:
144 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000145 # Don't report metrics if countdown hasn't reached 0.
Edward Lemur48836262018-10-18 02:08:06 +0000146 if self.opted_in is None and self.countdown > 0:
147 return False
148 return True
149
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000150 def decrease_countdown(self):
151 self._ensure_initialized()
152 if self.countdown == 0:
153 return
154 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000155 if self.countdown == 0:
156 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000157 self._write_config()
158
Edward Lemur48836262018-10-18 02:08:06 +0000159 def reset_config(self):
160 # Only reset countdown if we're already collecting metrics.
161 if self.should_collect_metrics:
162 self._ensure_initialized()
163 self._config['countdown'] = DEFAULT_COUNTDOWN
164 self._config['opt-in'] = None
165
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000166
167class MetricsCollector(object):
168 def __init__(self):
169 self._metrics_lock = threading.Lock()
170 self._reported_metrics = {}
171 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000172 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000173 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000174
175 @property
176 def config(self):
177 return self._config
178
Edward Lemur3298e7b2018-07-17 18:21:27 +0000179 @property
180 def collecting_metrics(self):
181 return self._collecting_metrics
182
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000183 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000184 if self._collect_custom_metrics:
185 with self._metrics_lock:
186 self._reported_metrics[name] = value
187
Edward Lemur149834e2018-10-22 19:15:13 +0000188 def add_repeated(self, name, value):
189 if self._collect_custom_metrics:
190 with self._metrics_lock:
191 self._reported_metrics.setdefault(name, []).append(value)
192
Edward Lemur6f812e12018-07-31 22:45:57 +0000193 @contextlib.contextmanager
194 def pause_metrics_collection(self):
195 collect_custom_metrics = self._collect_custom_metrics
196 self._collect_custom_metrics = False
197 try:
198 yield
199 finally:
200 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000201
202 def _upload_metrics_data(self):
203 """Upload the metrics data to the AppEngine app."""
Edward Lesmes73faeea2021-05-07 17:12:20 +0000204 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000205 # We invoke a subprocess, and use stdin.write instead of communicate(),
206 # so that we are able to return immediately, leaving the upload running in
207 # the background.
Edward Lemur73065b22019-07-22 20:12:01 +0000208 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
Edward Lesmes73faeea2021-05-07 17:12:20 +0000209 # ... but if we're running on a bot, wait until upload has completed.
210 if metrics_utils.REPORT_BUILD:
211 p.communicate()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000212
213 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000214 # If we're already collecting metrics, just execute the function.
215 # e.g. git-cl split invokes git-cl upload several times to upload each
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000216 # split CL.
Edward Lemur6f812e12018-07-31 22:45:57 +0000217 if self.collecting_metrics:
218 # Don't collect metrics for this function.
219 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
220 with self.pause_metrics_collection():
221 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000222
223 self._collecting_metrics = True
Edward Lemur18df41e2019-04-26 00:42:04 +0000224 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000225 self.add('command', command_name)
Joanna Wang5ed21de2023-02-02 21:17:41 +0000226 for env in DEPOT_TOOLS_ENV:
227 if env in os.environ:
Joanna Wang79499cf2023-02-02 22:07:02 +0000228 self.add_repeated('env_vars', {
Joanna Wang5ed21de2023-02-02 21:17:41 +0000229 'name': env,
230 'value': os.environ.get(env)
231 })
232
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000233 try:
234 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000235 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000236 exception = None
237 # pylint: disable=bare-except
238 except:
239 exception = sys.exc_info()
240 finally:
241 self.add('execution_time', time.time() - start)
242
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000243 exit_code = metrics_utils.return_code_from_exception(exception)
244 self.add('exit_code', exit_code)
245
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000246 # Add metrics regarding environment information.
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000247 self.add('timestamp', int(time.time()))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000248 self.add('python_version', metrics_utils.get_python_version())
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000249 self.add('host_os', gclient_utils.GetOperatingSystem())
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000250 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000251
252 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
253 if depot_tools_age is not None:
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000254 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000255
256 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
Edward Lesmes1e59a242021-04-30 18:38:25 +0000260 bot_metrics = metrics_utils.get_bot_metrics()
261 if bot_metrics:
262 self.add('bot_metrics', bot_metrics)
263
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000264 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000265 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000266 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000267 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000268
269 def collect_metrics(self, command_name):
270 """A decorator used to collect metrics over the life of a function.
271
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 """
275 def _decorator(func):
Edward Lemur48836262018-10-18 02:08:06 +0000276 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000277 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000278 # Needed to preserve the __name__ and __doc__ attributes of func.
279 @functools.wraps(func)
280 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000281 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000282 return _inner
283 return _decorator
284
Edward Lemur6f812e12018-07-31 22:45:57 +0000285 @contextlib.contextmanager
286 def print_notice_and_exit(self):
287 """A context manager used to print the notice and terminate execution.
288
289 This decorator executes the function and prints the monitoring notice if
290 necessary. If an exception is raised, we will catch it, and print it before
291 printing the metrics collection notice.
292 This will call sys.exit() with an appropriate exit code to ensure the notice
293 is the last thing printed."""
294 # Needed to preserve the __name__ and __doc__ attributes of func.
295 try:
296 yield
297 exception = None
298 # pylint: disable=bare-except
299 except:
300 exception = sys.exc_info()
301
302 # Print the exception before the metrics notice, so that the notice is
303 # clearly visible even if gclient fails.
304 if exception:
305 if isinstance(exception[1], KeyboardInterrupt):
306 sys.stderr.write('Interrupted\n')
307 elif not isinstance(exception[1], SystemExit):
308 traceback.print_exception(*exception)
309
Edward Lemur48836262018-10-18 02:08:06 +0000310 # Check if the version has changed
Edward Lesmes9c349062021-05-06 20:02:39 +0000311 if (self.config.is_googler
Edward Lemur48836262018-10-18 02:08:06 +0000312 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()
316
Edward Lemur6f812e12018-07-31 22:45:57 +0000317 # Print the notice
Edward Lesmes9c349062021-05-06 20:02:39 +0000318 if self.config.is_googler and self.config.opted_in is None:
Edward Lemur6f812e12018-07-31 22:45:57 +0000319 metrics_utils.print_notice(self.config.countdown)
320 self.config.decrease_countdown()
321
322 sys.exit(metrics_utils.return_code_from_exception(exception))
323
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000324
325collector = MetricsCollector()