eq2go/old/common/log.hpp
2025-08-06 19:00:30 -05:00

656 lines
18 KiB
C++

// Copyright (C) 2007 EQ2EMulator Development Team - GPL v3
#pragma once
#include <string>
#include <cstdint>
#include <cstdio>
#include <cstdarg>
#include <cstring>
#include <cerrno>
#include <ctime>
#include <cstdlib>
#include <sys/stat.h>
#include <thread>
#include <mutex>
#include <atomic>
#include <memory>
#include <unistd.h>
#include "types.hpp"
#include "xml_parser.hpp"
#include "../WorldServer/World.h"
#include "../WorldServer/client.h"
#include "../WorldServer/zoneserver.h"
extern ZoneList zone_list;
#define LOG_BUFFER_SIZE 4096
// Log output destinations
#define LOG_LOGFILE 1
#define LOG_CONSOLE 2
#define LOG_CLIENT 4
// Console color definitions using ANSI escape codes
#define FOREGROUND_WHITE 37
#define FOREGROUND_WHITE_BOLD 137
#define FOREGROUND_RED 31
#define FOREGROUND_RED_BOLD 131
#define FOREGROUND_GREEN 32
#define FOREGROUND_GREEN_BOLD 132
#define FOREGROUND_BLUE 34
#define FOREGROUND_BLUE_BOLD 134
#define FOREGROUND_YELLOW 33
#define FOREGROUND_YELLOW_BOLD 133
#define FOREGROUND_CYAN 36
#define FOREGROUND_CYAN_BOLD 136
#define FOREGROUND_MAGENTA 35
#define FOREGROUND_MAGENTA_BOLD 135
#define LOG_CATEGORY(category) LOG_ ##category ,
enum LogCategory
{
#include "log_types.hpp"
NUMBER_OF_LOG_CATEGORIES
};
#define LOG_TYPE(category, type, level, color, enabled, logfile, console, client, str) category##__##type ,
enum LogType
{
#include "log_types.hpp"
NUMBER_OF_LOG_TYPES
};
#define LOG_CATEGORY(category) #category,
const char *log_category_names[NUMBER_OF_LOG_CATEGORIES] = {
#include "log_types.hpp"
};
// Structure to hold log type configuration and status
struct LogTypeStatus
{
int8_t level; // Minimum log level for this type
int color; // Console color code
bool enabled; // Whether this log type is active
bool logfile; // Write to log file
bool console; // Write to console
bool client; // Send to connected clients
LogCategory category; // Category this log type belongs to
const char *name; // Internal name identifier
const char *display_name; // Human-readable display name
};
#define LOG_TYPE(category, type, level, color, enabled, logfile, console, client, str) { level, color, enabled, logfile, console, client, LOG_ ##category, #category "__" #type, ( strlen(str)>0 ) ? str : #category "__" #type },
static LogTypeStatus real_log_type_info[NUMBER_OF_LOG_TYPES+1] =
{
#include "log_types.hpp"
{ 0, 0, false, false, false, false, NUMBER_OF_LOG_CATEGORIES, "BAD TYPE", "Bad Name" } /* dummy trailing record */
};
LogTypeStatus *log_type_info = real_log_type_info;
// Logging system configuration constants
#define LOG_CYCLE 100 // milliseconds between each batch of log writes
#define LOGS_PER_CYCLE 50 // amount of logs to write per cycle
#define LOG_DIR "logs" // directory for log files
#define DATE_MAX 8 // maximum date string length
#define LOG_NAME_MAX 32 // maximum log name length
// Determine executable name for log file naming
#if defined LOGIN
#define EXE_NAME "login"
#elif defined WORLD
#define EXE_NAME "world"
#elif defined PARSER
#define EXE_NAME "parser"
#elif defined PATCHER
#define EXE_NAME "patcher"
#else
#define EXE_NAME "unknown"
#endif
// Structure for queued log entries in doubly-linked list
struct logq_t
{
LogType log_type; // Type of log entry
char date[DATE_MAX + 1]; // Timestamp string
char name[LOG_NAME_MAX + 1]; // Log category name
char *text; // Log message text
struct logq_t *next; // Next entry in queue
struct logq_t *prev; // Previous entry in queue
};
// Global logging system state
static logq_t head; // Head of log queue
static logq_t tail; // Tail of log queue
static std::atomic<int> num_logqs{0}; // Number of queued logs
static std::mutex log_mutex; // Mutex for log queue access
static std::atomic<bool> looping{false}; // Thread loop control
static std::atomic<bool> start_called{false}; // Initialization flag
extern const char* log_category_names[NUMBER_OF_LOG_CATEGORIES];
extern LogTypeStatus* log_type_info;
// Function declarations
void LogStart();
void LogStop();
int8_t GetLoggerLevel(LogType type);
void LogWrite(LogType type, int8_t log_level, const char *cat_text, const char *fmt, ...);
bool LogParseConfigs();
#ifdef PARSER
void ColorizeLog(int color, char *date, const char *display_name, const char *category, std::string buffer);
#endif
// Set console text color using ANSI escape codes
static void SetConsoleColor(int color)
{
switch (color) {
case FOREGROUND_WHITE:
case FOREGROUND_WHITE_BOLD:
case FOREGROUND_RED:
case FOREGROUND_RED_BOLD:
case FOREGROUND_GREEN:
case FOREGROUND_GREEN_BOLD:
case FOREGROUND_BLUE:
case FOREGROUND_BLUE_BOLD:
case FOREGROUND_YELLOW:
case FOREGROUND_YELLOW_BOLD:
case FOREGROUND_CYAN:
case FOREGROUND_CYAN_BOLD:
case FOREGROUND_MAGENTA:
case FOREGROUND_MAGENTA_BOLD:
printf("\033[%i;%i;40m", color > 100 ? 1 : 0, color > 100 ? color - 100 : color);
break;
default:
printf("\033[0;37;40m");
break;
}
}
// Open a log file for writing with date-based naming
static FILE * OpenLogFile()
{
char file[FILENAME_MAX + 1];
struct stat st;
struct tm *tm;
time_t now;
FILE *f;
now = time(nullptr);
tm = localtime(&now);
// Create logs directory if it doesn't exist
if (stat(LOG_DIR, &st) != 0) {
if (mkdir(LOG_DIR, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) {
fprintf(stderr, "Unable to create directory '%s': %s\n", LOG_DIR, strerror(errno));
return stderr;
}
}
// Generate filename with date and process ID
#ifdef NO_PIDLOG
snprintf(file, FILENAME_MAX, LOG_DIR"/%04i-%02i-%02i_eq2" EXE_NAME ".log",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
#else
snprintf(file, FILENAME_MAX, LOG_DIR"/%04i-%02i-%02i_eq2" EXE_NAME "_%04i.log",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, getpid());
#endif
if ((f = fopen(file, "a")) == nullptr) {
fprintf(stderr, "Could not open '%s' for writing: %s\n", file, strerror(errno));
return stderr;
}
return f;
}
// Write a batch of queued log entries to their destinations
static void WriteQueuedLogs(int count)
{
logq_t pending_head, pending_tail, *logq, *tmp;
int i = 0;
FILE *f;
// Initialize temporary list for processing
pending_head.next = &pending_tail;
pending_tail.prev = &pending_head;
// Move logs from main queue to temporary list for processing
{
std::lock_guard<std::mutex> lock(log_mutex);
while (head.next != &tail) {
// Remove from main list
logq = head.next;
logq->next->prev = &head;
head.next = logq->next;
// Add to temporary list
tmp = pending_tail.prev;
tmp->next = logq;
logq->prev = tmp;
logq->next = &pending_tail;
pending_tail.prev = logq;
--num_logqs;
// Check count limit
if (count > 0 && ++i == count)
break;
}
}
// Process logs from temporary list
logq = pending_head.next;
if (logq == &pending_tail)
return;
while (logq != &pending_tail) {
// Write to console if enabled
if (log_type_info[logq->log_type].console) {
SetConsoleColor(FOREGROUND_WHITE_BOLD);
printf("%s ", logq->date);
SetConsoleColor(log_type_info[logq->log_type].color);
printf("%s ", log_type_info[logq->log_type].display_name);
SetConsoleColor(FOREGROUND_WHITE_BOLD);
printf("%-10s: ", logq->name);
SetConsoleColor(log_type_info[logq->log_type].color);
printf("%s\n", logq->text);
SetConsoleColor(-1);
fflush(stdout);
}
// Write to log file if enabled
if (log_type_info[logq->log_type].logfile) {
f = OpenLogFile();
if (f != stderr || (f == stderr && !log_type_info[logq->log_type].console)) {
fprintf(f, "%s %s %s: %s\n", logq->date,
log_type_info[logq->log_type].display_name, logq->name, logq->text);
fflush(f);
if (f != stderr)
fclose(f);
}
}
#if defined WORLD
// Send to subscribed clients if enabled
if (log_type_info[logq->log_type].client) {
// TODO: Implement client logging subscription system
}
#endif
// Move to next log entry and clean up current
tmp = logq;
logq = logq->next;
free(tmp->text);
free(tmp);
}
}
// Main logging thread function - processes queued logs continuously
void LogLoop()
{
while (looping.load()) {
WriteQueuedLogs(LOGS_PER_CYCLE);
std::this_thread::sleep_for(std::chrono::milliseconds(LOG_CYCLE));
}
}
// Initialize the logging system and start the logging thread
void LogStart()
{
if (start_called.load())
return;
// Initialize doubly-linked list
head.prev = nullptr;
head.next = &tail;
tail.prev = &head;
tail.next = nullptr;
looping.store(true);
// Start logging thread
std::thread log_thread(LogLoop);
log_thread.detach();
start_called.store(true);
}
// Stop the logging system and flush remaining logs
void LogStop()
{
looping.store(false);
WriteQueuedLogs(-1); // Write all remaining logs
start_called.store(false);
}
// Add a log entry to the queue for processing
static void LogQueueAdd(LogType log_type, char *text, int len, const char *cat_text = nullptr)
{
logq_t *logq;
struct tm *tm;
time_t now;
// Allocate log queue entry
if ((logq = static_cast<logq_t*>(calloc(1, sizeof(logq_t)))) == nullptr) {
free(text);
fprintf(stderr, "%s: %u: Unable to allocate %zu bytes\n", __FUNCTION__, __LINE__, sizeof(logq_t));
return;
}
// Allocate text buffer
if ((logq->text = static_cast<char*>(calloc(len + 1, sizeof(char)))) == nullptr) {
free(text);
free(logq);
fprintf(stderr, "%s: %u: Unable to allocate %i bytes\n", __FUNCTION__, __LINE__, len + 1);
return;
}
// Set timestamp
now = time(nullptr);
tm = localtime(&now);
// Initialize log entry
logq->log_type = log_type;
snprintf(logq->date, DATE_MAX + 1, "%02i:%02i:%02i", tm->tm_hour, tm->tm_min, tm->tm_sec);
strncpy(logq->name, cat_text == nullptr || cat_text[0] == '\0' ?
log_type_info[log_type].name : cat_text, LOG_NAME_MAX);
strncpy(logq->text, text, len);
free(text);
// Ensure logging system is started
if (!start_called.load())
LogStart();
// Add to queue
{
std::lock_guard<std::mutex> lock(log_mutex);
tail.prev->next = logq;
logq->prev = tail.prev;
logq->next = &tail;
tail.prev = logq;
++num_logqs;
}
}
// Get the minimum log level for a specific log type
int8_t GetLoggerLevel(LogType type)
{
return log_type_info[type].level;
}
#ifndef PARSER
// Main logging function - formats message and queues for output
void LogWrite(LogType type, int8_t log_level, const char *cat_text, const char *fmt, ...)
{
int count, size = 64;
char *buf;
va_list ap;
// Check if logging is enabled and level is appropriate
if (!log_type_info[type].enabled || (log_level > 0 && log_type_info[type].level < log_level))
return;
// Dynamically allocate buffer for formatted message
while (true) {
if ((buf = static_cast<char*>(malloc(size))) == nullptr) {
fprintf(stderr, "%s: %i: Unable to allocate %i bytes\n", __FUNCTION__, __LINE__, size);
return;
}
va_start(ap, fmt);
count = vsnprintf(buf, size, fmt, ap);
va_end(ap);
if (count > -1 && count < size)
break;
free(buf);
if (count > 1)
size = count + 1;
else
size *= 2;
}
LogQueueAdd(type, buf, count, cat_text);
}
#else
// Parser-specific logging function - writes directly without queueing
void LogWrite(LogType type, int8_t log_level, const char *cat_text, const char *format, ...)
{
// Check if logging should proceed
if (!format || !log_type_info[type].enabled ||
(log_level > 0 && log_type_info[type].level < log_level))
return;
time_t clock;
struct tm *tm;
char buffer[LOG_BUFFER_SIZE], date[32];
va_list args;
FILE *f;
size_t cat_text_len = 0;
memset(buffer, 0, sizeof(buffer));
memset(date, 0, sizeof(date));
// Format the log message
va_start(args, format);
vsnprintf(buffer, sizeof(buffer) - 1, format, args);
va_end(args);
// Generate timestamp
time(&clock);
tm = localtime(&clock);
snprintf(date, sizeof(date)-1, "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
cat_text_len = strlen(cat_text);
// Write to log file if enabled
if (log_type_info[type].logfile) {
char exename[200] = "";
#ifdef LOGIN
snprintf(exename, sizeof(exename), "login");
#elif defined WORLD
snprintf(exename, sizeof(exename), "world");
#elif defined PARSER
snprintf(exename, sizeof(exename), "parser");
#elif defined PATCHER
snprintf(exename, sizeof(exename), "patcher");
#endif
char filename[200], log_header[200] = "";
// Generate filename with or without PID
#ifndef NO_PIDLOG
snprintf(filename, sizeof(filename)-1, "logs/%04d-%02d-%02d_eq2%s_%04i.log",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, exename, getpid());
#else
snprintf(filename, sizeof(filename)-1, "logs/%04d-%02d-%02d_eq2%s.log",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, exename);
#endif
// Check if new log file and create header
f = fopen(filename, "r");
if (!f)
snprintf(log_header, sizeof(log_header), "===[ New log '%s' started ]===\n\n", filename);
else
fclose(f);
// Write log entry
f = fopen(filename, "a");
if (f) {
if (strlen(log_header) > 0)
fprintf(f, "%s\n", log_header);
fprintf(f, "%s %s %s: %s\n", date, log_type_info[type].display_name, cat_text, buffer);
fclose(f);
}
}
// Write to console if enabled
if (log_type_info[type].console) {
printf("%s %s %s: %s\n", date, log_type_info[type].display_name,
cat_text_len == 0 ? log_type_info[type].name : cat_text, buffer);
}
}
// Colorize console output for parser (simplified for Linux)
void ColorizeLog(int color, char *date, const char *display_name, const char *category, std::string buffer)
{
printf("%s ", date);
SetConsoleColor(color);
printf("%s ", display_name);
SetConsoleColor(FOREGROUND_WHITE_BOLD);
printf("%s: ", category);
SetConsoleColor(color);
printf("%s\n", buffer.c_str());
SetConsoleColor(FOREGROUND_WHITE);
}
#endif
// Find log type status by category and type name
LogTypeStatus* GetLogTypeStatus(const char *category, const char *type)
{
char combined[256];
int i;
memset(combined, 0, sizeof(combined));
snprintf(combined, sizeof(combined) - 1, "%s__%s", category, type);
for (i = 0; i < NUMBER_OF_LOG_TYPES; i++) {
if (strcasecmp(log_type_info[i].name, combined) == 0)
return &log_type_info[i];
}
return &log_type_info[NUMBER_OF_LOG_TYPES];
}
// Process XML configuration for a log category
void ProcessLogConfig(XMLNode node)
{
int i;
const char *category, *type, *level, *color, *enabled, *logs;
LogTypeStatus *lfs;
XMLNode child;
// Get category attribute
category = node.getAttribute("Category");
if (!category) {
LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Config missing a Category");
return;
}
// Process each ConfigType child node
for (i = 0; i < node.nChildNode("ConfigType"); i++) {
child = node.getChildNode("ConfigType", i);
type = child.getAttribute("Type");
if (!type) {
LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Config missing a Type");
continue;
}
// Get log type status and configuration attributes
lfs = GetLogTypeStatus(category, type);
level = child.getAttribute("Level");
enabled = child.getAttribute("Enabled");
color = child.getAttribute("Color");
logs = child.getAttribute("Logs");
// Validate logs attribute
if (!logs) {
LogWrite(MISC__WARNING, 0, "Misc",
"Error parsing log config. Config missing 'Logs' attribute to specify which log(s) to write to");
continue;
}
if (!IsNumber(logs)) {
LogWrite(MISC__WARNING, 0, "Misc",
"Error parsing log config. Attribute 'Logs' must be a number. See LogTypes.h for the valid types.");
continue;
}
// Set enabled status
if (enabled) {
if (!strcasecmp("true", enabled) || !strcasecmp("on", enabled))
lfs->enabled = true;
else if (!strcasecmp("false", enabled) || !strcasecmp("off", enabled))
lfs->enabled = false;
else
LogWrite(MISC__WARNING, 0, "Misc",
"Error parsing log config. Log setting 'Enabled' has invalid value '%s'. "
"'true'/'on' or 'false'/'off' are valid values", enabled);
}
// Set log level
if (IsNumber(level))
lfs->level = atoi(level);
else
lfs->level = 0;
// Set color
if (color) {
if (IsNumber(color))
lfs->color = atoi(color);
else if (!strcasecmp("White", color))
lfs->color = FOREGROUND_WHITE;
else if (!strcasecmp("Green", color))
lfs->color = FOREGROUND_GREEN;
else if (!strcasecmp("Yellow", color))
lfs->color = FOREGROUND_YELLOW;
else if (!strcasecmp("Red", color))
lfs->color = FOREGROUND_RED;
else if (!strcasecmp("Blue", color))
lfs->color = FOREGROUND_BLUE;
else if (!strcasecmp("Cyan", color))
lfs->color = FOREGROUND_CYAN;
else if (!strcasecmp("Magenta", color))
lfs->color = FOREGROUND_MAGENTA;
else if (!strcasecmp("WhiteBold", color))
lfs->color = FOREGROUND_WHITE_BOLD;
else if (!strcasecmp("GreenBold", color))
lfs->color = FOREGROUND_GREEN_BOLD;
else if (!strcasecmp("YellowBold", color))
lfs->color = FOREGROUND_YELLOW_BOLD;
else if (!strcasecmp("RedBold", color))
lfs->color = FOREGROUND_RED_BOLD;
else if (!strcasecmp("BlueBold", color))
lfs->color = FOREGROUND_BLUE_BOLD;
else if (!strcasecmp("CyanBold", color))
lfs->color = FOREGROUND_CYAN_BOLD;
else if (!strcasecmp("MagentaBold", color))
lfs->color = FOREGROUND_MAGENTA_BOLD;
else
LogWrite(MISC__WARNING, 0, "Misc",
"Error parsing log config. Log setting 'Color' has invalid value '%s'", color);
}
// Set output destinations
lfs->logfile = (atoi(logs) & LOG_LOGFILE);
lfs->console = (atoi(logs) & LOG_CONSOLE);
lfs->client = (atoi(logs) & LOG_CLIENT);
}
}
// Parse log configuration from XML file
bool LogParseConfigs()
{
XMLNode main_node;
int i;
main_node = XMLNode::openFileHelper("log_config.xml", "EQ2EmuLogConfigs");
if (main_node.isEmpty()) {
LogWrite(MISC__WARNING, 0, "Misc",
"Unable to parse the file 'log_config.xml' or it does not exist. Default values will be used");
return false;
}
// Process each LogConfig node
for (i = 0; i < main_node.nChildNode("LogConfig"); i++)
ProcessLogConfig(main_node.getChildNode("LogConfig", i));
return true;
}