Examples

Event Logger

Track and analyze server events with async operations

This logger uses async operations to record events without impacting game performance.

Setup

local logsDB = db:Collection("event_logs")

-- Create indexes (sync is fine for initialization)
logsDB:CreateIndex({ timestamp = -1 }, false, "timestamp_desc")
logsDB:CreateIndex({ event_type = 1 }, false, "event_type")
logsDB:CreateIndex({ ["data.steamid"] = 1 }, false, "steamid")

Log Event

function LogEvent(eventType, data, callback)
    logsDB:InsertOneAsync({
        event_type = eventType,
        data = data,
        timestamp = os.time(),
        server_time = os.date("%Y-%m-%d %H:%M:%S"),
        map = game.GetMap()
    }, function(err, id)
        if err then
            ErrorNoHalt("Failed to log event '" .. eventType .. "': " .. err .. "\n")
            if callback then callback(false) end
        else
            if callback then callback(true, id) end
        end
    end)
end

Track Player Events

-- Player Join
hook.Add("PlayerInitialSpawn", "LogJoin", function(ply)
    LogEvent("player_join", {
        steamid = ply:SteamID(),
        username = ply:Nick(),
        ip = ply:IPAddress()
    })
    
    ply:ChatPrint("Welcome to the server!")
end)

-- Player Disconnect
hook.Add("PlayerDisconnected", "LogLeave", function(ply)
    LogEvent("player_leave", {
        steamid = ply:SteamID(),
        username = ply:Nick(),
        playtime = ply:GetNWInt("SessionPlaytime", 0)
    })
end)

-- Player Death
hook.Add("PlayerDeath", "LogDeath", function(victim, inflictor, attacker)
    local attackerSteamID = "world"
    local attackerName = "World"
    
    if IsValid(attacker) and attacker:IsPlayer() then
        attackerSteamID = attacker:SteamID()
        attackerName = attacker:Nick()
    end
    
    LogEvent("player_death", {
        victim_steamid = victim:SteamID(),
        victim_name = victim:Nick(),
        attacker_steamid = attackerSteamID,
        attacker_name = attackerName,
        weapon = IsValid(inflictor) and inflictor:GetClass() or "unknown",
        victim_pos = tostring(victim:GetPos())
    })
end)

-- Chat Messages
hook.Add("PlayerSay", "LogChat", function(ply, text, teamChat)
    if string.sub(text, 1, 1) == "!" or string.sub(text, 1, 1) == "/" then
        return -- Don't log commands
    end
    
    LogEvent("player_chat", {
        steamid = ply:SteamID(),
        username = ply:Nick(),
        message = text,
        team_chat = teamChat
    })
end)

Get Recent Events

function GetRecentEvents(eventType, limit, callback)
    local filter = {}
    if eventType then
        filter.event_type = eventType
    end
    
    logsDB:AggregateAsync({
        { ["$match"] = filter },
        { ["$sort"] = { timestamp = -1 } },
        { ["$limit"] = limit or 100 }
    }, function(err, results)
        if err then
            ErrorNoHalt("Failed to get events: " .. err .. "\n")
            callback({})
            return
        end
        
        callback(results or {})
    end)
end

View Logs Command

concommand.Add("viewlogs", function(ply, cmd, args)
    if IsValid(ply) and not ply:IsAdmin() then
        ply:ChatPrint("Admin only!")
        return
    end
    
    local eventType = args[1]
    local limit = tonumber(args[2]) or 20
    
    GetRecentEvents(eventType, limit, function(events)
        local output = IsValid(ply) and 
            function(msg) ply:ChatPrint(msg) end or 
            function(msg) print(msg) end
        
        output("=== Recent Events ===")
        
        if #events == 0 then
            output("No events found")
            return
        end
        
        for i, event in ipairs(events) do
            local time = os.date("%H:%M:%S", event.timestamp)
            output(string.format("[%s] %s", time, event.event_type))
            
            -- Print relevant data based on event type
            if event.event_type == "player_join" then
                output("  Player: " .. event.data.username)
            elseif event.event_type == "player_death" then
                output("  " .. event.data.victim_name .. " killed by " .. event.data.attacker_name)
            end
        end
    end)
end)

