Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<pid>.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
Expand Down
39 changes: 39 additions & 0 deletions src/foundation/log.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,49 @@
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
}
Expand Down
9 changes: 7 additions & 2 deletions src/foundation/log.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
102 changes: 102 additions & 0 deletions tests/test_log.c
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,113 @@ 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);
RUN_TEST(log_info_output);
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);
}