// mcpp.log file-based debug logger with verbose terminal output.
//
// Usage:
// mcpp::log::init(cfg); // call once at startup
// mcpp::log::debug("tag", "msg"); // writes to ~/.mcpp/log/mcpp.log
// mcpp::log::verbose("tag", "msg"); // file + stderr (when --verbose)
//
// Configuration (priority order):
// 1. MCPP_LOG_LEVEL env var: "debug" | "info" | "warn" | "error" | "off"
// 2. config.toml [log].level
// 3. Default: "off"
//
// Log file: /mcpp.log (rotated at max_file_size, keeps max_files)
module;
#include
#include
export module mcpp.log;
import std;
export namespace mcpp::log {
enum class Level { off, error, warn, info, debug };
struct Config {
Level level = Level::off;
std::size_t maxFileSize = 10 * 1024 * 1024; // 10MB
int maxFiles = 3;
std::filesystem::path logDir;
};
void init(const Config& cfg);
void debug(std::string_view tag, std::string_view message);
void info (std::string_view tag, std::string_view message);
void warn (std::string_view tag, std::string_view message);
void error(std::string_view tag, std::string_view message);
// verbose: writes to file (level >= info) AND stderr (when --verbose).
void set_verbose(bool v);
bool is_verbose();
void verbose(std::string_view tag, std::string_view message);
// Scoped verbose timer for diagnosing slow steps (e.g. first-run init / bootstrap
// hangs). Logs ": start" on construction and ": done (=ms)" on
// destruction, via verbose() always to the timestamped log file
// (~/.mcpp/log/mcpp.log), and to stderr under --verbose. Cheap: two log lines.
struct ScopedTimer {
std::string tag;
std::string label;
std::chrono::steady_clock::time_point t0;
ScopedTimer(std::string_view tag_, std::string_view label_);
~ScopedTimer();
ScopedTimer(const ScopedTimer&) = delete;
ScopedTimer& operator=(const ScopedTimer&) = delete;
// Emit an intermediate checkpoint: ": (+ms)".
void mark(std::string_view note) const;
};
// Check if a level is enabled (avoid constructing expensive messages).
bool is_enabled(Level l);
} // namespace mcpp::log
// Implementation
namespace mcpp::log {
namespace {
Level g_level = Level::off;
bool g_verbose = false;
std::filesystem::path g_logFile;
std::size_t g_maxFileSize = 10 * 1024 * 1024;
int g_maxFiles = 3;
std::mutex g_mutex;
Level parse_level(const char* s) {
if (!s || !*s) return Level::off;
std::string v(s);
for (auto& c : v) c = static_cast(std::tolower(static_cast(c)));
if (v == "debug") return Level::debug;
if (v == "info") return Level::info;
if (v == "warn") return Level::warn;
if (v == "error") return Level::error;
return Level::off;
}
std::string timestamp() {
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast(
now.time_since_epoch()) % 1000;
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
char buf[32];
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d.%03d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, static_cast(ms.count()));
return buf;
}
const char* level_str(Level l) {
switch (l) {
case Level::debug: return "DEBUG";
case Level::info: return "INFO ";
case Level::warn: return "WARN ";
case Level::error: return "ERROR";
default: return " ";
}
}
void rotate() {
if (g_logFile.empty() || g_maxFiles = 1; --i) {
auto src = g_logFile;
src += "." + std::to_string(i);
auto dst = g_logFile;
dst += "." + std::to_string(i + 1);
if (i == g_maxFiles - 1) {
std::filesystem::remove(src, ec);
} else {
std::filesystem::rename(src, dst, ec);
}
}
auto first = g_logFile;
first += ".1";
std::filesystem::rename(g_logFile, first, ec);
}
void write_log(Level level, std::string_view tag, std::string_view message) {
if (level > g_level || g_level == Level::off) return;
if (g_logFile.empty()) return;
std::lock_guard lock(g_mutex);
rotate();
std::ofstream ofs(g_logFile, std::ios::app);
if (!ofs) return;
ofs