Skip to content
RustPluginGenerator

How to make a Rust chat logger plugin (rolling log + search)

Records every chat message with timestamp, SteamID, name, channel and text into a capped oxide/data file, and adds a /chatlog command that prints a player's recent messages. Uses the OnPlayerChat hook with BasePlayer, string and Chat.ChatChannel.

Chat in Rust is not persisted anywhere. If a player reports harassment or an admin wants to check what someone said in team or global chat last night, the server console scrollback is often gone by then. This plugin keeps a rolling history instead of an unbounded log: every message is appended with a UTC timestamp, the sender's SteamID and display name, the chat channel and the text, and the oldest entries are trimmed so the file never grows past a configurable cap.

The hook used is OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel). Messages are buffered in memory and written on a timer plus a forced save on unload, so a busy server is not hitting the data file on every line. /chatlog <player> (name or SteamID) prints a player's most recent messages to the admin's console with index and channel. The config controls the maximum number of stored messages, how many /chatlog shows by default, the timestamp format and the save interval.

The plugin code

ChatLogger.cs
using System.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("ChatLogger", "RustPluginGenerator", "1.0.0")]
    [Description("Stores a rolling log of chat messages and lets admins read a player's recent messages.")]
    public class ChatLogger : RustPlugin
    {
        private const string PermUse = "chatlogger.use";

        private class PluginConfig
        {
            [JsonProperty("Maximum stored messages")]
            public int MaxStoredMessages = 3000;

            [JsonProperty("Messages shown per query")]
            public int MessagesShown = 50;

            [JsonProperty("Max message length")]
            public int MaxMessageLength = 500;

            [JsonProperty("Timestamp format")]
            public string TimestampFormat = "yyyy-MM-dd HH:mm:ss";

            [JsonProperty("Save interval seconds")]
            public float SaveIntervalSeconds = 20f;
        }

        private class ChatRecord
        {
            public string Time;
            public string SteamId;
            public string Name;
            public string Channel;
            public string Text;
        }

        private class LogData
        {
            public List<ChatRecord> Records = new List<ChatRecord>();
        }

        private PluginConfig config;
        private LogData data;
        private Timer saveTimer;
        private bool dirty;

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["NoPermission"] = "You do not have permission to use this command.",
                ["Usage"] = "Usage: /chatlog <player name or SteamID> [count]",
                ["NoMessages"] = "No stored chat messages for {0}.",
                ["Header"] = "Recent messages for {0} ({1} found):",
                ["Line"] = "{0}. [{1}] [{2}] {3}",
                ["BadCount"] = "Count must be a number between 1 and 200."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
        }

        private void OnServerInitialized()
        {
            try
            {
                data = Interface.Oxide.DataFileSystem.ReadObject<LogData>(Name);
            }
            catch
            {
                data = null;
            }
            if (data == null || data.Records == null) data = new LogData();
            if (data.Records.Count > config.MaxStoredMessages) TrimToLimit();
            saveTimer = timer.Every(config.SaveIntervalSeconds, () =>
            {
                if (dirty) SaveLog();
            });
        }

        private void Unload()
        {
            if (saveTimer != null) saveTimer.Destroy();
            SaveLog();
        }

        // Capture every chat message; return null so chat behavior is unchanged.
        private object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
        {
            if (player == null || string.IsNullOrEmpty(message)) return null;
            if (data == null) return null;

            string text = message.Length > config.MaxMessageLength
                ? message.Substring(0, config.MaxMessageLength) + "..."
                : message;

            data.Records.Add(new ChatRecord
            {
                Time = DateTime.UtcNow.ToString(config.TimestampFormat, CultureInfo.InvariantCulture),
                SteamId = player.userID.Get().ToString(),
                Name = player.displayName ?? "unknown",
                Channel = channel.ToString(),
                Text = text
            });

            TrimToLimit();
            dirty = true;
            return null;
        }

        private void TrimToLimit()
        {
            int limit = Mathf.Max(1, config.MaxStoredMessages);
            while (data.Records.Count > limit) data.Records.RemoveAt(0);
        }

        private void SaveLog()
        {
            if (data == null) return;
            Interface.Oxide.DataFileSystem.WriteObject(Name, data);
            dirty = false;
        }

        [ChatCommand("chatlog")]
        private void CmdChatLog(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length == 0)
            {
                SendReply(player, Msg("Usage", player.UserIDString));
                return;
            }

            int count = config.MessagesShown;
            int queryLength = args.Length;
            if (args.Length > 1 && int.TryParse(args[args.Length - 1], out int requested))
            {
                if (requested < 1 || requested > 200)
                {
                    SendReply(player, Msg("BadCount", player.UserIDString));
                    return;
                }
                count = requested;
                queryLength--;
            }

            string query = string.Join(" ", args, 0, queryLength);
            if (string.IsNullOrEmpty(query))
            {
                SendReply(player, Msg("Usage", player.UserIDString));
                return;
            }

            bool byId = query.Length == 17 && ulong.TryParse(query, out _);
            var matches = new List<ChatRecord>();
            foreach (var record in data.Records)
            {
                if (record == null) continue;
                if (byId)
                {
                    if (record.SteamId == query) matches.Add(record);
                }
                else if (string.Equals(record.Name, query, StringComparison.OrdinalIgnoreCase))
                {
                    matches.Add(record);
                }
            }

            if (matches.Count == 0)
            {
                SendReply(player, Msg("NoMessages", player.UserIDString, query));
                return;
            }

            int start = Mathf.Max(0, matches.Count - count);
            SendReply(player, Msg("Header", player.UserIDString, query, matches.Count));
            for (int i = start; i < matches.Count; i++)
            {
                var record = matches[i];
                SendReply(player, Msg("Line", player.UserIDString, i + 1, record.Time, record.Channel, record.Text));
            }
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerChat

