Leveled log-to-channel discord module, written for d.py
Go to file
disqualifier 7a5684e065 add package: pyproject + src
DPYLogger: leveled discord channel logger (debug/info/success/fail/task/
critical) over discord.py. config-free — embed identity injected at
construction, per-guild channel routing read live from bot.settings.
dual-sink: mirrors every call to the stdlib logger. embed_builder
callable + build_embed override for customization. raises by design;
object-only (no module proxy). src/ layout, hatchling build.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-24 21:36:43 -04:00
src/dpy_logger add package: pyproject + src 2026-06-24 21:36:43 -04:00
.gitignore init: leveled discord logger 2026-06-24 21:24:39 -04:00
pyproject.toml add package: pyproject + src 2026-06-24 21:36:43 -04:00
README.md init: leveled discord logger 2026-06-24 21:24:39 -04:00

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 ValueError at initialize/call time — a misconfigured logger should fail loudly at setup. Per-call send failures do not propagate: they fall back to the stdlib logger so a transient Discord failure 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.