Files
StarPilot/openpilot/system/sentry.py

158 lines
5.9 KiB
Python

"""Exception reporting through the packaged Sentry SDK."""
import datetime
import glob
import importlib
import os
import sys
import traceback
from enum import Enum
from pathlib import Path
from types import ModuleType
from openpilot.common.basedir import BASEDIR
from openpilot.common.hardware import HARDWARE, PC
from openpilot.common.hardware.hw import Paths
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.version import get_build_metadata
from openpilot.system.crash_storage import writer, write_text
class SentryProject(Enum):
SELFDRIVE = 'https://7305139359a548fcb348ec09497dc389@bugsink.firestar.link/1'
SELFDRIVE_NATIVE = SELFDRIVE
EXIT_FLUSH_TIMEOUT = 0.5
HTTP_TIMEOUT = 2
TRANSPORT_QUEUE_SIZE = 32
sdk: ModuleType | None = None
_initialized_pid: int | None = None
def _load_sdk() -> ModuleType:
# Device prebuilt launches do not install pyproject dependencies. This complete
# MIT package is tracked and included by the ordinary Git release packager.
vendor = str(Path(BASEDIR) / "sentry_sdk_repo")
if vendor not in sys.path:
sys.path.insert(0, vendor)
return importlib.import_module("sentry_sdk")
def init(project: SentryProject = SentryProject.SELFDRIVE) -> bool:
global sdk, _initialized_pid
if PC:
return False
fresh = _initialized_pid != os.getpid()
try:
if fresh:
sdk = _load_sdk()
transport_module = importlib.import_module("sentry_sdk.transport")
threading_module = importlib.import_module("sentry_sdk.integrations.threading")
class BoundedHttpTransport(transport_module.HttpTransport):
TIMEOUT = HTTP_TIMEOUT
# Explicit capture and the threading integration own errors. Enabling the
# logging integration would report cloudlog.exception a second time.
sdk.init(dsn=project.value, default_integrations=False, auto_enabling_integrations=False,
integrations=[threading_module.ThreadingIntegration(propagate_scope=True)],
transport=BoundedHttpTransport, transport_queue_size=TRANSPORT_QUEUE_SIZE,
shutdown_timeout=EXIT_FLUSH_TIMEOUT, enable_backpressure_handling=False, auto_session_tracking=False,
max_value_length=8192, traces_sample_rate=0.0,
release=os.getenv("GIT_COMMIT", "unknown"), environment="Release")
_initialized_pid = os.getpid()
if sdk is None:
return False
except Exception:
cloudlog.warning("Crash reporting unavailable; continuing without remote reporting")
sdk = None
_initialized_pid = None
return False
# Configure transport before reading metadata: a failed manager_init must be
# reportable even before registration/logmessaged. Refresh after registration
# and after fork, using the existing typed Params and current build metadata.
try:
build = get_build_metadata()
params = Params()
sdk.get_client().options.update(release=build.canonical,
environment="Testing" if "test" in build.channel.lower() else "Release")
sdk.set_user({"id": params.get("DongleId") or os.getenv("DONGLE_ID", "unregistered")})
for key, value in {"origin": build.openpilot.git_origin, "branch": build.channel,
"commit": build.openpilot.git_commit, "dirty": build.openpilot.is_dirty,
"installed": params.get("InstallDate"), "updated": params.get("LastUpdateTime"),
"device": HARDWARE.get_device_type()}.items():
sdk.set_tag(key, str(value) if value is not None else "unknown")
except Exception:
cloudlog.warning("Crash reporting metadata unavailable")
# Forked daemons must not close and upload an inherited manager session.
# Its deferred 60-second flush synchronizes background TLS work across them.
# Explicit exceptions retain the bounded transport and threading integration.
return True
def set_tag(key: str, value: str) -> None:
if sdk is not None:
try:
sdk.set_tag(key, value)
except Exception:
cloudlog.warning("Crash reporting tag unavailable")
def _attach_qlog(scope) -> None:
try:
qlogs = [path for name in ("qlog.zst", "qlog.bz2", "qlog")
for path in glob.glob(f"{Paths.log_root()}/*/{name}")]
if qlogs:
scope.add_attachment(path=max(qlogs, key=os.path.getmtime))
except Exception:
cloudlog.warning("Crash log attachment unavailable")
def save_exception(text: str) -> None:
path = Path(Paths.log_root()) / "crash"
stamp = datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S-%f")
with writer(path) as directory:
write_text(directory, f"{stamp}.log", text)
write_text(directory, "error.txt", "\n".join(text.splitlines()[-10:]))
def capture_exception(exc: BaseException | None = None, *, crash_log: bool = True) -> None:
text = "".join(traceback.format_exception(exc)) if exc is not None else traceback.format_exc()
if any(message in text for message in ("already exists. To overwrite it, set 'overwrite' to True", "failed after retry")):
return
if crash_log:
try:
save_exception(text)
except Exception:
cloudlog.warning("Local crash report could not be saved")
if sdk is not None:
try:
with sdk.new_scope() as scope:
_attach_qlog(scope)
sdk.capture_exception(exc)
except Exception:
cloudlog.warning("Crash report could not be queued")
def report_tombstone(fn: str, message: str, contents: str) -> None:
if sdk is not None:
try:
with sdk.new_scope() as scope:
scope.set_extra("tombstone_fn", fn)
scope.set_extra("tombstone", contents)
_attach_qlog(scope)
sdk.capture_message(message, level="error")
except Exception:
cloudlog.warning("Native crash report could not be queued")
def flush() -> None:
"""Only call on fatal process exit; ordinary captures never wait for HTTP."""
if sdk is not None:
try:
sdk.flush(timeout=EXIT_FLUSH_TIMEOUT)
except Exception:
cloudlog.warning("Crash report flush interrupted")