Skip to content

Commit 2dfcf48

Browse files
authored
feat: add observability — log module, --verbose, file logging (#68)
* feat: add observability — log module, --verbose, file logging Three-layer observability system: 1. mcpp::log module (src/log.cppm): - File-based logging to ~/.mcpp/log/mcpp.log - Log rotation (10MB, 3 files) - Levels: off/error/warn/info/debug - Configured via MCPP_LOG_LEVEL env var or config.toml [log] 2. --verbose global flag: - Shows detailed progress on stderr (dim gray) - Auto-enables info-level file logging - Covers: toolchain install, package fetch, post-install fixup 3. Key path instrumentation: - Toolchain install flow (target, deps, fixup steps) - Package fetcher (resolve path, copy workaround, xlings calls) - xlings event forwarding (LogEvent, ErrorEvent) - Toolchain probing (compiler binary, sysroot, payload paths) * fix: use localtime_s on Windows for log timestamp
1 parent 359aaa8 commit 2dfcf48

5 files changed

Lines changed: 319 additions & 8 deletions

File tree

src/cli.cppm

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexis
4141
import mcpp.pm.compat; // 0.0.6: namespace field + dotted-name compat shims
4242
import mcpp.pm.dep_spec;
4343
import mcpp.ui;
44+
import mcpp.log;
4445
import mcpp.bmi_cache;
4546
import mcpp.dyndep;
4647
import mcpp.version_req; // SemVer constraint resolution
@@ -559,6 +560,22 @@ struct CliInstallProgress : mcpp::fetcher::EventHandler {
559560
}
560561
}
561562

563+
void on_log(const mcpp::fetcher::LogEvent& e) override {
564+
if (e.level == "error")
565+
mcpp::log::error("xlings", e.message);
566+
else if (e.level == "warn")
567+
mcpp::log::warn("xlings", e.message);
568+
else
569+
mcpp::log::info("xlings", e.message);
570+
mcpp::log::verbose("xlings", std::format("[{}] {}", e.level, e.message));
571+
}
572+
573+
void on_error(const mcpp::fetcher::ErrorEvent& e) override {
574+
mcpp::log::error("xlings", std::format("{}: {}", e.code, e.message));
575+
if (!e.hint.empty())
576+
mcpp::log::info("xlings", std::format("hint: {}", e.hint));
577+
}
578+
562579
~CliInstallProgress() override { if (bar_) bar_->finish(); }
563580
};
564581

@@ -2637,7 +2654,7 @@ std::optional<int> try_fast_build(const std::filesystem::path& projectRoot,
26372654
}
26382655

26392656
int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
2640-
bool verbose = parsed.is_flag_set("verbose");
2657+
bool verbose = parsed.is_flag_set("verbose") || mcpp::log::is_verbose();
26412658
bool print_fp = parsed.is_flag_set("print-fingerprint");
26422659
bool no_cache = parsed.is_flag_set("no-cache");
26432660

@@ -3723,6 +3740,11 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) {
37233740

37243741
mcpp::ui::info("Installing",
37253742
std::format("{} {} via mcpp's xlings", spec->compiler, spec->version));
3743+
mcpp::log::verbose("toolchain", std::format(
3744+
"install: target='{}' xlingsHome='{}'", pkg.target(), cfg->xlingsHome().string()));
3745+
mcpp::log::debug("toolchain", std::format(
3746+
" ximName='{}' needsGccFixup={} xlingsBinary='{}'",
3747+
pkg.ximName, pkg.needsGccPostInstallFixup, cfg->xlingsBinary.string()));
37263748
mcpp::fetcher::Fetcher fetcher(*cfg);
37273749
CliInstallProgress progress;
37283750

@@ -3731,13 +3753,17 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) {
37313753
// musl-gcc is self-contained and doesn't need these.
37323754
if (!spec->isMusl) {
37333755
for (auto dep : {"xim:glibc", "xim:linux-headers"}) {
3756+
mcpp::log::verbose("toolchain", std::format("installing dep: {}", dep));
37343757
auto depPayload = fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress);
3735-
// Best-effort: linux-headers may not be in the index.
3736-
// glibc is usually a dependency of gcc/llvm and already installed.
3758+
mcpp::log::debug("toolchain", std::format("dep {} result: {}",
3759+
dep, depPayload ? "ok" : depPayload.error().message));
37373760
}
37383761
}
37393762

