blob: ccac340a1c1518d98567e3020d97b75a2350d09c [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()
111
112 @property
113 def config(self):
114 return self._config
115
116 def add(self, name, value):
117 with self._metrics_lock:
118 self._reported_metrics[name] = value
119
120 def _upload_metrics_data(self):
121 """Upload the metrics data to the AppEngine app."""
122 # We invoke a subprocess, and use stdin.write instead of communicate(),
123 # so that we are able to return immediately, leaving the upload running in
124 # the background.
125 p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE)
126 p.stdin.write(json.dumps(self._reported_metrics))
127
128 def _collect_metrics(self, func, command_name, *args, **kwargs):
129 self.add('command', command_name)
130 try:
131 start = time.time()
132 func(*args, **kwargs)
133 exception = None
134 # pylint: disable=bare-except
135 except:
136 exception = sys.exc_info()
137 finally:
138 self.add('execution_time', time.time() - start)
139
140 # Print the exception before the metrics notice, so that the notice is
141 # clearly visible even if gclient fails.
142 if exception and not isinstance(exception[1], SystemExit):
143 traceback.print_exception(*exception)
144
145 exit_code = metrics_utils.return_code_from_exception(exception)
146 self.add('exit_code', exit_code)
147
148 # Print the metrics notice only if the user has not explicitly opted in
149 # or out.
150 if self.config.opted_in is None:
151 metrics_utils.print_notice(self.config.countdown)
152
153 # Add metrics regarding environment information.
154 self.add('timestamp', metrics_utils.seconds_to_weeks(time.time()))
155 self.add('python_version', metrics_utils.get_python_version())
156 self.add('host_os', gclient_utils.GetMacWinOrLinux())
157 self.add('host_arch', detect_host_arch.HostArch())
158 self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS))
159
160 self._upload_metrics_data()
161 sys.exit(exit_code)
162
163 def collect_metrics(self, command_name):
164 """A decorator used to collect metrics over the life of a function.
165
166 This decorator executes the function and collects metrics about the system
167 environment and the function performance. It also catches all the Exceptions
168 and invokes sys.exit once the function is done executing.
169 """
170 def _decorator(func):
171 # Do this first so we don't have to read, and possibly create a config
172 # file.
173 if DISABLE_METRICS_COLLECTION:
174 return func
175 # If the user has opted out or the user is not a googler, then there is no
176 # need to do anything.
177 if self.config.opted_in == False or not self.config.is_googler:
178 return func
179 # If the user hasn't opted in or out, and the countdown is not yet 0, just
180 # display the notice.
181 if self.config.opted_in == None and self.config.countdown > 0:
182 metrics_utils.print_notice(self.config.countdown)
183 self.config.decrease_countdown()
184 return func
185 # Otherwise, collect the metrics.
186 # Needed to preserve the __name__ and __doc__ attributes of func.
187 @functools.wraps(func)
188 def _inner(*args, **kwargs):
189 self._collect_metrics(func, command_name, *args, **kwargs)
190 return _inner
191 return _decorator
192
193
194collector = MetricsCollector()