blob: 902a2286119f465f7688a34439ad4e1592fe9a1d [file] [log] [blame]
Edward Lemur32e3d1e2018-07-12 00:54:05 +00001#!/usr/bin/env python
2# 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
65 # We are running on a bot. Ignore config and collect metrics.
66 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."""
206 # We invoke a subprocess, and use stdin.write instead of communicate(),
207 # so that we are able to return immediately, leaving the upload running in
208 # the background.
Edward Lesmese1a9c8d2020-04-15 02:54:55 +0000209 p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
Edward Lemur73065b22019-07-22 20:12:01 +0000210 p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000211
212 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000213 # 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
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000215 # split CL.
Edward Lemur6f812e12018-07-31 22:45:57 +0000216 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 upload.
219 with self.pause_metrics_collection():
220 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000221
222 self._collecting_metrics = True
Edward Lemur18df41e2019-04-26 00:42:04 +0000223 self.add('metrics_version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000224 self.add('command', command_name)
225 try:
226 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000227 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000228 exception = None
229 # pylint: disable=bare-except
230 except:
231 exception = sys.exc_info()
232 finally:
233 self.add('execution_time', time.time() - start)
234
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000235 exit_code = metrics_utils.return_code_from_exception(exception)
236 self.add('exit_code', exit_code)
237
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000238 # Add metrics regarding environment information.
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000239 self.add('timestamp', int(time.time()))
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000240 self.add('python_version', metrics_utils.get_python_version())
Milad Fa52fdd1f2020-09-15 21:24:46 +0000241 self.add('host_os', gclient_utils.GetMacWinAixOrLinux())
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000242 self.add('host_arch', detect_host_arch.HostArch())
Edward Lemur861640f2018-10-31 19:45:31 +0000243
244 depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
245 if depot_tools_age is not None:
Edward Lemur9b51f3e2019-04-26 01:14:36 +0000246 self.add('depot_tools_age', int(depot_tools_age))
Edward Lemur861640f2018-10-31 19:45:31 +0000247
248 git_version = metrics_utils.get_git_version()
249 if git_version:
250 self.add('git_version', git_version)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000251
Edward Lesmes1e59a242021-04-30 18:38:25 +0000252 bot_metrics = metrics_utils.get_bot_metrics()
253 if bot_metrics:
254 self.add('bot_metrics', bot_metrics)
255
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000256 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000257 if exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000258 gclient_utils.reraise(exception[0], exception[1], exception[2])
Edward Lemur6f812e12018-07-31 22:45:57 +0000259 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000260
261 def collect_metrics(self, command_name):
262 """A decorator used to collect metrics over the life of a function.
263
264 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000265 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000266 """
267 def _decorator(func):
Edward Lemur48836262018-10-18 02:08:06 +0000268 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000269 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000270 # Needed to preserve the __name__ and __doc__ attributes of func.
271 @functools.wraps(func)
272 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000273 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000274 return _inner
275 return _decorator
276
Edward Lemur6f812e12018-07-31 22:45:57 +0000277 @contextlib.contextmanager
278 def print_notice_and_exit(self):
279 """A context manager used to print the notice and terminate execution.
280
281 This decorator executes the function and prints the monitoring notice if
282 necessary. If an exception is raised, we will catch it, and print it before
283 printing the metrics collection notice.
284 This will call sys.exit() with an appropriate exit code to ensure the notice
285 is the last thing printed."""
286 # Needed to preserve the __name__ and __doc__ attributes of func.
287 try:
288 yield
289 exception = None
290 # pylint: disable=bare-except
291 except:
292 exception = sys.exc_info()
293
294 # Print the exception before the metrics notice, so that the notice is
295 # clearly visible even if gclient fails.
296 if exception:
297 if isinstance(exception[1], KeyboardInterrupt):
298 sys.stderr.write('Interrupted\n')
299 elif not isinstance(exception[1], SystemExit):
300 traceback.print_exception(*exception)
301
Edward Lemur48836262018-10-18 02:08:06 +0000302 # Check if the version has changed
Edward Lesmes9c349062021-05-06 20:02:39 +0000303 if (self.config.is_googler
Edward Lemur48836262018-10-18 02:08:06 +0000304 and self.config.opted_in is not False
305 and self.config.version != metrics_utils.CURRENT_VERSION):
306 metrics_utils.print_version_change(self.config.version)
307 self.config.reset_config()
308
Edward Lemur6f812e12018-07-31 22:45:57 +0000309 # Print the notice
Edward Lesmes9c349062021-05-06 20:02:39 +0000310 if self.config.is_googler and self.config.opted_in is None:
Edward Lemur6f812e12018-07-31 22:45:57 +0000311 metrics_utils.print_notice(self.config.countdown)
312 self.config.decrease_countdown()
313
314 sys.exit(metrics_utils.return_code_from_exception(exception))
315
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000316
317collector = MetricsCollector()