3763+
mcpp::log::verbose("toolchain", std::format("installing main: {}", pkg.target()));
37403764
auto payload = fetcher.resolve_xpkg_path(pkg.target(), /*autoInstall=*/true, &progress);
3765+
mcpp::log::verbose("toolchain", std::format("main install result: {}",
3766+
payload ? ("ok → " + payload->root.string()) : payload.error().message));
37413767
if (!payload) {
37423768
mcpp::ui::error(std::format("install failed: {}", payload.error().message));
37433769
return 1;
@@ -3786,6 +3812,8 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) {
37863812

37873813
// (1) patchelf walk: rewrite PT_INTERP + RUNPATH for binutils
37883814
// and gcc xpkgs so they're self-contained in sandbox.
3815+
mcpp::log::verbose("toolchain", std::format(
3816+
"gcc fixup: patchelf_walk rpath='{}'", rpath));
37893817
auto binutilsRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "binutils");
37903818
if (std::filesystem::exists(binutilsRoot)) {
37913819
for (auto& v : std::filesystem::directory_iterator(binutilsRoot))
@@ -3794,6 +3822,7 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) {
37943822
patchelf_walk(payload->root, loader, rpath, patchelfBin);
37953823

37963824
// (2) specs fixup.
3825+
mcpp::log::verbose("toolchain", "gcc fixup: fixup_gcc_specs");
37973826
fixup_gcc_specs(payload->root, glibcLibDir, gccLibDir);
37983827
} else {
37993828
mcpp::ui::warning(
@@ -3841,9 +3870,12 @@ int cmd_toolchain(const mcpplibs::cmdline::ParsedArgs& parsed) {
38413870
std::string rpath = llvmTargetLib.string()
38423871
+ ":" + llvmLib.string()
38433872
+ ":" + glibcLibDir.string();
3873+
mcpp::log::verbose("toolchain", std::format(
3874+
"llvm fixup: patchelf_walk lib/ rpath='{}'", rpath));
38443875
patchelf_walk(llvmLib, loader, rpath, patchelfBin);
38453876
}
38463877

3878+
mcpp::log::verbose("toolchain", "llvm fixup: fixup_clang_cfg");
38473879
fixup_clang_cfg(payload->root, glibcLibDir);
38483880
}
38493881

@@ -4353,6 +4385,7 @@ int run(int argc, char** argv) {
43534385
std::string_view a = argv[i];
43544386
if (a == "--quiet" || a == "-q") mcpp::ui::set_quiet(true);
43554387
else if (a == "--no-color") mcpp::ui::disable_color();
4388+
else if (a == "--verbose" || a == "-v") mcpp::log::set_verbose(true);
43564389
}
43574390

43584391
// ─── top-level --help / -h / --version intercept ────────────────────
@@ -4441,6 +4474,8 @@ int run(int argc, char** argv) {
44414474
.description("modern C++ build tool")
44424475
.option(cl::Option("quiet").short_name('q')
44434476
.help("Suppress status output").global())
4477+
.option(cl::Option("verbose").short_name('v')
4478+
.help("Show detailed progress on stderr").global())
44444479
.option(cl::Option("no-color")
44454480
.help("Disable colored output").global())
44464481

@@ -4451,8 +4486,6 @@ int run(int argc, char** argv) {
44514486
.action(wrap_rc(cmd_new)))
44524487
.subcommand(cl::App("build")
44534488
.description("Build the current package")
4454-
.option(cl::Option("verbose").short_name('v')
4455-
.help("Verbose compiler output"))
44564489
.option(cl::Option("print-fingerprint")
44574490
.help("Show toolchain fingerprint and 10 inputs"))
44584491
.option(cl::Option("no-cache")

src/config.cppm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import mcpp.libs.toml;
2323
import mcpp.pm.index_spec;
2424
import mcpp.xlings;
2525
import mcpp.platform;
26+
import mcpp.log;
2627

2728
export namespace mcpp::config {
2829

@@ -460,6 +461,13 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
460461
std::format("cannot create '{}': {}", d.string(), ec.message())});
461462
}
462463

464+
// 2b. Initialize logger (early init with defaults; re-init after config load)
465+
{
466+
mcpp::log::Config logCfg;
467+
logCfg.logDir = cfg.logDir;
468+
mcpp::log::init(logCfg);
469+
}
470+
463471
// 3. Seed config.toml if missing
464472
bool fresh_config = !std::filesystem::exists(cfg.configFile);
465473
if (fresh_config) write_default_config_toml(cfg.configFile);
@@ -481,6 +489,22 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
481489
cfg.defaultBackend = doc->get_string("build.default_backend").value_or("ninja");
482490
cfg.defaultToolchain = doc->get_string("toolchain.default").value_or("");
483491

492+
// [log] section — re-initialize logger with config values
493+
{
494+
mcpp::log::Config logCfg;
495+
logCfg.logDir = cfg.logDir;
496+
auto levelStr = doc->get_string("log.level").value_or("off");
497+
if (levelStr == "debug") logCfg.level = mcpp::log::Level::debug;
498+
else if (levelStr == "info") logCfg.level = mcpp::log::Level::info;
499+
else if (levelStr == "warn") logCfg.level = mcpp::log::Level::warn;
500+
else if (levelStr == "error") logCfg.level = mcpp::log::Level::error;
501+
logCfg.maxFileSize = static_cast<std::size_t>(
502+
doc->get_int("log.max_file_size").value_or(10 * 1024 * 1024));
503+
logCfg.maxFiles = static_cast<int>(
504+
doc->get_int("log.max_files").value_or(3));
505+
mcpp::log::init(logCfg);
506+
}
507+
484508
// [index.repos.NAME] tables
485509
if (auto* repos = doc->get_table("index.repos")) {
486510
for (auto& [name, val] : *repos) {

src/log.cppm

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// mcpp.log — file-based debug logger with verbose terminal output.
2+
//
3+
// Usage:
4+
// mcpp::log::init(cfg); // call once at startup
5+
// mcpp::log::debug("tag", "msg"); // writes to ~/.mcpp/log/mcpp.log
6+
// mcpp::log::verbose("tag", "msg"); // file + stderr (when --verbose)
7+
//
8+
// Configuration (priority order):
9+
// 1. MCPP_LOG_LEVEL env var: "debug" | "info" | "warn" | "error" | "off"
10+
// 2. config.toml [log].level
11+
// 3. Default: "off"
12+
//
13+
// Log file: <logDir>/mcpp.log (rotated at max_file_size, keeps max_files)
14+
15+
module;
16+
#include <ctime>
17+
#include <cstdio>
18+
19+
export module mcpp.log;
20+
21+
import std;
22+
23+
export namespace mcpp::log {
24+
25+
enum class Level { off, error, warn, info, debug };
26+
27+
struct Config {
28+
Level level = Level::off;
29+
std::size_t maxFileSize = 10 * 1024 * 1024; // 10MB
30+
int maxFiles = 3;
31+
std::filesystem::path logDir;
32+
};
33+
34+
void init(const Config& cfg);
35+
36+
void debug(std::string_view tag, std::string_view message);
37+
void info (std::string_view tag, std::string_view message);
38+
void warn (std::string_view tag, std::string_view message);
39+
void error(std::string_view tag, std::string_view message);
40+
41+
// verbose: writes to file (level >= info) AND stderr (when --verbose).
42+
void set_verbose(bool v);
43+
bool is_verbose();
44+
void verbose(std::string_view tag, std::string_view message);
45+
46+
// Check if a level is enabled (avoid constructing expensive messages).
47+
bool is_enabled(Level l);
48+
49+
} // namespace mcpp::log
50+
51+
// ─── Implementation ─────────────────────────────────────────────────
52+
53+
namespace mcpp::log {
54+
55+
namespace {
56+
57+
Level g_level = Level::off;
58+
bool g_verbose = false;
59+
std::filesystem::path g_logFile;
60+
std::size_t g_maxFileSize = 10 * 1024 * 1024;
61+
int g_maxFiles = 3;
62+
std::mutex g_mutex;
63+
64+
Level parse_level(const char* s) {
65+
if (!s || !*s) return Level::off;
66+
std::string v(s);
67+
for (auto& c : v) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
68+
if (v == "debug") return Level::debug;
69+
if (v == "info") return Level::info;
70+
if (v == "warn") return Level::warn;
71+
if (v == "error") return Level::error;
72+
return Level::off;
73+
}
74+
75+
std::string timestamp() {
76+
auto now = std::chrono::system_clock::now();
77+
auto tt = std::chrono::system_clock::to_time_t(now);
78+
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
79+
now.time_since_epoch()) % 1000;
80+
std::tm tm{};
81+
#if defined(_WIN32)
82+
localtime_s(&tm, &tt);
83+
#else
84+
localtime_r(&tt, &tm);
85+
#endif
86+
char buf[32];
87+
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d.%03d",
88+
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
89+
tm.tm_hour, tm.tm_min, tm.tm_sec, static_cast<int>(ms.count()));
90+
return buf;
91+
}
92+
93+
const char* level_str(Level l) {
94+
switch (l) {
95+
case Level::debug: return "DEBUG";
96+
case Level::info: return "INFO ";
97+
case Level::warn: return "WARN ";
98+
case Level::error: return "ERROR";
99+
default: return " ";
100+
}
101+
}
102+
103+
void rotate() {
104+
if (g_logFile.empty() || g_maxFiles <= 0) return;
105+
std::error_code ec;
106+
auto size = std::filesystem::file_size(g_logFile, ec);
107+
if (ec || size < g_maxFileSize) return;
108+
109+
// mcpp.log.2 → delete, mcpp.log.1 → mcpp.log.2, mcpp.log → mcpp.log.1
110+
for (int i = g_maxFiles - 1; i >= 1; --i) {
111+
auto src = g_logFile;
112+
src += "." + std::to_string(i);
113+
auto dst = g_logFile;
114+
dst += "." + std::to_string(i + 1);
115+
if (i == g_maxFiles - 1) {
116+
std::filesystem::remove(src, ec);
117+
} else {
118+
std::filesystem::rename(src, dst, ec);
119+
}
120+
}
121+
auto first = g_logFile;
122+
first += ".1";
123+
std::filesystem::rename(g_logFile, first, ec);
124+
}
125+
126+
void write_log(Level level, std::string_view tag, std::string_view message) {
127+
if (level > g_level || g_level == Level::off) return;
128+
if (g_logFile.empty()) return;
129+
130+
std::lock_guard lock(g_mutex);
131+
rotate();
132+
std::ofstream ofs(g_logFile, std::ios::app);
133+
if (!ofs) return;
134+
ofs << timestamp() << " [" << level_str(level) << "] "
135+
<< tag << ": " << message << '\n';
136+
}
137+
138+
void write_stderr(std::string_view tag, std::string_view message) {
139+
// Dim gray for verbose output so it doesn't compete with ui::status
140+
std::fprintf(stderr, "\033[2m[VERBOSE] %.*s: %.*s\033[0m\n",
141+
static_cast<int>(tag.size()), tag.data(),
142+
static_cast<int>(message.size()), message.data());
143+
}
144+
145+
} // namespace
146+
147+
void init(const Config& cfg) {
148+
// Priority: env var > --verbose > config > default
149+
if (auto* e = std::getenv("MCPP_LOG_LEVEL"); e && *e) {
150+
g_level = parse_level(e);
151+
} else if (g_verbose && cfg.level == Level::off) {
152+
g_level = Level::info; // --verbose auto-enables info
153+
} else {
154+
g_level = cfg.level;
155+
}
156+
157+
g_maxFileSize = cfg.maxFileSize;
158+
g_maxFiles = cfg.maxFiles;
159+
160+
if (g_level == Level::off && !g_verbose) return;
161+
162+
std::error_code ec;
163+
std::filesystem::create_directories(cfg.logDir, ec);
164+
g_logFile = cfg.logDir / "mcpp.log";
165+
166+
// Only write session marker on first init (avoid duplicate from re-init)
167+
static bool session_started = false;
168+
if (!session_started) {
169+
session_started = true;
170+
write_log(Level::info, "log",
171+
std::format("=== session start (level={} verbose={}) ===",
172+
level_str(g_level), g_verbose));
173+
}
174+
}
175+
176+
void set_verbose(bool v) {
177+
g_verbose = v;
178+
// If verbose enabled but file logging off, auto-enable info level
179+
if (v && g_level == Level::off) {
180+
g_level = Level::info;
181+
if (!g_logFile.empty()) return;
182+
// Deferred: logDir not set yet, init() will handle it
183+
}
184+
}
185+
186+
bool is_verbose() { return g_verbose; }
187+
188+
bool is_enabled(Level l) {
189+
return l <= g_level && g_level != Level::off;
190+
}
191+
192+
void debug(std::string_view tag, std::string_view message) {
193+
write_log(Level::debug, tag, message);
194+
}
195+
196+
void info(std::string_view tag, std::string_view message) {
197+
write_log(Level::info, tag, message);
198+
}
199+
200+
void warn(std::string_view tag, std::string_view message) {
201+
write_log(Level::warn, tag, message);
202+
}
203+
204+
void error(std::string_view tag, std::string_view message) {
205+
write_log(Level::error, tag, message);
206+
}
207+
208+
void verbose(std::string_view tag, std::string_view message) {
209+
write_log(Level::info, tag, message);
210+
if (g_verbose) {
211+
write_stderr(tag, message);
212+
}
213+
}
214+
215+
} // namespace mcpp::log

0 commit comments

Comments
 (0)