Skip to content

Commit d014ca4

Browse files
authored
Merge pull request #4172 from OpenShot/integrate-sentry-tracing
Initial integration of Sentry (crash monitoring)
2 parents 0b7a32f + 8ed9511 commit d014ca4

File tree

11 files changed

+140
-95
lines changed

11 files changed

+140
-95
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
sudo apt install libopenshot-audio-dev libopenshot-dev python3-openshot
2121
sudo apt install qttranslations5-l10n libssl-dev xvfb
2222
sudo apt install python3-pyqt5 python3-pyqt5.qtsvg python3-pyqt5.qtwebengine python3-pyqt5.qtopengl python3-zmq python3-xdg
23-
pip3 install setuptools wheel
23+
pip3 install setuptools wheel sentry-sdk
2424
pip3 install cx_Freeze==6.1 distro defusedxml requests certifi chardet urllib3
2525
2626
- name: Build Python package

freeze.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"time",
8181
"uuid",
8282
"idna",
83+
"sentry_sdk",
8384
"shutil",
8485
"threading",
8586
"subprocess",
@@ -95,7 +96,16 @@
9596
]
9697

9798
# Modules to include
98-
python_modules = ["idna.idnadata"]
99+
python_modules = ["idna.idnadata",
100+
"sentry_sdk.integrations.stdlib",
101+
"sentry_sdk.integrations.excepthook",
102+
"sentry_sdk.integrations.dedupe",
103+
"sentry_sdk.integrations.atexit",
104+
"sentry_sdk.integrations.modules",
105+
"sentry_sdk.integrations.argv",
106+
"sentry_sdk.integrations.logging",
107+
"sentry_sdk.integrations.threading",
108+
]
99109

100110
# Determine absolute PATH of OpenShot folder
101111
PATH = os.path.dirname(os.path.realpath(__file__)) # Primary openshot folder
@@ -427,7 +437,14 @@ def find_files(directory, patterns):
427437
build_exe_options["packages"] = python_packages
428438
build_exe_options["include_files"] = src_files + external_so_files
429439
build_exe_options["includes"] = python_modules
430-
build_exe_options["excludes"] = ["distutils", "numpy", "setuptools", "tkinter", "pydoc_data", "pycparser", "pkg_resources"]
440+
build_exe_options["excludes"] = ["distutils",
441+
"sentry_sdk.integrations.django",
442+
"numpy",
443+
"setuptools",
444+
"tkinter",
445+
"pydoc_data",
446+
"pycparser",
447+
"pkg_resources"]
431448

432449
# Set options
433450
build_options["build_exe"] = build_exe_options

src/classes/app.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import traceback
3535
import json
3636

37+
from classes import exceptions
3738
from PyQt5.QtCore import PYQT_VERSION_STR
3839
from PyQt5.QtCore import QT_VERSION_STR
3940
from PyQt5.QtCore import pyqtSlot
@@ -136,6 +137,10 @@ def __init__(self, *args, mode=None):
136137
# Set location of OpenShot program (for libopenshot)
137138
openshot.Settings.Instance().PATH_OPENSHOT_INSTALL = info.PATH
138139

140+
# Check to disable sentry
141+
if not self.settings.get('send_metrics'):
142+
exceptions.disable_sentry_tracing()
143+
139144
def show_environment(self, info, openshot):
140145
log = self.log
141146
try:
@@ -163,10 +168,6 @@ def show_environment(self, info, openshot):
163168
except Exception:
164169
log.debug("Error displaying dependency/system details", exc_info=1)
165170

166-
# Init and attach exception handler
167-
from classes import exceptions
168-
sys.excepthook = exceptions.ExceptionHandler
169-
170171
def check_libopenshot_version(self, info, openshot):
171172
"""Detect minimum libopenshot version"""
172173
_ = self._tr

src/classes/exceptions.py

+38-18
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,40 @@
2828
import os
2929
import traceback
3030
import platform
31+
import distro
32+
import sentry_sdk
3133

3234
from classes import info
33-
from classes.logger import log
3435

