diff --git a/API.md b/API.md index ac00b9cd4..316311af8 100644 --- a/API.md +++ b/API.md @@ -47,6 +47,7 @@ The addons from `addons` are only installed one. "wait_boot": "int", "debug": "bool", "debug_block": "bool", + "diagnostics": "None|bool", "addons": [ { "name": "xy bla", diff --git a/requirements.txt b/requirements.txt index e8d204079..0e5b15dfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,5 +14,6 @@ pulsectl==20.5.1 pytz==2020.1 pyudev==0.22.0 ruamel.yaml==0.15.100 +sentry-sdk==0.16.3 uvloop==0.14.0 voluptuous==0.11.7 diff --git a/supervisor/api/supervisor.py b/supervisor/api/supervisor.py index 85356ad3c..68d2cca78 100644 --- a/supervisor/api/supervisor.py +++ b/supervisor/api/supervisor.py @@ -17,6 +17,7 @@ from ..const import ( ATTR_DEBUG, ATTR_DEBUG_BLOCK, ATTR_DESCRIPTON, + ATTR_DIAGNOSTICS, ATTR_ICON, ATTR_INSTALLED, ATTR_IP_ADDRESS, @@ -58,6 +59,7 @@ SCHEMA_OPTIONS = vol.Schema( vol.Optional(ATTR_LOGGING): vol.Coerce(LogLevel), vol.Optional(ATTR_DEBUG): vol.Boolean(), vol.Optional(ATTR_DEBUG_BLOCK): vol.Boolean(), + vol.Optional(ATTR_DIAGNOSTICS): vol.Boolean(), } ) @@ -102,6 +104,7 @@ class APISupervisor(CoreSysAttributes): ATTR_LOGGING: self.sys_config.logging, ATTR_DEBUG: self.sys_config.debug, ATTR_DEBUG_BLOCK: self.sys_config.debug_block, + ATTR_DIAGNOSTICS: self.sys_config.diagnostics, ATTR_ADDONS: list_addons, ATTR_ADDONS_REPOSITORIES: self.sys_config.addons_repositories, } @@ -126,6 +129,9 @@ class APISupervisor(CoreSysAttributes): if ATTR_DEBUG_BLOCK in body: self.sys_config.debug_block = body[ATTR_DEBUG_BLOCK] + if ATTR_DIAGNOSTICS in body: + self.sys_config.diagnostics = body[ATTR_DIAGNOSTICS] + if ATTR_LOGGING in body: self.sys_config.logging = body[ATTR_LOGGING] diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index 4b6b413ae..f16e0c0b7 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -6,6 +6,13 @@ import shutil import signal from colorlog import ColoredFormatter +import sentry_sdk +from sentry_sdk.integrations.aiohttp import AioHttpIntegration +from sentry_sdk.integrations.atexit import AtexitIntegration +from sentry_sdk.integrations.dedupe import DedupeIntegration +from sentry_sdk.integrations.excepthook import ExcepthookIntegration +from sentry_sdk.integrations.stdlib import StdlibIntegration +from sentry_sdk.integrations.threading import ThreadingIntegration from .addons import AddonManager from .api import RestAPI @@ -17,6 +24,8 @@ from .const import ( ENV_SUPERVISOR_NAME, ENV_SUPERVISOR_SHARE, SOCKET_DOCKER, + SUPERVISOR_VERSION, + CoreStates, LogLevel, UpdateChannels, ) @@ -73,6 +82,9 @@ async def initialize_coresys() -> CoreSys: coresys.secrets = SecretsManager(coresys) coresys.scheduler = Scheduler(coresys) + # diagnostics + setup_diagnostics(coresys) + # bootstrap config initialize_system_data(coresys) @@ -270,3 +282,54 @@ def supervisor_debugger(coresys: CoreSys) -> None: if coresys.config.debug_block: _LOGGER.info("Wait until debugger is attached") ptvsd.wait_for_attach() + + +def setup_diagnostics(coresys: CoreSys) -> None: + """Sentry diagnostic backend.""" + + def filter_data(event, hint): + # Ignore issue if system is not supported or diagnostics is disabled + if not coresys.config.diagnostics or not coresys.core.healthy: + return None + + # Not full startup - missing information + if coresys.core.state in (CoreStates.INITIALIZE, CoreStates.SETUP): + return event + + # Update information + with sentry_sdk.configure_scope() as scope: + scope.set_context( + "supervisor", + { + "machine": coresys.machine, + "arch": coresys.arch.default, + "docker": coresys.docker.info.version, + "channel": coresys.updater.channel, + "supervisor": coresys.supervisor.version, + "os": coresys.hassos.version, + "core": coresys.homeassistant.version, + "audio": coresys.plugins.audio.version, + "dns": coresys.plugins.dns.version, + "multicast": coresys.plugins.multicast.version, + "cli": coresys.plugins.cli.version, + }, + ) + + return event + + sentry_sdk.init( + dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612", + before_send=filter_data, + default_integrations=False, + integrations=[ + AioHttpIntegration(), + AtexitIntegration(), + ExcepthookIntegration(), + DedupeIntegration(), + StdlibIntegration(), + ThreadingIntegration(), + ], + ) + + with sentry_sdk.configure_scope() as scope: + scope.set_tag("version", SUPERVISOR_VERSION) diff --git a/supervisor/config.py b/supervisor/config.py index aa2b7f6c1..b040e0178 100644 --- a/supervisor/config.py +++ b/supervisor/config.py @@ -3,12 +3,13 @@ from datetime import datetime import logging import os from pathlib import Path, PurePath -from typing import List +from typing import List, Optional from .const import ( ATTR_ADDONS_CUSTOM_LIST, ATTR_DEBUG, ATTR_DEBUG_BLOCK, + ATTR_DIAGNOSTICS, ATTR_LAST_BOOT, ATTR_LOGGING, ATTR_TIMEZONE, @@ -101,6 +102,16 @@ class CoreConfig(JsonConfig): """Set debug wait mode.""" self._data[ATTR_DEBUG_BLOCK] = value + @property + def diagnostics(self) -> Optional[bool]: + """Return bool if diagnostics is set otherwise None.""" + return self._data[ATTR_DIAGNOSTICS] + + @diagnostics.setter + def diagnostics(self, value: bool) -> None: + """Set diagnostics settings.""" + self._data[ATTR_DIAGNOSTICS] = value + @property def logging(self) -> LogLevel: """Return log level of system.""" diff --git a/supervisor/const.py b/supervisor/const.py index f0e2c1b4e..fe74f6133 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -240,6 +240,7 @@ ATTR_INDEX = "index" ATTR_ACTIVE = "active" ATTR_APPLICATION = "application" ATTR_INIT = "init" +ATTR_DIAGNOSTICS = "diagnostics" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" @@ -355,6 +356,7 @@ class CoreStates(str, Enum): """Represent current loading state.""" INITIALIZE = "initialize" + SETUP = "setup" STARTUP = "startup" RUNNING = "running" FREEZE = "freeze" diff --git a/supervisor/core.py b/supervisor/core.py index 14dfb7dd0..19e21ad0f 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -55,7 +55,7 @@ class Core(CoreSysAttributes): async def setup(self): """Start setting up supervisor orchestration.""" - self.state = CoreStates.STARTUP + self.state = CoreStates.SETUP # Load DBus await self.sys_dbus.load() @@ -104,6 +104,7 @@ class Core(CoreSysAttributes): async def start(self): """Start Supervisor orchestration.""" + self.state = CoreStates.STARTUP await self.sys_api.start() # Mark booted partition as healthy diff --git a/supervisor/validate.py b/supervisor/validate.py index b5ca1250a..b8e6d1137 100644 --- a/supervisor/validate.py +++ b/supervisor/validate.py @@ -18,6 +18,7 @@ from .const import ( ATTR_CLI, ATTR_DEBUG, ATTR_DEBUG_BLOCK, + ATTR_DIAGNOSTICS, ATTR_DNS, ATTR_HASSOS, ATTR_HOMEASSISTANT, @@ -176,6 +177,7 @@ SCHEMA_SUPERVISOR_CONFIG = vol.Schema( vol.Optional(ATTR_LOGGING, default=LogLevel.INFO): vol.Coerce(LogLevel), vol.Optional(ATTR_DEBUG, default=False): vol.Boolean(), vol.Optional(ATTR_DEBUG_BLOCK, default=False): vol.Boolean(), + vol.Optional(ATTR_DIAGNOSTICS, default=None): vol.Maybe(vol.Boolean()), }, extra=vol.REMOVE_EXTRA, ) diff --git a/tests/conftest.py b/tests/conftest.py index c231ff8d2..f7896c7b4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,8 @@ def docker(): async def coresys(loop, docker): """Create a CoreSys Mock.""" with patch("supervisor.bootstrap.initialize_system_data"), patch( + "supervisor.bootstrap.setup_diagnostics" + ), patch( "supervisor.bootstrap.fetch_timezone", return_value="Europe/Zurich", ): coresys_obj = await initialize_coresys()