From 61ec593c95f3210bdfcd22539d43ef507210981f Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Wed, 21 Jan 2026 19:24:49 +0100 Subject: Add logging --- README.md | 19 ++++++++++ examples/.gitignore | 1 + examples/Makefile | 7 +++- examples/logging.c | 25 +++++++++++++ nonstd.h | 103 ++++++++++++++++++++++++++++++++++++++++++++++++---- tests.c | 66 +++++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 examples/logging.c diff --git a/README.md b/README.md index b509377..7004467 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ ergonomic and productive. It aims to be C99 compliant. - **Slices (`slice`)**: Generic non-owning views into arrays. - **Memory Arena**: Simple block-based arena allocator for bulk memory management. - **File I/O**: Helper functions to read and write entire files with a single call. +- **Logging**: Simple, leveled logging with ANSI colors and timestamps. ## Installation @@ -165,6 +166,24 @@ sb_free(&file_sb); ``` +### 6. Logging + +Simple logging with levels (`ERROR`, `WARN`, `INFO`, `DEBUG`), timestamps, and colors. + +```c +// Set log level (default is INFO) +set_log_level(LOG_DEBUG); + +// Use macros for logging +LOG_INFO_MSG("Starting application..."); +LOG_DEBUG_MSG("Variable x = %d", 42); +LOG_WARN_MSG("Low memory warning"); +LOG_ERROR_MSG("Connection failed"); + +// Environment variable override supported: +// LOG_LEVEL=0 (ERROR) ... 3 (DEBUG) +``` + ## Testing The project includes a test suite using `minunit`. diff --git a/examples/.gitignore b/examples/.gitignore index 0707ee1..dcb0ae9 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -5,4 +5,5 @@ array slice arena files +logging diff --git a/examples/Makefile b/examples/Makefile index a614627..cd83e92 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -5,7 +5,7 @@ CFLAGS = -Wall -Wextra -std=c99 -fsanitize=address -g -O0 LDFLAGS = # Example targets -EXAMPLES = foreach stringv stringb array slice arena files +EXAMPLES = foreach stringv stringb array slice arena files logging # Default target all: $(EXAMPLES) @@ -32,6 +32,9 @@ arena: arena.c files: files.c $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) +logging: logging.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + # Run all examples run: all @echo "\n=== Running stringv ===\n" @@ -48,6 +51,8 @@ run: all @./arena @echo "\n=== Running files ===\n" @./files + @echo "\n=== Running logging ===\n" + @./logging # Clean build artifacts clean: diff --git a/examples/logging.c b/examples/logging.c new file mode 100644 index 0000000..1d2744c --- /dev/null +++ b/examples/logging.c @@ -0,0 +1,25 @@ +#define NONSTD_IMPLEMENTATION +#include "../nonstd.h" + +int main(void) { + // Default level is LOG_INFO + LOG_INFO_MSG("This is an info message: %d", 42); + LOG_DEBUG_MSG("This debug message will NOT be shown by default"); + + // Change level to LOG_DEBUG + set_log_level(LOG_DEBUG); + LOG_DEBUG_MSG("Now debug messages are shown: %s", "hello"); + + // Warnings and Errors + LOG_WARN_MSG("This is a warning!"); + LOG_ERROR_MSG("This is an error!"); + + // Environment variable override test + // You can set LOG_LEVEL=1 (WARN) etc. + LogLevel env_level = get_log_level_from_env(); + if (env_level != LOG_DEBUG) { + printf("Environment overrides level to: %d\n", env_level); + } + + return 0; +} diff --git a/nonstd.h b/nonstd.h index 57e8da4..b826dfb 100644 --- a/nonstd.h +++ b/nonstd.h @@ -1,3 +1,9 @@ +#ifdef NONSTD_IMPLEMENTATION +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif + #ifndef NONSTD_H #define NONSTD_H @@ -6,6 +12,10 @@ #include #include #include +#include +#include +#include +#include #ifndef NONSTD_DEF #ifdef NONSTD_STATIC @@ -88,9 +98,7 @@ NONSTD_DEF void sb_append_char(stringb *sb, char c); NONSTD_DEF stringv sb_as_sv(const stringb *sb); // Slice - generic non-owning view into an array -// Usage: -// SLICE_DEF(int); // Define slice_int type -// slice(int) view = ...; // Use it +// Usage: SLICE_DEF(int); slice(int) view = ...; #define SLICE_DEF(T) \ typedef struct { \ T *data; \ @@ -228,8 +236,6 @@ NONSTD_DEF stringv sb_as_sv(const stringb *sb); index < (arr).length && ((var) = (arr).data[index], 1); ++index) // Arena - block-based memory allocator -// Usage: Arena a = arena_make(); void* p = arena_alloc(&a, 100); -// arena_free(&a); typedef struct { char *ptr; char *end; @@ -246,11 +252,33 @@ NONSTD_DEF void arena_free(Arena *a); // File I/O helpers NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size); NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t size); -// read_entire_file_sv removed for security (ownership confusion) NONSTD_DEF stringb read_entire_file_sb(const char *filepath); NONSTD_DEF int write_file_sv(const char *filepath, stringv sv); NONSTD_DEF int write_file_sb(const char *filepath, const stringb *sb); +// Logging +typedef enum { + LOG_ERROR, + LOG_WARN, + LOG_INFO, + LOG_DEBUG, +} LogLevel; + +NONSTD_DEF void set_log_level(LogLevel level); +NONSTD_DEF LogLevel get_log_level_from_env(void); +NONSTD_DEF void log_message(FILE *stream, LogLevel level, const char *format, ...); + +#define LOG_INFO_MSG(...) log_message(stdout, LOG_INFO, __VA_ARGS__) +#define LOG_DEBUG_MSG(...) log_message(stdout, LOG_DEBUG, __VA_ARGS__) +#define LOG_WARN_MSG(...) log_message(stderr, LOG_WARN, __VA_ARGS__) +#define LOG_ERROR_MSG(...) log_message(stderr, LOG_ERROR, __VA_ARGS__) + +#define COLOR_RESET "\033[0m" +#define COLOR_INFO "\033[32m" +#define COLOR_DEBUG "\033[36m" +#define COLOR_WARNING "\033[33m" +#define COLOR_ERROR "\033[31m" + #endif // NONSTD_H #ifdef NONSTD_IMPLEMENTATION @@ -474,8 +502,6 @@ NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t return written == size; } -// read_entire_file_sv removed - NONSTD_DEF stringb read_entire_file_sb(const char *filepath) { size_t size = 0; char *data = read_entire_file(filepath, &size); @@ -496,4 +522,65 @@ NONSTD_DEF int write_file_sb(const char *filepath, const stringb *sb) { return write_entire_file(filepath, sb->data, sb->length); } +// Logging Implementation + +static LogLevel max_level = LOG_INFO; + +static const char *level_strings[] = { + "ERROR", + "WARN", + "INFO", + "DEBUG", +}; + +static const char *level_colors[] = { + COLOR_ERROR, + COLOR_WARNING, + COLOR_INFO, + COLOR_DEBUG, +}; + +NONSTD_DEF void set_log_level(LogLevel level) { + max_level = level; +} + +NONSTD_DEF LogLevel get_log_level_from_env(void) { + const char *env = getenv("LOG_LEVEL"); + if (env) { + int level = atoi(env); + if (level >= 0 && level <= 3) { + return (LogLevel)level; + } + } + + return max_level; +} + +NONSTD_DEF void log_message(FILE *stream, LogLevel level, const char *format, ...) { + if (max_level < level) + return; + + struct timeval tv; + gettimeofday(&tv, NULL); + struct tm *tm_info = localtime(&tv.tv_sec); + + char time_str[24]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + + const char *color = isatty(fileno(stream)) ? level_colors[level] : ""; + const char *reset = isatty(fileno(stream)) ? COLOR_RESET : ""; + + const char *log_format = "%s[%s.%03d] [%-5s] "; + fprintf(stream, log_format, color, time_str, (int)(tv.tv_usec / 1000), + level_strings[level]); + + va_list args; + va_start(args, format); + vfprintf(stream, format, args); + va_end(args); + + fprintf(stream, "%s\n", reset); + fflush(stream); +} + #endif // NONSTD_IMPLEMENTATION \ No newline at end of file diff --git a/tests.c b/tests.c index 32da92f..9eb7469 100644 --- a/tests.c +++ b/tests.c @@ -857,6 +857,64 @@ MU_TEST(test_file_io_read_missing_sb) { mu_assert_int_eq(0, sb.length); } +// Logging tests +MU_TEST(test_logging_level_filtering) { + FILE *tmp = tmpfile(); + mu_check(tmp != NULL); + + set_log_level(LOG_WARN); + + log_message(tmp, LOG_INFO, "Info message"); // Should not be logged + log_message(tmp, LOG_ERROR, "Error message"); // Should be logged + + rewind(tmp); + char buffer[1024]; + size_t read = fread(buffer, 1, sizeof(buffer), tmp); + buffer[read] = '\0'; + + mu_check(strstr(buffer, "Info message") == NULL); + mu_check(strstr(buffer, "Error message") != NULL); + mu_check(strstr(buffer, "[ERROR]") != NULL); + + fclose(tmp); +} + +MU_TEST(test_logging_env_level) { + // Enum: ERROR=0, WARN=1, INFO=2, DEBUG=3 + setenv("LOG_LEVEL", "0", 1); // ERROR + mu_assert_int_eq(LOG_ERROR, get_log_level_from_env()); + + setenv("LOG_LEVEL", "3", 1); // DEBUG + mu_assert_int_eq(LOG_DEBUG, get_log_level_from_env()); + + // Invalid level + set_log_level(LOG_INFO); // Reset + setenv("LOG_LEVEL", "99", 1); + mu_assert_int_eq(LOG_INFO, get_log_level_from_env()); // Should return current max_level (defaults check) + + unsetenv("LOG_LEVEL"); +} + +MU_TEST(test_logging_format) { + FILE *tmp = tmpfile(); + mu_check(tmp != NULL); + + set_log_level(LOG_INFO); + log_message(tmp, LOG_INFO, "Test %d %s", 123, "format"); + + rewind(tmp); + char buffer[1024]; + size_t read = fread(buffer, 1, sizeof(buffer), tmp); + buffer[read] = '\0'; + + mu_check(strstr(buffer, "Test 123 format") != NULL); + mu_check(strstr(buffer, "[INFO ]") != NULL); + // Check timestamp format roughly (YYYY-MM-DD) + mu_check(strstr(buffer, "20") != NULL); + + fclose(tmp); +} + // Test suites MU_TEST_SUITE(test_suite_stringv) { printf("\n[String View Tests]\n"); @@ -960,6 +1018,13 @@ MU_TEST_SUITE(test_suite_files) { RUN_TEST_WITH_NAME(test_file_io_read_missing_sb); } +MU_TEST_SUITE(test_suite_logging) { + printf("\n[Logging Tests]\n"); + RUN_TEST_WITH_NAME(test_logging_level_filtering); + RUN_TEST_WITH_NAME(test_logging_env_level); + RUN_TEST_WITH_NAME(test_logging_format); +} + int main(int argc, char *argv[]) { (void)argc; (void)argv; @@ -972,6 +1037,7 @@ int main(int argc, char *argv[]) { MU_RUN_SUITE(test_suite_types); MU_RUN_SUITE(test_suite_arena); MU_RUN_SUITE(test_suite_files); + MU_RUN_SUITE(test_suite_logging); MU_REPORT(); -- cgit v1.2.3