blob: 2bf999dcafc1f09e4a80c164f6f2dd7e5094184d [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 Lemur6f812e12018-07-31 22:45:57 +00006import contextlib
Edward Lemur32e3d1e2018-07-12 00:54:05 +00007import functools
8import json
9import os
10import subprocess
11import sys
12import tempfile
13import threading
14import time
15import traceback
16import urllib2
17
18import detect_host_arch
19import gclient_utils
20import metrics_utils
21
22
23DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
24CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
25UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
26
Edward Lemur32e3d1e2018-07-12 00:54:05 +000027DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0'
28DEFAULT_COUNTDOWN = 10
29
30INVALID_CONFIG_WARNING = (
31 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one has '
32 'been created'
33)
34
35
36class _Config(object):
37 def __init__(self):
38 self._initialized = False
39 self._config = {}
40
41 def _ensure_initialized(self):
42 if self._initialized:
43 return
44
45 try:
46 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
47 except (IOError, ValueError):
48 config = {}
49
50 self._config = config.copy()
51
52 if 'is-googler' not in self._config:
53 # /should-upload is only accessible from Google IPs, so we only need to
54 # check if we can reach the page. An external developer would get access
55 # denied.
56 try:
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000057 req = urllib2.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000058 self._config['is-googler'] = req.getcode() == 200
59 except (urllib2.URLError, urllib2.HTTPError):
60 self._config['is-googler'] = False
61
62 # Make sure the config variables we need are present, and initialize them to
63 # safe values otherwise.
64 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
65 self._config.setdefault('opt-in', None)
66
67 if config != self._config:
68 print INVALID_CONFIG_WARNING
69 self._write_config()
70
71 self._initialized = True
72
73 def _write_config(self):
74 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
75
76 @property
77 def is_googler(self):
78 self._ensure_initialized()
79 return self._config['is-googler']
80
81 @property
82 def opted_in(self):
83 self._ensure_initialized()
84 return self._config['opt-in']
85
86 @opted_in.setter
87 def opted_in(self, value):
88 self._ensure_initialized()
89 self._config['opt-in'] = value
90 self._write_config()
91
92 @property
93 def countdown(self):
94 self._ensure_initialized()
95 return self._config['countdown']
96
97 def decrease_countdown(self):
98 self._ensure_initialized()
99 if self.countdown == 0:
100 return
101 self._config['countdown'] -= 1
102 self._write_config()
103
104
105class MetricsCollector(object):
106 def __init__(self):
107 self._metrics_lock = threading.Lock()
108 self._reported_metrics = {}
109 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000110 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000111 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000112
113 @property
114 def config(self):
115 return self._config
116
Edward Lemur3298e7b2018-07-17 18:21:27 +0000117 @property
118 def collecting_metrics(self):
119 return self._collecting_metrics
120
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000121 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000122 if self._collect_custom_metrics:
123 with self._metrics_lock:
124 self._reported_metrics[name] = value
125
126 @contextlib.contextmanager
127 def pause_metrics_collection(self):
128 collect_custom_metrics = self._collect_custom_metrics
129 self._collect_custom_metrics = False
130 try:
131 yield
132 finally:
133 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000134
135 def _upload_metrics_data(self):
136 """Upload the metrics data to the AppEngine app."""
137 # We invoke a subprocess, and use stdin.write instead of communicate(),
138 # so that we are able to return immediately, leaving the upload running in
139 # the background.
140 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
141 p.stdin.write(json.dumps(self._reported_metrics))
142
143 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000144 # If we're already collecting metrics, just execute the function.
145 # e.g. git-cl split invokes git-cl upload several times to upload each
146 # splitted CL.
147 if self.collecting_metrics:
148 # Don't collect metrics for this function.
149 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
150 with self.pause_metrics_collection():
151 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000152
153 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000154 self.add('command', command_name)
155 try:
156 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000157 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000158 exception = None
159 # pylint: disable=bare-except
160 except:
161 exception = sys.exc_info()
162 finally:
163 self.add('execution_time', time.time() - start)
164
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000165 exit_code = metrics_utils.return_code_from_exception(exception)
166 self.add('exit_code', exit_code)
167
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000168 # Add metrics regarding environment information.
169 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
170 self.add('python_version', metrics_utils.get_python_version())
171 self.add('host_os', gclient_utils.GetMacWinOrLinux())
172 self.add('host_arch', detect_host_arch.HostArch())
173 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
174
175 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000176 if exception:
177 raise exception[0], exception[1], exception[2]
178 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000179
180 def collect_metrics(self, command_name):
181 """A decorator used to collect metrics over the life of a function.
182
183 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000184 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000185 """
186 def _decorator(func):
187 # Do this first so we don't have to read, and possibly create a config
188 # file.
189 if DISABLE_METRICS_COLLECTION:
190 return func
Edward Lemur6f812e12018-07-31 22:45:57 +0000191 # Don't collect the metrics unless the user is a googler, the user has
192 # opted in, or the countdown has expired.
193 if (not self.config.is_googler or self.config.opted_in == False
194 or (self.config.opted_in is None and self.config.countdown > 0)):
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000195 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000196 # Otherwise, collect the metrics.
197 # Needed to preserve the __name__ and __doc__ attributes of func.
198 @functools.wraps(func)
199 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000200 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000201 return _inner
202 return _decorator
203
Edward Lemur6f812e12018-07-31 22:45:57 +0000204 @contextlib.contextmanager
205 def print_notice_and_exit(self):
206 """A context manager used to print the notice and terminate execution.
207
208 This decorator executes the function and prints the monitoring notice if
209 necessary. If an exception is raised, we will catch it, and print it before
210 printing the metrics collection notice.
211 This will call sys.exit() with an appropriate exit code to ensure the notice
212 is the last thing printed."""
213 # Needed to preserve the __name__ and __doc__ attributes of func.
214 try:
215 yield
216 exception = None
217 # pylint: disable=bare-except
218 except:
219 exception = sys.exc_info()
220
221 # Print the exception before the metrics notice, so that the notice is
222 # clearly visible even if gclient fails.
223 if exception:
224 if isinstance(exception[1], KeyboardInterrupt):
225 sys.stderr.write('Interrupted\n')
226 elif not isinstance(exception[1], SystemExit):
227 traceback.print_exception(*exception)
228
229 # Print the notice
230 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
231 and self.config.opted_in is None):
232 metrics_utils.print_notice(self.config.countdown)
233 self.config.decrease_countdown()
234
235 sys.exit(metrics_utils.return_code_from_exception(exception))
236
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000237
238collector = MetricsCollector()