diff --git a/README.md b/README.md index 1efbd25..3766a66 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,18 @@ live from `bot.settings` so it can change at runtime via a command. `requirements.txt`: ``` -dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.3 +dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.4 ``` Direct: ```bash -pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.3" +pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.4" ``` Requires `discord.py` (pulled transitively). -Drop the `@v0.1.3` suffix from the line above to install the latest unpinned. +Drop the `@v0.1.4` suffix from the line above to install the latest unpinned. ## Usage diff --git a/pyproject.toml b/pyproject.toml index afdc534..e1ea0a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "dpy_logger" -version = "0.1.3" +version = "0.1.4" description = "Leveled Discord channel logger for discord.py — config-free, injectable, installable." requires-python = ">=3.10" dependencies = [ diff --git a/src/dpy_logger/dpy_logger.py b/src/dpy_logger/dpy_logger.py index b121142..815963e 100644 --- a/src/dpy_logger/dpy_logger.py +++ b/src/dpy_logger/dpy_logger.py @@ -179,14 +179,16 @@ class DPYLogger: if self._embed_builder is not None: return self._embed_builder(self, level, action, actor, details) em = discord.Embed(color=self.colors[level]) - if action: + # test `is not None` so a falsy-but-valid value (0, False) still renders; only a + # genuinely absent field (None) or an empty string is dropped/substituted below + if action is not None and str(action) != "": em.add_field(name="Action", value=f"`{action}`", inline=True) - if actor: + if actor is not None and str(actor) != "": em.add_field(name="Actor", value=f"`{actor}`", inline=True) # discord rejects an empty field value (50035) and truncates nothing itself, so # an empty or >1024-char message would 400 the send; substitute + cap to keep # every logging call producing a valid embed - log_value = str(details) if details else "(no message)" + log_value = str(details) if details is not None and str(details) != "" else "(no message)" if len(log_value) > 1024: log_value = log_value[:1021] + "..." em.add_field(name="Log", value=log_value, inline=False) @@ -200,20 +202,28 @@ class DPYLogger: def _emit_stdlib(self, level, action, actor, log_msg): """mirror the log line to the stdlib logger (routed to file/console by the app)""" - parts = [p for p in (action, str(actor) if actor else None, log_msg) if p] + # render falsy-but-valid parts (0, False); drop only genuinely-absent (None) ones + raw = (action, str(actor) if actor is not None else None, log_msg) + parts = [str(p) for p in raw if p is not None and str(p) != ""] _log.log(LEVEL_MAP.get(level, logging.INFO), f"[{level}] " + " | ".join(parts)) - async def _send(self, level, log_msg, action=None, actor=None, guild=None, log_to_file=None): + async def _send(self, level, log_msg, action=None, actor=None, guild=None, + log_to_file=None, content=None): """resolve channel and dispatch a leveled embed; mirror to stdlib unless opted out stdlib emit happens first so the record survives even if the discord send fails. a failed send falls back to stdlib rather than propagating into the caller. + `content` carries the critical-level ping/@here text (None for the other levels), + so every level shares this one send/error contract. """ if self.log_to_file if log_to_file is None else log_to_file: self._emit_stdlib(level, action, actor, log_msg) try: channel = await self._resolve(guild) - return await channel.send(embed=self.build_embed(level, action, actor, log_msg)) + return await channel.send( + content=content, + embed=self.build_embed(level, action, actor, log_msg), + ) except Exception: _log.exception(f"[dpy_logger] failed to send {level} log: {log_msg}") return None @@ -245,21 +255,15 @@ class DPYLogger: return await self._send("task", log, action, "SYSTEM/TASK", guild, log_to_file) async def critical(self, log, action=None, actor=None, guild=None, log_to_file=None): - """log a critical-level message and ping configured devs""" - if self.log_to_file if log_to_file is None else log_to_file: - self._emit_stdlib("critical", action, actor, log) + """log a critical-level message and ping configured devs + + delegates to _send with the ping/@here content so the send + error contract has a + single source of truth; critical only adds the alert `content`. + """ if self.pings: content = "alert: " + " ".join(f"<@{u}>" for u in self.pings) elif self.alert_here: content = "alert: @here" else: content = None - try: - channel = await self._resolve(guild) - return await channel.send( - content=content, - embed=self.build_embed("critical", action, actor, log), - ) - except Exception: - _log.exception(f"[dpy_logger] failed to send critical log: {log}") - return None + return await self._send("critical", log, action, actor, guild, log_to_file, content)