3536

36-
def ExceptionHandler(exeception_type, exeception_value, exeception_traceback):
37-
"""Callback for any unhandled exceptions"""
38-
from classes.metrics import track_exception_stacktrace
37+
def init_sentry_tracing():
38+
"""Init all Sentry tracing"""
3939

40-
log.error(
41-
'Unhandled Exception',
42-
exc_info=(exeception_type, exeception_value, exeception_traceback))
40+
# Determine sample rate for exceptions
41+
traces_sample_rate = 0.1
42+
environment = "production"
43+
if "-dev" in info.VERSION:
44+
# Dev mode, trace all exceptions
45+
traces_sample_rate = 1.0
46+
environment = "development"
4347

44-
# Build string of stack trace
45-
stacktrace = "Python %s" % "".join(
46-
traceback.format_exception(
47-
exeception_type, exeception_value, exeception_traceback))
48+
# Initialize sentry exception tracing
49+
sentry_sdk.init(
50+
"https://[email protected]/5795985",
51+
traces_sample_rate=traces_sample_rate,
52+
release=f"openshot@{info.VERSION}",
53+
environment=environment
54+
)
55+
sentry_sdk.set_tag("system", platform.system())
56+
sentry_sdk.set_tag("machine", platform.machine())
57+
sentry_sdk.set_tag("processor", platform.processor())
58+
sentry_sdk.set_tag("platform", platform.platform())
59+
sentry_sdk.set_tag("distro", " ".join(distro.linux_distribution()))
4860

49-
# Report traceback to webservice (if enabled)
50-
track_exception_stacktrace(stacktrace, "openshot-qt")
61+
62+
def disable_sentry_tracing():
63+
"""Disable all Sentry tracing requests"""
64+
sentry_sdk.init()
5165

5266

5367
def tail_file(f, n, offset=None):
@@ -72,7 +86,7 @@ def tail_file(f, n, offset=None):
7286

7387
def libopenshot_crash_recovery():
7488
"""Walk libopenshot.log for the last line before this launch"""
75-
from classes.metrics import track_exception_stacktrace, track_metric_error
89+
from classes.metrics import track_metric_error
7690

7791
log_path = os.path.join(info.USER_PATH, "libopenshot.log")
7892
last_log_line = ""
@@ -84,7 +98,8 @@ def libopenshot_crash_recovery():
8498
with open(log_path, "rb") as f:
8599
# Read from bottom up
86100
for raw_line in reversed(tail_file(f, 500)):
87-
line = str(raw_line, 'utf-8')
101+
# Format and remove extra spaces from line
102+
line = " ".join(str(raw_line, 'utf-8').split()) + "\n"
88103
# Detect stack trace
89104
if "End of Stack Trace" in line:
90105
found_stack = True
@@ -113,10 +128,13 @@ def libopenshot_crash_recovery():
113128
# Split last stack trace (if any)
114129
if last_stack_trace:
115130
# Get top line of stack trace (for metrics)
116-
last_log_line = last_stack_trace.split("\n")[0].strip()
131+
exception_lines = last_stack_trace.split("\n")
132+
last_log_line = exception_lines[0].strip()
117133

118-
# Send stacktrace for debugging (if send metrics is enabled)
119-
track_exception_stacktrace(last_stack_trace, "libopenshot")
134+
# Format and add exception log to Sentry context
135+
# Split list of lines into smaller lists (so we don't exceed Sentry limits)
136+
sentry_sdk.set_context("libopenshot", {"stack-trace": exception_lines})
137+
sentry_sdk.set_tag("component", "libopenshot")
120138

121139
# Clear / normalize log line (so we can roll them up in the analytics)
122140
if last_log_line:
@@ -142,3 +160,5 @@ def libopenshot_crash_recovery():
142160

143161
# Report exception (with last libopenshot line... if found)
144162
track_metric_error("unhandled-crash%s" % last_log_line, True)
163+
164+
return last_log_line

src/classes/info.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import os
2929
from time import strftime
3030

