feat(log): introdueix Log:: wrapper sobre SDL_Log

Logger lleuger a source/utils/log.{hpp,cpp} amb API basada en std::format
i filtre runtime via la variable d'entorn AEE_LOG (sintaxi
*:info,Resource::Helper:warn,scene:debug). Substitueix gradualment els
std::cout/cerr dispersos.

- Categories CUSTOM registrades sota demanda; per defecte INFO.
- Categories SDL natives no es toquen tret que l'usuari posi *:level.
- Migra resource_helper.cpp i els marcadors d'inici/fi (main.cpp) com a
  testimoni; la resta de subsistemes es migra en commits següents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
JailDesignerandClaude Opus 4.7 committed 2026-05-08 09:01:09 +02:00
1 parent c910a16834
commit 1243b457be
5 files changed
+226 -12

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
#include "utils/log.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <format>
#include <mutex>
#include <string>
#include <string_view>
#include <unordered_map>
namespace Log {
namespace {
std::mutex g_mutex;
std::unordered_map<std::string, int> g_categories;
int g_next_category = SDL_LOG_CATEGORY_CUSTOM;
// Prioritat per defecte aplicada a tota CUSTOM nova. Es modifica només
// si l'usuari ha posat `*:level` (o un nivell pelat) a AEE_LOG.
SDL_LogPriority g_default_priority = SDL_LOG_PRIORITY_INFO;
auto categoryFor(std::string_view tag) -> int {
std::lock_guard lk{g_mutex};
const std::string KEY{tag};
auto it = g_categories.find(KEY);
if (it != g_categories.end()) { return it->second; }
const int CAT = g_next_category++;
g_categories.emplace(KEY, CAT);
// Cada categoria CUSTOM neix amb la prioritat default de l'app
// (sense aquesta línia, SDL retorna ERROR per a qualsevol CUSTOM
// no inicialitzada, silenciant els nostres `info`).
SDL_SetLogPriority(CAT, g_default_priority);
return CAT;
}
auto toSDL(Level lvl) -> SDL_LogPriority {
switch (lvl) {
case Level::TRACE:
return SDL_LOG_PRIORITY_TRACE;
case Level::DEBUG:
return SDL_LOG_PRIORITY_DEBUG;
case Level::INFO:
return SDL_LOG_PRIORITY_INFO;
case Level::WARN:
return SDL_LOG_PRIORITY_WARN;
case Level::ERROR:
return SDL_LOG_PRIORITY_ERROR;
case Level::CRITICAL:
return SDL_LOG_PRIORITY_CRITICAL;
}
return SDL_LOG_PRIORITY_INFO;
}
// Parser tolerant de noms de prioritat. Sinònims acceptats per costum:
// verbose↔debug, warning↔warn, err↔error, fatal/crit↔critical.
auto parsePriority(std::string_view s) -> SDL_LogPriority {
std::string lower(s.size(), '\0');
std::ranges::transform(s, lower.begin(), [](char c) {
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
});
if (lower == "trace") { return SDL_LOG_PRIORITY_TRACE; }
if (lower == "debug" || lower == "verbose") { return SDL_LOG_PRIORITY_DEBUG; }
if (lower == "info") { return SDL_LOG_PRIORITY_INFO; }
if (lower == "warn" || lower == "warning") { return SDL_LOG_PRIORITY_WARN; }
if (lower == "error" || lower == "err") { return SDL_LOG_PRIORITY_ERROR; }
if (lower == "critical" || lower == "crit" || lower == "fatal") {
return SDL_LOG_PRIORITY_CRITICAL;
}
return SDL_LOG_PRIORITY_INFO;
}
auto trim(std::string_view s) -> std::string_view {
while (!s.empty() && (std::isspace(static_cast<unsigned char>(s.front())) != 0)) {
s.remove_prefix(1);
}
while (!s.empty() && (std::isspace(static_cast<unsigned char>(s.back())) != 0)) {
s.remove_suffix(1);
}
return s;
}
// Itera entrades d'una especificació AEE_LOG separades per `,`,
// saltant les buides.
template <typename Fn>
void forEachEntry(std::string_view spec, Fn fn) {
std::size_t i = 0;
while (i <= spec.size()) {
const std::size_t COMMA = spec.find(',', i);
const std::size_t END = (COMMA == std::string_view::npos) ? spec.size() : COMMA;
const std::string_view ENTRY = trim(spec.substr(i, END - i));
i = (COMMA == std::string_view::npos) ? spec.size() + 1 : COMMA + 1;
if (!ENTRY.empty()) { fn(ENTRY); }
}
}
// Una entrada AEE_LOG global aplica a totes les categories (SDL i
// CUSTOM). Forma curta `level` ↔ tag explícit `*:level`.
void applyGlobalEntry(std::string_view entry) {
const std::size_t COLON = entry.rfind(':');
const bool IS_BARE = (COLON == std::string_view::npos);
const std::string_view TAG =
IS_BARE ? std::string_view{} : trim(entry.substr(0, COLON));
if (!IS_BARE && TAG != "*" && !TAG.empty()) { return; }
const SDL_LogPriority PRIO =
parsePriority(IS_BARE ? entry : trim(entry.substr(COLON + 1)));
SDL_SetLogPriorities(PRIO);
g_default_priority = PRIO;
}
// Override per tag concret (sobreescriu el global per a aquesta
// categoria). Si la categoria és nova es registra al vol.
void applyTagEntry(std::string_view entry) {
const std::size_t COLON = entry.rfind(':');
if (COLON == std::string_view::npos) { return; }
const std::string_view TAG = trim(entry.substr(0, COLON));
if (TAG == "*" || TAG.empty()) { return; }
const SDL_LogPriority PRIO = parsePriority(trim(entry.substr(COLON + 1)));
SDL_SetLogPriority(categoryFor(TAG), PRIO);
}
} // namespace
void log(Level lvl, std::string_view tag, std::string_view msg) {
// `"%s"` + cos com a argument: si `msg` conté `%` (rar però possible
// en paths o rutes amb urlencoded), no s'interpreta com a format spec.
const std::string BODY = std::format("{}: {}", tag, msg);
SDL_LogMessage(categoryFor(tag), toSDL(lvl), "%s", BODY.c_str());
}
void init() {
// No toquem cap categoria SDL nativa per defecte: així preservem el
// silenci de SDL_LOG_CATEGORY_GPU, RENDER, etc. (default = ERROR), i
// només el que arriba via Log::* es veu, mantenint l'output històric.
g_default_priority = SDL_LOG_PRIORITY_INFO;
const char* env = std::getenv("AEE_LOG");
if (env == nullptr || *env == '\0') { return; }
// Tags poden contenir `::` (p. ex. `Resource::Helper`); busquem el
// separador amb rfind. Dues passades per fer l'override per-tag
// insensible a l'ordre (`Tag:debug,*:warn` deixa Tag a debug).
const std::string_view SPEC{env};
forEachEntry(SPEC, applyGlobalEntry);
forEachEntry(SPEC, applyTagEntry);
}
} // namespace Log
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <cstdint>
#include <format>
#include <string_view>
#include <utility>
// Logger lleuger embolicant `SDL_Log*`. Una sola entrada per nivell, format
// type-safe via `std::format`. Filtre runtime amb la variable d'entorn
// AEE_LOG (sintaxi `*:info,Resource::Helper:warn,scene:debug`). Ús típic:
//
// Log::info("Resource::Helper", "pack loaded ({} entries)", n);
// Log::warn("Options", "preset \"{}\" no trobat; aplicant defaults.", name);
// Log::error("Director", "no s'ha pogut carregar config.yaml a {}", path);
//
namespace Log {
enum class Level : std::uint8_t {
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
CRITICAL,
};
// Llig AEE_LOG i configura les prioritats SDL. Crida una sola vegada,
// tan aviat com sigui possible (abans del primer `Log::*`).
void init();
// Backend usat per les funcions per nivell. Composa "tag: msg" i ho
// entrega a SDL_LogMessage amb la categoria registrada per al tag.
void log(Level lvl, std::string_view tag, std::string_view msg);
template <typename... Args>
void trace(std::string_view tag, std::format_string<Args...> fmt, Args&&... args) {
log(Level::TRACE, tag, std::format(fmt, std::forward<Args>(args)...));
}
template <typename... Args>
void debug(std::string_view tag, std::format_string<Args...> fmt, Args&&... args) {
log(Level::DEBUG, tag, std::format(fmt, std::forward<Args>(args)...));
}
template <typename... Args>
void info(std::string_view tag, std::format_string<Args...> fmt, Args&&... args) {
log(Level::INFO, tag, std::format(fmt, std::forward<Args>(args)...));
}
template <typename... Args>
void warn(std::string_view tag, std::format_string<Args...> fmt, Args&&... args) {
log(Level::WARN, tag, std::format(fmt, std::forward<Args>(args)...));
}
template <typename... Args>
void error(std::string_view tag, std::format_string<Args...> fmt, Args&&... args) {
log(Level::ERROR, tag, std::format(fmt, std::forward<Args>(args)...));
}
template <typename... Args>
void critical(std::string_view tag, std::format_string<Args...> fmt, Args&&... args) {
log(Level::CRITICAL, tag, std::format(fmt, std::forward<Args>(args)...));
}
} // namespace Log