Get Player Activity

function GetPlayerActivity(steamid, callback)
    logsDB:AggregateAsync({
        {
            ["$match"] = {
                ["$or"] = {
                    { ["data.steamid"] = steamid },
                    { ["data.victim_steamid"] = steamid },
                    { ["data.attacker_steamid"] = steamid }
                }
            }
        },
        {
            ["$group"] = {
                _id = "$event_type",
                count = { ["$sum"] = 1 }
            }
        },
        {
            ["$sort"] = { count = -1 }
        }
    }, function(err, results)
        if err then
            ErrorNoHalt("Failed to get player activity: " .. err .. "\n")
            callback({})
            return
        end
        
        callback(results or {})
    end)
end

concommand.Add("playeractivity", function(ply, cmd, args)
    if IsValid(ply) and not ply:IsAdmin() then
        ply:ChatPrint("Admin only!")
        return
    end
    
    local targetSteamID = args[1] or (IsValid(ply) and ply:SteamID())
    
    if not targetSteamID then
        print("Usage: playeractivity <steamid>")
        return
    end
    
    GetPlayerActivity(targetSteamID, function(activity)
        local output = IsValid(ply) and 
            function(msg) ply:ChatPrint(msg) end or 
            function(msg) print(msg) end
        
        output("=== Player Activity for " .. targetSteamID .. " ===")
        
        for _, stat in ipairs(activity) do
            output(string.format("%s: %d events", stat._id, stat.count))
        end
    end)
end)

Cleanup Old Logs

-- Clean up logs older than 30 days, every 24 hours
timer.Create("CleanupLogs", 24 * 60 * 60, 0, function()
    local thirtyDaysAgo = os.time() - (30 * 24 * 60 * 60)
    
    logsDB:DeleteManyAsync({
        timestamp = { ["$lt"] = thirtyDaysAgo }
    }, function(err, deleted)
        if err then
            ErrorNoHalt("Failed to cleanup logs: " .. err .. "\n")
        else
            print("✓ Cleaned up " .. deleted .. " old log entries")
        end
    end)
end)

Get Server Statistics

function GetServerStats(callback)
    logsDB:AggregateAsync({
        {
            ["$group"] = {
                _id = "$event_type",
                count = { ["$sum"] = 1 }
            }
        },
        {
            ["$sort"] = { count = -1 }
        }
    }, function(err, results)
        if err then
            ErrorNoHalt("Failed to get stats: " .. err .. "\n")
            callback({})
            return
        end
        
        callback(results or {})
    end)
end

concommand.Add("serverstats", function(ply)
    if IsValid(ply) and not ply:IsAdmin() then
        ply:ChatPrint("Admin only!")
        return
    end
    
    GetServerStats(function(stats)
        local output = IsValid(ply) and 
            function(msg) ply:ChatPrint(msg) end or 
            function(msg) print(msg) end
        
        output("=== Server Statistics ===")
        
        local total = 0
        for _, stat in ipairs(stats) do
            output(string.format("%s: %d", stat._id, stat.count))
            total = total + stat.count
        end
        
        output("\nTotal Events: " .. total)
    end)
end)

Export Logs

-- Export recent logs to JSON file
concommand.Add("exportlogs", function(ply, cmd, args)
    if IsValid(ply) and not ply:IsAdmin() then
        ply:ChatPrint("Admin only!")
        return
    end
    
    local limit = tonumber(args[1]) or 1000
    
    GetRecentEvents(nil, limit, function(events)
        if #events == 0 then
            print("No events to export")
            return
        end
        
        local json = util.TableToJSON(events, true)
        local filename = "logs_" .. os.date("%Y%m%d_%H%M%S") .. ".json"
        local path = "data/" .. filename
        
        file.Write(path, json)
        
        print("✓ Exported " .. #events .. " events to " .. path)
        if IsValid(ply) then
            ply:ChatPrint("Logs exported to " .. filename)
        end
    end)
end)
  • Zero performance impact - logging never blocks gameplay
  • High-volume events - can log thousands of events per minute
  • Batch cleanup - removes old logs without server lag
  • Real-time analytics - async aggregation for instant stats