31-
VERSION = "2.5.1-dev2"
31+
VERSION = "2.5.1-dev3"
3232
MINIMUM_LIBOPENSHOT_VERSION = "0.2.5"
3333
DATE = "20200228000000"
3434
NAME = "openshot-qt"

src/classes/language.py

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import os
3030
import locale
31+
import sentry_sdk
3132

3233
from PyQt5.QtCore import QLocale, QLibraryInfo, QTranslator, QCoreApplication
3334

@@ -119,6 +120,7 @@ def init_language():
119120
if found_language:
120121
log.debug("Exiting translation system (since we successfully loaded: {})".format(locale_name))
121122
info.CURRENT_LANGUAGE = locale_name
123+
sentry_sdk.set_tag("locale", locale_name)
122124
break
123125

124126

src/classes/logger.py

+56-38
Original file line numberDiff line numberDiff line change
@@ -32,73 +32,91 @@
3232

3333
from classes import info
3434

35-
# Dictionary of logging handlers we create, keyed by type
36-
handlers = {}
37-
# Another dictionary of streams we've redirected (stdout, stderr)
38-
streams = {}
39-
4035

4136
class StreamToLogger(object):
4237
"""Custom class to log all stdout and stderr streams (from libopenshot / and other libraries)"""
43-
def __init__(self, logger, log_level=logging.INFO):
44-
self.logger = logger
38+
def __init__(self, parent_stream, log_level=logging.INFO):
39+
self.parent = parent_stream or sys.__stderr__
40+
self.logger = logging.LoggerAdapter(
41+
logging.getLogger('OpenShot.stderr'), {'source': 'stream'})
4542
self.log_level = log_level
46-
self.linebuf = ''
43+
self.logbuf = ''
4744

48-
def write(self, buf):
49-
for line in buf.rstrip().splitlines():
50-
self.logger.log(self.log_level, line.rstrip())
45+
def write(self, text):
46+
self.logbuf += str(text) or ""
47+
self.parent.write(text)
5148

5249
def flush(self):
53-
pass
50+
if self.logbuf.rstrip():
51+
self.logger.log(self.log_level, self.logbuf.rstrip())
52+
self.logbuf = ''
5453

5554
def errors(self):
5655
pass
5756

5857

59-
# Create logger instance
60-
log = logging.Logger('OpenShot')
58+
class StreamFilter(logging.Filter):
59+
"""Filter out lines that originated on the output"""
60+
def filter(self, record):
61+
source = getattr(record, "source", "")
62+
return bool(source != "stream")
63+
6164

6265
# Set up log formatters
6366
template = '%(levelname)s %(module)s: %(message)s'
6467
console_formatter = logging.Formatter(template)
6568
file_formatter = logging.Formatter('%(asctime)s ' + template, datefmt='%H:%M:%S')
6669

67-
# Add normal stderr stream handler
68-
sh = logging.StreamHandler()
69-
sh.setFormatter(console_formatter)
70-
sh.setLevel(info.LOG_LEVEL_CONSOLE)
71-
log.addHandler(sh)
72-
handlers['stream'] = sh
70+
# Configure root logger for minimal logging
71+
logging.basicConfig(level=logging.ERROR)
72+
root_log = logging.getLogger()
73+
74+
# Set up our top-level logging context
75+
log = root_log.getChild('OpenShot')
76+
log.setLevel(info.LOG_LEVEL_FILE)
77+
# Don't pass messages on to root logger
78+
log.propagate = False
7379

74-
# Add rotating file handler
80+
#
81+
# Create rotating file handler
82+
#
7583
fh = logging.handlers.RotatingFileHandler(
76-
os.path.join(info.USER_PATH, 'openshot-qt.log'), encoding="utf-8", maxBytes=25*1024*1024, backupCount=3)
77-
fh.setFormatter(file_formatter)
84+
os.path.join(info.USER_PATH, 'openshot-qt.log'),
85+
encoding="utf-8",
86+
maxBytes=25*1024*1024, backupCount=3)
7887
fh.setLevel(info.LOG_LEVEL_FILE)
88+
fh.setFormatter(file_formatter)
89+
7990
log.addHandler(fh)
80-
handlers['file'] = fh
91+
92+
#
93+
# Create typical stream handler which logs to stderr
94+
#
95+
sh = logging.StreamHandler(sys.stderr)
96+
sh.setLevel(info.LOG_LEVEL_CONSOLE)
97+
sh.setFormatter(console_formatter)
98+
99+
# Filter out redirected output on console, to avoid duplicates
100+
filt = StreamFilter()
101+
sh.addFilter(filt)
102+
103+
log.addHandler(sh)
81104

