143 lines
5.1 KiB
Markdown
143 lines
5.1 KiB
Markdown
# 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
|
|
```
|
|
|
|
Direct:
|
|
|
|
```bash
|
|
pip install "dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git"
|
|
```
|
|
|
|
Requires `discord.py` (pulled transitively).
|
|
|
|
## Usage
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
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/`:
|
|
|
|
```python
|
|
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 is unpinned and tracks the latest on the default branch; append `@vX.Y.Z` to pin a specific release for reproducible installs.
|