|
1 | | -import os |
2 | | -import platform |
3 | | -import subprocess |
4 | | -import sys |
5 | | -import threading |
6 | | -import time |
7 | | -import uuid |
8 | | - |
9 | | -import simplejson as json |
10 | | - |
11 | | -from backtracepython.attributes.attribute_manager import AttributeManager |
12 | | - |
| 1 | +from .client import finalize, initialize, send_last_exception, send_report |
| 2 | +from .report import BacktraceReport |
13 | 3 | from .version import version, version_string |
14 | 4 |
|
15 | | -if sys.version_info.major >= 3: |
16 | | - from urllib.parse import urlencode |
17 | | -else: |
18 | | - from urllib import urlencode |
19 | | - |
20 | 5 | __all__ = [ |
21 | 6 | "BacktraceReport", |
22 | 7 | "initialize", |
23 | 8 | "finalize", |
24 | | - "terminate", |
25 | 9 | "version", |
26 | 10 | "version_string", |
27 | 11 | "send_last_exception", |
28 | 12 | "send_report", |
29 | 13 | ] |
30 | | - |
31 | | -attribute_manager = AttributeManager() |
32 | | - |
33 | | - |
34 | | -class globs: |
35 | | - endpoint = None |
36 | | - next_except_hook = None |
37 | | - debug_backtrace = False |
38 | | - timeout = None |
39 | | - tab_width = None |
40 | | - attributes = {} |
41 | | - context_line_count = None |
42 | | - worker = None |
43 | | - next_source_code_id = 0 |
44 | | - |
45 | | - |
46 | | -child_py_path = os.path.join(os.path.dirname(__file__), "child.py") |
47 | | - |
48 | | - |
49 | | -def get_python_version(): |
50 | | - return "{} {}.{}.{}-{}".format( |
51 | | - platform.python_implementation(), |
52 | | - sys.version_info.major, |
53 | | - sys.version_info.minor, |
54 | | - sys.version_info.micro, |
55 | | - sys.version_info.releaselevel, |
56 | | - ) |
57 | | - |
58 | | - |
59 | | -def send_worker_msg(msg): |
60 | | - payload = json.dumps(msg, ignore_nan=True).encode("utf-8") |
61 | | - globs.worker.stdin.write(payload) |
62 | | - globs.worker.stdin.write("\n".encode("utf-8")) |
63 | | - globs.worker.stdin.flush() |
64 | | - |
65 | | - |
66 | | -def walk_tb_backwards(tb): |
67 | | - while tb is not None: |
68 | | - yield tb.tb_frame, tb.tb_lineno |
69 | | - tb = tb.tb_next |
70 | | - |
71 | | - |
72 | | -def walk_tb(tb): |
73 | | - return reversed(list(walk_tb_backwards(tb))) |
74 | | - |
75 | | - |
76 | | -def make_unique_source_code_id(): |
77 | | - result = str(globs.next_source_code_id) |
78 | | - globs.next_source_code_id += 1 |
79 | | - return result |
80 | | - |
81 | | - |
82 | | -def add_source_code(source_path, source_code_dict, source_path_dict, line): |
83 | | - try: |
84 | | - the_id = source_path_dict[source_path] |
85 | | - except KeyError: |
86 | | - the_id = make_unique_source_code_id() |
87 | | - source_path_dict[source_path] = the_id |
88 | | - source_code_dict[the_id] = { |
89 | | - "minLine": line, |
90 | | - "maxLine": line, |
91 | | - "path": source_path, |
92 | | - } |
93 | | - return the_id |
94 | | - |
95 | | - if line < source_code_dict[the_id]["minLine"]: |
96 | | - source_code_dict[the_id]["minLine"] = line |
97 | | - if line > source_code_dict[the_id]["maxLine"]: |
98 | | - source_code_dict[the_id]["maxLine"] = line |
99 | | - return the_id |
100 | | - |
101 | | - |
102 | | -def process_frame(tb_frame, line, source_code_dict, source_path_dict): |
103 | | - source_file = os.path.abspath(tb_frame.f_code.co_filename) |
104 | | - frame = { |
105 | | - "funcName": tb_frame.f_code.co_name, |
106 | | - "line": line, |
107 | | - "sourceCode": add_source_code( |
108 | | - source_file, source_code_dict, source_path_dict, line |
109 | | - ), |
110 | | - } |
111 | | - return frame |
112 | | - |
113 | | - |
114 | | -def get_main_thread(): |
115 | | - if sys.version_info.major >= 3: |
116 | | - return threading.main_thread() |
117 | | - first = None |
118 | | - for thread in threading.enumerate(): |
119 | | - if thread.name == "MainThread": |
120 | | - return thread |
121 | | - if first is None: |
122 | | - first = thread |
123 | | - return first |
124 | | - |
125 | | - |
126 | | -class BacktraceReport: |
127 | | - def __init__(self): |
128 | | - self.fault_thread = threading.current_thread() |
129 | | - self.source_code = {} |
130 | | - self.source_path_dict = {} |
131 | | - entry_source_code_id = None |
132 | | - import __main__ |
133 | | - |
134 | | - cwd_path = os.path.abspath(os.getcwd()) |
135 | | - entry_thread = get_main_thread() |
136 | | - if hasattr(__main__, "__file__"): |
137 | | - entry_source_code_id = ( |
138 | | - add_source_code( |
139 | | - __main__.__file__, self.source_code, self.source_path_dict, 1 |
140 | | - ) |
141 | | - if hasattr(__main__, "__file__") |
142 | | - else None |
143 | | - ) |
144 | | - |
145 | | - init_attrs = {"error.type": "Exception"} |
146 | | - init_attrs.update(attribute_manager.get()) |
147 | | - |
148 | | - self.log_lines = [] |
149 | | - |
150 | | - self.report = { |
151 | | - "uuid": str(uuid.uuid4()), |
152 | | - "timestamp": int(time.time()), |
153 | | - "lang": "python", |
154 | | - "langVersion": get_python_version(), |
155 | | - "agent": "backtrace-python", |
156 | | - "agentVersion": version_string, |
157 | | - "mainThread": str(self.fault_thread.ident), |
158 | | - "entryThread": str(entry_thread.ident), |
159 | | - "cwd": cwd_path, |
160 | | - "attributes": init_attrs, |
161 | | - "annotations": { |
162 | | - "Environment Variables": dict(os.environ), |
163 | | - }, |
164 | | - } |
165 | | - if entry_source_code_id is not None: |
166 | | - self.report["entrySourceCode"] = entry_source_code_id |
167 | | - |
168 | | - def set_exception(self, garbage, ex_value, ex_traceback): |
169 | | - self.report["classifiers"] = [ex_value.__class__.__name__] |
170 | | - self.report["attributes"]["error.message"] = str(ex_value) |
171 | | - |
172 | | - threads = {} |
173 | | - for thread in threading.enumerate(): |
174 | | - if thread.ident == self.fault_thread.ident: |
175 | | - threads[str(self.fault_thread.ident)] = { |
176 | | - "name": self.fault_thread.name, |
177 | | - "stack": [ |
178 | | - process_frame( |
179 | | - frame, line, self.source_code, self.source_path_dict |
180 | | - ) |
181 | | - for frame, line in walk_tb(ex_traceback) |
182 | | - ], |
183 | | - } |
184 | | - else: |
185 | | - threads[str(thread.ident)] = { |
186 | | - "name": thread.name, |
187 | | - } |
188 | | - |
189 | | - self.report["threads"] = threads |
190 | | - |
191 | | - def capture_last_exception(self): |
192 | | - self.set_exception(*sys.exc_info()) |
193 | | - |
194 | | - def set_attribute(self, key, value): |
195 | | - self.report["attributes"][key] = value |
196 | | - |
197 | | - def set_dict_attributes(self, target_dict): |
198 | | - self.report["attributes"].update(target_dict) |
199 | | - |
200 | | - def set_annotation(self, key, value): |
201 | | - self.report["annotations"][key] = value |
202 | | - |
203 | | - def get_attributes(self): |
204 | | - return self.report["attributes"] |
205 | | - |
206 | | - def set_dict_annotations(self, target_dict): |
207 | | - self.report["annotations"].update(target_dict) |
208 | | - |
209 | | - def log(self, line): |
210 | | - self.log_lines.append( |
211 | | - { |
212 | | - "ts": time.time(), |
213 | | - "msg": line, |
214 | | - } |
215 | | - ) |
216 | | - |
217 | | - def send(self): |
218 | | - if len(self.log_lines) != 0 and "Log" not in self.report["annotations"]: |
219 | | - self.report["annotations"]["Log"] = self.log_lines |
220 | | - send_worker_msg( |
221 | | - { |
222 | | - "id": "send", |
223 | | - "report": self.report, |
224 | | - "context_line_count": globs.context_line_count, |
225 | | - "timeout": globs.timeout, |
226 | | - "endpoint": globs.endpoint, |
227 | | - "tab_width": globs.tab_width, |
228 | | - "debug_backtrace": globs.debug_backtrace, |
229 | | - "source_code": self.source_code, |
230 | | - } |
231 | | - ) |
232 | | - |
233 | | - |
234 | | -def create_and_send_report(ex_type, ex_value, ex_traceback): |
235 | | - report = BacktraceReport() |
236 | | - report.set_exception(ex_type, ex_value, ex_traceback) |
237 | | - report.set_attribute("error.type", "Unhandled exception") |
238 | | - report.send() |
239 | | - |
240 | | - |
241 | | -def bt_except_hook(ex_type, ex_value, ex_traceback): |
242 | | - if globs.debug_backtrace: |
243 | | - # Go back to normal exceptions while we do our work here. |
244 | | - sys.excepthook = globs.next_except_hook |
245 | | - |
246 | | - # Now if this fails we'll get a normal exception. |
247 | | - create_and_send_report(ex_type, ex_value, ex_traceback) |
248 | | - |
249 | | - # Put our exception handler back in place, and then also |
250 | | - # pass the exception down the chain. |
251 | | - sys.excepthook = bt_except_hook |
252 | | - else: |
253 | | - # Failure here is silent. |
254 | | - try: |
255 | | - create_and_send_report(ex_type, ex_value, ex_traceback) |
256 | | - except: |
257 | | - pass |
258 | | - |
259 | | - # Send the exception on to the next thing in the chain. |
260 | | - globs.next_except_hook(ex_type, ex_value, ex_traceback) |
261 | | - |
262 | | - |
263 | | -def initialize(**kwargs): |
264 | | - globs.endpoint = construct_submission_url( |
265 | | - kwargs["endpoint"], kwargs.get("token", None) |
266 | | - ) |
267 | | - globs.debug_backtrace = kwargs.get("debug_backtrace", False) |
268 | | - globs.timeout = kwargs.get("timeout", 4) |
269 | | - globs.tab_width = kwargs.get("tab_width", 8) |
270 | | - globs.context_line_count = kwargs.get("context_line_count", 200) |
271 | | - |
272 | | - attribute_manager.add(kwargs.get("attributes", {})) |
273 | | - stdio_value = None if globs.debug_backtrace else subprocess.PIPE |
274 | | - globs.worker = subprocess.Popen( |
275 | | - [sys.executable, child_py_path], |
276 | | - stdin=subprocess.PIPE, |
277 | | - stdout=stdio_value, |
278 | | - stderr=stdio_value, |
279 | | - ) |
280 | | - |
281 | | - disable_global_handler = kwargs.get("disable_global_handler", False) |
282 | | - if not disable_global_handler: |
283 | | - globs.next_except_hook = sys.excepthook |
284 | | - sys.excepthook = bt_except_hook |
285 | | - |
286 | | - |
287 | | -def construct_submission_url(endpoint, token): |
288 | | - if "submit.backtrace.io" in endpoint or token is None: |
289 | | - return endpoint |
290 | | - |
291 | | - return "{}/post?{}".format( |
292 | | - endpoint, |
293 | | - urlencode( |
294 | | - { |
295 | | - "token": token, |
296 | | - "format": "json", |
297 | | - } |
298 | | - ), |
299 | | - ) |
300 | | - |
301 | | - |
302 | | -def finalize(): |
303 | | - send_worker_msg({"id": "terminate"}) |
304 | | - if not globs.debug_backtrace: |
305 | | - globs.worker.stdout.close() |
306 | | - globs.worker.stderr.close() |
307 | | - globs.worker.wait() |
308 | | - |
309 | | - |
310 | | -def send_last_exception(**kwargs): |
311 | | - report = BacktraceReport() |
312 | | - report.capture_last_exception() |
313 | | - report.set_dict_attributes(kwargs.get("attributes", {})) |
314 | | - report.set_dict_annotations(kwargs.get("annotations", {})) |
315 | | - report.set_attribute("error.type", "Exception") |
316 | | - report.send() |
317 | | - |
318 | | - |
319 | | -def make_an_exception(): |
320 | | - try: |
321 | | - raise Exception |
322 | | - except: |
323 | | - return sys.exc_info() |
324 | | - |
325 | | - |
326 | | -def send_report(msg, **kwargs): |
327 | | - report = BacktraceReport() |
328 | | - report.set_exception(*make_an_exception()) |
329 | | - report.set_dict_attributes(kwargs.get("attributes", {})) |
330 | | - report.set_dict_annotations(kwargs.get("annotations", {})) |
331 | | - report.set_attribute("error.message", msg) |
332 | | - report.set_attribute("error.type", "Message") |
333 | | - report.send() |
0 commit comments