diff --git a/README.rst b/README.rst index b8cbe8f..ebf9fa8 100644 --- a/README.rst +++ b/README.rst @@ -39,16 +39,6 @@ Note that the DCOS CLI has tight integration with DCOS and certain functionality may not work as expected or at all when using it directly with Mesos and Marathon. -Controlling Analytics Reporting -------------------------------- - -The CLI reports usage and exception information to Segment.io for production -usage. - -If you wish to turn reporting of analytics off entirely, -you can set the :code:`core.reporting` property to false:: - dcos config set core.reporting false - Dependencies ------------ diff --git a/cli/dcoscli/analytics.py b/cli/dcoscli/analytics.py deleted file mode 100644 index 6c08128..0000000 --- a/cli/dcoscli/analytics.py +++ /dev/null @@ -1,232 +0,0 @@ -import json -import sys -import uuid - -import dcoscli -import docopt -import rollbar -import six -from dcos import http, util -from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY, - SEGMENT_IO_CLI_ERROR_EVENT, - SEGMENT_IO_CLI_EVENT, SEGMENT_IO_WRITE_KEY_PROD, - SEGMENT_URL) -from dcoscli.subcommand import default_doc -from requests.auth import HTTPBasicAuth - -logger = util.get_logger(__name__) -session_id = uuid.uuid4().hex - - -def _track(conf): - """ - Whether or not to send reporting information - - :param conf: dcos config file - :type conf: Toml - :returns: whether to send reporting information - :rtype: bool - """ - - return dcoscli.version != 'SNAPSHOT' and conf.get('core.reporting', True) - - -def _segment_track(event, conf, properties): - """ - Send a segment.io 'track' event - - :param event: name of event - :type event: string - :param conf: dcos config file - :type conf: Toml - :param properties: event properties - :type properties: dict - :rtype: None - """ - - data = {'event': event, - 'properties': properties, - 'anonymousId': session_id} - - _segment_request('track', data) - - -def _segment_request(path, data): - """ - Send a segment.io HTTP request - - :param path: URL path - :type path: str - :param data: json POST data - :type data: dict - :rtype: None - """ - - key = SEGMENT_IO_WRITE_KEY_PROD - - try: - # Set both the connect timeout and the request timeout to 1s, - # to prevent rollbar from hanging the CLI commands - http.post('{}/{}'.format(SEGMENT_URL, path), - json=data, - auth=HTTPBasicAuth(key, ''), - timeout=(1, 1)) - except Exception as e: - logger.exception(e) - - -def track_err(pool, exit_code, err, conf, cluster_id): - """ - Report error details to analytics services. - - :param pool: thread pool - :type pool: ThreadPoolExecutor - :param exit_code: exit code of tracked process - :type exit_code: int - :param err: stderr of tracked process - :type err: str - :param conf: dcos config file - :type conf: Toml - :param cluster_id: dcos cluster id to send to segment - :type cluster_id: str - :rtype: None - """ - - if not _track(conf): - return - - # Segment.io calls are async, but rollbar is not, so for - # parallelism, we must call segment first. - _segment_track_err(pool, conf, cluster_id, err, exit_code) - _rollbar_track_err(conf, cluster_id, err, exit_code) - - -def segment_track_cli(pool, conf, cluster_id): - """ - Send segment.io cli event. - - :param pool: thread pool - :type pool: ThreadPoolExecutor - :param conf: dcos config file - :type conf: Toml - :param cluster_id: dcos cluster id to send to segment - :type cluster_id: str - :rtype: None - """ - - if not _track(conf): - return - - props = _base_properties(conf, cluster_id) - pool.submit(_segment_track, SEGMENT_IO_CLI_EVENT, conf, props) - - -def _segment_track_err(pool, conf, cluster_id, err, exit_code): - """ - Send segment.io error event. - - :param pool: thread pool - :type segment: ThreadPoolExecutor - :param conf: dcos config file - :type conf: Toml - :param cluster_id: dcos cluster id to send to segment - :type cluster_id: str - :param err: stderr of tracked process - :type err: str - :param exit_code: exit code of tracked process - :type exit_code: int - :rtype: None - """ - - props = _base_properties(conf, cluster_id) - props['err'] = err - props['exit_code'] = exit_code - pool.submit(_segment_track, SEGMENT_IO_CLI_ERROR_EVENT, conf, props) - - -def _rollbar_track_err(conf, cluster_id, err, exit_code): - """ - Report to rollbar. Synchronous. - - :param conf: dcos config file - :type conf: Toml - :param cluster_id: dcos cluster id to send to segment - :type cluster_id: str - :param err: stderr of tracked process - :type err: str - :param exit_code: exit code of tracked process - :type exit_code: int - :rtype: None - """ - - rollbar.init(ROLLBAR_SERVER_POST_KEY, 'prod') - - props = _base_properties(conf, cluster_id) - props['exit_code'] = exit_code - - lines = err.split('\n') - if len(lines) >= 2: - title = lines[-2] - else: - title = err - props['stderr'] = err - - try: - rollbar.report_message(title, 'error', extra_data=props) - except Exception as e: - logger.exception(e) - - -def _command(): - """ Return the subcommand used in this dcos process. - - :returns: subcommand used in this dcos process - :rtype: str - """ - - args = docopt.docopt(default_doc("dcos"), - help=False, - options_first=True) - return args.get('', "") or "" - - -def _base_properties(conf=None, cluster_id=None): - """ - These properties are sent with every analytics event. - - :param conf: dcos config file - :type conf: Toml - :param cluster_id: dcos cluster id to send to segment - :type cluster_id: str - :rtype: dict - """ - - if not conf: - conf = util.get_config() - - if len(sys.argv) > 1: - cmd = 'dcos ' + _command() - full_cmd = 'dcos ' + ' '.join(sys.argv[1:]) - else: - cmd = 'dcos' - full_cmd = 'dcos' - - try: - dcos_hostname = six.moves.urllib.parse.urlparse( - conf.get('core.dcos_url')).hostname - except: - logger.exception('Unable to find the hostname of the cluster.') - dcos_hostname = None - - conf = [prop for prop in list(conf.property_items()) - if prop[0] != "core.dcos_acs_token"] - - return { - 'cmd': cmd, - 'full_cmd': full_cmd, - 'dcoscli.version': dcoscli.version, - 'python_version': str(sys.version_info), - 'config': json.dumps(conf), - 'DCOS_HOSTNAME': dcos_hostname, - 'CLUSTER_ID': cluster_id - } diff --git a/cli/dcoscli/constants.py b/cli/dcoscli/constants.py deleted file mode 100644 index d8c0f7c..0000000 --- a/cli/dcoscli/constants.py +++ /dev/null @@ -1,8 +0,0 @@ -ROLLBAR_SERVER_POST_KEY = '62f87c5df3674629b143a137de3d3244' - -SEGMENT_IO_WRITE_KEY_PROD = '51ybGTeFEFU1xo6u10XMDrr6kATFyRyh' -SEGMENT_IO_CLI_EVENT = 'dcos-cli' -SEGMENT_IO_CLI_ERROR_EVENT = 'dcos-cli-error' -SEGMENT_URL = 'https://api.segment.io/v1' - -DCOS_PRODUCTION_ENV = 'DCOS_PRODUCTION' diff --git a/cli/dcoscli/main.py b/cli/dcoscli/main.py index a3f43a3..f34b1fc 100644 --- a/cli/dcoscli/main.py +++ b/cli/dcoscli/main.py @@ -1,13 +1,11 @@ import os import signal import sys -from concurrent.futures import ThreadPoolExecutor import dcoscli import docopt -from dcos import constants, emitting, errors, http, mesos, subcommand, util -from dcos.errors import DCOSAuthenticationException, DCOSException -from dcoscli import analytics +from dcos import constants, emitting, errors, http, subcommand, util +from dcos.errors import DCOSException from dcoscli.subcommand import SubcommandMain, default_doc logger = util.get_logger(__name__) @@ -48,39 +46,15 @@ def _main(): if not command: command = "help" - cluster_id = None - if dcoscli.version != 'SNAPSHOT' and command and \ - command not in ["config", "help"]: - try: - cluster_id = mesos.DCOSClient().metadata().get('CLUSTER_ID') - except DCOSAuthenticationException: - raise - except: - msg = 'Unable to get the cluster_id of the cluster.' - logger.exception(msg) + if command in subcommand.default_subcommands(): + sc = SubcommandMain(command, args['']) + else: + executable = subcommand.command_executables(command) + sc = subcommand.SubcommandProcess( + executable, command, args['']) - # send args call to segment.io - with ThreadPoolExecutor(max_workers=2) as reporting_executor: - analytics.segment_track_cli(reporting_executor, config, cluster_id) - - # the call to retrieve cluster_id must happen before we run the - # subcommand so that if you have auth enabled we don't ask for - # user/pass multiple times (with the text being out of order) - # before we can cache the auth token - if command in subcommand.default_subcommands(): - sc = SubcommandMain(command, args['']) - else: - executable = subcommand.command_executables(command) - sc = subcommand.SubcommandProcess( - executable, command, args['']) - - exitcode, err = sc.run_and_capture() - - if err: - analytics.track_err( - reporting_executor, exitcode, err, config, cluster_id) - - return exitcode + exitcode, _ = sc.run_and_capture() + return exitcode def _config_log_level_environ(log_level): diff --git a/cli/tests/unit/test_analytics.py b/cli/tests/unit/test_analytics.py deleted file mode 100644 index f49c197..0000000 --- a/cli/tests/unit/test_analytics.py +++ /dev/null @@ -1,136 +0,0 @@ -import json -import os -from functools import wraps - -import dcoscli.analytics -import rollbar -from dcos import constants -from dcoscli.main import main - -from mock import patch - - -ANON_ID = 0 -USER_ID = 'test@mail.com' - - -def _mock(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - with patch('rollbar.init'), \ - patch('rollbar.report_message'), \ - patch('dcos.http.post'), \ - patch('dcos.http.get'), \ - patch('dcoscli.analytics.session_id'): - - dcoscli.analytics.session_id = ANON_ID - fn() - - return wrapper - - -@_mock -def test_cluster_id_not_sent_on_config_call(): - """Tests that cluster_id is not sent to segment.io on call to config - subcommand - """ - - args = ['dcos', 'config', 'show'] - - with patch('sys.argv', args), \ - patch('dcos.mesos.DCOSClient.metadata') as get_cluster_id: - assert main() == 0 - - assert get_cluster_id.call_count == 0 - - -def test_dont_send_acs_token(): - """Tests that we donn't send acs token""" - - args = ['dcos', 'help'] - env = _env_reporting() - version = 'release' - - with patch('sys.argv', args), \ - patch.dict(os.environ, env), \ - patch('dcoscli.version', version): - to_send = dcoscli.analytics._base_properties() - config_to_send = json.loads(to_send.get("config")) - assert "core.dcos_acs_token" not in config_to_send - - -@_mock -def test_no_exc(): - '''Tests that a command which does not raise an exception does not - report an exception. - - ''' - - args = ['dcos'] - env = _env_reporting() - version = 'release' - - with patch('sys.argv', args), \ - patch.dict(os.environ, env), \ - patch('dcoscli.version', version): - assert main() == 0 - - assert rollbar.report_message.call_count == 0 - - -@_mock -def test_exc(): - '''Tests that a command which does raise an exception does report an - exception. - - ''' - - args = ['dcos'] - env = _env_reporting() - version = 'release' - with patch('sys.argv', args), \ - patch('dcoscli.version', version), \ - patch.dict(os.environ, env), \ - patch('dcoscli.subcommand.SubcommandMain.run_and_capture', - return_value=(1, "Traceback")), \ - patch('dcoscli.analytics._segment_track') as track: - - assert main() == 1 - assert track.call_count == 2 - assert rollbar.report_message.call_count == 1 - - -def _env_reporting(): - path = os.path.join('tests', 'data', 'analytics', 'dcos_reporting.toml') - return {constants.DCOS_CONFIG_ENV: path} - - -@_mock -def test_config_reporting_false(): - '''Test that "core.reporting = false" blocks exception reporting.''' - - args = ['dcos'] - env = _env_no_reporting() - version = 'release' - - with patch('sys.argv', args), \ - patch('dcoscli.version', version), \ - patch.dict(os.environ, env), \ - patch('dcoscli.subcommand.SubcommandMain.run_and_capture', - return_value=(1, "Traceback")), \ - patch('dcoscli.analytics._segment_track') as track: - - assert main() == 1 - assert track.call_count == 0 - - -def _env_no_reporting(): - path = os.path.join('tests', 'data', 'analytics', 'dcos_no_reporting.toml') - return {constants.DCOS_CONFIG_ENV: path} - - -def test_command_always_returns_str(): - """Test that _command() returns str even if not subcommand specified""" - args = ['dcos'] - with patch('sys.argv', args): - assert dcoscli.analytics._command() == ""