blob: c7edc365be939a79661bbd42f95c088d58f5f299 [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 '
34 'been created.'
35)
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)
72
73 if config != self._config:
Edward Lemurdd5051f2018-08-08 00:56:41 +000074 print(INVALID_CONFIG_WARNING, file=sys.stderr)
Edward Lemur32e3d1e2018-07-12 00:54:05 +000075 self._write_config()
76
77 self._initialized = True
78
79 def _write_config(self):
Edward Lemurdd5051f2018-08-08 00:56:41 +000080 try:
81 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
82 except IOError as e:
83 print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
84 self._config['opt-in'] = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +000085
86 @property
87 def is_googler(self):
88 self._ensure_initialized()
89 return self._config['is-googler']
90
91 @property
92 def opted_in(self):
93 self._ensure_initialized()
94 return self._config['opt-in']
95
96 @opted_in.setter
97 def opted_in(self, value):
98 self._ensure_initialized()
99 self._config['opt-in'] = value
100 self._write_config()
101
102 @property
103 def countdown(self):
104 self._ensure_initialized()
105 return self._config['countdown']
106
107 def decrease_countdown(self):
108 self._ensure_initialized()
109 if self.countdown == 0:
110 return
111 self._config['countdown'] -= 1
112 self._write_config()
113
114
115class MetricsCollector(object):
116 def __init__(self):
117 self._metrics_lock = threading.Lock()
118 self._reported_metrics = {}
119 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000120 self._collecting_metrics = False
Edward Lemur6f812e12018-07-31 22:45:57 +0000121 self._collect_custom_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000122
123 @property
124 def config(self):
125 return self._config
126
Edward Lemur3298e7b2018-07-17 18:21:27 +0000127 @property
128 def collecting_metrics(self):
129 return self._collecting_metrics
130
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000131 def add(self, name, value):
Edward Lemur6f812e12018-07-31 22:45:57 +0000132 if self._collect_custom_metrics:
133 with self._metrics_lock:
134 self._reported_metrics[name] = value
135
136 @contextlib.contextmanager
137 def pause_metrics_collection(self):
138 collect_custom_metrics = self._collect_custom_metrics
139 self._collect_custom_metrics = False
140 try:
141 yield
142 finally:
143 self._collect_custom_metrics = collect_custom_metrics
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000144
145 def _upload_metrics_data(self):
146 """Upload the metrics data to the AppEngine app."""
147 # We invoke a subprocess, and use stdin.write instead of communicate(),
148 # so that we are able to return immediately, leaving the upload running in
149 # the background.
150 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
151 p.stdin.write(json.dumps(self._reported_metrics))
152
153 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000154 # If we're already collecting metrics, just execute the function.
155 # e.g. git-cl split invokes git-cl upload several times to upload each
156 # splitted CL.
157 if self.collecting_metrics:
158 # Don't collect metrics for this function.
159 # e.g. Don't record the arguments git-cl split passes to git-cl upload.
160 with self.pause_metrics_collection():
161 return func(*args, **kwargs)
Edward Lemur7fa0f192018-07-17 21:33:37 +0000162
163 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000164 self.add('command', command_name)
165 try:
166 start = time.time()
Edward Lemur6f812e12018-07-31 22:45:57 +0000167 result = func(*args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000168 exception = None
169 # pylint: disable=bare-except
170 except:
171 exception = sys.exc_info()
172 finally:
173 self.add('execution_time', time.time() - start)
174
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000175 exit_code = metrics_utils.return_code_from_exception(exception)
176 self.add('exit_code', exit_code)
177
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000178 # Add metrics regarding environment information.
179 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
180 self.add('python_version', metrics_utils.get_python_version())
181 self.add('host_os', gclient_utils.GetMacWinOrLinux())
182 self.add('host_arch', detect_host_arch.HostArch())
183 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
184
185 self._upload_metrics_data()
Edward Lemur6f812e12018-07-31 22:45:57 +0000186 if exception:
187 raise exception[0], exception[1], exception[2]
188 return result
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000189
190 def collect_metrics(self, command_name):
191 """A decorator used to collect metrics over the life of a function.
192
193 This decorator executes the function and collects metrics about the system
Edward Lemur6f812e12018-07-31 22:45:57 +0000194 environment and the function performance.
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000195 """
196 def _decorator(func):
197 # Do this first so we don't have to read, and possibly create a config
198 # file.
199 if DISABLE_METRICS_COLLECTION:
200 return func
Edward Lemur6f812e12018-07-31 22:45:57 +0000201 # Don't collect the metrics unless the user is a googler, the user has
202 # opted in, or the countdown has expired.
203 if (not self.config.is_googler or self.config.opted_in == False
204 or (self.config.opted_in is None and self.config.countdown > 0)):
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000205 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000206 # Otherwise, collect the metrics.
207 # Needed to preserve the __name__ and __doc__ attributes of func.
208 @functools.wraps(func)
209 def _inner(*args, **kwargs):
Edward Lemur6f812e12018-07-31 22:45:57 +0000210 return self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000211 return _inner
212 return _decorator
213
Edward Lemur6f812e12018-07-31 22:45:57 +0000214 @contextlib.contextmanager
215 def print_notice_and_exit(self):
216 """A context manager used to print the notice and terminate execution.
217
218 This decorator executes the function and prints the monitoring notice if
219 necessary. If an exception is raised, we will catch it, and print it before
220 printing the metrics collection notice.
221 This will call sys.exit() with an appropriate exit code to ensure the notice
222 is the last thing printed."""
223 # Needed to preserve the __name__ and __doc__ attributes of func.
224 try:
225 yield
226 exception = None
227 # pylint: disable=bare-except
228 except:
229 exception = sys.exc_info()
230
231 # Print the exception before the metrics notice, so that the notice is
232 # clearly visible even if gclient fails.
233 if exception:
234 if isinstance(exception[1], KeyboardInterrupt):
235 sys.stderr.write('Interrupted\n')
236 elif not isinstance(exception[1], SystemExit):
237 traceback.print_exception(*exception)
238
239 # Print the notice
240 if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
241 and self.config.opted_in is None):
242 metrics_utils.print_notice(self.config.countdown)
243 self.config.decrease_countdown()
244
245 sys.exit(metrics_utils.return_code_from_exception(exception))
246
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000247
248collector = MetricsCollector()