diff --git a/clr/__init__.py b/clr/__init__.py index fb2df02c0f..0655fb7e08 100644 --- a/clr/__init__.py +++ b/clr/__init__.py @@ -1,2 +1,3 @@ # This is the main entry point for the tool. -from clr.main import main +from .main import main +from ._version import __version__ diff --git a/clr/_version.py b/clr/_version.py new file mode 100644 index 0000000000..50d85c89d0 --- /dev/null +++ b/clr/_version.py @@ -0,0 +1 @@ +__version__ = "0.3.18" diff --git a/clr/commands.py b/clr/commands.py index e297fd22ae..cf438ced4a 100644 --- a/clr/commands.py +++ b/clr/commands.py @@ -18,9 +18,9 @@ from itertools import takewhile import traceback -import clr.config +from .config import read_namespaces -NAMESPACE_MODULE_PATHS = clr.config.read_namespaces() +NAMESPACE_MODULE_PATHS = read_namespaces() # Sorted list of command namespace keys. NAMESPACE_KEYS = sorted({"system", *NAMESPACE_MODULE_PATHS.keys()}) diff --git a/clr/main.py b/clr/main.py index 0ba245d4e6..bdbfcc95f7 100644 --- a/clr/main.py +++ b/clr/main.py @@ -2,53 +2,66 @@ import os import getpass import beeline -import platform +import time import traceback -import signal +import atexit from contextlib import contextmanager -from clr.commands import resolve_command, get_namespace +from .commands import resolve_command, get_namespace +from ._version import __version__ DEBUG_MODE = os.environ.get("CLR_DEBUG", "").lower() in ("true", "1") -def on_exit(signum, frame): - beeline.add_trace_field("killed_by_signal", signal.Signals(signum).name) - beeline.close() +# Store data to send to honeycomb as a global so it can be accessed from an +# atexit method. None of this code should ever be called from within a long +# running process. +honeycomb_data = {} -signal.signal(signal.SIGINT, on_exit) -signal.signal(signal.SIGTERM, on_exit) -@contextmanager -def init_beeline(namespace_key, cmd_name): +def send_to_honeycomb(): + """Attempts to log usage data to honeycomb. + + Honeycomb logging is completely optional. If there are any failures + simply continue as normal. This includes if clrenv can not be loaded. + + These metrics are sent to honeycomb as an event rather than a trace for two + reasons: + 1) They don't really fit the web requests model. + 2) Downstream code might have its own tracing which we don't want to interfere + with. + """ try: from clrenv import env # clrenv < 0.2.0 has a bug in the `in` operator at the root level. - if env.get("honeycomb") is not None: - beeline.init( - writekey=env.honeycomb.writekey, - dataset="clr", - service_name="clr", - debug=False, - ) + if env.get("honeycomb") is None: + return + + beeline.init( + writekey=env.honeycomb.writekey, + dataset="clr", + service_name="clr", + ) + + # Convert start_time into a duration. + honeycomb_data["duration_ms"] = int( + 1000 * (time.time() - honeycomb_data["start_time"]) + ) + del honeycomb_data["start_time"] + + honeycomb_data["username"] = getpass.getuser() + honeycomb_data["clr_version"] = __version__ + honeycomb_data["color_key_mode"] = env.key_mode + + beeline.send_now(honeycomb_data) + beeline.close() except: - # Honeycomb logging is completely optional and all later calls to - # beeline are silently no-ops if not initialized. Simply log the - # failure and continue normally. This includes if clrenv can not be - # loaded. if DEBUG_MODE: print("Failed to initialize beeline.", file=sys.stderr) - traceback.print_exc() + print(traceback.format_exc(), file=sys.stderr) - with beeline.tracer("cmd"): - beeline.add_trace_field("namespace", namespace_key) - beeline.add_trace_field("cmd", cmd_name) - beeline.add_trace_field("username", getpass.getuser()) - beeline.add_trace_field("hostname", platform.node()) - # Bounce back to the calling code. - yield - - beeline.close() +# Will get run on normal completion, exceptions and sys.exit. +atexit.register(send_to_honeycomb) def main(argv=None): @@ -59,33 +72,36 @@ def main(argv=None): query = argv[1] namespace_key, cmd_name = resolve_command(query) + + honeycomb_data["namespace_key"] = namespace_key + honeycomb_data["cmd_name"] = cmd_name + honeycomb_data["start_time"] = time.time() + namespace = get_namespace(namespace_key) # Default successful exit code. exit_code = 0 - with init_beeline(namespace_key, cmd_name): - with beeline.tracer("parse_args"): - bound_args = namespace.parse_args(cmd_name, argv[2:]) - - # Some namespaces define a cmdinit function which should be run first. - if hasattr(namespace.instance, "cmdinit"): - with beeline.tracer("cmdinit"): - namespace.instance.cmdinit() - - with beeline.tracer("cmdrun"): - result = None - try: - result = namespace.command_callables[cmd_name]( - *bound_args.args, **bound_args.kwargs - ) - if isinstance(result, (int, bool)): - exit_code = int(result) - except: - print(traceback.format_exc(), file=sys.stderr) - beeline.add_trace_field("raised_exception", True) - exit_code = 999 - - beeline.add_trace_field("exit_code", exit_code) + bound_args = namespace.parse_args(cmd_name, argv[2:]) + # TODO(michael.cusack): Args are currently not logged to honeycomb for PHI reasons. + # Evaluate whether this is really an issue. + # honeycomb_data['args'] = bound_args.arguments + + # Some namespaces define a cmdinit function which should be run first. + if hasattr(namespace.instance, "cmdinit"): + namespace.instance.cmdinit() + result = None + try: + result = namespace.command_callables[cmd_name]( + *bound_args.args, **bound_args.kwargs + ) + if isinstance(result, (int, bool)): + exit_code = int(result) + except BaseException as e: + # BaseException so we still will see KeyboardInterrupts + honeycomb_data["raised_exception"] = repr(e) + raise + + honeycomb_data["exit_code"] = exit_code sys.exit(exit_code) diff --git a/setup.py b/setup.py index 88e2b6a5d2..620cdee450 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,13 @@ from setuptools import setup +from pathlib import Path +import re requirements = ["dataclasses;python_version<'3.7'", "honeycomb-beeline"] +version = re.findall('__version__ = "(.+)"', Path("clr/_version.py").read_text())[0] setup( name="clr", - version="0.3.15", + version=version, description="A command line tool for executing custom python scripts.", author="Color", author_email="dev@getcolor.com",