blob: c2c88e0d8e93f6bd65b4a4f26ca6b9a3bb85a196 [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
164 @contextlib.contextmanager
165 def pause_metrics_collection(self):
166 collect_custom_metrics = self._collect_custom_metrics
167 self._collect_custom_metrics = False
168 try:
169 yield
170 finally:
171 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000172
173 def _upload_metrics_data(self):
174 """Upload the metrics data to the AppEngine app."""
175 # We invoke a subprocess, and use stdin.write instead of communicate(),
176 # so that we are able to return immediately, leaving the upload running in
177 # the background.
178 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
179 p.stdin.write(json.dumps(self._reported_metrics))
180
181 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000182 # If we're already collecting metrics, just execute the function.
183 # e.g. git-cl split invokes git-cl upload several times to upload each
184 # splitted CL.
185 if self.collecting_metrics:
186 # Don't collect metrics for this function.
187 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
188 with self.pause_metrics_collection():
189 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000190
191 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000192 self.add('command', command_name)
193 try:
194 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000195 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000196 exception = None
197 # pylint: disable=bare-except
198 except:
199 exception = sys.exc_info()
200 finally:
201 self.add('execution_time', time.time() - start)
202
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000203 exit_code = metrics_utils.return_code_from_exception(exception)
204 self.add('exit_code', exit_code)
205
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000206 # Add metrics regarding environment information.
207 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
208 self.add('python_version', metrics_utils.get_python_version())
209 self.add('host_os', gclient_utils.GetMacWinOrLinux())
210 self.add('host_arch', detect_host_arch.HostArch())
211 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
212
213 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000214 if exception:
215 raise exception[0], exception[1], exception[2]
216 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000217
218 def collect_metrics(self, command_name):
219 """A decorator used to collect metrics over the life of a function.
220
221 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000222 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000223 """
224 def _decorator(func):
225 # Do this first so we don't have to read, and possibly create a config
226 # file.
227 if DISABLE_METRICS_COLLECTION:
228 return func
Edward Lemur48836262018-10-18 02:08:06 +0000229 if not self.config.should_collect_metrics:
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000230 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000231 # Otherwise, collect the metrics.
232 # Needed to preserve the __name__ and __doc__ attributes of func.
233 @functools.wraps(func)
234 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000235 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000236 return _inner
237 return _decorator
238
Edward Lemur6f812e12018-07-31 22:45:57 +0000239 @contextlib.contextmanager
240 def print_notice_and_exit(self):
241 """A context manager used to print the notice and terminate execution.
242
243 This decorator executes the function and prints the monitoring notice if
244 necessary. If an exception is raised, we will catch it, and print it before
245 printing the metrics collection notice.
246 This will call sys.exit() with an appropriate exit code to ensure the notice
247 is the last thing printed."""
248 # Needed to preserve the __name__ and __doc__ attributes of func.
249 try:
250 yield
251 exception = None
252 # pylint: disable=bare-except
253 except:
254 exception = sys.exc_info()
255
256 # Print the exception before the metrics notice, so that the notice is
257 # clearly visible even if gclient fails.
258 if exception:
259 if isinstance(exception[1], KeyboardInterrupt):
260 sys.stderr.write('Interrupted\n')
261 elif not isinstance(exception[1], SystemExit):
262 traceback.print_exception(*exception)
263
Edward Lemur48836262018-10-18 02:08:06 +0000264 # Check if the version has changed
265 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
266 and self.config.opted_in is not False
267 and self.config.version != metrics_utils.CURRENT_VERSION):
268 metrics_utils.print_version_change(self.config.version)
269 self.config.reset_config()
270
Edward Lemur6f812e12018-07-31 22:45:57 +0000271 # Print the notice
272 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
273 and self.config.opted_in is None):
274 metrics_utils.print_notice(self.config.countdown)
275 self.config.decrease_countdown()
276
277 sys.exit(metrics_utils.return_code_from_exception(exception))
278
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000279
280collector = MetricsCollector()