segment.io
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
if [ -n "$BASH_SOURCE" ] ; then
|
||||
BIN_DIR=$(dirname "$BASH_SOURCE")
|
||||
elif [ $(basename -- "$0") = "env-setup" ]; then
|
||||
elif [ $(basename -- "$0") = "env-setup-dev" ]; then
|
||||
BIN_DIR=$(dirname "$0")
|
||||
else
|
||||
BIN_DIR=$PWD/bin
|
||||
|
||||
215
cli/dcoscli/analytics.py
Normal file
215
cli/dcoscli/analytics.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import dcoscli
|
||||
import requests
|
||||
import rollbar
|
||||
from dcos.api import config, constants
|
||||
from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY,
|
||||
SEGMENT_IO_CLI_ERROR_EVENT,
|
||||
SEGMENT_IO_CLI_EVENT, SEGMENT_IO_WRITE_KEY_DEV,
|
||||
SEGMENT_IO_WRITE_KEY_PROD, SEGMENT_URL)
|
||||
from futures import ThreadPoolExecutor
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
session_id = uuid.uuid4().hex
|
||||
|
||||
|
||||
def wait_and_track(subproc):
|
||||
"""
|
||||
Run a command and report it to analytics services.
|
||||
|
||||
:param subproc: Subprocess to capture
|
||||
:type subproc: Popen
|
||||
:returns: exit code of subproc
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
rollbar.init(ROLLBAR_SERVER_POST_KEY,
|
||||
'prod' if _is_prod() else 'dev')
|
||||
|
||||
conf = _conf()
|
||||
report = conf.get('core.reporting', True)
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
if report:
|
||||
_segment_track_cli(pool, conf)
|
||||
|
||||
exit_code, err = _wait_and_capture(subproc)
|
||||
|
||||
# We only want to catch exceptions, not other stderr messages
|
||||
# (such as "task does not exist", so we look for the 'Traceback'
|
||||
# string. This only works for python, so we'll need to revisit
|
||||
# this in the future when we support subcommands written in other
|
||||
# languages.
|
||||
if report and 'Traceback' in err:
|
||||
_track_err(pool, exit_code, err, conf)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def _send_segment_event(event, properties):
|
||||
"""
|
||||
Send a segment event
|
||||
|
||||
:param event: name of event
|
||||
:type event: string
|
||||
:param properties: event properties
|
||||
:type properties: dict
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
data = {'anonymousId': session_id,
|
||||
'event': event,
|
||||
'properties': properties}
|
||||
|
||||
key = SEGMENT_IO_WRITE_KEY_PROD if _is_prod() else \
|
||||
SEGMENT_IO_WRITE_KEY_DEV
|
||||
try:
|
||||
requests.post(SEGMENT_URL,
|
||||
json=data,
|
||||
auth=HTTPBasicAuth(key, ''),
|
||||
timeout=3)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
def _is_prod():
|
||||
""" True if this process is in production. """
|
||||
return os.environ.get('DCOS_PRODUCTION', 'true') != 'false'
|
||||
|
||||
|
||||
def _conf():
|
||||
"""
|
||||
Get config file.
|
||||
|
||||
:rtype: Toml
|
||||
"""
|
||||
|
||||
return config.load_from_path(
|
||||
os.environ[constants.DCOS_CONFIG_ENV])
|
||||
|
||||
|
||||
def _wait_and_capture(subproc):
|
||||
"""
|
||||
Run a subprocess and capture its stderr.
|
||||
|
||||
:param subproc: Subprocess to capture
|
||||
:type subproc: Popen
|
||||
:returns: exit code of subproc
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
err = ''
|
||||
while subproc.poll() is None:
|
||||
err_buff = subproc.stderr.read().decode('utf-8')
|
||||
sys.stderr.write(err_buff)
|
||||
err += err_buff
|
||||
|
||||
exit_code = subproc.poll()
|
||||
|
||||
return exit_code, err
|
||||
|
||||
|
||||
def _track_err(pool, exit_code, err, conf):
|
||||
"""
|
||||
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
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
# Segment.io calls are async, but rollbar is not, so for
|
||||
# parallelism, we must call segment first.
|
||||
_segment_track_err(pool, conf, err, exit_code)
|
||||
_rollbar_track_err(conf, err, exit_code)
|
||||
|
||||
|
||||
def _segment_track_cli(pool, conf):
|
||||
"""
|
||||
Send segment.io cli event.
|
||||
|
||||
:param pool: thread pool
|
||||
:type pool: ThreadPoolExecutor
|
||||
:param conf: dcos config file
|
||||
:type conf: Toml
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
props = _base_properties(conf)
|
||||
pool.submit(_send_segment_event, SEGMENT_IO_CLI_EVENT, props)
|
||||
|
||||
|
||||
def _segment_track_err(pool, conf, err, exit_code):
|
||||
"""
|
||||
Send segment.io error event.
|
||||
|
||||
:param pool: thread pool
|
||||
:type segment: ThreadPoolExecutor
|
||||
:param conf: dcos config file
|
||||
:type conf: Toml
|
||||
: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)
|
||||
props['err'] = err
|
||||
props['exit_code'] = exit_code
|
||||
pool.submit(_send_segment_event, SEGMENT_IO_CLI_ERROR_EVENT, props)
|
||||
|
||||
|
||||
def _rollbar_track_err(conf, err, exit_code):
|
||||
"""
|
||||
Report to rollbar. Synchronous.
|
||||
|
||||
: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
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
props = _base_properties(conf)
|
||||
props['exit_code'] = exit_code
|
||||
|
||||
try:
|
||||
rollbar.report_message(err, 'error', extra_data=props)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
def _base_properties(conf=None):
|
||||
"""
|
||||
These properties are sent with every analytics event.
|
||||
|
||||
:param conf: dcos config file
|
||||
:type conf: Toml
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if not conf:
|
||||
conf = _conf()
|
||||
|
||||
cmd = 'dcos' + (' {}'.format(sys.argv[1]) if len(sys.argv) > 1 else '')
|
||||
return {
|
||||
'cmd': cmd,
|
||||
'full_cmd': ' '.join(sys.argv),
|
||||
'dcoscli.version': dcoscli.version,
|
||||
'python_version': str(sys.version_info),
|
||||
'config': json.dumps(list(conf.property_items()))
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
ROLLBAR_SERVER_POST_KEY = '62f87c5df3674629b143a137de3d3244'
|
||||
|
||||
SEGMENT_IO_WRITE_KEY_PROD = '51ybGTeFEFU1xo6u10XMDrr6kATFyRyh'
|
||||
SEGMENT_IO_WRITE_KEY_DEV = '39uhSEOoRHMw6cMR6st9tYXDbAL3JSaP'
|
||||
SEGMENT_IO_CLI_EVENT = 'dcos-cli'
|
||||
SEGMENT_IO_CLI_ERROR_EVENT = 'dcos-cli-error'
|
||||
SEGMENT_URL = 'https://api.segment.io/v1/track'
|
||||
|
||||
DCOS_PRODUCTION_ENV = 'DCOS_PRODUCTION'
|
||||
|
||||
@@ -26,8 +26,6 @@ Environment Variables:
|
||||
to read about a specific subcommand.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
@@ -35,12 +33,9 @@ from subprocess import PIPE, Popen
|
||||
|
||||
import dcoscli
|
||||
import docopt
|
||||
import rollbar
|
||||
from dcos.api import (config, constants, emitting, errors, http, subcommand,
|
||||
util)
|
||||
from dcoscli.constants import ROLLBAR_SERVER_POST_KEY
|
||||
from dcos.api import constants, emitting, errors, http, subcommand, util
|
||||
from dcoscli import analytics
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
emitter = emitting.FlatEmitter()
|
||||
|
||||
|
||||
@@ -77,78 +72,7 @@ def main():
|
||||
subproc = Popen([executable, command] + args['<args>'],
|
||||
stderr=PIPE)
|
||||
|
||||
prod = os.environ.get('DCOS_PRODUCTION', 'true') != 'false'
|
||||
rollbar.init(ROLLBAR_SERVER_POST_KEY,
|
||||
'prod' if prod else 'dev')
|
||||
return _wait_and_track(subproc)
|
||||
|
||||
|
||||
def _wait_and_capture(subproc):
|
||||
"""
|
||||
:param subproc: Subprocess to capture
|
||||
:type subproc: Popen
|
||||
:returns: exit code of subproc
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
# capture and print stderr
|
||||
err = ''
|
||||
while subproc.poll() is None:
|
||||
err_buff = subproc.stderr.read().decode('utf-8')
|
||||
sys.stderr.write(err_buff)
|
||||
err += err_buff
|
||||
|
||||
exit_code = subproc.poll()
|
||||
|
||||
return exit_code, err
|
||||
|
||||
|
||||
def _wait_and_track(subproc):
|
||||
"""
|
||||
:param subproc: Subprocess to capture
|
||||
:type subproc: Popen
|
||||
:returns: exit code of subproc
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
exit_code, err = _wait_and_capture(subproc)
|
||||
|
||||
conf = config.load_from_path(
|
||||
os.environ[constants.DCOS_CONFIG_ENV])
|
||||
|
||||
# We only want to catch exceptions, not other stderr messages
|
||||
# (such as "task does not exist", so we look for the 'Traceback'
|
||||
# string. This only works for python, so we'll need to revisit
|
||||
# this in the future when we support subcommands written in other
|
||||
# languages.
|
||||
if 'Traceback' in err and conf.get('core.reporting', True):
|
||||
_track(exit_code, err, conf)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def _track(exit_code, err, conf):
|
||||
"""
|
||||
: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
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
# rollbar analytics
|
||||
try:
|
||||
rollbar.report_message(err, 'error', extra_data={
|
||||
'cmd': ' '.join(sys.argv),
|
||||
'exit_code': exit_code,
|
||||
'python_version': str(sys.version_info),
|
||||
'dcoscli.version': dcoscli.version,
|
||||
'config': json.dumps(list(conf.property_items()))
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return analytics.wait_and_track(subproc)
|
||||
|
||||
|
||||
def _config_log_level_environ(log_level):
|
||||
|
||||
@@ -67,7 +67,8 @@ setup(
|
||||
'pkginfo',
|
||||
'toml',
|
||||
'virtualenv',
|
||||
'rollbar'
|
||||
'rollbar',
|
||||
'futures'
|
||||
],
|
||||
|
||||
# If there are data files included in your packages that need to be
|
||||
|
||||
@@ -7,4 +7,4 @@ host = "localhost"
|
||||
cache = "tmp/cache"
|
||||
sources = [ "git://github.com/mesosphere/universe.git", "https://github.com/mesosphere/universe/archive/master.zip",]
|
||||
[core]
|
||||
reporting = true
|
||||
reporting = false
|
||||
|
||||
@@ -1,66 +1,136 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from functools import wraps
|
||||
|
||||
import dcoscli
|
||||
import dcoscli.analytics
|
||||
import requests
|
||||
import rollbar
|
||||
from dcos.api import config, constants, util
|
||||
from dcoscli.constants import ROLLBAR_SERVER_POST_KEY
|
||||
from dcos.api import constants, util
|
||||
from dcoscli.analytics import _base_properties
|
||||
from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY,
|
||||
SEGMENT_IO_CLI_ERROR_EVENT,
|
||||
SEGMENT_IO_CLI_EVENT, SEGMENT_IO_WRITE_KEY_DEV,
|
||||
SEGMENT_IO_WRITE_KEY_PROD, SEGMENT_URL)
|
||||
from dcoscli.main import main
|
||||
|
||||
from mock import Mock, patch
|
||||
from mock import patch
|
||||
|
||||
ANON_ID = 0
|
||||
|
||||
|
||||
def _mock(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
with patch('rollbar.init'), \
|
||||
patch('rollbar.report_message'), \
|
||||
patch('requests.post'), \
|
||||
patch('dcoscli.analytics.session_id'):
|
||||
|
||||
dcoscli.analytics.session_id = ANON_ID
|
||||
fn()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@_mock
|
||||
def test_no_exc():
|
||||
'''Tests that a command which does not raise an exception does not
|
||||
report an exception.
|
||||
|
||||
'''
|
||||
|
||||
# args
|
||||
args = [util.which('dcos')]
|
||||
exit_code = _mock_analytics_run(args)
|
||||
env = _env_reporting()
|
||||
|
||||
assert rollbar.report_message.call_count == 0
|
||||
assert exit_code == 0
|
||||
with patch('sys.argv', args), patch.dict(os.environ, env):
|
||||
assert main() == 0
|
||||
|
||||
# segment.io
|
||||
args, kwargs = requests.post.call_args
|
||||
assert args == (SEGMENT_URL,)
|
||||
|
||||
props = _base_properties()
|
||||
assert kwargs['json'] == {'anonymousId': ANON_ID,
|
||||
'event': SEGMENT_IO_CLI_EVENT,
|
||||
'properties': props}
|
||||
assert kwargs['timeout'] == 3
|
||||
|
||||
# rollbar
|
||||
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
|
||||
args = [util.which('dcos')]
|
||||
exit_code = _mock_analytics_run_exc(args)
|
||||
env = _env_reporting()
|
||||
|
||||
props = _analytics_properties(args, exit_code=1)
|
||||
rollbar.report_message.assert_called_with('Traceback', 'error',
|
||||
extra_data=props)
|
||||
assert exit_code == 1
|
||||
with patch('sys.argv', args), \
|
||||
patch.dict(os.environ, env), \
|
||||
patch('dcoscli.analytics._wait_and_capture',
|
||||
return_value=(1, 'Traceback')):
|
||||
assert main() == 1
|
||||
|
||||
# segment.io
|
||||
_, kwargs = requests.post.call_args_list[1]
|
||||
|
||||
props = _base_properties()
|
||||
props['err'] = 'Traceback'
|
||||
props['exit_code'] = 1
|
||||
assert kwargs['json'] == {'anonymousId': ANON_ID,
|
||||
'event': SEGMENT_IO_CLI_ERROR_EVENT,
|
||||
'properties': props}
|
||||
|
||||
props = _base_properties()
|
||||
props['exit_code'] = 1
|
||||
rollbar.report_message.assert_called_with('Traceback', 'error',
|
||||
extra_data=props)
|
||||
|
||||
|
||||
@_mock
|
||||
def test_config_reporting_false():
|
||||
'''Test that "core.reporting = false" blocks exception reporting.'''
|
||||
|
||||
args = [util.which('dcos')]
|
||||
exit_code = _mock_analytics_run_exc(args, False)
|
||||
env = _env_no_reporting()
|
||||
|
||||
assert rollbar.report_message.call_count == 0
|
||||
assert exit_code == 1
|
||||
with patch('sys.argv', args), \
|
||||
patch.dict(os.environ, env), \
|
||||
patch('dcoscli.analytics._wait_and_capture',
|
||||
return_value=(1, 'Traceback')):
|
||||
|
||||
assert main() == 1
|
||||
|
||||
assert rollbar.report_message.call_count == 0
|
||||
assert requests.post.call_count == 0
|
||||
|
||||
|
||||
@_mock
|
||||
def test_production_setting_true():
|
||||
'''Test that env var DCOS_PRODUCTION=true sends exceptions to
|
||||
the 'prod' environment.
|
||||
'''Test that env var DCOS_PRODUCTION as empty string sends exceptions
|
||||
to the 'prod' environment.
|
||||
|
||||
'''
|
||||
|
||||
args = [util.which('dcos')]
|
||||
with patch.dict(os.environ, {'DCOS_PRODUCTION': 'true'}):
|
||||
_mock_analytics_run(args)
|
||||
env = _env_reporting()
|
||||
env['DCOS_PRODUCTION'] = ''
|
||||
|
||||
with patch('sys.argv', args), patch.dict(os.environ, env):
|
||||
assert main() == 0
|
||||
|
||||
_, kwargs = requests.post.call_args_list[0]
|
||||
assert kwargs['auth'].username == SEGMENT_IO_WRITE_KEY_PROD
|
||||
|
||||
rollbar.init.assert_called_with(ROLLBAR_SERVER_POST_KEY, 'prod')
|
||||
|
||||
|
||||
@_mock
|
||||
def test_production_setting_false():
|
||||
'''Test that env var DCOS_PRODUCTION=false sends exceptions to
|
||||
the 'dev' environment.
|
||||
@@ -68,47 +138,23 @@ def test_production_setting_false():
|
||||
'''
|
||||
|
||||
args = [util.which('dcos')]
|
||||
with patch.dict(os.environ, {'DCOS_PRODUCTION': 'false'}):
|
||||
_mock_analytics_run(args)
|
||||
env = _env_reporting()
|
||||
env['DCOS_PRODUCTION'] = 'false'
|
||||
|
||||
with patch('sys.argv', args), patch.dict(os.environ, env):
|
||||
assert main() == 0
|
||||
|
||||
_, kwargs = requests.post.call_args_list[0]
|
||||
assert kwargs['auth'].username == SEGMENT_IO_WRITE_KEY_DEV
|
||||
|
||||
rollbar.init.assert_called_with(ROLLBAR_SERVER_POST_KEY, 'dev')
|
||||
|
||||
|
||||
def _config_path_reporting():
|
||||
return os.path.join('tests', 'data', 'analytics', 'dcos_reporting.toml')
|
||||
|
||||
|
||||
def _config_path_no_reporting():
|
||||
return os.path.join('tests', 'data', 'analytics', 'dcos_no_reporting.toml')
|
||||
|
||||
|
||||
def _env_reporting():
|
||||
return {constants.DCOS_CONFIG_ENV: _config_path_reporting()}
|
||||
path = os.path.join('tests', 'data', 'analytics', 'dcos_reporting.toml')
|
||||
return {constants.DCOS_CONFIG_ENV: path}
|
||||
|
||||
|
||||
def _env_no_reporting():
|
||||
return {constants.DCOS_CONFIG_ENV: _config_path_no_reporting()}
|
||||
|
||||
|
||||
def _mock_analytics_run_exc(args, reporting=True):
|
||||
dcoscli.main._wait_and_capture = Mock(return_value=(1, 'Traceback'))
|
||||
return _mock_analytics_run(args, reporting)
|
||||
|
||||
|
||||
def _mock_analytics_run(args, reporting=True):
|
||||
env = _env_reporting() if reporting else _env_no_reporting()
|
||||
|
||||
with patch('sys.argv', args), patch.dict(os.environ, env):
|
||||
rollbar.init = Mock()
|
||||
rollbar.report_message = Mock()
|
||||
return main()
|
||||
|
||||
|
||||
def _analytics_properties(sysargs, **kwargs):
|
||||
conf = config.load_from_path(_config_path_reporting())
|
||||
defaults = {'cmd': ' '.join(sysargs),
|
||||
'exit_code': 0,
|
||||
'dcoscli.version': dcoscli.version,
|
||||
'python_version': str(sys.version_info),
|
||||
'config': json.dumps(list(conf.property_items()))}
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
path = os.path.join('tests', 'data', 'analytics', 'dcos_no_reporting.toml')
|
||||
return {constants.DCOS_CONFIG_ENV: path}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import dcoscli.constants as cli_constants
|
||||
import six
|
||||
from dcos.api import constants
|
||||
|
||||
@@ -12,7 +13,8 @@ from common import exec_command
|
||||
def env():
|
||||
return {
|
||||
constants.PATH_ENV: os.environ[constants.PATH_ENV],
|
||||
constants.DCOS_CONFIG_ENV: os.path.join("tests", "data", "dcos.toml")
|
||||
constants.DCOS_CONFIG_ENV: os.path.join("tests", "data", "dcos.toml"),
|
||||
cli_constants.DCOS_PRODUCTION_ENV: 'false'
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +69,7 @@ def test_list_property(env):
|
||||
env)
|
||||
|
||||
assert returncode == 0
|
||||
assert stdout == b"""core.reporting=True
|
||||
assert stdout == b"""core.reporting=False
|
||||
marathon.host=localhost
|
||||
marathon.port=8080
|
||||
package.cache=tmp/cache
|
||||
@@ -341,9 +343,9 @@ def test_set_missing_property(env):
|
||||
|
||||
|
||||
def test_set_core_property(env):
|
||||
_set_value('core.reporting', 'false', env)
|
||||
_get_value('core.reporting', False, env)
|
||||
_set_value('core.reporting', 'true', env)
|
||||
_get_value('core.reporting', True, env)
|
||||
_set_value('core.reporting', 'false', env)
|
||||
|
||||
|
||||
def _set_value(key, value, env):
|
||||
|
||||
Reference in New Issue
Block a user