dpy_logger/README.md
disqualifier f480a7c077 docs: correct README error contract (initialize-time, raw discord exceptions)
the README Errors section claimed resolution raises ValueError 'at call time', but per-call resolution is swallowed with send failures and the raise happens at initialize() — and the raised type can be an underlying discord exception (NotFound/Forbidden/HTTPException), not only ValueError. corrected to match the code and the module docstring.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 01:39:21 -04:00

5.0 KiB

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.0

Direct:

pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.0"

Requires discord.py (pulled transitively).

Usage

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.

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:

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/:

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.