123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- #!/usr/bin/env python
- # Copyright (c) 2018 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- from __future__ import print_function
- import contextlib
- import functools
- import json
- import os
- import sys
- import tempfile
- import threading
- import time
- import traceback
- try:
- import urllib2 as urllib
- except ImportError: # For Py3 compatibility
- import urllib.request as urllib
- import detect_host_arch
- import gclient_utils
- import metrics_utils
- import subprocess2
- DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
- CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg')
- UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py')
- DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0'
- DEFAULT_COUNTDOWN = 10
- INVALID_CONFIG_WARNING = (
- 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will '
- 'be created.'
- )
- PERMISSION_DENIED_WARNING = (
- 'Could not write the metrics collection config:\n\t%s\n'
- 'Metrics collection will be disabled.'
- )
- class _Config(object):
- def __init__(self):
- self._initialized = False
- self._config = {}
- def _ensure_initialized(self):
- if self._initialized:
- return
- try:
- config = json.loads(gclient_utils.FileRead(CONFIG_FILE))
- except (IOError, ValueError):
- config = {}
- self._config = config.copy()
- if 'is-googler' not in self._config:
- # /should-upload is only accessible from Google IPs, so we only need to
- # check if we can reach the page. An external developer would get access
- # denied.
- try:
- req = urllib.urlopen(metrics_utils.APP_URL + '/should-upload')
- self._config['is-googler'] = req.getcode() == 200
- except (urllib.URLError, urllib.HTTPError):
- self._config['is-googler'] = False
- # Make sure the config variables we need are present, and initialize them to
- # safe values otherwise.
- self._config.setdefault('countdown', DEFAULT_COUNTDOWN)
- self._config.setdefault('opt-in', None)
- self._config.setdefault('version', metrics_utils.CURRENT_VERSION)
- if config != self._config:
- print(INVALID_CONFIG_WARNING, file=sys.stderr)
- self._write_config()
- self._initialized = True
- def _write_config(self):
- try:
- gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config))
- except IOError as e:
- print(PERMISSION_DENIED_WARNING % e, file=sys.stderr)
- self._config['opt-in'] = False
- @property
- def version(self):
- self._ensure_initialized()
- return self._config['version']
- @property
- def is_googler(self):
- self._ensure_initialized()
- return self._config['is-googler']
- @property
- def opted_in(self):
- self._ensure_initialized()
- return self._config['opt-in']
- @opted_in.setter
- def opted_in(self, value):
- self._ensure_initialized()
- self._config['opt-in'] = value
- self._config['version'] = metrics_utils.CURRENT_VERSION
- self._write_config()
- @property
- def countdown(self):
- self._ensure_initialized()
- return self._config['countdown']
- @property
- def should_collect_metrics(self):
- # Don't collect the metrics unless the user is a googler, the user has opted
- # in, or the countdown has expired.
- if not self.is_googler:
- return False
- if self.opted_in is False:
- return False
- if self.opted_in is None and self.countdown > 0:
- return False
- return True
- def decrease_countdown(self):
- self._ensure_initialized()
- if self.countdown == 0:
- return
- self._config['countdown'] -= 1
- if self.countdown == 0:
- self._config['version'] = metrics_utils.CURRENT_VERSION
- self._write_config()
- def reset_config(self):
- # Only reset countdown if we're already collecting metrics.
- if self.should_collect_metrics:
- self._ensure_initialized()
- self._config['countdown'] = DEFAULT_COUNTDOWN
- self._config['opt-in'] = None
- class MetricsCollector(object):
- def __init__(self):
- self._metrics_lock = threading.Lock()
- self._reported_metrics = {}
- self._config = _Config()
- self._collecting_metrics = False
- self._collect_custom_metrics = True
- @property
- def config(self):
- return self._config
- @property
- def collecting_metrics(self):
- return self._collecting_metrics
- def add(self, name, value):
- if self._collect_custom_metrics:
- with self._metrics_lock:
- self._reported_metrics[name] = value
- def add_repeated(self, name, value):
- if self._collect_custom_metrics:
- with self._metrics_lock:
- self._reported_metrics.setdefault(name, []).append(value)
- @contextlib.contextmanager
- def pause_metrics_collection(self):
- collect_custom_metrics = self._collect_custom_metrics
- self._collect_custom_metrics = False
- try:
- yield
- finally:
- self._collect_custom_metrics = collect_custom_metrics
- def _upload_metrics_data(self):
- """Upload the metrics data to the AppEngine app."""
- # We invoke a subprocess, and use stdin.write instead of communicate(),
- # so that we are able to return immediately, leaving the upload running in
- # the background.
- p = subprocess2.Popen(['vpython3', UPLOAD_SCRIPT], stdin=subprocess2.PIPE)
- p.stdin.write(json.dumps(self._reported_metrics).encode('utf-8'))
- def _collect_metrics(self, func, command_name, *args, **kwargs):
- # If we're already collecting metrics, just execute the function.
- # e.g. git-cl split invokes git-cl upload several times to upload each
- # split CL.
- if self.collecting_metrics:
- # Don't collect metrics for this function.
- # e.g. Don't record the arguments git-cl split passes to git-cl upload.
- with self.pause_metrics_collection():
- return func(*args, **kwargs)
- self._collecting_metrics = True
- self.add('metrics_version', metrics_utils.CURRENT_VERSION)
- self.add('command', command_name)
- try:
- start = time.time()
- result = func(*args, **kwargs)
- exception = None
- # pylint: disable=bare-except
- except:
- exception = sys.exc_info()
- finally:
- self.add('execution_time', time.time() - start)
- exit_code = metrics_utils.return_code_from_exception(exception)
- self.add('exit_code', exit_code)
- # Add metrics regarding environment information.
- self.add('timestamp', int(time.time()))
- self.add('python_version', metrics_utils.get_python_version())
- self.add('host_os', gclient_utils.GetMacWinAixOrLinux())
- self.add('host_arch', detect_host_arch.HostArch())
- depot_tools_age = metrics_utils.get_repo_timestamp(DEPOT_TOOLS)
- if depot_tools_age is not None:
- self.add('depot_tools_age', int(depot_tools_age))
- git_version = metrics_utils.get_git_version()
- if git_version:
- self.add('git_version', git_version)
- self._upload_metrics_data()
- if exception:
- gclient_utils.reraise(exception[0], exception[1], exception[2])
- return result
- def collect_metrics(self, command_name):
- """A decorator used to collect metrics over the life of a function.
- This decorator executes the function and collects metrics about the system
- environment and the function performance.
- """
- def _decorator(func):
- # Do this first so we don't have to read, and possibly create a config
- # file.
- if DISABLE_METRICS_COLLECTION:
- return func
- if not self.config.should_collect_metrics:
- return func
- # Otherwise, collect the metrics.
- # Needed to preserve the __name__ and __doc__ attributes of func.
- @functools.wraps(func)
- def _inner(*args, **kwargs):
- return self._collect_metrics(func, command_name, *args, **kwargs)
- return _inner
- return _decorator
- @contextlib.contextmanager
- def print_notice_and_exit(self):
- """A context manager used to print the notice and terminate execution.
- This decorator executes the function and prints the monitoring notice if
- necessary. If an exception is raised, we will catch it, and print it before
- printing the metrics collection notice.
- This will call sys.exit() with an appropriate exit code to ensure the notice
- is the last thing printed."""
- # Needed to preserve the __name__ and __doc__ attributes of func.
- try:
- yield
- exception = None
- # pylint: disable=bare-except
- except:
- exception = sys.exc_info()
- # Print the exception before the metrics notice, so that the notice is
- # clearly visible even if gclient fails.
- if exception:
- if isinstance(exception[1], KeyboardInterrupt):
- sys.stderr.write('Interrupted\n')
- elif not isinstance(exception[1], SystemExit):
- traceback.print_exception(*exception)
- # Check if the version has changed
- if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
- and self.config.opted_in is not False
- and self.config.version != metrics_utils.CURRENT_VERSION):
- metrics_utils.print_version_change(self.config.version)
- self.config.reset_config()
- # Print the notice
- if (not DISABLE_METRICS_COLLECTION and self.config.is_googler
- and self.config.opted_in is None):
- metrics_utils.print_notice(self.config.countdown)
- self.config.decrease_countdown()
- sys.exit(metrics_utils.return_code_from_exception(exception))
- collector = MetricsCollector()
|