# dpy_logger Leveled Discord channel logger for discord.py. Logs to a channel via embeds at `debug` / `info` / `success` / `fail` / `task` / `critical` levels. Config-free: static identity is injected at construction; per-guild channel routing is read live from `bot.settings` so it can change at runtime via a command. ## Install `requirements.txt`: ``` dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.2 ``` Direct: ```bash pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.2" ``` Requires `discord.py` (pulled transitively). ## Usage ```python from dpy_logger import DPYLogger bot.log = DPYLogger( bot, guild_id, channel_id, colors=log_colors, # optional; merged over sensible defaults pings=authorized_devs, # mentioned on critical() timezone=tz, footer=bot_footer, avatar=bot_avatar, ) await bot.log.initialize() # resolves ids -> objects, call once await bot.log.info("started") await bot.log.success("user promoted", action="promote", actor=ctx.author) await bot.log.critical("db unreachable") # pings configured devs ``` ## Dynamic routing Pass `guild=` on any call to log to that guild's configured channel (`bot.settings[guild.id]['channels']['logs']`) instead of the construction channel. Because it reads `bot.settings` at call time, a command that updates that setting changes routing with no restart. ## Dual sink (Discord + file) Every call also mirrors to the stdlib logger (`getLogger(__name__)`), which your app routes to file/console via its root logging config. So one `bot.log.info(...)` writes to both the Discord channel and your log file. ```python bot.log = DPYLogger(bot, guild, channel, log_to_file=True) # default await bot.log.info("user joined") # -> Discord + file await bot.log.debug("noisy", log_to_file=False) # -> Discord only ``` - `log_to_file=` at construction sets the default; pass `log_to_file=True/False` on any call to override for that call. - The stdlib emit happens **before** the Discord send, so the record survives even if Discord fails. - Levels map to stdlib: `debug`→DEBUG, `info`/`success`/`task`→INFO, `fail`→ERROR, `critical`→CRITICAL. ## Errors Resolution failures (unresolvable guild/channel, bad config) raise from `initialize()` — a misconfigured logger should fail loudly at setup. The raised type is usually `ValueError`, but an underlying `discord` exception (`NotFound` / `Forbidden` / `HTTPException`) from `fetch_guild`/`fetch_channel` can also propagate. On a **per-call** send, neither resolution nor send failures propagate: they fall back to the stdlib logger so a transient Discord failure (or a per-call `guild=` that doesn't resolve) never breaks the caller's command. ## Construction contract The host injects everything; the lib never imports `config`: - `colors` (dict, optional) — per-level colors, merged over defaults - `pings` (list of user ids) — mentioned on `critical()` - `timezone`, `footer`, `avatar` — embed identity - `alert_here` (bool) — if no `pings` are set, `critical()` falls back to `@here` only when this is `True`; otherwise it sends no mention - `embed_builder` (callable, optional) — restyle embeds without subclassing - `log_to_file` (bool, default `True`) — mirror every call to the stdlib logger It also expects `bot.settings[guild.id]['channels']['logs']` to exist for the dynamic-routing path. A project with a different settings shape should override `_get_channel`. ## Customizing the embed Three ways, easiest first. **Pass a function** — no subclass. The callable receives the logger instance (so it can read `self.colors`, `self.footer`, etc.) plus the call args, and returns a `discord.Embed`. Used for every level including `critical`: ```python def my_embed(logger, level, action, actor, details): em = discord.Embed(description=details, color=logger.colors[level]) if level == "critical": em.set_thumbnail(url=logger.avatar) if actor: em.set_author(name=str(actor)) return em bot.log = DPYLogger(bot, guild, channel, embed_builder=my_embed) ``` **Override the method** — for complex/stateful customization, subclass and override `build_embed(level, action, actor, details)`. Same routing: every level flows through it. **Default** — pass neither and you get the standard fielded embed. ## Adding a log type To add new methods (e.g. a feed), subclass and reuse `_resolve`. This base has no feed by design; a project that wants one adds it in its own local `libs/`: ```python class FeedLogger(DPYLogger): """project-local: dpy_logger plus a feed-style announcement embed""" async def feed(self, log, title, icon=None, guild=None): channel = await self._resolve(guild) embed = discord.Embed( description=log, color=self.colors.get("feed", self.colors["info"]) ).set_author(name=title, icon_url=icon) return await channel.send(embed=embed) ``` ## Versioning Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.