Config file

Created automatically on first load at oxide/config/ChatLogger.json.

ChatLogger.json
{
  "Maximum stored messages": 3000,
  "Messages shown per query": 50,
  "Max message length": 500,
  "Timestamp format": "yyyy-MM-dd HH:mm:ss",
  "Save interval seconds": 20
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/chatlog <player> [count]Prints the most recent chat messages of a player (matched by display name or SteamID) to the caller's console.chatlogger.use
Permissions
PermissionGrants
chatlogger.useAllows using /chatlog to read stored chat messages.

How the code works

  1. Capture messages in OnPlayerChat

    The hook signature is object OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel). Null-check player, ignore empty text, and build a record from DateTime.UtcNow, player.userID.Get().ToString(), player.displayName, the channel enum and the message. Return null so the chat itself is untouched by the plugin. Because this hook fires on every line typed, nothing is printed here; all output happens from the command.

  2. Rolling buffer and trimming

    Records are kept in a List<ChatRecord> field and also persisted through Interface.Oxide.DataFileSystem.WriteObject. After each append, a while loop removes entries from the front until config.MaxStoredMessages is respected, so the data file only ever holds the newest N lines. Set Maximum stored messages low (2000-5000) if your server is very busy, since the whole list is serialized on each save.

  3. Batched saving with a timer

    A Timer created in OnServerInitialized calls SaveLog() every Save interval seconds. A dirty flag avoids rewriting the file when no new message arrived. Unload calls SaveLog() once more and destroys the timer, so nothing is lost on a reload. Data is loaded in OnServerInitialized with Interface.Oxide.DataFileSystem.ReadObject<LogData>(Name), and a null result falls back to an empty list.

  4. Configuration

    The nested PluginConfig class has [JsonProperty] fields for Maximum stored messages, Messages shown per query, Timestamp format and Save interval seconds. LoadConfig reads the file, falls back to LoadDefaultConfig on failure and always calls SaveConfig, which writes the object back so admins see every key with its current value.

  5. Reading a player's history

    [ChatCommand("chatlog")] checks permission.UserHasPermission(player.UserIDString, PermUse), then joins the arguments. It compares each record's Name case-insensitively and its SteamId for a match. The first argument may be a full SteamID (17 digits); otherwise the joined text is treated as a name. Entries are filtered, the last count are printed with SendReply, and the channel is included as text.

  6. Output format

    Each printed line uses the index in the stored list and the pre-formatted Time string, which the hook fills with DateTime.UtcNow.ToString(config.TimestampFormat). Keep the format parseable, like yyyy-MM-dd HH:mm:ss, so other tools can read the file. Messages are truncated at Max message length characters before storage so a single pasted wall of text cannot bloat the record.

Install it on your server

  1. Save ChatLogger.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/ChatLogger.json (Carbon: carbon/configs/ChatLogger.json). After editing, reload it:oxide.reload ChatLogger c.reload ChatLogger # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default chatlogger.use c.grant group default chatlogger.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

FAQ

Does the plugin block or modify chat?

No. OnPlayerChat returns null, so the message is delivered exactly as typed. The plugin only copies the text into the data file. Filtering or muting belongs in a separate plugin that returns a value or non-null result from the hook.

Where is the log stored and how big does it get?

In oxide/data/ChatLogger.json. Its size is bounded by Maximum stored messages in the config; the oldest lines are dropped as new ones arrive. A few thousand messages is a small file, but keep the cap modest on very busy servers because the whole list is rewritten on save.

Why do timestamps show UTC?

The record is written with DateTime.UtcNow, so the log is stable regardless of server time zone. Change Timestamp format in the config if you want a different layout, and convert to local time yourself when reading the file.

How do I install it on Oxide or Carbon?

Drop ChatLogger.cs into oxide/plugins, then run oxide.reload ChatLogger from the server console. Carbon loads the same file. Grant chatlogger.use to your admin group with oxide.grant group admin chatlogger.use.

Can I search by SteamID?

Yes. If the first argument is a 17-digit SteamID, /chatlog 7656119... matches records by ID. Otherwise the joined arguments are treated as a display name, matched case-insensitively.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create an Oxide Rust chat logger plugin that stores the last 3000 chat messages (time, SteamID, name, channel, text) in oxide/data and adds /chatlog <player> [count] for admins.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize chat logger

Customize