a028928bc7
Logger global thread-safe con ring buffer in-memory de 2000 entradas + escritura opcional a archivo. log_window flotante consume el ring buffer con filtros por nivel, busqueda y autoscroll; se abre desde Settings -> Logs en la menubar. selectable_text cubre el patron drag-to-select + Ctrl+C en cualquier ventana. app_menubar y framework run_app integran log_window_render() en el frame loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
157 lines
4.7 KiB
C++
157 lines
4.7 KiB
C++
#include "core/logger.h"
|
|
|
|
#include <chrono>
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
namespace fn_log {
|
|
|
|
namespace {
|
|
|
|
std::mutex g_mu;
|
|
FILE* g_file = nullptr;
|
|
std::string g_path;
|
|
Level g_min_level = Level::Info;
|
|
|
|
// Ring buffer in-memory. g_count es el numero de entradas vivas (clamp a
|
|
// capacity); g_head es el indice donde se escribira la proxima entrada.
|
|
Entry g_buf[kBufferCapacity];
|
|
std::size_t g_count = 0;
|
|
std::size_t g_head = 0;
|
|
|
|
long long now_ms() {
|
|
using namespace std::chrono;
|
|
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
// Formato: "YYYY-MM-DD HH:MM:SS.mmm". out debe tener al menos 24 bytes.
|
|
void format_ts(long long ts_ms, char* out, std::size_t out_size) {
|
|
std::time_t secs = static_cast<std::time_t>(ts_ms / 1000);
|
|
int millis = static_cast<int>(ts_ms % 1000);
|
|
if (millis < 0) millis = 0;
|
|
if (millis > 999) millis = 999;
|
|
std::tm tm_buf{};
|
|
#ifdef _WIN32
|
|
localtime_s(&tm_buf, &secs);
|
|
#else
|
|
localtime_r(&secs, &tm_buf);
|
|
#endif
|
|
std::snprintf(out, out_size, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
|
|
(tm_buf.tm_year + 1900) % 10000,
|
|
(tm_buf.tm_mon + 1) % 100,
|
|
tm_buf.tm_mday % 100,
|
|
tm_buf.tm_hour % 100,
|
|
tm_buf.tm_min % 100,
|
|
tm_buf.tm_sec % 100,
|
|
millis);
|
|
}
|
|
|
|
void push_entry(Level level, long long ts_ms, const char* text) {
|
|
Entry& e = g_buf[g_head];
|
|
e.level = level;
|
|
e.ts_ms = ts_ms;
|
|
std::snprintf(e.text, kEntryTextMax, "%s", text);
|
|
g_head = (g_head + 1) % kBufferCapacity;
|
|
if (g_count < kBufferCapacity) ++g_count;
|
|
}
|
|
|
|
// Convierte el indice "logico" (0 = mas antigua) al indice fisico del array.
|
|
std::size_t logical_to_physical(std::size_t i) {
|
|
if (g_count < kBufferCapacity) return i; // buffer aun no lleno
|
|
return (g_head + i) % kBufferCapacity;
|
|
}
|
|
|
|
void emit(Level level, const char* fmt, std::va_list ap) {
|
|
if (static_cast<int>(level) < static_cast<int>(g_min_level)) return;
|
|
|
|
// msg deja 64 bytes para que el prefijo "[ts] [LEVEL] " quepa siempre en
|
|
// el buffer destino sin que -Wformat-truncation se queje.
|
|
char msg[kEntryTextMax - 64];
|
|
std::vsnprintf(msg, sizeof(msg), fmt, ap);
|
|
|
|
long long ts = now_ms();
|
|
char ts_buf[32];
|
|
format_ts(ts, ts_buf, sizeof(ts_buf));
|
|
|
|
char line[kEntryTextMax];
|
|
std::snprintf(line, sizeof(line), "[%s] [%s] %s",
|
|
ts_buf, level_label(level), msg);
|
|
|
|
std::lock_guard<std::mutex> lk(g_mu);
|
|
push_entry(level, ts, line);
|
|
if (g_file) {
|
|
std::fputs(line, g_file);
|
|
std::fputc('\n', g_file);
|
|
std::fflush(g_file);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool logger_init(const char* file_path, Level min_level) {
|
|
std::lock_guard<std::mutex> lk(g_mu);
|
|
if (g_file) {
|
|
std::fclose(g_file);
|
|
g_file = nullptr;
|
|
}
|
|
g_min_level = min_level;
|
|
g_path.clear();
|
|
if (!file_path || !*file_path) return false;
|
|
|
|
g_file = std::fopen(file_path, "a");
|
|
if (!g_file) {
|
|
std::fprintf(stderr, "[fn_log] no pude abrir %s para append\n", file_path);
|
|
return false;
|
|
}
|
|
g_path = file_path;
|
|
return true;
|
|
}
|
|
|
|
void logger_close() {
|
|
std::lock_guard<std::mutex> lk(g_mu);
|
|
if (g_file) {
|
|
std::fclose(g_file);
|
|
g_file = nullptr;
|
|
}
|
|
g_path.clear();
|
|
}
|
|
|
|
void logger_set_level(Level level) { std::lock_guard<std::mutex> lk(g_mu); g_min_level = level; }
|
|
Level logger_level() { std::lock_guard<std::mutex> lk(g_mu); return g_min_level; }
|
|
const char* logger_path() { return g_path.c_str(); }
|
|
|
|
void log_debug(const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Debug, fmt, ap); va_end(ap); }
|
|
void log_info (const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Info, fmt, ap); va_end(ap); }
|
|
void log_warn (const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Warn, fmt, ap); va_end(ap); }
|
|
void log_error(const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Error, fmt, ap); va_end(ap); }
|
|
|
|
std::size_t buffer_size() { std::lock_guard<std::mutex> lk(g_mu); return g_count; }
|
|
|
|
const Entry* buffer_at(std::size_t i) {
|
|
std::lock_guard<std::mutex> lk(g_mu);
|
|
if (i >= g_count) return nullptr;
|
|
return &g_buf[logical_to_physical(i)];
|
|
}
|
|
|
|
void buffer_clear() {
|
|
std::lock_guard<std::mutex> lk(g_mu);
|
|
g_count = 0;
|
|
g_head = 0;
|
|
}
|
|
|
|
const char* level_label(Level level) {
|
|
switch (level) {
|
|
case Level::Debug: return "DEBUG";
|
|
case Level::Info: return "INFO";
|
|
case Level::Warn: return "WARN";
|
|
case Level::Error: return "ERROR";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
} // namespace fn_log
|