blob: 674c78b9a59c60a1f18d95f57b22f6eac0cd66f4 [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
12import subprocess
13import sys
14import tempfile
15import threading
16import time
17import traceback
18import urllib2
19
20import detect_host_arch
21import gclient_utils
22import metrics_utils
23
24
25DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
26CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
27UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
28
Edward Lemur32e3d1e2018-07-12 00:54:05 +000029DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0'
30DEFAULT_COUNTDOWN = 10
31
32INVALID_CONFIG_WARNING = (
Edward Lemurdd5051f2018-08-08 00:56:41 +000033 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
Tomasz Ĺšniatowskibc56d8c2018-08-28 17:56:25 +000034 'be created.'
Edward Lemurdd5051f2018-08-08 00:56:41 +000035)
36PERMISSION_DENIED_WARNING = (
37 'Could not write the metrics collection config:\n\t%s\n'
38 'Metrics collection will be disabled.'
Edward Lemur32e3d1e2018-07-12 00:54:05 +000039)
40
41
42class _Config(object):
43 def __init__(self):
44 self._initialized = False
45 self._config = {}
46
47 def _ensure_initialized(self):
48 if self._initialized:
49 return
50
51 try:
52 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
53 except (IOError, ValueError):
54 config = {}
55
56 self._config = config.copy()
57
58 if 'is-googler' not in self._config:
59 # /should-upload is only accessible from Google IPs, so we only need to
60 # check if we can reach the page. An external developer would get access
61 # denied.
62 try:
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000063 req = urllib2.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000064 self._config['is-googler'] = req.getcode() == 200
65 except (urllib2.URLError, urllib2.HTTPError):
66 self._config['is-googler'] = False
67
68 # Make sure the config variables we need are present, and initialize them to
69 # safe values otherwise.
70 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
71 self._config.setdefault('opt-in', None)
Edward Lemur48836262018-10-18 02:08:06 +000072 self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000073
74 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +000075 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000076 self._write_config()
77
78 self._initialized = True
79
80 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +000081 try:
82 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
83 except IOError as e:
84 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
85 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +000086
87 @property
Edward Lemur48836262018-10-18 02:08:06 +000088 def version(self):
89 self._ensure_initialized()
90 return self._config['version']
91
92 @property
Edward Lemur32e3d1e2018-07-12 00:54:05 +000093 def is_googler(self):
94 self._ensure_initialized()
95 return self._config['is-googler']
96
97 @property
98 def opted_in(self):
99 self._ensure_initialized()
100 return self._config['opt-in']
101
102 @opted_in.setter
103 def opted_in(self, value):
104 self._ensure_initialized()
105 self._config['opt-in'] = value
Edward Lemur48836262018-10-18 02:08:06 +0000106 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000107 self._write_config()
108
109 @property
110 def countdown(self):
111 self._ensure_initialized()
112 return self._config['countdown']
113
Edward Lemur48836262018-10-18 02:08:06 +0000114 @property
115 def should_collect_metrics(self):
116 # Don't collect the metrics unless the user is a googler, the user has opted
117 # in, or the countdown has expired.
118 if not self.is_googler:
119 return False
120 if self.opted_in is False:
121 return False
122 if self.opted_in is None and self.countdown > 0:
123 return False
124 return True
125
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000126 def decrease_countdown(self):
127 self._ensure_initialized()
128 if self.countdown == 0:
129 return
130 self._config['countdown'] -= 1
Edward Lemur48836262018-10-18 02:08:06 +0000131 if self.countdown == 0:
132 self._config['version'] = metrics_utils.CURRENT_VERSION
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000133 self._write_config()
134
Edward Lemur48836262018-10-18 02:08:06 +0000135 def reset_config(self):
136 # Only reset countdown if we're already collecting metrics.
137 if self.should_collect_metrics:
138 self._ensure_initialized()
139 self._config['countdown'] = DEFAULT_COUNTDOWN
140 self._config['opt-in'] = None
141
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000142
143class MetricsCollector(object):
144 def __init__(self):
145 self._metrics_lock = threading.Lock()
146 self._reported_metrics = {}
147 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000148 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000149 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000150
151 @property
152 def config(self):
153 return self._config
154
Edward Lemur3298e7b2018-07-17 18:21:27 +0000155 @property
156 def collecting_metrics(self):
157 return self._collecting_metrics
158
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000159 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000160 if self._collect_custom_metrics:
161 with self._metrics_lock:
162 self._reported_metrics[name] = value
163
Edward Lemur149834e2018-10-22 19:15:13 +0000164 def add_repeated(self, name, value):
165 if self._collect_custom_metrics:
166 with self._metrics_lock:
167 self._reported_metrics.setdefault(name, []).append(value)
168
Edward Lemur6f812e12018-07-31 22:45:57 +0000169 @contextlib.contextmanager
170 def pause_metrics_collection(self):
171 collect_custom_metrics = self._collect_custom_metrics
172 self._collect_custom_metrics = False
173 try:
174 yield
175 finally:
176 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000177
178 def _upload_metrics_data(self):
179 """Upload the metrics data to the AppEngine app."""
180 # We invoke a subprocess, and use stdin.write instead of communicate(),
181 # so that we are able to return immediately, leaving the upload running in
182 # the background.
183 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
184 p.stdin.write(json.dumps(self._reported_metrics))
185
186 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000187 # If we're already collecting metrics, just execute the function.
188 # e.g. git-cl split invokes git-cl upload several times to upload each
189 # splitted CL.
190 if self.collecting_metrics:
191 # Don't collect metrics for this function.
192 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
193 with self.pause_metrics_collection():
194 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000195
196 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000197 self.add('command', command_name)
198 try:
199 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000200 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000201 exception = None
202 # pylint: disable=bare-except
203 except:
204 exception = sys.exc_info()
205 finally:
206 self.add('execution_time', time.time() - start)
207
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000208 exit_code = metrics_utils.return_code_from_exception(exception)
209 self.add('exit_code', exit_code)
210
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000211 # Add metrics regarding environment information.
212 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
213 self.add('python_version', metrics_utils.get_python_version())
214 self.add('host_os', gclient_utils.GetMacWinOrLinux())
215 self.add('host_arch', detect_host_arch.HostArch())
216 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
217
218 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000219 if exception:
220 raise exception[0], exception[1], exception[2]
221 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000222
223 def collect_metrics(self, command_name):
224 """A decorator used to collect metrics over the life of a function.
225
226 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000227 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000228 """
229 def _decorator(func):
230 # Do this first so we don't have to read, and possibly create a config
231 # file.
232 if DISABLE_METRICS_COLLECTION:
233 return func
Edward Lemur48836262018-10-18 02:08:06 +0000234 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000235 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000236 # Otherwise, collect the metrics.
237 # Needed to preserve the __name__ and __doc__ attributes of func.
238 @functools.wraps(func)
239 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000240 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000241 return _inner
242 return _decorator
243
Edward Lemur6f812e12018-07-31 22:45:57 +0000244 @contextlib.contextmanager
245 def print_notice_and_exit(self):
246 """A context manager used to print the notice and terminate execution.
247
248 This decorator executes the function and prints the monitoring notice if
249 necessary. If an exception is raised, we will catch it, and print it before
250 printing the metrics collection notice.
251 This will call sys.exit() with an appropriate exit code to ensure the notice
252 is the last thing printed."""
253 # Needed to preserve the __name__ and __doc__ attributes of func.
254 try:
255 yield
256 exception = None
257 # pylint: disable=bare-except
258 except:
259 exception = sys.exc_info()
260
261 # Print the exception before the metrics notice, so that the notice is
262 # clearly visible even if gclient fails.
263 if exception:
264 if isinstance(exception[1], KeyboardInterrupt):
265 sys.stderr.write('Interrupted\n')
266 elif not isinstance(exception[1], SystemExit):
267 traceback.print_exception(*exception)
268
Edward Lemur48836262018-10-18 02:08:06 +0000269 # Check if the version has changed
270 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
271 and self.config.opted_in is not False
272 and self.config.version != metrics_utils.CURRENT_VERSION):
273 metrics_utils.print_version_change(self.config.version)
274 self.config.reset_config()
275
Edward Lemur6f812e12018-07-31 22:45:57 +0000276 # Print the notice
277 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
278 and self.config.opted_in is None):
279 metrics_utils.print_notice(self.config.countdown)
280 self.config.decrease_countdown()
281
282 sys.exit(metrics_utils.return_code_from_exception(exception))
283
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000284
285collector = MetricsCollector()