Leveled log-to-channel discord module, written for d.py
Go to file
disqualifier 48de7d7065 init: leveled discord logger
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-23 15:12:03 -04:00
.gitignore init: leveled discord logger 2026-06-23 15:12:03 -04:00
README.md init: leveled discord logger 2026-06-23 15:12:03 -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

Usage

from dpy_logger import DPYLogger

bot.log = DPYLogger(
    bot, guild_id, channel_id,
    colors=cfg.log_colors,        # optional; merged over sensible defaults
    pings=cfg.authorized_devs,    # mentioned on critical()
    timezone=cfg.timezone,
    footer=cfg.bot_footer,
    avatar=cfg.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.

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

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.