8000 Make honeycomb logging an event rather than a span by mpcusack-color · Pull Request #40 · color/clr · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
This repository was archived by the owner on Mar 31, 2025. It is now read-only.

Make honeycomb logging an event rather than a span #40

Merged
merged 4 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion clr/__init__.py
Original file line number Diff line number Diff line change
@@ -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__
1 change: 1 addition & 0 deletions clr/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.3.18"
4 changes: 2 additions & 2 deletions clr/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()})

Expand Down
124 changes: 70 additions & 54 deletions clr/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
0