82105

83106
def reroute_output():
84107
"""Route stdout and stderr to logger (custom handler)"""
85-
if not getattr(sys, 'frozen', False):
86-
# Hang on to the original objects
87-
streams.update({
88-
'stderr': sys.stderr,
89-
'stdout': sys.stdout,
90-
})
91-
# Re-route output streams
92-
handlers['stdout'] = StreamToLogger(log, logging.INFO)
93-
sys.stdout = handlers['stdout']
94-
handlers['stderr'] = StreamToLogger(log, logging.ERROR)
95-
sys.stderr = handlers['stderr']
108+
if (getattr(sys, 'frozen', False)
109+
or sys.stdout != sys.__stdout__):
110+
return
111+
sys.stdout = StreamToLogger(sys.stdout, logging.INFO)
112+
sys.stderr = StreamToLogger(sys.stderr, logging.WARNING)
96113

97114

98115
def set_level_file(level=logging.INFO):
99-
handlers['file'].setLevel(level)
116+
"""Adjust the minimum log level written to our logfile"""
117+
fh.setLevel(level)
100118

101119

102120
def set_level_console(level=logging.INFO):
103-
handlers['stream'].setLevel(level)
104-
121+
"""Adjust the minimum log level for output to the terminal"""
122+
sh.setLevel(level)

src/classes/metrics.py

-27
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,6 @@ def track_metric_error(error_name, is_fatal=False):
133133
t.start()
134134

135135

136-
def track_exception_stacktrace(stacktrace, source):
137-
"""Track an exception/stacktrace has occurred"""
138-
t = threading.Thread(target=send_exception, args=[stacktrace, source])
139-
t.daemon = True
140-
t.start()
141-
142-
143136
def track_metric_session(is_start=True):
144137
"""Track a GUI screen being shown"""
145138
metric_params = deepcopy(params)
@@ -183,23 +176,3 @@ def send_metric(params):
183176
# All metrics have been sent (or attempted to send)
184177
# Clear the queue
185178
metric_queue.clear()
186-
187-
188-
def send_exception(stacktrace, source):
189-
"""Send exception stacktrace over HTTP for tracking"""
190-
# Check if the user wants to send metrics and errors
191-
if s.get("send_metrics"):
192-
193-
data = urllib.parse.urlencode({"stacktrace": stacktrace,
194-
"platform": platform.system(),
195-
"version": info.VERSION,
196-
"source": source,
197-
"unique_install_id": s.get("unique_install_id")})
198-
url = "http://www.openshot.org/exception/json/"
199-
200-
# Send exception HTTP data
201-
try:
202-
r = requests.post(url, data=data, headers={"user-agent": user_agent, "content-type": "application/x-www-form-urlencoded"}, verify=False)
203-
log.info("Track exception: [%s] %s | %s", r.status_code, r.url, r.text)
204-
except Exception:
205-
log.warning("Failed to track exception", exc_info=1)

src/launch.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,14 @@
7070
pass # Quietly fail for older Qt5 versions
7171

7272
try:
73-
from classes import info
73+
from classes import info, exceptions
7474
except ImportError:
7575
import openshot_qt
7676
sys.path.append(openshot_qt.OPENSHOT_PATH)
77-
from classes import info
77+
from classes import info, exceptions
78+
79+
# Initialize sentry exception tracing
80+
exceptions.init_sentry_tracing()
7881

7982
# Global holder for QApplication instance
8083
app = None

0 commit comments

Comments
 (0)