216 lines
5.3 KiB
Python
216 lines
5.3 KiB
Python
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()))
|
|
}
|