Skip to content

Commit

Permalink
Merge pull request #18 from backtrace-labs/bugfix/expose-from-init
Browse files Browse the repository at this point in the history
Move code from init to client/report/utils
  • Loading branch information
konraddysput authored Sep 12, 2024
2 parents 78f26a8 + adec9e0 commit 2834643
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 324 deletions.
324 changes: 2 additions & 322 deletions backtracepython/__init__.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 3 additions & 0 deletions backtracepython/attributes/attribute_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ def get_predefined_dynamic_attribute_providers(self):
result.append(LinuxMemoryAttributeProvider())

return result


attribute_manager = AttributeManager()
Loading

0 comments on commit 2834643

Please sign in to comment.