🔐 Logging & Privacy¶
This page documents Magikal's logging and privacy rules.
The goal is simple:
- logs must be useful for debugging
- logs must not expose private user data
- logs must not contain raw Discord IDs, mentions, tokens, or secrets
- user-related records should use privacy-safe PIDs
Standard logger¶
Use structlog.
Common pattern:
import structlog
logger = structlog.get_logger(__name__)
Then log with stable event names:
logger.info(event="tickets.open.created", result="ok")
Event names¶
Use stable, searchable event names.
Good:
logger.info(event="tickets.open.created")
logger.warning(event="tickets.record.update_failed", error=str(e))
logger.info(event="tempvc.owner_transferred")
logger.warning(event="modlog.channel_missing")
Avoid vague logs:
logger.info("done")
logger.warning("failed")
logger.error("something broke")
Good event names make logs searchable with grep.
Example:
sudo journalctl -u magikal-bot -n 300 --no-pager | grep "tickets."
Key/value logging¶
Use structured key/value fields.
Good:
logger.info(
event="ticket.closed",
result="ok",
ticket_type="recruitment",
)
Avoid string-building logs that include live objects or IDs:
logger.info(f"Closed ticket for user {user.id}")
Privacy-safe PIDs¶
Use make_pid for user-related log fields and database records.
Helper:
from bot.utils.hasher import make_pid
Example:
user_pid = make_pid(guild.id, member.id)
actor_pid = make_pid(guild.id, interaction.user.id)
Common field names:
| Field | Meaning |
|---|---|
guild_pid |
Privacy-safe guild identifier |
user_pid |
Privacy-safe target user identifier |
actor_pid |
Privacy-safe staff/action user identifier |
channel_pid |
Privacy-safe channel identifier |
member_pid |
Privacy-safe member identifier |
What not to log¶
Do not log:
- raw Discord user IDs
- raw Discord guild IDs
- raw Discord channel IDs
- Discord mentions
- Discord objects
- bot tokens
- API keys
- database URLs
- OAuth secrets
- session cookies
- private message contents
- recruitment answers unless intentionally stored in approved records
- full tracebacks in user-facing messages
Discord objects¶
Never log full Discord objects.
Avoid:
logger.info(event="thing", user=member)
logger.info(event="thing", channel=channel)
logger.info(event="thing", guild=guild)
Use safe fields instead:
logger.info(
event="thing",
user_pid=make_pid(guild.id, member.id),
channel_pid=make_pid(guild.id, channel.id),
guild_pid=make_pid(guild.id, guild.id),
)
Error logging¶
Keep errors useful but short.
Good:
logger.warning(event="tickets.safe_edit_failed", error=str(e))
Better if context is needed:
logger.warning(
event="tickets.safe_edit_failed",
error=str(e),
result="ignored_after_channel_delete",
)
Avoid dumping huge objects or raw request payloads.
Expected Discord errors¶
Expected Discord errors are not always fatal
Some Discord errors are expected in normal flows.
Example:
- ticket finalisation may delete a channel
- a later edit may fail with
Unknown Channel - that should be logged as a handled warning or ignored after delete
Do not treat every Discord HTTP error as a fatal bot error.
Document expected errors in the relevant feature page.
User-facing errors¶
User-facing messages should be simple.
Good:
Something went wrong while saving that setting. Please try again or check the logs.
Bad:
Full Python traceback...
database connection string...
raw exception with private details...
Log buckets and modlogs¶
Bot logs and Discord modlogs are different things.
System logs are for debugging the bot.
Discord modlogs are for server staff visibility.
System logs must stay privacy-safe.
Discord modlog embeds may show normal Discord-facing context where appropriate, because they are posted inside the server for staff/admin use.
Existing legacy logs¶
Some older live files still contain older patterns, including raw guild_id or channel_id fields.
Do not copy those patterns into new code.
When editing older code, improve logging to privacy-safe fields if:
- it is safe
- it is in scope
- it does not risk breaking the feature
Good log examples¶
Ticket action:
logger.info(
event="tickets.recruit.approved",
guild_pid=make_pid(guild.id, guild.id),
user_pid=make_pid(guild.id, applicant.id),
actor_pid=make_pid(guild.id, staff.id),
)
Temp VC owner transfer:
logger.info(
event="tempvc.owner_transferred",
guild_pid=make_pid(guild.id, guild.id),
channel_pid=make_pid(guild.id, channel.id),
old_owner_pid=old_owner_pid,
new_owner_pid=make_pid(guild.id, new_owner.id),
)
Moderation action:
logger.info(
event="modaction.persisted",
action="timeout",
pid=make_pid(guild.id, member.id),
actor_pid=make_pid(guild.id, moderator.id),
)
Bad log examples¶
Do not do this:
logger.info(f"User {member.id} was timed out by {ctx.author.id}")
Do not do this:
logger.info(event="ticket", user=interaction.user)
Do not do this:
logger.error(event="config.failed", database_url=config.database_url)
Do not do this:
logger.warning(event="oauth.failed", token=access_token)
Searching logs¶
Recent bot logs:
sudo journalctl -u magikal-bot -n 120 --no-pager
Follow live logs:
sudo journalctl -u magikal-bot -f
Search by event prefix:
sudo journalctl -u magikal-bot -n 300 --no-pager | grep "tickets."
Search for errors:
sudo journalctl -u magikal-bot -n 300 --no-pager | grep -i error
Search for tracebacks:
sudo journalctl -u magikal-bot -n 300 --no-pager | grep -i traceback
Sharing logs safely¶
Before pasting logs into chat:
- Check for tokens.
- Check for database URLs.
- Check for OAuth secrets.
- Check for raw Discord IDs.
- Check for private text content.
- Prefer the smallest useful log section.
Useful standard paste for bot issues:
sudo systemctl status magikal-bot --no-pager
sudo journalctl -u magikal-bot -n 120 --no-pager
New feature checklist¶
When adding a new cog, command, or panel-backed feature:
- add a stable event name for important actions
- log success/failure where useful
- avoid raw Discord IDs
- use
make_pidfor user/member/staff references - avoid logging secrets
- keep error strings short
- add feature-specific troubleshooting notes to docs if needed
Rule¶
Logs should help fix problems without exposing people.