_get_channel compared guild == self.guild; when initialize() was not called, self.guild stays an int while the passed guild is a Guild object, so the comparison was always False and routing silently fell through to the bot.settings lookup instead of using the constructed channel. now compares by .id on both sides. Signed-off-by: disqualifier <dev@disqualifier.me> |
||
|---|---|---|
| src/dpy_logger | ||
| .gitignore | ||
| pyproject.toml | ||
| README.md | ||
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; passlog_to_file=True/Falseon 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 defaultspings(list of user ids) — mentioned oncritical()timezone,footer,avatar— embed identityalert_here(bool) — if nopingsare set,critical()falls back to@hereonly when this isTrue; otherwise it sends no mentionembed_builder(callable, optional) — restyle embeds without subclassinglog_to_file(bool, defaultTrue) — 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.