diff --git a/backtracepython/__init__.py b/backtracepython/__init__.py index 869697b..57e0033 100644 --- a/backtracepython/__init__.py +++ b/backtracepython/__init__.py @@ -1,333 +1,13 @@ -import os -import platform -import subprocess -import sys -import threading -import time -import uuid - -import simplejson as json - -from backtracepython.attributes.attribute_manager import AttributeManager - +from .client import finalize, initialize, send_last_exception, send_report +from .report import BacktraceReport from .version import version, version_string -if sys.version_info.major >= 3: - from urllib.parse import urlencode -else: - from urllib import urlencode - __all__ = [ "BacktraceReport", "initialize", "finalize", - "terminate", "version", "version_string", "send_last_exception", "send_report", ] - -attribute_manager = AttributeManager() - - -class globs: - endpoint = None - next_except_hook = None - debug_backtrace = False - timeout = None - tab_width = None - attributes = {} - context_line_count = None - worker = None - next_source_code_id = 0 - - -child_py_path = os.path.join(os.path.dirname(__file__), "child.py") - - -def get_python_version(): - return "{} {}.{}.{}-{}".format( - platform.python_implementation(), - sys.version_info.major, - sys.version_info.minor, - sys.version_info.micro, - sys.version_info.releaselevel, - ) - - -def send_worker_msg(msg): - payload = json.dumps(msg, ignore_nan=True).encode("utf-8") - globs.worker.stdin.write(payload) - globs.worker.stdin.write("\n".encode("utf-8")) - globs.worker.stdin.flush() - - -def walk_tb_backwards(tb): - while tb is not None: - yield tb.tb_frame, tb.tb_lineno - tb = tb.tb_next - - -def walk_tb(tb): - return reversed(list(walk_tb_backwards(tb))) - - -def make_unique_source_code_id(): - result = str(globs.next_source_code_id) - globs.next_source_code_id += 1 - return result - - -def add_source_code(source_path, source_code_dict, source_path_dict, line): - try: - the_id = source_path_dict[source_path] - except KeyError: - the_id = make_unique_source_code_id() - source_path_dict[source_path] = the_id - source_code_dict[the_id] = { - "minLine": line, - "maxLine": line, - "path": source_path, - } - return the_id - - if line < source_code_dict[the_id]["minLine"]: - source_code_dict[the_id]["minLine"] = line - if line > source_code_dict[the_id]["maxLine"]: - source_code_dict[the_id]["maxLine"] = line - return the_id - - -def process_frame(tb_frame, line, source_code_dict, source_path_dict): - source_file = os.path.abspath(tb_frame.f_code.co_filename) - frame = { - "funcName": tb_frame.f_code.co_name, - "line": line, - "sourceCode": add_source_code( - source_file, source_code_dict, source_path_dict, line - ), - } - return frame - - -def get_main_thread(): - if sys.version_info.major >= 3: - return threading.main_thread() - first = None - for thread in threading.enumerate(): - if thread.name == "MainThread": - return thread - if first is None: - first = thread - return first - - -class BacktraceReport: - def __init__(self): - self.fault_thread = threading.current_thread() - self.source_code = {} - self.source_path_dict = {} - entry_source_code_id = None - import __main__ - - cwd_path = os.path.abspath(os.getcwd()) - entry_thread = get_main_thread() - if hasattr(__main__, "__file__"): - entry_source_code_id = ( - add_source_code( - __main__.__file__, self.source_code, self.source_path_dict, 1 - ) - if hasattr(__main__, "__file__") - else None - ) - - init_attrs = {"error.type": "Exception"} - init_attrs.update(attribute_manager.get()) - - self.log_lines = [] - - self.report = { - "uuid": str(uuid.uuid4()), - "timestamp": int(time.time()), - "lang": "python", - "langVersion": get_python_version(), - "agent": "backtrace-python", - "agentVersion": version_string, - "mainThread": str(self.fault_thread.ident), - "entryThread": str(entry_thread.ident), - "cwd": cwd_path, - "attributes": init_attrs, - "annotations": { - "Environment Variables": dict(os.environ), - }, - } - if entry_source_code_id is not None: - self.report["entrySourceCode"] = entry_source_code_id - - def set_exception(self, garbage, ex_value, ex_traceback): - self.report["classifiers"] = [ex_value.__class__.__name__] - self.report["attributes"]["error.message"] = str(ex_value) - - threads = {} - for thread in threading.enumerate(): - if thread.ident == self.fault_thread.ident: - threads[str(self.fault_thread.ident)] = { - "name": self.fault_thread.name, - "stack": [ - process_frame( - frame, line, self.source_code, self.source_path_dict - ) - for frame, line in walk_tb(ex_traceback) - ], - } - else: - threads[str(thread.ident)] = { - "name": thread.name, - } - - self.report["threads"] = threads - - def capture_last_exception(self): - self.set_exception(*sys.exc_info()) - - def set_attribute(self, key, value): - self.report["attributes"][key] = value - - def set_dict_attributes(self, target_dict): - self.report["attributes"].update(target_dict) - - def set_annotation(self, key, value): - self.report["annotations"][key] = value - - def get_attributes(self): - return self.report["attributes"] - - def set_dict_annotations(self, target_dict): - self.report["annotations"].update(target_dict) - - def log(self, line): - self.log_lines.append( - { - "ts": time.time(), - "msg": line, - } - ) - - def send(self): - if len(self.log_lines) != 0 and "Log" not in self.report["annotations"]: - self.report["annotations"]["Log"] = self.log_lines - send_worker_msg( - { - "id": "send", - "report": self.report, - "context_line_count": globs.context_line_count, - "timeout": globs.timeout, - "endpoint": globs.endpoint, - "tab_width": globs.tab_width, - "debug_backtrace": globs.debug_backtrace, - "source_code": self.source_code, - } - ) - - -def create_and_send_report(ex_type, ex_value, ex_traceback): - report = BacktraceReport() - report.set_exception(ex_type, ex_value, ex_traceback) - report.set_attribute("error.type", "Unhandled exception") - report.send() - - -def bt_except_hook(ex_type, ex_value, ex_traceback): - if globs.debug_backtrace: - # Go back to normal exceptions while we do our work here. - sys.excepthook = globs.next_except_hook - - # Now if this fails we'll get a normal exception. - create_and_send_report(ex_type, ex_value, ex_traceback) - - # Put our exception handler back in place, and then also - # pass the exception down the chain. - sys.excepthook = bt_except_hook - else: - # Failure here is silent. - try: - create_and_send_report(ex_type, ex_value, ex_traceback) - except: - pass - - # Send the exception on to the next thing in the chain. - globs.next_except_hook(ex_type, ex_value, ex_traceback) - - -def initialize(**kwargs): - globs.endpoint = construct_submission_url( - kwargs["endpoint"], kwargs.get("token", None) - ) - globs.debug_backtrace = kwargs.get("debug_backtrace", False) - globs.timeout = kwargs.get("timeout", 4) - globs.tab_width = kwargs.get("tab_width", 8) - globs.context_line_count = kwargs.get("context_line_count", 200) - - attribute_manager.add(kwargs.get("attributes", {})) - stdio_value = None if globs.debug_backtrace else subprocess.PIPE - globs.worker = subprocess.Popen( - [sys.executable, child_py_path], - stdin=subprocess.PIPE, - stdout=stdio_value, - stderr=stdio_value, - ) - - disable_global_handler = kwargs.get("disable_global_handler", False) - if not disable_global_handler: - globs.next_except_hook = sys.excepthook - sys.excepthook = bt_except_hook - - -def construct_submission_url(endpoint, token): - if "submit.backtrace.io" in endpoint or token is None: - return endpoint - - return "{}/post?{}".format( - endpoint, - urlencode( - { - "token": token, - "format": "json", - } - ), - ) - - -def finalize(): - send_worker_msg({"id": "terminate"}) - if not globs.debug_backtrace: - globs.worker.stdout.close() - globs.worker.stderr.close() - globs.worker.wait() - - -def send_last_exception(**kwargs): - report = BacktraceReport() - report.capture_last_exception() - report.set_dict_attributes(kwargs.get("attributes", {})) - report.set_dict_annotations(kwargs.get("annotations", {})) - report.set_attribute("error.type", "Exception") - report.send() - - -def make_an_exception(): - try: - raise Exception - except: - return sys.exc_info() - - -def send_report(msg, **kwargs): - report = BacktraceReport() - report.set_exception(*make_an_exception()) - report.set_dict_attributes(kwargs.get("attributes", {})) - report.set_dict_annotations(kwargs.get("annotations", {})) - report.set_attribute("error.message", msg) - report.set_attribute("error.type", "Message") - report.send() diff --git a/backtracepython/attributes/attribute_manager.py b/backtracepython/attributes/attribute_manager.py index 3f46308..e5c361e 100644 --- a/backtracepython/attributes/attribute_manager.py +++ b/backtracepython/attributes/attribute_manager.py @@ -63,3 +63,6 @@ def get_predefined_dynamic_attribute_providers(self): result.append(LinuxMemoryAttributeProvider()) return result + + +attribute_manager = AttributeManager() diff --git a/backtracepython/client.py b/backtracepython/client.py new file mode 100644 index 0000000..46ee862 --- /dev/null +++ b/backtracepython/client.py @@ -0,0 +1,156 @@ +import os +import subprocess +import sys + +import simplejson as json + +from backtracepython.attributes.attribute_manager import attribute_manager + +from .report import BacktraceReport + +if sys.version_info.major >= 3: + from urllib.parse import urlencode +else: + from urllib import urlencode + + +class globs: + endpoint = None + next_except_hook = None + debug_backtrace = False + timeout = None + tab_width = None + attributes = {} + context_line_count = None + worker = None + + +child_py_path = os.path.join(os.path.dirname(__file__), "child.py") + + +def get_attributes(): + return attribute_manager.get() + + +def send_worker_report(report, source_code): + send_worker_msg( + { + "id": "send", + "report": report, + "context_line_count": globs.context_line_count, + "timeout": globs.timeout, + "endpoint": globs.endpoint, + "tab_width": globs.tab_width, + "debug_backtrace": globs.debug_backtrace, + "source_code": source_code, + } + ) + + +def send_worker_msg(msg): + payload = json.dumps(msg, ignore_nan=True).encode("utf-8") + globs.worker.stdin.write(payload) + globs.worker.stdin.write("\n".encode("utf-8")) + globs.worker.stdin.flush() + + +def create_and_send_report(ex_type, ex_value, ex_traceback): + report = BacktraceReport() + report.set_exception(ex_type, ex_value, ex_traceback) + report.set_attribute("error.type", "Unhandled exception") + report.send() + + +def bt_except_hook(ex_type, ex_value, ex_traceback): + if globs.debug_backtrace: + # Go back to normal exceptions while we do our work here. + sys.excepthook = globs.next_except_hook + + # Now if this fails we'll get a normal exception. + create_and_send_report(ex_type, ex_value, ex_traceback) + + # Put our exception handler back in place, and then also + # pass the exception down the chain. + sys.excepthook = bt_except_hook + else: + # Failure here is silent. + try: + create_and_send_report(ex_type, ex_value, ex_traceback) + except: + pass + + # Send the exception on to the next thing in the chain. + globs.next_except_hook(ex_type, ex_value, ex_traceback) + + +def initialize(**kwargs): + globs.endpoint = construct_submission_url( + kwargs["endpoint"], kwargs.get("token", None) + ) + globs.debug_backtrace = kwargs.get("debug_backtrace", False) + globs.timeout = kwargs.get("timeout", 4) + globs.tab_width = kwargs.get("tab_width", 8) + globs.context_line_count = kwargs.get("context_line_count", 200) + + attribute_manager.add(kwargs.get("attributes", {})) + stdio_value = None if globs.debug_backtrace else subprocess.PIPE + globs.worker = subprocess.Popen( + [sys.executable, child_py_path], + stdin=subprocess.PIPE, + stdout=stdio_value, + stderr=stdio_value, + ) + + disable_global_handler = kwargs.get("disable_global_handler", False) + if not disable_global_handler: + globs.next_except_hook = sys.excepthook + sys.excepthook = bt_except_hook + + +def construct_submission_url(endpoint, token): + if "submit.backtrace.io" in endpoint or token is None: + return endpoint + + return "{}/post?{}".format( + endpoint, + urlencode( + { + "token": token, + "format": "json", + } + ), + ) + + +def finalize(): + send_worker_msg({"id": "terminate"}) + if not globs.debug_backtrace: + globs.worker.stdout.close() + globs.worker.stderr.close() + globs.worker.wait() + + +def send_last_exception(**kwargs): + report = BacktraceReport() + report.capture_last_exception() + report.set_dict_attributes(kwargs.get("attributes", {})) + report.set_dict_annotations(kwargs.get("annotations", {})) + report.set_attribute("error.type", "Exception") + report.send() + + +def make_an_exception(): + try: + raise Exception + except: + return sys.exc_info() + + +def send_report(msg, **kwargs): + report = BacktraceReport() + report.set_exception(*make_an_exception()) + report.set_dict_attributes(kwargs.get("attributes", {})) + report.set_dict_annotations(kwargs.get("annotations", {})) + report.set_attribute("error.message", msg) + report.set_attribute("error.type", "Message") + report.send() diff --git a/backtracepython/report.py b/backtracepython/report.py new file mode 100644 index 0000000..691e446 --- /dev/null +++ b/backtracepython/report.py @@ -0,0 +1,163 @@ +import os +import sys +import threading +import time +import uuid + +from backtracepython.attributes.attribute_manager import attribute_manager + +from .utils import python_version +from .version import version_string + + +def add_source_code(source_path, source_code_dict, source_path_dict, line): + try: + the_id = source_path_dict[source_path] + except KeyError: + the_id = str(uuid.uuid4()) + source_path_dict[source_path] = the_id + source_code_dict[the_id] = { + "minLine": line, + "maxLine": line, + "path": source_path, + } + return the_id + + if line < source_code_dict[the_id]["minLine"]: + source_code_dict[the_id]["minLine"] = line + if line > source_code_dict[the_id]["maxLine"]: + source_code_dict[the_id]["maxLine"] = line + return the_id + + +def process_frame(tb_frame, line, source_code_dict, source_path_dict): + source_file = os.path.abspath(tb_frame.f_code.co_filename) + frame = { + "funcName": tb_frame.f_code.co_name, + "line": line, + "sourceCode": add_source_code( + source_file, source_code_dict, source_path_dict, line + ), + } + return frame + + +def get_main_thread(): + if sys.version_info.major >= 3: + return threading.main_thread() + first = None + for thread in threading.enumerate(): + if thread.name == "MainThread": + return thread + if first is None: + first = thread + return first + + +def walk_tb_backwards(tb): + while tb is not None: + yield tb.tb_frame, tb.tb_lineno + tb = tb.tb_next + + +def walk_tb(tb): + return reversed(list(walk_tb_backwards(tb))) + + +class BacktraceReport: + def __init__(self): + self.fault_thread = threading.current_thread() + self.source_code = {} + self.source_path_dict = {} + entry_source_code_id = None + import __main__ + + cwd_path = os.path.abspath(os.getcwd()) + entry_thread = get_main_thread() + if hasattr(__main__, "__file__"): + entry_source_code_id = ( + add_source_code( + __main__.__file__, self.source_code, self.source_path_dict, 1 + ) + if hasattr(__main__, "__file__") + else None + ) + + init_attrs = {"error.type": "Exception"} + init_attrs.update(attribute_manager.get()) + + self.log_lines = [] + + self.report = { + "uuid": str(uuid.uuid4()), + "timestamp": int(time.time()), + "lang": "python", + "langVersion": python_version, + "agent": "backtrace-python", + "agentVersion": version_string, + "mainThread": str(self.fault_thread.ident), + "entryThread": str(entry_thread.ident), + "cwd": cwd_path, + "attributes": init_attrs, + "annotations": { + "Environment Variables": dict(os.environ), + }, + } + if entry_source_code_id is not None: + self.report["entrySourceCode"] = entry_source_code_id + + def set_exception(self, garbage, ex_value, ex_traceback): + self.report["classifiers"] = [ex_value.__class__.__name__] + self.report["attributes"]["error.message"] = str(ex_value) + + threads = {} + for thread in threading.enumerate(): + if thread.ident == self.fault_thread.ident: + threads[str(self.fault_thread.ident)] = { + "name": self.fault_thread.name, + "stack": [ + process_frame( + frame, line, self.source_code, self.source_path_dict + ) + for frame, line in walk_tb(ex_traceback) + ], + } + else: + threads[str(thread.ident)] = { + "name": thread.name, + } + + self.report["threads"] = threads + + def capture_last_exception(self): + self.set_exception(*sys.exc_info()) + + def set_attribute(self, key, value): + self.report["attributes"][key] = value + + def set_dict_attributes(self, target_dict): + self.report["attributes"].update(target_dict) + + def set_annotation(self, key, value): + self.report["annotations"][key] = value + + def get_attributes(self): + return self.report["attributes"] + + def set_dict_annotations(self, target_dict): + self.report["annotations"].update(target_dict) + + def log(self, line): + self.log_lines.append( + { + "ts": time.time(), + "msg": line, + } + ) + + def send(self): + if len(self.log_lines) != 0 and "Log" not in self.report["annotations"]: + self.report["annotations"]["Log"] = self.log_lines + from backtracepython.client import send_worker_report + + send_worker_report(self.report, self.source_code) diff --git a/backtracepython/utils.py b/backtracepython/utils.py new file mode 100644 index 0000000..12358cc --- /dev/null +++ b/backtracepython/utils.py @@ -0,0 +1,14 @@ +import platform +import sys + +python_version = "{} {}.{}.{}-{}".format( + platform.python_implementation(), + sys.version_info.major, + sys.version_info.minor, + sys.version_info.micro, + sys.version_info.releaselevel, +) + + +def get_python_version(): + return python_version diff --git a/tests/test_report_attributes.py b/tests/test_report_attributes.py index fdae45a..339e73f 100644 --- a/tests/test_report_attributes.py +++ b/tests/test_report_attributes.py @@ -1,4 +1,4 @@ -from backtracepython import BacktraceReport +from backtracepython.report import BacktraceReport report = BacktraceReport() diff --git a/tests/test_submission_url.py b/tests/test_submission_url.py index 8a73350..fae693c 100644 --- a/tests/test_submission_url.py +++ b/tests/test_submission_url.py @@ -1,4 +1,4 @@ -from backtracepython import construct_submission_url +from backtracepython.client import construct_submission_url universe = "test" hostname = "https://{}.sp.backtrace.io".format(universe)