A discord log bot is one of the most valuable tools you can add to a server: it quietly records what happens and gives your moderation team a solid audit trail. Who deleted which message, who left the server, when someone was granted which role — seeing all of this in a permanent channel moves arguments from "I didn't do it" to evidence-based moderation. In this guide we will build a practical bot with discord.js v14 that logs message deletions, member joins and leaves, and role changes.
What does a discord log bot do?
Discord's built-in audit log only keeps certain administrative actions for a limited time and never shows message content. When you write your own log bot, you decide what to record, how, and where. Typical use cases include:
- Message auditing: preserving the content of deleted and edited messages.
- Member activity: tracking joins and leaves together with account age (valuable for raid detection).
- Permission tracking: seeing role grants and removals, and who performed them.
We will send all of these events to a single #logs channel as readable embeds.
Required intents and partial configuration
Logging needs privileged intents. GuildMembers is required for member events and MessageContent for reading message content. You must also enable these two in your application settings on the Discord Developer Portal. To catch events for older messages that are not in the cache, you need to declare partials:
// index.js
const { Client, GatewayIntentBits, Partials } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Message, Partials.Channel, Partials.GuildMember],
});
const LOG_CHANNEL_ID = '123456789012345678';
client.login(process.env.TOKEN);
To avoid repetition, let's define a small helper we will reuse for every event:
const { EmbedBuilder } = require('discord.js');
function log(guild, embed) {
const channel = guild.channels.cache.get(LOG_CHANNEL_ID);
if (channel) channel.send({ embeds: [embed] }).catch(() => {});
}
Logging message deletions and edits
We catch deletions with the Events.MessageDelete event. There is an important limitation: if a message sent before the bot started — and not in the cache — gets deleted, the event arrives only as a partial and you cannot recover its content, because the message no longer exists on Discord. So we skip partial messages gracefully:
const { Events } = require('discord.js');
client.on(Events.MessageDelete, async (message) => {
if (message.partial) return; // content unavailable if not cached
if (message.author?.bot) return;
log(message.guild, new EmbedBuilder()
.setColor(0xED4245)
.setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
.setDescription(
`🗑️ **Message deleted** — <#${message.channel.id}>\n` +
(message.content || '*No content*'),
)
.setTimestamp());
});
We listen for edits with Events.MessageUpdate. This event also fires when a link preview (embed) loads on a message, so to avoid noise we bail out early when the content has not actually changed:
client.on(Events.MessageUpdate, async (oldMsg, newMsg) => {
if (oldMsg.partial || newMsg.author?.bot) return;
if (oldMsg.content === newMsg.content) return; // only an embed loaded
log(newMsg.guild, new EmbedBuilder()
.setColor(0xFEE75C)
.setAuthor({ name: newMsg.author.tag, iconURL: newMsg.author.displayAvatarURL() })
.setDescription(`✏️ **Message edited** — [Jump to message](${newMsg.url})`)
.addFields(
{ name: 'Before', value: (oldMsg.content || '—').slice(0, 1024) },
{ name: 'After', value: (newMsg.content || '—').slice(0, 1024) },
)
.setTimestamp());
});
Because an embed field can hold at most 1024 characters, we trim with slice(0, 1024); otherwise long messages would make the send fail.
Logging member joins and leaves
Joins arrive via Events.GuildMemberAdd and leaves via Events.GuildMemberRemove. Showing when a newcomer's account was created as a relative timestamp with the <t:...:R> format makes it easy to spot a raid wave of brand-new accounts:
client.on(Events.GuildMemberAdd, (member) => {
const created = Math.floor(member.user.createdTimestamp / 1000);
log(member.guild, new EmbedBuilder()
.setColor(0x57F287)
.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() })
.setDescription(`📥 **${member} joined the server**`)
.addFields({ name: 'Account age', value: `<t:${created}:R>` })
.setTimestamp());
});
client.on(Events.GuildMemberRemove, (member) => {
log(member.guild, new EmbedBuilder()
.setColor(0xED4245)
.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() })
.setDescription(`📤 **${member.user.tag} left the server**`)
.setTimestamp());
});
Logging role changes
Role changes are not a separate event; they are part of the member update (Events.GuildMemberUpdate). To find the added and removed roles, we compare the old and new member's role collections:
client.on(Events.GuildMemberUpdate, (oldMember, newMember) => {
const oldRoles = oldMember.roles.cache;
const newRoles = newMember.roles.cache;
const added = newRoles.filter((r) => !oldRoles.has(r.id));
const removed = oldRoles.filter((r) => !newRoles.has(r.id));
if (added.size === 0 && removed.size === 0) return; // e.g. only nickname changed
const lines = [];
if (added.size) lines.push(`➕ Added: ${added.map((r) => r).join(', ')}`);
if (removed.size) lines.push(`➖ Removed: ${removed.map((r) => r).join(', ')}`);
log(newMember.guild, new EmbedBuilder()
.setColor(0x5865F2)
.setAuthor({ name: newMember.user.tag, iconURL: newMember.user.displayAvatarURL() })
.setDescription(`🎭 **Role change** — ${newMember}\n${lines.join('\n')}`)
.setTimestamp());
});
If you also want to know who performed the action, you can query the audit log. For this the bot needs the ViewAuditLog permission:
const { AuditLogEvent } = require('discord.js');
const audit = await newMember.guild.fetchAuditLogs({
type: AuditLogEvent.MemberRoleUpdate, limit: 1,
});
const entry = audit.entries.first();
const executor = entry?.target.id === newMember.id ? entry.executor : null;
Frequently Asked Questions
Why is the content of some deleted messages empty?
Because that message was not in the bot's cache. discord.js only caches messages it sees while running; if an older message sent before the bot started gets deleted, its content cannot be recovered. This is a limitation of the Discord API, not bad code. If you want fuller history, you need to store messages in your own database.
Can I log events that happen while the bot is offline?
No. Gateway events are only streamed live while the bot is connected; anything that happens during downtime is missed. That's why it is important to keep the log bot running 24/7 with a tool like PM2 and to catch error events.
Can I split different event types into separate channels?
Yes. Instead of a single LOG_CHANNEL_ID, define different channel IDs per event type and pass the target channel as a parameter to the log() function; this lets you keep message logs separate from member logs.
Want a reliable logging setup for your server? I can design and build a Discord bot tailored end to end to your moderation needs. Get in touch to talk about your project.