248 lines
5.9 KiB
Python
248 lines
5.9 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
import uuid
|
|
|
|
import dcoscli
|
|
import requests
|
|
import rollbar
|
|
from dcos import util
|
|
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 = util.get_logger(__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 = util.get_config()
|
|
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 _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}
|
|
|
|
if 'core.email' in conf:
|
|
data['userId'] = conf['core.email']
|
|
else:
|
|
data['anonymousId'] = session_id
|
|
|
|
_segment_request('track', data)
|
|
|
|
|
|
def segment_identify(conf):
|
|
"""
|
|
Send a segment.io 'identify' event
|
|
|
|
:param conf: dcos config file
|
|
:type conf: Toml
|
|
:rtype: None
|
|
"""
|
|
|
|
if 'core.email' in conf:
|
|
data = {'userId': conf.get('core.email')}
|
|
else:
|
|
data = {'anonymousId': session_id}
|
|
|
|
_segment_request('identify', 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 if _is_prod() else \
|
|
SEGMENT_IO_WRITE_KEY_DEV
|
|
|
|
try:
|
|
requests.post('{}/{}'.format(SEGMENT_URL, path),
|
|
json=data,
|
|
auth=HTTPBasicAuth(key, ''),
|
|
timeout=1)
|
|
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 _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(_segment_track, SEGMENT_IO_CLI_EVENT, conf, 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(_segment_track, SEGMENT_IO_CLI_ERROR_EVENT, conf, 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 = util.get_config()
|
|
|
|
if len(sys.argv) > 1:
|
|
cmd = 'dcos ' + sys.argv[1]
|
|
full_cmd = 'dcos ' + ' '.join(sys.argv[1:])
|
|
else:
|
|
cmd = 'dcos'
|
|
full_cmd = 'dcos'
|
|
|
|
return {
|
|
'cmd': cmd,
|
|
'full_cmd': full_cmd,
|
|
'dcoscli.version': dcoscli.version,
|
|
'python_version': str(sys.version_info),
|
|
'config': json.dumps(list(conf.property_items()))
|
|
}
|