|
|
||
|---|---|---|
| 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.2
Direct:
pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.2"
Requires discord.py (pulled transitively).
Drop the @v0.1.2 suffix from the line above to install the latest unpinned.
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, a non-text channel) raise
ValueError from initialize() — a misconfigured logger should fail loudly at setup.
Underlying discord exceptions (NotFound / Forbidden / HTTPException) from
fetch_guild/fetch_channel are normalized to that ValueError so callers see one
error type. 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 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
Releases are tagged vX.Y.Z. The install line above pins a release; drop the @vX.Y.Z suffix to install the latest unpinned. Pin deliberately for reproducible installs.