From 5e06c9c2a425245c79c523835f4ed32c7092279e Mon Sep 17 00:00:00 2001 From: Santanu Sinha Date: Thu, 4 Jun 2026 10:24:43 +0530 Subject: [PATCH 1/2] feat(log): Read log level from the env Log level can be set at runtime using the CBM_LOG_LEVEL environment variable. Allowed values: - Numeric: - 0 - DEBUG - 1 - INFO - 2 - WARN - 3 - ERROR - 4 - NONE - Textual: - DEBUG/debug - DEBUG - INFO/info - INFO - WARN/warn - WARN - ERROR/error - ERROR - NONE/none - NONE Unknown values will be ignored and the system will stay at current behaviour. Tests added and the README has been updated. Note: cbm_log_init_from_env() is called on startup once before the first log statement. Changes: - src/foundation/log.c: implement cbm_log_init_from_env() - src/foundation/log.h: declare cbm_log_init_from_env(); update comment - src/main.c: call cbm_log_init_from_env() early in main() - tests/test_log.c: add 5 test cases covering valid strings, case-insensitivity, unknown values, unset var, and numeric values - README.md: document CBM_LOG_LEVEL in the environment variables table --- README.md | 1 + src/foundation/log.c | 39 ++++++++++++++++ src/foundation/log.h | 9 +++- src/main.c | 1 + tests/test_log.c | 104 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 151 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ab03e9f4..e8962235 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,7 @@ codebase-memory-mcp config reset auto_index # reset to default | `CBM_CACHE_DIR` | `~/.cache/codebase-memory-mcp` | Override the database storage directory. All project indexes and config are stored here. | | `CBM_DIAGNOSTICS` | `false` | Set to `1` or `true` to enable periodic diagnostics output to `/tmp/cbm-diagnostics-.json`. | | `CBM_DOWNLOAD_URL` | *(GitHub releases)* | Override the download URL for updates. Used for testing or self-hosted deployments. | +| `CBM_LOG_LEVEL` | `info` | Set the minimum log level. Accepted values (case-insensitive): `debug`, `info`, `warn`, `error`, `none` — or their numeric equivalents `0`–`4` matching the internal enum. Logs go to stderr; stdout is reserved for MCP JSON-RPC. | | `CBM_WORKERS` | *(detected)* | Override the parallel-indexing worker count returned by `cbm_default_worker_count`. Useful inside containers where `sysconf(_SC_NPROCESSORS_ONLN)` reports host CPUs rather than the cgroup's effective quota. Range 1–256; invalid values are ignored with a warning. | ```bash diff --git a/src/foundation/log.c b/src/foundation/log.c index b4daa229..a036a86b 100644 --- a/src/foundation/log.c +++ b/src/foundation/log.c @@ -7,10 +7,49 @@ #include #include #include +#include +#include static CBMLogLevel g_log_level = CBM_LOG_INFO; static cbm_log_sink_fn g_log_sink = NULL; +void cbm_log_init_from_env(void) { + /* Read CBM_LOG_LEVEL — use getenv() directly: this runs before any threads + * are created, so there is no concurrent setenv() race to worry about. */ + const char *val = getenv("CBM_LOG_LEVEL"); + if (!val || val[0] == '\0') { + return; + } + + /* Case-insensitive comparison via tolower on first char + strcmp */ + char lo[16]; + size_t i = 0; + while (i < sizeof(lo) - 1 && val[i] != '\0') { + char c = val[i]; + if (c >= 'A' && c <= 'Z') { + c = (char)(c + ('a' - 'A')); + } + lo[i] = c; + i++; + } + lo[i] = '\0'; + + if (strcmp(lo, "debug") == 0) { cbm_log_set_level(CBM_LOG_DEBUG); } + else if (strcmp(lo, "info") == 0) { cbm_log_set_level(CBM_LOG_INFO); } + else if (strcmp(lo, "warn") == 0) { cbm_log_set_level(CBM_LOG_WARN); } + else if (strcmp(lo, "error") == 0) { cbm_log_set_level(CBM_LOG_ERROR); } + else if (strcmp(lo, "none") == 0) { cbm_log_set_level(CBM_LOG_NONE); } + else { + /* Try numeric: 0=debug 1=info 2=warn 3=error 4=none */ + char *end; + long n = strtol(val, &end, 10); + if (end != val && *end == '\0' && n >= CBM_LOG_DEBUG && n <= CBM_LOG_NONE) { + cbm_log_set_level((CBMLogLevel)n); + } + /* Unknown value: leave level unchanged (fail-open). */ + } +} + void cbm_log_set_sink(cbm_log_sink_fn fn) { g_log_sink = fn; } diff --git a/src/foundation/log.h b/src/foundation/log.h index e61eb013..32c79e86 100644 --- a/src/foundation/log.h +++ b/src/foundation/log.h @@ -5,7 +5,7 @@ * - All output goes to stderr (stdout is reserved for MCP JSON-RPC) * - Structured format: "level=info msg=pass.timing pass=defs elapsed_ms=42" * - Levels: DEBUG, INFO, WARN, ERROR - * - Level filtering at compile time (CBM_LOG_MIN_LEVEL) and runtime + * - Level filtering at runtime via CBM_LOG_LEVEL env var, or cbm_log_set_level() * - Thread-safe (each fprintf is atomic on POSIX for lines < PIPE_BUF) */ #ifndef CBM_LOG_H @@ -21,12 +21,17 @@ typedef enum { CBM_LOG_NONE = 4 /* disable all logging */ } CBMLogLevel; +/* Initialise log level from the CBM_LOG_LEVEL environment variable. + * Accepted values (case-insensitive): debug, info, warn, error, none. + * Unknown values are silently ignored and the level stays at its current + * value. Call once at startup, before the first log statement. */ +void cbm_log_init_from_env(void); + /* Set minimum log level (default: INFO). */ void cbm_log_set_level(CBMLogLevel level); /* Get current log level. */ CBMLogLevel cbm_log_get_level(void); - /* Core logging function. msg is a short semantic tag. * Variadic args are key-value pairs: (const char *key, const char *value)... * Terminated by NULL key. diff --git a/src/main.c b/src/main.c index d413d9dd..dc3578c2 100644 --- a/src/main.c +++ b/src/main.c @@ -340,6 +340,7 @@ static void setup_signal_handlers(void) { int main(int argc, char **argv) { cbm_profile_init(); /* reads CBM_PROFILE env var, gates all prof macros */ + cbm_log_init_from_env(); /* apply CBM_LOG_LEVEL before any output */ int subcmd = handle_subcommand(argc, argv); if (subcmd >= 0) { return subcmd; diff --git a/tests/test_log.c b/tests/test_log.c index 4d55a3b6..effdc679 100644 --- a/tests/test_log.c +++ b/tests/test_log.c @@ -6,7 +6,7 @@ */ #include "test_framework.h" #include "../src/foundation/log.h" -#include "../src/foundation/compat.h" +#include "../src/foundation/compat.h" /* cbm_setenv / cbm_unsetenv */ #include #ifndef _WIN32 #include @@ -112,6 +112,103 @@ TEST(log_int_helper) { PASS(); } +TEST(log_level_from_env_valid) { + /* Valid level strings set the expected level */ + cbm_setenv("CBM_LOG_LEVEL", "error", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_ERROR); + + cbm_setenv("CBM_LOG_LEVEL", "warn", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_WARN); + + cbm_setenv("CBM_LOG_LEVEL", "debug", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_DEBUG); + + cbm_setenv("CBM_LOG_LEVEL", "none", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_NONE); + + cbm_setenv("CBM_LOG_LEVEL", "info", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_INFO); + + cbm_unsetenv("CBM_LOG_LEVEL"); + cbm_log_set_level(CBM_LOG_INFO); /* restore */ + PASS(); +} + +TEST(log_level_from_env_case_insensitive) { + /* Level strings are matched case-insensitively */ + cbm_setenv("CBM_LOG_LEVEL", "ERROR", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_ERROR); + + cbm_setenv("CBM_LOG_LEVEL", "Warn", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_WARN); + + cbm_unsetenv("CBM_LOG_LEVEL"); + cbm_log_set_level(CBM_LOG_INFO); /* restore */ + PASS(); +} + +TEST(log_level_from_env_unknown_ignored) { + /* Unknown values leave the level unchanged */ + cbm_log_set_level(CBM_LOG_INFO); + cbm_setenv("CBM_LOG_LEVEL", "verbose", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_INFO); + + cbm_unsetenv("CBM_LOG_LEVEL"); + PASS(); +} + +TEST(log_level_from_env_unset_ignored) { + /* Missing env var leaves the level unchanged */ + cbm_unsetenv("CBM_LOG_LEVEL"); + cbm_log_set_level(CBM_LOG_WARN); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_WARN); + + cbm_log_set_level(CBM_LOG_INFO); /* restore */ + PASS(); +} + +TEST(log_level_from_env_numeric) { + /* Numeric values mirror the CBMLogLevel enum: 0=debug 1=info 2=warn 3=error 4=none */ + cbm_setenv("CBM_LOG_LEVEL", "0", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_DEBUG); + + cbm_setenv("CBM_LOG_LEVEL", "1", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_INFO); + + cbm_setenv("CBM_LOG_LEVEL", "2", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_WARN); + + cbm_setenv("CBM_LOG_LEVEL", "3", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_ERROR); + + cbm_setenv("CBM_LOG_LEVEL", "4", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_NONE); + + /* Out-of-range numeric: leave level unchanged */ + cbm_log_set_level(CBM_LOG_INFO); + cbm_setenv("CBM_LOG_LEVEL", "5", 1); + cbm_log_init_from_env(); + ASSERT_EQ(cbm_log_get_level(), CBM_LOG_INFO); + + cbm_unsetenv("CBM_LOG_LEVEL"); + cbm_log_set_level(CBM_LOG_INFO); /* restore */ + PASS(); +} + SUITE(log) { RUN_TEST(log_level_default); RUN_TEST(log_level_set); @@ -119,4 +216,9 @@ SUITE(log) { RUN_TEST(log_filtered_by_level); RUN_TEST(log_error_output); RUN_TEST(log_int_helper); + RUN_TEST(log_level_from_env_valid); + RUN_TEST(log_level_from_env_case_insensitive); + RUN_TEST(log_level_from_env_unknown_ignored); + RUN_TEST(log_level_from_env_unset_ignored); + RUN_TEST(log_level_from_env_numeric); } From a886bea0ff521914a8ebb4eba6b41d25ca21f717 Mon Sep 17 00:00:00 2001 From: Santanu Sinha Date: Thu, 4 Jun 2026 10:59:51 +0530 Subject: [PATCH 2/2] Removed pointless comment --- tests/test_log.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_log.c b/tests/test_log.c index effdc679..de5583e3 100644 --- a/tests/test_log.c +++ b/tests/test_log.c @@ -6,7 +6,7 @@ */ #include "test_framework.h" #include "../src/foundation/log.h" -#include "../src/foundation/compat.h" /* cbm_setenv / cbm_unsetenv */ +#include "../src/foundation/compat.h" #include #ifndef _WIN32 #include