blob: ec862b604d560ec05758398fd1420ff6f2763f7d [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
6import functools
7import json
8import os
9import subprocess
10import sys
11import tempfile
12import threading
13import time
14import traceback
15import urllib2
16
17import detect_host_arch
18import gclient_utils
19import metrics_utils
20
21
22DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
23CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
24UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
25
Edward Lemur32e3d1e2018-07-12 00:54:05 +000026DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0'
27DEFAULT_COUNTDOWN = 10
28
29INVALID_CONFIG_WARNING = (
30 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one has '
31 'been created'
32)
33
34
35class _Config(object):
36 def __init__(self):
37 self._initialized = False
38 self._config = {}
39
40 def _ensure_initialized(self):
41 if self._initialized:
42 return
43
44 try:
45 config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
46 except (IOError, ValueError):
47 config = {}
48
49 self._config = config.copy()
50
51 if 'is-googler' not in self._config:
52 # /should-upload is only accessible from Google IPs, so we only need to
53 # check if we can reach the page. An external developer would get access
54 # denied.
55 try:
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000056 req = urllib2.urlopen(metrics_utils.APP_URL + '/should-upload')
Edward Lemur32e3d1e2018-07-12 00:54:05 +000057 self._config['is-googler'] = req.getcode() == 200
58 except (urllib2.URLError, urllib2.HTTPError):
59 self._config['is-googler'] = False
60
61 # Make sure the config variables we need are present, and initialize them to
62 # safe values otherwise.
63 self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
64 self._config.setdefault('opt-in', None)
65
66 if config != self._config:
67 print INVALID_CONFIG_WARNING
68 self._write_config()
69
70 self._initialized = True
71
72 def _write_config(self):
73 gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
74
75 @property
76 def is_googler(self):
77 self._ensure_initialized()
78 return self._config['is-googler']
79
80 @property
81 def opted_in(self):
82 self._ensure_initialized()
83 return self._config['opt-in']
84
85 @opted_in.setter
86 def opted_in(self, value):
87 self._ensure_initialized()
88 self._config['opt-in'] = value
89 self._write_config()
90
91 @property
92 def countdown(self):
93 self._ensure_initialized()
94 return self._config['countdown']
95
96 def decrease_countdown(self):
97 self._ensure_initialized()
98 if self.countdown == 0:
99 return
100 self._config['countdown'] -= 1
101 self._write_config()
102
103
104class MetricsCollector(object):
105 def __init__(self):
106 self._metrics_lock = threading.Lock()
107 self._reported_metrics = {}
108 self._config = _Config()
Edward Lemur3298e7b2018-07-17 18:21:27 +0000109 self._collecting_metrics = False
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000110
111 @property
112 def config(self):
113 return self._config
114
Edward Lemur3298e7b2018-07-17 18:21:27 +0000115 @property
116 def collecting_metrics(self):
117 return self._collecting_metrics
118
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000119 def add(self, name, value):
120 with self._metrics_lock:
121 self._reported_metrics[name] = value
122
123 def _upload_metrics_data(self):
124 """Upload the metrics data to the AppEngine app."""
125 # We invoke a subprocess, and use stdin.write instead of communicate(),
126 # so that we are able to return immediately, leaving the upload running in
127 # the background.
128 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
129 p.stdin.write(json.dumps(self._reported_metrics))
130
131 def _collect_metrics(self, func, command_name, *args, **kwargs):
Edward Lemur7fa0f192018-07-17 21:33:37 +0000132 # If the user hasn't opted in or out, and the countdown is not yet 0, just
133 # display the notice.
134 if self.config.opted_in == None and self.config.countdown > 0:
135 metrics_utils.print_notice(self.config.countdown)
136 self.config.decrease_countdown()
137 func(*args, **kwargs)
138 return
139
140 self._collecting_metrics = True
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000141 self.add('command', command_name)
142 try:
143 start = time.time()
144 func(*args, **kwargs)
145 exception = None
146 # pylint: disable=bare-except
147 except:
148 exception = sys.exc_info()
149 finally:
150 self.add('execution_time', time.time() - start)
151
152 # Print the exception before the metrics notice, so that the notice is
153 # clearly visible even if gclient fails.
154 if exception and not isinstance(exception[1], SystemExit):
155 traceback.print_exception(*exception)
156
157 exit_code = metrics_utils.return_code_from_exception(exception)
158 self.add('exit_code', exit_code)
159
160 # Print the metrics notice only if the user has not explicitly opted in
161 # or out.
162 if self.config.opted_in is None:
163 metrics_utils.print_notice(self.config.countdown)
164
165 # Add metrics regarding environment information.
166 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
167 self.add('python_version', metrics_utils.get_python_version())
168 self.add('host_os', gclient_utils.GetMacWinOrLinux())
169 self.add('host_arch', detect_host_arch.HostArch())
170 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
171
172 self._upload_metrics_data()
173 sys.exit(exit_code)
174
175 def collect_metrics(self, command_name):
176 """A decorator used to collect metrics over the life of a function.
177
178 This decorator executes the function and collects metrics about the system
179 environment and the function performance. It also catches all the Exceptions
180 and invokes sys.exit once the function is done executing.
181 """
182 def _decorator(func):
183 # Do this first so we don't have to read, and possibly create a config
184 # file.
185 if DISABLE_METRICS_COLLECTION:
186 return func
187 # If the user has opted out or the user is not a googler, then there is no
188 # need to do anything.
189 if self.config.opted_in == False or not self.config.is_googler:
190 return func
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000191 # Otherwise, collect the metrics.
192 # Needed to preserve the __name__ and __doc__ attributes of func.
193 @functools.wraps(func)
194 def _inner(*args, **kwargs):
195 self._collect_metrics(func, command_name, *args, **kwargs)
Edward Lemur32e3d1e2018-07-12 00:54:05 +0000196 return _inner
197 return _decorator
198
199
200collector = MetricsCollector()