diff --git a/src/room_control/button.py b/src/room_control/button.py index df8c5ec..00839a2 100644 --- a/src/room_control/button.py +++ b/src/room_control/button.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, List from appdaemon.plugins.mqtt.mqttapi import Mqtt from . import console +from .loki import LokiHandler from .model import ButtonConfig if TYPE_CHECKING: @@ -21,13 +22,22 @@ class Button(Mqtt): async def initialize(self): self.app: 'RoomController' = await self.get_app(self.args['app']) - self.logger = console.load_rich_config(self.app.name, type(self).__name__) + self.configure_logging() self.config = ButtonConfig(**self.args) self.log(f'Connected to AD app [room]{self.app.name}[/]', level='DEBUG') self.button = self.config.button self.setup_buttons(self.button) + def configure_logging(self): + self.logger = console.load_rich_config(room=self.app.name, component=type(self).__name__) + if url := self.args.get('loki_url'): + logger: Logger = self.logger.logger + handler = LokiHandler(loop=self.AD.loop, loki_url=url) + handler.addFilter(console.UnMarkupFilter()) + logger.addHandler(handler) + self.log('Added LokiHandler') + def setup_buttons(self, buttons): if isinstance(buttons, list): for button in buttons: diff --git a/src/room_control/door.py b/src/room_control/door.py index 6b15f81..81d5fc7 100644 --- a/src/room_control/door.py +++ b/src/room_control/door.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from appdaemon.plugins.hass.hassapi import Hass from . import console +from .loki import LokiHandler if TYPE_CHECKING: from room_control import RoomController @@ -15,7 +16,7 @@ class Door(Hass): async def initialize(self): self.app: 'RoomController' = await self.get_app(self.args['app']) - self.logger = console.load_rich_config(room=self.app.name, component=type(self).__name__) + self.configure_logging() self.log(f'Connected to AD app [room]{self.app.name}[/]', level='DEBUG') await self.listen_state( @@ -24,3 +25,12 @@ class Door(Hass): new='on', cause='door open', ) + + def configure_logging(self): + self.logger = console.load_rich_config(room=self.app.name, component=type(self).__name__) + if url := self.args.get('loki_url'): + logger: Logger = self.logger.logger + handler = LokiHandler(loop=self.AD.loop, loki_url=url) + handler.addFilter(console.UnMarkupFilter()) + logger.addHandler(handler) + self.log('Added LokiHandler') diff --git a/src/room_control/loki.py b/src/room_control/loki.py new file mode 100644 index 0000000..af9ca95 --- /dev/null +++ b/src/room_control/loki.py @@ -0,0 +1,40 @@ +import asyncio +from logging import Handler, LogRecord + +import aiohttp + + +class LokiHandler(Handler): + loop: asyncio.BaseEventLoop + loki_url: str + level: int | str = 0 + + def __init__(self, loop: asyncio.BaseEventLoop, loki_url: str, level: int | str = 0) -> None: + self.loop: asyncio.BaseEventLoop = loop + self.loki_url: str = loki_url + super().__init__(level) + + def emit(self, record: LogRecord) -> None: + self.loop.create_task(self.send_to_loki(record)) + + async def send_to_loki(self, record: LogRecord): + message = self.format(record) + ns = round(record.created * 1_000_000_000) + + labels = {'level': record.levelname, 'room': record.room} + + if comp := getattr(record, 'component', None): + labels['component'] = comp + + # https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs + payload = { + 'streams': [ + { + 'stream': labels, + 'values': [[str(ns), message]], + } + ] + } + + async with aiohttp.ClientSession() as session: + await session.post(self.loki_url, json=payload, timeout=3) diff --git a/src/room_control/motion.py b/src/room_control/motion.py index 19bdedf..5101709 100644 --- a/src/room_control/motion.py +++ b/src/room_control/motion.py @@ -8,6 +8,7 @@ from appdaemon.plugins.hass.hassapi import Hass from pydantic import BaseModel, ValidationError from . import console +from .loki import LokiHandler if TYPE_CHECKING: from room_control import RoomController @@ -53,7 +54,7 @@ class Motion(Hass): def initialize(self): self.app: 'RoomController' = self.get_app(self.args['app']) - self.logger = console.load_rich_config(self.app.name, type(self).__name__) + self.configure_logging() assert self.entity_exists(self.args['sensor']) assert self.entity_exists(self.args['ref_entity']) @@ -84,6 +85,16 @@ class Motion(Hass): self.log(f'Initialized [bold green]{type(self).__name__}[/]') + def configure_logging(self): + self.logger = console.load_rich_config(self.app.name, type(self).__name__) + + if url := self.args.get('loki_url'): + logger: Logger = self.logger.logger + handler = LokiHandler(loop=self.AD.loop, loki_url=url) + handler.addFilter(console.UnMarkupFilter()) + logger.addHandler(handler) + self.log('Added LokiHandler') + def callbacks(self): """Returns a dictionary of validated CallbackEntry objects that are associated with this app""" self_callbacks = self.get_callback_entries().get(self.name, {}) diff --git a/src/room_control/room_control.py b/src/room_control/room_control.py index df14104..5410b72 100755 --- a/src/room_control/room_control.py +++ b/src/room_control/room_control.py @@ -10,6 +10,7 @@ from appdaemon.plugins.hass.hassapi import Hass from appdaemon.plugins.mqtt.mqttapi import Mqtt from . import console +from .loki import LokiHandler from .model import ControllerStateConfig, RoomControllerConfig logger = logging.getLogger(__name__) @@ -35,7 +36,7 @@ class RoomController(Hass, Mqtt): self._room_config.states = new def initialize(self): - self.logger = console.load_rich_config(self.name) + self.configure_logging() self.app_entities = self.gather_app_entities() # self.log(f'entities: {self.app_entities}') self.refresh_state_times() @@ -45,6 +46,16 @@ class RoomController(Hass, Mqtt): def terminate(self): self.log('[bold red]Terminating[/]', level='DEBUG') + def configure_logging(self): + self.logger = console.load_rich_config(self.name) + + if url := self.args.get('loki_url'): + logger: logging.Logger = self.logger.logger + handler = LokiHandler(loop=self.AD.loop, loki_url=url) + handler.addFilter(console.UnMarkupFilter()) + logger.addHandler(handler) + self.log('Added LokiHandler') + def gather_app_entities(self) -> List[str]: """Returns a list of all the entities involved in any of the states"""