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