From b96c699e337209556c3636ddc5331036ad5d88b8 Mon Sep 17 00:00:00 2001 From: lai3d Date: Fri, 22 May 2026 21:02:45 +0800 Subject: [PATCH] feat: scaffold CMake project + DESIGN.md (source of truth for Codex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lays down the C++23 / LLVM-only scaffold that Codex will implement against. The architecture mirrors merlion-node-exporter-rs one-to-one so a scrape diff between the two binaries reduces to numeric / label- order noise. - CMakeLists.txt — C++23, LLVM clang fatal-errors on Apple Clang, matches the toolchain pattern from merlion-tsdb-cpp - cmake/ToolchainLLVM.cmake — pins Homebrew LLVM - src/main.cpp + src/placeholder.cpp — entry point + structural stub - include/merlion_node_exporter/version.hpp.in — generated header - tests/placeholder_test.cpp — first GoogleTest case (will be replaced when real collectors land) - docs/DESIGN.md — source-of-truth design doc covering Metric model, encoder, registry, HTTP server, collector contract, CLI, and the 15-collector MVP plan. Disagreement between this doc and code is a bug; update the doc before introducing a non-trivial design choice. - .github/workflows/ci.yml — Linux + macOS LLVM build/test matrix Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 89 ++++ CMakeLists.txt | 186 ++++++++ cmake/ToolchainLLVM.cmake | 29 ++ docs/DESIGN.md | 436 +++++++++++++++++++ include/merlion_node_exporter/version.hpp.in | 11 + src/main.cpp | 28 ++ src/placeholder.cpp | 6 + tests/placeholder_test.cpp | 8 + 8 files changed, 793 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 CMakeLists.txt create mode 100644 cmake/ToolchainLLVM.cmake create mode 100644 docs/DESIGN.md create mode 100644 include/merlion_node_exporter/version.hpp.in create mode 100644 src/main.cpp create mode 100644 src/placeholder.cpp create mode 100644 tests/placeholder_test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8320a12 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: build (${{ matrix.os }} / ${{ matrix.compiler }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + compiler: clang-18 + cc: clang-18 + cxx: clang++-18 + extra_apt: clang-18 libc++-18-dev libc++abi-18-dev + cxxflags: "-stdlib=libc++" + ldflags: "-stdlib=libc++" + - os: ubuntu-24.04 + compiler: gcc-14 + cc: gcc-14 + cxx: g++-14 + extra_apt: gcc-14 g++-14 + cxxflags: "" + ldflags: "" + - os: macos-14 + compiler: brew-llvm + cc: brew-clang + cxx: brew-clang++ + extra_apt: "" + cxxflags: "" + ldflags: "" + steps: + - uses: actions/checkout@v4 + + - name: Install Linux toolchain + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ${{ matrix.extra_apt }} cmake ninja-build + echo "CC=${{ matrix.cc }}" >> "$GITHUB_ENV" + echo "CXX=${{ matrix.cxx }}" >> "$GITHUB_ENV" + + - name: Install macOS toolchain + if: runner.os == 'macOS' + run: | + brew update + brew install llvm cmake ninja + LLVM_PREFIX="$(brew --prefix llvm)" + echo "CC=$LLVM_PREFIX/bin/clang" >> "$GITHUB_ENV" + echo "CXX=$LLVM_PREFIX/bin/clang++" >> "$GITHUB_ENV" + + - name: Configure + env: + CXXFLAGS: ${{ matrix.cxxflags }} + LDFLAGS: ${{ matrix.ldflags }} + run: | + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DMNE_BUILD_TESTS=ON + + - name: Build + run: cmake --build build -j + + - name: Run smoke binary + run: ./build/merlion-node-exporter --version + + - name: Run tests + run: ctest --test-dir build --output-on-failure + + clang-format: + name: clang-format + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install clang-format + run: sudo apt-get update && sudo apt-get install -y clang-format-18 + # clang-format-18 supports c++20 mode used by our .clang-format + - name: Check formatting + run: | + files=$(git ls-files '*.cpp' '*.hpp' '*.hpp.in' || true) + if [ -n "$files" ]; then + clang-format-18 --dry-run --Werror $files + fi diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6270bbd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,186 @@ +cmake_minimum_required(VERSION 3.28) + +project(merlion-node-exporter-cpp + VERSION 0.1.0 + DESCRIPTION "Modern C++23 reimplementation of Prometheus node_exporter." + HOMEPAGE_URL "https://github.com/MerlionOS/merlion-node-exporter-cpp" + LANGUAGES CXX +) + +# --------------------------------------------------------------------------- +# Project options +# --------------------------------------------------------------------------- +option(MNE_BUILD_TESTS "Build Catch2 unit tests" ON) +option(MNE_ENABLE_LTO "Enable link-time optimisation in release builds" ON) +option(MNE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" ON) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) +endif() + +# --------------------------------------------------------------------------- +# Compiler sanity check +# --------------------------------------------------------------------------- +# Apple Clang's libc++ still lags on std::expected / std::format. Fail fast +# instead of silently producing a broken build. +if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + message(FATAL_ERROR + "Apple Clang is not supported. Install Homebrew LLVM and re-configure:\n" + " brew install llvm\n" + " export CC=\"$(brew --prefix llvm)/bin/clang\"\n" + " export CXX=\"$(brew --prefix llvm)/bin/clang++\"\n" + "Or pass -DCMAKE_TOOLCHAIN_FILE=cmake/ToolchainLLVM.cmake." + ) +endif() + +# --------------------------------------------------------------------------- +# Warnings +# --------------------------------------------------------------------------- +add_library(mne_compile_flags INTERFACE) +target_compile_features(mne_compile_flags INTERFACE cxx_std_23) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(mne_compile_flags INTERFACE + -Wall -Wextra -Wpedantic + -Wshadow -Wconversion -Wsign-conversion + -Wno-unused-parameter + ) + if(MNE_WARNINGS_AS_ERRORS) + target_compile_options(mne_compile_flags INTERFACE -Werror) + endif() +endif() + +# Release-only flags. +add_library(mne_release_flags INTERFACE) +target_compile_options(mne_release_flags INTERFACE + $<$:-O3> + $<$:-ffunction-sections> + $<$:-fdata-sections> +) +if(UNIX AND NOT APPLE) + target_link_options(mne_release_flags INTERFACE + $<$:LINKER:--gc-sections> + ) +elseif(APPLE) + target_link_options(mne_release_flags INTERFACE + $<$:LINKER:-dead_strip> + ) +endif() + +# LTO when supported. +if(MNE_ENABLE_LTO) + include(CheckIPOSupported) + check_ipo_supported(RESULT _ipo_ok OUTPUT _ipo_msg) + if(_ipo_ok) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO TRUE) + else() + message(STATUS "IPO/LTO not supported: ${_ipo_msg}") + endif() +endif() + +# --------------------------------------------------------------------------- +# Dependencies (FetchContent) +# --------------------------------------------------------------------------- +include(FetchContent) +set(FETCHCONTENT_QUIET FALSE) + +FetchContent_Declare( + cpp-httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.18.5 + GIT_SHALLOW TRUE +) +FetchContent_Declare( + CLI11 + GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git + GIT_TAG v2.4.2 + GIT_SHALLOW TRUE +) +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog.git + GIT_TAG v1.14.1 + GIT_SHALLOW TRUE +) + +set(HTTPLIB_COMPILE OFF CACHE BOOL "" FORCE) # header-only +set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE) +set(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(cpp-httplib CLI11 spdlog) + +# --------------------------------------------------------------------------- +# Generated version header +# --------------------------------------------------------------------------- +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/include/merlion_node_exporter/version.hpp.in + ${CMAKE_CURRENT_BINARY_DIR}/generated/include/merlion_node_exporter/version.hpp + @ONLY +) + +# --------------------------------------------------------------------------- +# Library target — everything except main.cpp +# --------------------------------------------------------------------------- +# Populated as files land. For now, define the target so dependencies wire up. +add_library(merlion_node_exporter_lib STATIC) +target_include_directories(merlion_node_exporter_lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_BINARY_DIR}/generated/include +) +target_link_libraries(merlion_node_exporter_lib + PUBLIC mne_compile_flags + PRIVATE httplib::httplib CLI11::CLI11 spdlog::spdlog mne_release_flags +) +# Empty source list — placeholder until collectors land. +target_sources(merlion_node_exporter_lib PRIVATE + src/placeholder.cpp +) + +# --------------------------------------------------------------------------- +# Executable +# --------------------------------------------------------------------------- +add_executable(merlion-node-exporter src/main.cpp) +target_link_libraries(merlion-node-exporter + PRIVATE merlion_node_exporter_lib mne_release_flags +) +set_target_properties(merlion-node-exporter PROPERTIES + OUTPUT_NAME merlion-node-exporter +) + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- +if(MNE_BUILD_TESTS) + enable_testing() + FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.7.1 + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(Catch2) + list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) + + add_executable(merlion_node_exporter_tests + tests/placeholder_test.cpp + ) + target_link_libraries(merlion_node_exporter_tests + PRIVATE merlion_node_exporter_lib Catch2::Catch2WithMain + ) + include(Catch) + catch_discover_tests(merlion_node_exporter_tests) +endif() + +# --------------------------------------------------------------------------- +# Install +# --------------------------------------------------------------------------- +include(GNUInstallDirs) +install(TARGETS merlion-node-exporter + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/cmake/ToolchainLLVM.cmake b/cmake/ToolchainLLVM.cmake new file mode 100644 index 0000000..ea3e73c --- /dev/null +++ b/cmake/ToolchainLLVM.cmake @@ -0,0 +1,29 @@ +# Optional toolchain file that forces the build to use Homebrew LLVM clang +# on macOS instead of Apple Clang. Apple Clang ships with a libc++ that +# trails mainline by several C++23 features we use (std::expected, +# std::format), so we fail fast rather than degrade silently. +# +# Usage: +# cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=cmake/ToolchainLLVM.cmake +# +# Or, equivalently, set $CC / $CXX in the environment before configuring. + +if(APPLE) + execute_process( + COMMAND brew --prefix llvm + OUTPUT_VARIABLE _LLVM_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _BREW_RESULT + ) + if(NOT _BREW_RESULT EQUAL 0) + message(FATAL_ERROR + "ToolchainLLVM.cmake: `brew --prefix llvm` failed. " + "Install with `brew install llvm` or unset the toolchain file." + ) + endif() + set(CMAKE_C_COMPILER "${_LLVM_PREFIX}/bin/clang" CACHE FILEPATH "" FORCE) + set(CMAKE_CXX_COMPILER "${_LLVM_PREFIX}/bin/clang++" CACHE FILEPATH "" FORCE) + # Make sure the rpath finds Homebrew's libc++ at runtime. + set(CMAKE_INSTALL_RPATH "${_LLVM_PREFIX}/lib/c++") + set(CMAKE_BUILD_RPATH "${_LLVM_PREFIX}/lib/c++") +endif() diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..2d6b50a --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,436 @@ +# merlion-node-exporter-cpp — Design + +This document is the **source of truth** for the architecture of the C++ +sibling of [`merlion-node-exporter-rs`][rs]. It captures every decision +that Codex (or any implementer) should not have to re-litigate. + +If you find yourself making a non-trivial choice that this document +doesn't cover, **stop and update this document first** so the choice +shows up in the design history rather than in code archaeology. + +[rs]: https://github.com/MerlionOS/merlion-node-exporter-rs + +--- + +## 1. Goals & non-goals + +### Goals + +- **Wire compatibility with upstream `node_exporter`.** A Prometheus + scrape config / dashboard / alerting rule that works against upstream + `node_exporter` must work against this binary, byte-for-byte, for the + collectors we ship. Metric names, label sets, type lines, and HELP + text all follow upstream conventions. +- **Wire compatibility with `merlion-node-exporter-rs`.** The two + Merlion implementations are interchangeable. A scrape diff between + them should reduce to numeric value differences and label-ordering + noise — never metric-name or shape differences. +- **Linux MVP scope.** The 15 collectors listed in + [§7 Implementation Plan](#7-implementation-plan) — same set as `-rs`. +- **Modern C++ idioms.** C++23, `std::expected`, `std::format`, + `std::string_view` at boundaries, RAII everywhere, `constexpr` where + it pays for itself. +- **Headers-only-where-practical dependencies.** Keep `FetchContent` + pulls minimal so the project builds cleanly on Linux and macOS without + package-manager assistance beyond `brew install llvm cmake`. + +### Non-goals + +- BSD / Darwin / Solaris / AIX collectors. macOS is supported as a + **build** target so contributors can compile on their laptops, but + the only collector that actually returns data on macOS is `uname`. + Everything else returns an error and degrades to + `node_scrape_collector_success{collector="..."} 0`, matching `-rs`'s + behaviour. +- Histograms, summaries, or OpenMetrics protobuf negotiation in the + MVP. The exposition encoder targets the text 0.0.4 subset that + node_exporter actually emits today. +- A Prometheus client-library dependency. The Metric model is + hand-rolled — see §3.2 for the rationale. + +--- + +## 2. Tech stack + +| Concern | Choice | Notes | +| --- | --- | --- | +| Language | C++23 | `rust-version` parallel: `cmake_minimum_required(VERSION 3.28)` | +| Compiler | Clang ≥ 18 (Homebrew LLVM on macOS) | Apple Clang is **not** supported — `std::expected` / `std::format` lag in libc++ | +| Build | CMake 3.28+ | `FetchContent` for deps, `ctest` for tests | +| HTTP | `cpp-httplib` v0.18+ | Blocking, header-only, perfect for the per-scrape model | +| CLI | `CLI11` v2.4+ | Header-only | +| Logging | `spdlog` v1.14+ | Mirrors the `tracing` setup in `-rs` | +| Tests | `Catch2` v3 | `FetchContent`'d | +| Format | `clang-format` | Style pinned in `.clang-format` (LLVM base + 4-space indent + 100-col line) | +| CI | GitHub Actions | Matrix: `ubuntu-24.04 (clang-18, gcc-14)` + `macos-14 (brew llvm)` | + +### Why these and not the alternatives + +- **`cpp-httplib` over Boost.Beast / Crow / Drogon.** Per-scrape blocking + handlers are the right model for a node exporter — there is nothing to + await. `cpp-httplib` is the smallest dependency that gives us a + single-header blocking HTTP server with thread-pool handling. Boost is + enormous; Crow / Drogon are async frameworks we don't need. +- **Hand-rolled Metric model.** Identical reasoning to `-rs`'s decision + to skip `prometheus-client`: the typed pre-registration pattern + popular client libraries optimise for is more verbose than helpful + when every scrape re-reads `/proc` fresh. +- **`CLI11` over Boost.Program_options / `argparse`.** Header-only, no + Boost, declarative builder API maps cleanly onto `clap` in `-rs` so + the two CLIs stay in sync. +- **Homebrew LLVM clang, not Apple Clang.** `std::expected` and + `std::format` are still partial in Apple's libc++. This matches the + project-wide toolchain decision in `merlion-tsdb-cpp`. + +--- + +## 3. Architecture + +### 3.1 Component map + +The C++ tree mirrors `-rs`'s module layout one-to-one. A reviewer who +knows one project should be able to find the equivalent file in the +other in five seconds. + +``` +merlion-node-exporter-cpp/ +├── include/merlion_node_exporter/ +│ ├── metric.hpp # Metric / Sample / MetricType +│ ├── encoding.hpp # Prometheus text-format encoder +│ ├── registry.hpp # Collector interface + Registry +│ ├── config.hpp # path.{procfs,sysfs,rootfs} +│ ├── server.hpp # cpp-httplib /metrics server +│ ├── cli.hpp # CLI11 argument struct +│ └── version.hpp # generated by CMake from project version +├── src/ +│ ├── encoding.cpp +│ ├── registry.cpp +│ ├── config.cpp +│ ├── server.cpp +│ ├── cli.cpp +│ ├── main.cpp +│ └── collectors/ +│ ├── loadavg.cpp +│ ├── meminfo.cpp +│ └── uname.cpp +├── tests/ +│ ├── encoding_test.cpp +│ ├── loadavg_test.cpp +│ ├── meminfo_test.cpp +│ └── uname_test.cpp +├── cmake/ +│ └── ToolchainLLVM.cmake # optional: forces brew LLVM on macOS +├── docs/ +│ └── DESIGN.md # ← you are here +├── .clang-format +├── .github/workflows/ci.yml +├── CMakeLists.txt +├── README.md +├── LICENSE +└── NOTICE +``` + +The `-rs` equivalents are: + +| C++ file | Rust file | +| --- | --- | +| `metric.hpp` | `src/metric.rs` | +| `encoding.{hpp,cpp}` | `src/encoding.rs` | +| `registry.{hpp,cpp}` | `src/registry.rs` | +| `config.{hpp,cpp}` | `src/config.rs` | +| `server.{hpp,cpp}` | `src/server.rs` | +| `cli.{hpp,cpp}` | `src/cli.rs` | +| `collectors/.cpp` | `src/collectors/.rs` | +| `main.cpp` | `src/main.rs` | + +### 3.2 Metric model + +Mirrors `-rs` exactly. Public surface: + +```cpp +namespace merlion::node_exporter { + +enum class MetricType { Counter, Gauge, Untyped }; + +struct Sample { + // Ordered key/value pairs; order is preserved as supplied by the + // collector so output is deterministic. + std::vector> labels; + double value = 0.0; +}; + +struct Metric { + std::string name; + std::string help; + MetricType mtype = MetricType::Untyped; + std::vector samples; +}; + +} // namespace merlion::node_exporter +``` + +Construction is plain aggregate / brace-init; no builder pattern. The +hand-rolled fluent style in `-rs` is unnecessary in C++ where +designated initialisers exist. + +### 3.3 Encoding + +The encoder writes Prometheus text format 0.0.4 byte-for-byte +compatible with `-rs`'s `src/encoding.rs`. Public surface: + +```cpp +namespace merlion::node_exporter::encoding { + +inline constexpr std::string_view content_type = + "text/plain; version=0.0.4; charset=utf-8"; + +std::string encode(std::span metrics); + +} // namespace merlion::node_exporter::encoding +``` + +Rules (must match `-rs`): + +1. Skip metric families with no samples. +2. Emit `# HELP \n` then `# TYPE \n` + per family. +3. One sample per line: `{} \n`. +4. Label-value escaping: `\` → `\\`, `"` → `\"`, `\n` → `\n` (literal + backslash-n). +5. Integer-valued doubles with `|v| < 1e15` print without a decimal + point; everything else uses `std::format("{}", v)`. +6. `NaN`, `+Inf`, `-Inf` printed literally. + +A round-trip test fixture in `tests/encoding_test.cpp` asserts that the +canonical output exactly equals the byte string produced by `-rs`'s +encoder for the same input. **This fixture is the contract** that keeps +the two implementations interchangeable. + +### 3.4 Collector interface + +```cpp +namespace merlion::node_exporter { + +struct Config { + std::filesystem::path procfs = "/proc"; + std::filesystem::path sysfs = "/sys"; + std::filesystem::path rootfs = "/"; + + // /proc/foo and foo both resolve to /foo. + std::filesystem::path proc_path(std::string_view rel) const; + std::filesystem::path sys_path(std::string_view rel) const; +}; + +class Collector { +public: + virtual ~Collector() = default; + virtual std::string_view name() const noexcept = 0; + virtual std::expected, std::string> + collect(const Config&) const = 0; +}; + +} // namespace merlion::node_exporter +``` + +- `std::expected` (C++23) is the canonical mechanism — no exceptions + across the collector boundary. Exceptions inside a collector + implementation are caught by the registry and converted to + `std::unexpected("…")`. +- `name()` returns a string literal (`"loadavg"`, `"meminfo"`, …). + Stable identifier used for `--no-collector.` flags and the + `collector` label on scrape-status metrics. +- Collectors are stateless and reused across scrapes. + +### 3.5 Registry + +```cpp +class Registry { +public: + void register_collector(std::unique_ptr); + std::vector enabled_names() const; + + // Runs every collector, appends per-collector + // node_scrape_collector_success and + // node_scrape_collector_duration_seconds, returns the flat metric list. + std::vector gather(const Config&) const; +}; +``` + +The two synthesised metric families (`node_scrape_collector_success`, +`node_scrape_collector_duration_seconds`) match `-rs` byte-for-byte — +same names, same labels, same HELP text. See +`src/registry.rs::Registry::gather` for the reference behaviour. + +### 3.6 HTTP server + +`cpp-httplib` blocking server bound to `--web.listen-address`. Single +`GET ` route. Steps per request: + +1. Run `registry.gather(config)`. +2. `encoding::encode(metrics)` into a `std::string`. +3. Respond with `Content-Type: text/plain; version=0.0.4; charset=utf-8`. + +Errors are logged via `spdlog` and produce a 500 with body +`"internal error\n"` — same wording as `-rs`. + +Graceful shutdown: `SIGINT` / `SIGTERM` triggers `server.stop()`. + +### 3.7 CLI + +Flag-for-flag parity with `-rs` (and therefore upstream `node_exporter`). +Use `CLI11`: + +```text +--web.listen-address default :9100 (env MNE_LISTEN_ADDRESS) +--web.telemetry-path default /metrics (env MNE_TELEMETRY_PATH) +--path.procfs default /proc (env MNE_PROCFS) +--path.sysfs default /sys (env MNE_SYSFS) +--path.rootfs default / (env MNE_ROOTFS) +--no-collector repeatable +--collector.only repeatable +``` + +`:9100` resolves to `0.0.0.0:9100` in the server-bind step, matching +`-rs`'s `Cli::resolved_listen_address`. + +### 3.8 Logging + +`spdlog` default logger; level controlled by `MNE_LOG_LEVEL` env +(values: `trace|debug|info|warn|error`, default `info`). The Rust side +honours `RUST_LOG`; the C++ side uses `MNE_LOG_LEVEL` to avoid +ambiguity. Document this divergence in the binary's `--help` output. + +--- + +## 4. Build + +`CMakeLists.txt` at the project root drives everything. Highlights: + +- `cmake_minimum_required(VERSION 3.28)` and `set(CMAKE_CXX_STANDARD 23)`. +- Detects the toolchain. On macOS, if `CMAKE_CXX_COMPILER` is not set, + emit a `FATAL_ERROR` pointing the user at the README's Homebrew LLVM + instructions — silently falling back to Apple Clang is a footgun. +- `FetchContent_Declare` blocks for cpp-httplib, CLI11, spdlog, Catch2, + pinned to specific versions (no `GIT_TAG main`). +- Builds two targets: + - `merlion_node_exporter_lib` (static): everything in `src/` except + `main.cpp`. + - `merlion-node-exporter` (executable): links the lib + `main.cpp`. +- `MNE_BUILD_TESTS` option (default `ON` when the project is the top + level, `OFF` when included as a subdir). When on, builds + `merlion_node_exporter_tests` against Catch2 and registers it with + `ctest`. +- Generates `include/merlion_node_exporter/version.hpp` from + `PROJECT_VERSION` so `--version` output stays in sync with + `CMakeLists.txt`. + +Performance-relevant flags for the release config: + +``` +-O3 -fno-plt -ffunction-sections -fdata-sections +-Wl,--gc-sections # Linux +-Wl,-dead_strip # macOS +``` + +LTO is enabled when supported (`check_ipo_supported`). + +--- + +## 5. Errors & observability + +- **Inside a collector**: prefer `std::expected` and short-circuit + helpers. If the kernel surface throws (`std::filesystem`), catch at + the collector boundary and return `std::unexpected`. +- **Registry**: catches exceptions from `Collector::collect`, logs them + via `spdlog::error`, records `node_scrape_collector_success=0`, still + records the duration sample. +- **Server**: any uncaught exception in a handler becomes a 500 + log + line. The process never exits because a single scrape failed. + +--- + +## 6. Test strategy + +- **Pure parsers** (`encoding`, `loadavg::parse`, `meminfo::parse`) + are tested with hard-coded fixtures stored inline in the test source. + Tests must include the same fixtures used by `-rs` so we know we + agree byte-for-byte on synthetic input. +- **Filesystem-bound collectors** are tested by pointing `Config::procfs` + at a temp directory the test populates. No `/proc` access in unit + tests. +- **Encoder round-trip**: `tests/encoding_test.cpp` includes a snapshot + text file (`tests/data/expected_scrape.txt`) and asserts the encoder + output equals it. The same snapshot is checked in to `-rs` and a CI + job in both repos runs the snapshot through both implementations + (TODO — track in [issue #2]). +- **Smoke test**: `ctest` starts the binary on a random port, scrapes + `/metrics`, asserts response code 200 and `Content-Type` correct. + +--- + +## 7. Implementation plan + +Ordered so the project is useful as soon as possible. One PR per +checkbox. Land scaffold + first three collectors first to validate the +architecture; everything after that is mechanical extension. + +### Scaffold + +- [ ] PR #1: `CMakeLists.txt`, `.clang-format`, `cmake/ToolchainLLVM.cmake`, + stub `main.cpp`, CI workflow. **Builds and runs `--version` and + exits 0**, even though no collectors exist yet. No HTTP server + wired up. + +### Core + +- [ ] PR #2: `include/.../metric.hpp` + tests +- [ ] PR #3: `encoding.{hpp,cpp}` + tests (must pass the cross-language + snapshot fixture) +- [ ] PR #4: `config.{hpp,cpp}` + tests +- [ ] PR #5: `registry.{hpp,cpp}` + tests (includes synthesised scrape + metrics) +- [ ] PR #6: `cli.{hpp,cpp}` + `server.{hpp,cpp}`, wires up `/metrics`. + `main.cpp` becomes the production entry point. + +### Seed collectors (mirror `-rs` scaffold PR) + +- [ ] PR #7: `loadavg` +- [ ] PR #8: `meminfo` +- [ ] PR #9: `uname` + +### Linux MVP (one PR each; same order as `-rs`) + +- [ ] PR #10: `cpu` — `/proc/stat` per-CPU jiffies +- [ ] PR #11: `diskstats` — `/proc/diskstats` +- [ ] PR #12: `netdev` — `/proc/net/dev` +- [ ] PR #13: `filesystem` — `getmntinfo` + `statvfs` +- [ ] PR #14: `stat` — `/proc/stat` (boot time, intr, ctxt, processes) +- [ ] PR #15: `vmstat` — `/proc/vmstat` +- [ ] PR #16: `netstat` — `/proc/net/{netstat,snmp,snmp6}` +- [ ] PR #17: `sockstat` — `/proc/net/sockstat{,6}` +- [ ] PR #18: `pressure` — `/proc/pressure/{cpu,memory,io}` +- [ ] PR #19: `hwmon` — `/sys/class/hwmon/` +- [ ] PR #20: `thermal_zone` — `/sys/class/thermal/thermal_zone*` +- [ ] PR #21: `time` — system clock + NTP sync state +- [ ] PR #22: `textfile` — `*.prom` files from a configured directory + +### Past MVP + +- [ ] Container image (`Dockerfile`) +- [ ] Homebrew formula in the `MerlionOS/homebrew-merlion` tap +- [ ] eBPF-backed collectors behind a CMake option (Linux only) + +--- + +## 8. Cross-implementation contract + +Whenever this document or `-rs`'s public behaviour changes in a way +that affects scrape output (new metric, new label, renamed metric, +changed HELP text, …), update **both** repos in lock-step: + +1. PR against this repo with the design update. +2. PR against `-rs` implementing the change. +3. Cross-link the PRs in both descriptions. + +Disagreement between the two implementations is a bug in whichever one +diverged from this document. diff --git a/include/merlion_node_exporter/version.hpp.in b/include/merlion_node_exporter/version.hpp.in new file mode 100644 index 0000000..7e3ad9a --- /dev/null +++ b/include/merlion_node_exporter/version.hpp.in @@ -0,0 +1,11 @@ +// Generated by CMake from CMakeLists.txt — do not edit by hand. +#pragma once + +namespace merlion::node_exporter { + +inline constexpr char version_string[] = "@PROJECT_VERSION@"; +inline constexpr int version_major = @PROJECT_VERSION_MAJOR@; +inline constexpr int version_minor = @PROJECT_VERSION_MINOR@; +inline constexpr int version_patch = @PROJECT_VERSION_PATCH@; + +} // namespace merlion::node_exporter diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0a8334f --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,28 @@ +// Entry point — replaced with the real CLI + server wiring once the core +// modules land (see docs/DESIGN.md §7 Implementation Plan). +// +// Today this binary just reports its version and exits, so the scaffold +// can be smoke-tested in CI before any collectors exist. + +#include +#include + +#include "merlion_node_exporter/version.hpp" + +namespace mne = merlion::node_exporter; + +int main(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + std::string_view arg{argv[i]}; + if (arg == "--version" || arg == "-V") { + std::printf("merlion-node-exporter %s\n", mne::version_string); + return 0; + } + } + std::printf( + "merlion-node-exporter %s — scaffold build, no collectors wired up.\n" + "See https://github.com/MerlionOS/merlion-node-exporter-cpp/blob/main/docs/DESIGN.md\n", + mne::version_string + ); + return 0; +} diff --git a/src/placeholder.cpp b/src/placeholder.cpp new file mode 100644 index 0000000..4f30fc8 --- /dev/null +++ b/src/placeholder.cpp @@ -0,0 +1,6 @@ +// Keeps merlion_node_exporter_lib non-empty until the first real translation +// unit lands. Delete this file in the PR that adds src/encoding.cpp. + +namespace merlion::node_exporter::detail { +inline constexpr int kScaffoldSentinel = 0; +} diff --git a/tests/placeholder_test.cpp b/tests/placeholder_test.cpp new file mode 100644 index 0000000..3ad46c6 --- /dev/null +++ b/tests/placeholder_test.cpp @@ -0,0 +1,8 @@ +// Smoke test asserting the test runner itself works before any collectors +// have a test of their own. Delete in the PR that adds the first real test. + +#include + +TEST_CASE("scaffold smoke test", "[scaffold]") { + REQUIRE(1 + 1 == 2); +}