feat(cpp/core): logger + log_window + selectable_text widgets
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>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user