blob: 11e43c6ad36ada83c1798ecf23fc518e2f32901a [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
35INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000036 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Tomasz Ĺšniatowskibc56d8c2018-08-28 17:56:25 +000037 'be created.'
Edward Lemurdd5051f2018-08-08 00:56:41 +000038)
39PERMISSION_DENIED_WARNING = (
40 'Could not write the metrics collection config:\n\t%s\n'
41 'Metrics collection will be disabled.'
Edward Lemur32e3d1e2018-07-12 00:54:05 +000042)
43
44
45class _Config(object):
46 def __init__(self):
47 self._initialized = False
48 self._config = {}
49
50 def _ensure_initialized(self):
51 if self._initialized:
52 return
53
Edward Lesmes9c349062021-05-06 20:02:39 +000054 # Metrics collection is disabled, so don't collect any metrics.
55 if not metrics_utils.COLLECT_METRICS:
56 self._config = {
57 'is-googler': False,
58 'countdown': 0,
59 'opt-in': False,
60 'version': metrics_utils.CURRENT_VERSION,
61 }
62 self._initialized = True
63 return
64
Josip Sokcevic4de5dea2022-03-23 21:15:14 +000065 # We are running on a bot. Ignore config and collect metrics.
Edward Lesmes9c349062021-05-06 20:02:39 +000066 if metrics_utils.REPORT_BUILD:
67 self._config = {
68 'is-googler': True,
69 'countdown': 0,
70 'opt-in': True,
71 'version': metrics_utils.CURRENT_VERSION,
72 }
73 self._initialized = True
74 return
75
Edward Lemur32e3d1e2018-07-12 00:54:05 +000076 try:
77 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
78 except (IOError, ValueError):
79 config = {}
80
81 self._config = config.copy()
82
83 if 'is-googler' not in self._config:
84 # /should-upload is only accessible from Google IPs, so we only need to
85 # check if we can reach the page. An external developer would get access
86 # denied.
87 try:
Raul Tambreb946b232019-03-26 14:48:46 +000088 req = urllib.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000089 self._config['is-googler'] = req.getcode() == 200
Raul Tambreb946b232019-03-26 14:48:46 +000090 except (urllib.URLError, urllib.HTTPError):
Edward Lemur32e3d1e2018-07-12 00:54:05 +000091 self._config['is-googler'] = False
92
93 # Make sure the config variables we need are present, and initialize them to
94 # safe values otherwise.
95 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
96 self._config.setdefault('opt-in', None)
Edward Lemur48836262018-10-18 02:08:06 +000097 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000098
99 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +0000100 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000101 self._write_config()
102
103 self._initialized = True
104
105 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +0000106 try:
107 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
108 except IOError as e:
109 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
110 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000111
112 @property
Edward Lemur48836262018-10-18 02:08:06 +0000113 def version(self):
114 self._ensure_initialized()
115 return self._config['version']
116
117 @property
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000118 def is_googler(self):
119 self._ensure_initialized()
120 return self._config['is-googler']
121
122 @property
123 def opted_in(self):
124 self._ensure_initialized()
125 return self._config['opt-in']
126
127 @opted_in.setter
128 def opted_in(self, value):
129 self._ensure_initialized()
130 self._config['opt-in'] = value
Edward Lemur48836262018-10-18 02:08:06 +0000131 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000132 self._write_config()
133
134 @property
135 def countdown(self):
136 self._ensure_initialized()
137 return self._config['countdown']
138
Edward Lemur48836262018-10-18 02:08:06 +0000139 @property
140 def should_collect_metrics(self):
Edward Lesmes1e59a242021-04-30 18:38:25 +0000141 # Don't report metrics if user is not a Googler.
Edward Lemur48836262018-10-18 02:08:06 +0000142 if not self.is_googler:
143 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000144 # Don't report metrics if user has opted out.
Edward Lemur48836262018-10-18 02:08:06 +0000145 if self.opted_in is False:
146 return False
Edward Lesmes1e59a242021-04-30 18:38:25 +0000147 # Don't report metrics if countdown hasn't reached 0.
Edward Lemur48836262018-10-18 02:08:06 +0000148 if self.opted_in is None and self.countdown > 0:
149 return False
150 return True
151
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000152 def decrease_countdown(self):
153 self._ensure_initialized()
154 if self.countdown == 0:
155 return
156 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000157 if self.countdown == 0:
158 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000159 self._write_config()
160
Edward Lemur48836262018-10-18 02:08:06 +0000161 def reset_config(self):
162 # Only reset countdown if we're already collecting metrics.
163 if self.should_collect_metrics:
164 self._ensure_initialized()
165 self._config['countdown'] = DEFAULT_COUNTDOWN
166 self._config['opt-in'] = None
167
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000168
169class MetricsCollector(object):
170 def __init__(self):
171 self._metrics_lock = threading.Lock()
172 self._reported_metrics = {}
173 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000174 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000175 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000176
177 @property
178 def config(self):
179 return self._config
180
Edward Lemur3298e7b2018-07-17 18:21:27 +0000181 @property
182 def collecting_metrics(self):
183 return self._collecting_metrics
184
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000185 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000186 if self._collect_custom_metrics:
187 with self._metrics_lock:
188 self._reported_metrics[name] = value
189
Edward Lemur149834e2018-10-22 19:15:13 +0000190 def add_repeated(self, name, value):
191 if self._collect_custom_metrics:
192 with self._metrics_lock:
193 self._reported_metrics.setdefault(name, []).append(value)
194
Edward Lemur6f812e12018-07-31 22:45:57 +0000195 @contextlib.contextmanager
196 def pause_metrics_collection(self):
197 collect_custom_metrics = self._collect_custom_metrics
198 self._collect_custom_metrics = False
199 try:
200 yield
201 finally:
202 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000203
204 def _upload_metrics_data(self):
205 """Upload the metrics data to the AppEngine app."""
Edward Lesmes73faeea2021-05-07 17:12:20 +0000206 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000207 # We invoke a subprocess, and use stdin.write instead of communicate(),
208 # so that we are able to return immediately, leaving the upload running in
209 # the background.
Edward Lemur73065b22019-07-22 20:12:01 +0000210 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
Edward Lesmes73faeea2021-05-07 17:12:20 +0000211 # ... but if we're running on a bot, wait until upload has completed.
212 if metrics_utils.REPORT_BUILD:
213 p.communicate()
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000214
215 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000216 # If we're already collecting metrics, just execute the function.
217 # e.g. git-cl split invokes git-cl upload several times to upload each
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000218 # split CL.
Edward Lemur6f812e12018-07-31 22:45:57 +0000219 if self.collecting_metrics:
220 # Don't collect metrics for this function.
221 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
222 with self.pause_metrics_collection():
223 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000224
225 self._collecting_metrics = True
Edward Lemur18df41e2019-04-26 00:42:04 +0000226 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000227 self.add('command', command_name)
228 try:
229 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000230 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000231 exception = None
232 # pylint: disable=bare-except
233 except:
234 exception = sys.exc_info()
235 finally:
236 self.add('execution_time', time.time() - start)
237
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000238 exit_code = metrics_utils.return_code_from_exception(exception)
239 self.add('exit_code', exit_code)
240
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000241 # Add metrics regarding environment information.
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000242 self.add('timestamp', int(time.time()))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000243 self.add('python_version', metrics_utils.get_python_version())
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000244 self.add('host_os', gclient_utils.GetOperatingSystem())
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000245 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000246
247 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
248 if depot_tools_age is not None:
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000249 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000250
251 git_version = metrics_utils.get_git_version()
252 if git_version:
253 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000254
Edward Lesmes1e59a242021-04-30 18:38:25 +0000255 bot_metrics = metrics_utils.get_bot_metrics()
256 if bot_metrics:
257 self.add('bot_metrics', bot_metrics)
258
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000259 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000260 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000261 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000262 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000263
264 def collect_metrics(self, command_name):
265 """A decorator used to collect metrics over the life of a function.
266
267 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000268 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000269 """
270 def _decorator(func):
Edward Lemur48836262018-10-18 02:08:06 +0000271 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000272 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000273 # Needed to preserve the __name__ and __doc__ attributes of func.
274 @functools.wraps(func)
275 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000276 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000277 return _inner
278 return _decorator
279
Edward Lemur6f812e12018-07-31 22:45:57 +0000280 @contextlib.contextmanager
281 def print_notice_and_exit(self):
282 """A context manager used to print the notice and terminate execution.
283
284 This decorator executes the function and prints the monitoring notice if
285 necessary. If an exception is raised, we will catch it, and print it before
286 printing the metrics collection notice.
287 This will call sys.exit() with an appropriate exit code to ensure the notice
288 is the last thing printed."""
289 # Needed to preserve the __name__ and __doc__ attributes of func.
290 try:
291 yield
292 exception = None
293 # pylint: disable=bare-except
294 except:
295 exception = sys.exc_info()
296
297 # Print the exception before the metrics notice, so that the notice is
298 # clearly visible even if gclient fails.
299 if exception:
300 if isinstance(exception[1], KeyboardInterrupt):
301 sys.stderr.write('Interrupted\n')
302 elif not isinstance(exception[1], SystemExit):
303 traceback.print_exception(*exception)
304
Edward Lemur48836262018-10-18 02:08:06 +0000305 # Check if the version has changed
Edward Lesmes9c349062021-05-06 20:02:39 +0000306 if (self.config.is_googler
Edward Lemur48836262018-10-18 02:08:06 +0000307 and self.config.opted_in is not False
308 and self.config.version != metrics_utils.CURRENT_VERSION):
309 metrics_utils.print_version_change(self.config.version)
310 self.config.reset_config()
311
Edward Lemur6f812e12018-07-31 22:45:57 +0000312 # Print the notice
Edward Lesmes9c349062021-05-06 20:02:39 +0000313 if self.config.is_googler and self.config.opted_in is None:
Edward Lemur6f812e12018-07-31 22:45:57 +0000314 metrics_utils.print_notice(self.config.countdown)
315 self.config.decrease_countdown()
316
317 sys.exit(metrics_utils.return_code_from_exception(exception))
318
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000319
320collector = MetricsCollector()