This logger uses async operations to record events without impacting game performance.
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")
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
-- 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)
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
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)
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)
-- 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)
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 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)