From cc7de6c0dda39344c19b6654024f4e52a41fc489 Mon Sep 17 00:00:00 2001 From: yukkop Date: Wed, 23 Apr 2025 01:02:17 +0000 Subject: [PATCH] feat `hectic` C: logger log in file --- package/c/hectic/docs/file_logging.md | 132 +++++++++++++ .../c/hectic/examples/file_logging_example.c | 52 +++++ package/c/hectic/hectic.c | 165 ++++++++++++++-- package/c/hectic/hectic.h | 45 +++++ package/c/hectic/test/00-logger.c | 181 +++++++++++++++++- 5 files changed, 557 insertions(+), 18 deletions(-) create mode 100755 package/c/hectic/docs/file_logging.md create mode 100755 package/c/hectic/examples/file_logging_example.c diff --git a/package/c/hectic/docs/file_logging.md b/package/c/hectic/docs/file_logging.md new file mode 100755 index 0000000..e900cfe --- /dev/null +++ b/package/c/hectic/docs/file_logging.md @@ -0,0 +1,132 @@ +# File Logging in Hectic Library + +This document covers the file logging functionality in the Hectic library, including its configuration and usage in different scenarios. + +## Overview + +Hectic's logging system now supports logging to files, offering three output modes: +1. Stderr only (default) +2. File only +3. Both stderr and file + +This gives you flexibility to route logs where they're most needed while maintaining the same structured logging interface. + +## Configuration Methods + +### 1. Environment Variables + +Configure logging with environment variables: + +```sh +# Set log file path +export LOG_FILE=/path/to/your/logfile.log + +# Set output mode (STDERR_ONLY, FILE_ONLY, BOTH) +export LOG_OUTPUT_MODE=BOTH + +# Set log level as usual +export LOG_LEVEL=DEBUG + +# Run your application +./your_program +``` + +### 2. Programmatic Configuration + +Configure logging in your code: + +```c +#include "hectic.h" + +int main() { + // Initialize logger + logger_init(); + + // Set log file (returns 0 on success, -1 on failure) + if (logger_set_file("/path/to/logfile.log") != 0) { + raise_exception("Failed to open log file"); + return 1; + } + + // Set output mode + logger_set_output_mode(LOG_OUTPUT_BOTH); + + // Your application code here + raise_info("Application started"); + + // Clean up on exit + logger_free(); + + return 0; +} +``` + +## Output Modes + +### `LOG_OUTPUT_STDERR_ONLY` (Default) +- All log messages go to stderr only +- No file output even if a log file is set + +### `LOG_OUTPUT_FILE_ONLY` +- All log messages go to the log file only +- Nothing is printed to stderr (useful for daemon processes) +- ANSI color codes are automatically stripped from file output + +### `LOG_OUTPUT_BOTH` +- All log messages go to both stderr and the log file +- ANSI colors appear on stderr but are stripped from file output + +## File Handling Details + +- Log files are opened in append mode +- The library automatically flushes after each log message to ensure logs are written immediately +- ANSI color codes are automatically stripped from file output to avoid cluttering log files with escape sequences +- If a file cannot be opened, an error message is printed to stderr + +## API Reference + +### Setting the Log File + +```c +int logger_set_file(const char *file_path); +``` + +- **Parameters**: `file_path` - Path to the log file, or NULL to disable file logging +- **Returns**: 0 on success, -1 on failure (e.g., unable to open file) +- **Notes**: + - Automatically closes any previously opened log file + - Opens the new file in append mode + - If NULL is passed, disables file logging and resets output mode to stderr only + +### Setting the Output Mode + +```c +void logger_set_output_mode(LogOutputMode mode); +``` + +- **Parameters**: `mode` - One of `LOG_OUTPUT_STDERR_ONLY`, `LOG_OUTPUT_FILE_ONLY`, or `LOG_OUTPUT_BOTH` +- **Notes**: + - Has no effect if file logging is not configured and mode is file-related + - Does not check if the log file is successfully opened + +## Example + +See `examples/file_logging_example.c` for a complete working example of file logging. + +## Best Practices + +1. **Always check the return value of `logger_set_file()`**: + ```c + if (logger_set_file("/path/to/logfile.log") != 0) { + // Handle error + } + ``` + +2. **Use appropriate output modes**: + - For interactive CLI applications: `LOG_OUTPUT_STDERR_ONLY` or `LOG_OUTPUT_BOTH` + - For daemon/service applications: `LOG_OUTPUT_FILE_ONLY` + - For debugging sessions: `LOG_OUTPUT_BOTH` + +3. **Consider log rotation**: The library doesn't handle log rotation, so for long-running applications, consider external log rotation solutions. + +4. **Close properly**: Always call `logger_free()` to ensure log files are properly closed. \ No newline at end of file diff --git a/package/c/hectic/examples/file_logging_example.c b/package/c/hectic/examples/file_logging_example.c new file mode 100755 index 0000000..674877a --- /dev/null +++ b/package/c/hectic/examples/file_logging_example.c @@ -0,0 +1,52 @@ +/** + * File Logging Example for Hectic Library + * + * This example demonstrates how to use the file logging capabilities + * of the Hectic library, showing both programmatic configuration + * and environment variable-based configuration. + */ + +#include "../hectic.h" +#include + +int main(int argc, char *argv[]) { + // Initialize the logger + logger_init(); + + // Log a message to stderr + raise_info("Starting file logging example"); + + // Enable file logging programmatically + const char *log_file = "example_log.txt"; + if (logger_set_file(log_file) != 0) { + raise_exception("Failed to open log file: %s", log_file); + return 1; + } + + // Set output mode to write to both stderr and file + logger_set_output_mode(LOG_OUTPUT_BOTH); + + // Log messages at different levels + raise_debug("This is a debug message"); + raise_info("This is an info message"); + raise_notice("This is a notice message"); + raise_warn("This is a warning message"); + raise_exception("This is an exception message"); + + // Switch to file-only mode + logger_set_output_mode(LOG_OUTPUT_FILE_ONLY); + raise_info("This message will only appear in the log file"); + + // Switch back to stderr-only mode + logger_set_output_mode(LOG_OUTPUT_STDERR_ONLY); + raise_info("This message will only appear on stderr"); + + // Clean up + logger_free(); + + printf("\nLog file demonstration complete. Check %s for logged messages.\n", log_file); + printf("\nYou can also run this program with environment variables:\n"); + printf(" LOG_FILE=custom.log LOG_OUTPUT_MODE=BOTH LOG_LEVEL=DEBUG ./file_logging_example\n"); + + return 0; +} \ No newline at end of file diff --git a/package/c/hectic/hectic.c b/package/c/hectic/hectic.c index 22b564d..efc3d9e 100644 --- a/package/c/hectic/hectic.c +++ b/package/c/hectic/hectic.c @@ -3,6 +3,7 @@ #include #include #include +#include #include // On systems without strsep, provide a custom implementation @@ -45,6 +46,62 @@ LogLevel current_log_level = LOG_LEVEL_INFO; LogRule *log_rules = NULL; Arena *log_rules_arena = NULL; +// File logging configuration +static FILE *log_file = NULL; +static LogOutputMode log_output_mode = LOG_OUTPUT_STDERR_ONLY; +static char *log_file_path = NULL; + +/** + * Set log output mode + * @param mode The output mode (stderr only, file only, or both) + */ +void logger_set_output_mode(LogOutputMode mode) { + log_output_mode = mode; +} + +/** + * Set log file path + * @param file_path Path to the log file. If NULL, file logging is disabled. + * @return 0 on success, -1 on failure (e.g., unable to open file) + */ +int logger_set_file(const char *file_path) { + // Close current log file if open + if (log_file != NULL && log_file != stderr) { + fclose(log_file); + log_file = NULL; + } + + // Free previous path if it exists + if (log_file_path != NULL) { + free(log_file_path); + log_file_path = NULL; + } + + // If path is NULL, disable file logging + if (file_path == NULL) { + log_output_mode = LOG_OUTPUT_STDERR_ONLY; + return 0; + } + + // Copy the file path + log_file_path = strdup(file_path); + if (log_file_path == NULL) { + fprintf(stderr, "ERROR: Failed to allocate memory for log file path\n"); + return -1; + } + + // Open the log file + log_file = fopen(file_path, "a"); + if (log_file == NULL) { + fprintf(stderr, "ERROR: Failed to open log file %s: %s\n", file_path, strerror(errno)); + free(log_file_path); + log_file_path = NULL; + return -1; + } + + return 0; +} + const char* color_mode_to_string(ColorMode mode) { switch (mode) { case COLOR_MODE_AUTO: return "AUTO"; @@ -207,6 +264,35 @@ void logger_init(void) { fprintf(stderr, "INIT: Logger initialized with default level %s\n", log_level_to_string(current_log_level)); } + + // Check for file logging environment variables + const char* log_file_env = getenv("LOG_FILE"); + if (log_file_env) { + if (logger_set_file(log_file_env) == 0) { + fprintf(stderr, "INIT: Logging to file: %s\n", log_file_env); + + // Check for output mode + const char* log_mode_env = getenv("LOG_OUTPUT_MODE"); + if (log_mode_env) { + if (strcmp(log_mode_env, "FILE_ONLY") == 0) { + logger_set_output_mode(LOG_OUTPUT_FILE_ONLY); + fprintf(stderr, "INIT: Log output mode set to FILE_ONLY\n"); + } else if (strcmp(log_mode_env, "BOTH") == 0) { + logger_set_output_mode(LOG_OUTPUT_BOTH); + fprintf(stderr, "INIT: Log output mode set to BOTH\n"); + } else { + logger_set_output_mode(LOG_OUTPUT_STDERR_ONLY); + fprintf(stderr, "INIT: Log output mode set to STDERR_ONLY\n"); + } + } else { + // Default to both if file is specified but mode isn't + logger_set_output_mode(LOG_OUTPUT_BOTH); + fprintf(stderr, "INIT: Log output mode set to BOTH (default)\n"); + } + } else { + fprintf(stderr, "INIT: Failed to open log file: %s\n", log_file_env); + } + } } void logger_free(void) { @@ -216,6 +302,21 @@ void logger_free(void) { free(log_rules_arena); log_rules_arena = NULL; } + + // Close log file if open + if (log_file != NULL && log_file != stderr) { + fclose(log_file); + log_file = NULL; + } + + // Free log file path if allocated + if (log_file_path != NULL) { + free(log_file_path); + log_file_path = NULL; + } + + // Reset output mode + log_output_mode = LOG_OUTPUT_STDERR_ONLY; } char* raise_message( @@ -237,25 +338,57 @@ char* raise_message( static char timeStr[20]; strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &tm_info); - // Print timestamp, log level with color, location info - fprintf(stderr, "%s %s%s%s %s:%s:%s%d%s ", - timeStr, - log_level_to_color(level), - log_level_to_string(level), - OPTIONAL_COLOR(COLOR_RESET), - file, - func, - OPTIONAL_COLOR(COLOR_GREEN), - line, - OPTIONAL_COLOR(COLOR_RESET)); - - // Print the actual message with variable arguments + // Format the message first va_list args; va_start(args, format); - vfprintf(stderr, format, args); + + // Create a buffer for the message + char message_buffer[4096]; // Adjust size as needed + int header_len = snprintf(message_buffer, sizeof(message_buffer), + "%s %s%s%s %s:%s:%s%d%s ", + timeStr, + log_level_to_color(level), + log_level_to_string(level), + OPTIONAL_COLOR(COLOR_RESET), + file, + func, + OPTIONAL_COLOR(COLOR_GREEN), + line, + OPTIONAL_COLOR(COLOR_RESET)); + + // Add the formatted message + vsnprintf(message_buffer + header_len, sizeof(message_buffer) - header_len, format, args); va_end(args); - - fprintf(stderr, "\n"); + + // Add newline + strcat(message_buffer, "\n"); + + // Write to stderr if needed + if (log_output_mode == LOG_OUTPUT_STDERR_ONLY || log_output_mode == LOG_OUTPUT_BOTH) { + fprintf(stderr, "%s", message_buffer); + } + + // Write to file if configured + if ((log_output_mode == LOG_OUTPUT_FILE_ONLY || log_output_mode == LOG_OUTPUT_BOTH) && log_file != NULL) { + // Remove ANSI color codes for file output + char file_buffer[4096]; + char *src = message_buffer; + char *dst = file_buffer; + + while (*src) { + if (*src == '\033') { + // Skip ANSI escape sequence + while (*src && *src != 'm') src++; + if (*src) src++; // Skip the 'm' + } else { + *dst++ = *src++; + } + } + *dst = '\0'; + + fprintf(log_file, "%s", file_buffer); + fflush(log_file); // Ensure log is written immediately + } return timeStr; } diff --git a/package/c/hectic/hectic.h b/package/c/hectic/hectic.h index 3e02d83..26588f6 100644 --- a/package/c/hectic/hectic.h +++ b/package/c/hectic/hectic.h @@ -13,6 +13,29 @@ #include #include +/* + * Hectic Library - A C utility library + * + * This library includes several components: + * - Logging system with multiple severity levels + * - Memory management with arenas + * - JSON parsing and serialization + * - Template engine + * + * Logging System Usage: + * - Set global log level: logger_level(LOG_LEVEL_DEBUG); + * - Log messages: raise_debug("Debug message with %s", value); + * + * File Logging: + * - Set log file: logger_set_file("/path/to/logfile.log"); + * - Select output mode: logger_set_output_mode(LOG_OUTPUT_BOTH); + * + * Environment Variables: + * - LOG_LEVEL: Set global log level ("TRACE", "DEBUG", etc.) + * - LOG_FILE: Set log file path + * - LOG_OUTPUT_MODE: Set output mode ("STDERR_ONLY", "FILE_ONLY", "BOTH") + */ + // ------------- // -- Helpers -- // ------------- @@ -224,6 +247,28 @@ typedef struct LogRule { struct LogRule *next; // Next rule in the chain } LogRule; +/* + * Log output mode - controls how logs are written to files + */ +typedef enum { + LOG_OUTPUT_STDERR_ONLY, // Write only to stderr (default) + LOG_OUTPUT_FILE_ONLY, // Write only to file + LOG_OUTPUT_BOTH // Write to both stderr and file +} LogOutputMode; + +/** + * Set log output mode + * @param mode The output mode (stderr only, file only, or both) + */ +void logger_set_output_mode(LogOutputMode mode); + +/** + * Set log file path + * @param file_path Path to the log file. If NULL, file logging is disabled. + * @return 0 on success, -1 on failure (e.g., unable to open file) + */ +int logger_set_file(const char *file_path); + void logger_level_reset(); void logger_init(void); diff --git a/package/c/hectic/test/00-logger.c b/package/c/hectic/test/00-logger.c index ced90ca..7ef020c 100644 --- a/package/c/hectic/test/00-logger.c +++ b/package/c/hectic/test/00-logger.c @@ -2,8 +2,26 @@ #include #include #include +#include #include "hectic.h" +#define ASSERT_STR_EQ(actual, expected) do { \ + if (strcmp(actual, expected) != 0) { \ + fprintf(stderr, "\n--- STRING COMPARISON ERROR ---\n"); \ + fprintf(stderr, "Expected (%zu bytes):\n'%s'\n", strlen(expected), expected);\ + fprintf(stderr, "Got (%zu bytes):\n'%s'\n", strlen(actual), actual); \ + fprintf(stderr, "----------------------------\n"); \ + for (size_t i = 0; i < strlen(expected) && i < strlen(actual); i++) { \ + if (expected[i] != actual[i]) { \ + fprintf(stderr, "First mismatch at position %zu: '%c' != '%c'\n", \ + i, expected[i], actual[i]); \ + break; \ + } \ + } \ + assert(0 && "Strings do not match"); \ + } \ +} while(0) + #define TEST_RAISE_GENERIC(LOG_MACRO, LEVEL, LEVEL_STR) do { \ FILE *orig_stderr = stderr; \ FILE *temp = tmpfile(); \ @@ -22,10 +40,156 @@ char expected_buffer[256]; \ const char* func = __func__; \ sprintf(expected_buffer, "%s " LEVEL_STR " " __FILE__ ":%s:%d message\n", time_str, func, __LINE__); \ - assert(strcmp(result_buffer, expected_buffer) == 0); \ + ASSERT_STR_EQ(result_buffer, expected_buffer); \ +} while(0) + +#define TEST_FILE_LOGGING(LOG_MACRO, LEVEL_STR, MESSAGE) do { \ + char log_path[256]; \ + snprintf(log_path, sizeof(log_path), "/tmp/hectic-test-%d.log", getpid()); \ + assert(logger_set_file(log_path) == 0); \ + logger_set_output_mode(LOG_OUTPUT_FILE_ONLY); \ + const char* time_str = LOG_MACRO(MESSAGE); \ + fflush(NULL); \ + \ + FILE *log_file = fopen(log_path, "r"); \ + assert(log_file != NULL); \ + char file_buffer[256]; \ + size_t file_read = fread(file_buffer, 1, sizeof(file_buffer)-1, log_file); \ + file_buffer[file_read] = '\0'; \ + fclose(log_file); \ + unlink(log_path); \ + \ + char expected[256]; \ + const char* func = __func__; \ + snprintf(expected, sizeof(expected), "%s %s %s:%s:%d %s\n", \ + time_str, LEVEL_STR, __FILE__, func, __LINE__, MESSAGE); \ + ASSERT_STR_EQ(file_buffer, expected); \ + logger_free(); \ +} while(0) + +#define TEST_DUAL_LOGGING(MESSAGE) do { \ + char log_path[256]; \ + snprintf(log_path, sizeof(log_path), "/tmp/hectic-test-%d.log", getpid()); \ + assert(logger_set_file(log_path) == 0); \ + logger_set_output_mode(LOG_OUTPUT_BOTH); \ + \ + FILE *orig_stderr = stderr; \ + FILE *temp_stderr = tmpfile(); \ + assert(temp_stderr != NULL); \ + stderr = temp_stderr; \ + \ + raise_info(MESSAGE); \ + fflush(stderr); \ + \ + stderr = orig_stderr; \ + \ + FILE *log_file = fopen(log_path, "r"); \ + assert(log_file != NULL); \ + char file_buffer[256]; \ + size_t file_read = fread(file_buffer, 1, sizeof(file_buffer)-1, log_file); \ + file_buffer[file_read] = '\0'; \ + fclose(log_file); \ + \ + fseek(temp_stderr, 0, SEEK_SET); \ + char stderr_buffer[256]; \ + size_t stderr_read = fread(stderr_buffer, 1, sizeof(stderr_buffer)-1, temp_stderr);\ + stderr_buffer[stderr_read] = '\0'; \ + fclose(temp_stderr); \ + \ + unlink(log_path); \ + \ + fprintf(stdout, "stderr content (%zu bytes):\n", stderr_read); \ + for (size_t i = 0; i < stderr_read; i++) { \ + unsigned char c = (unsigned char)stderr_buffer[i]; \ + if (c < 32 || c > 126) \ + fprintf(stdout, "\\x%02x", c); \ + else \ + fputc(c, stdout); \ + } \ + fprintf(stdout, "\n"); \ + \ + fprintf(stdout, "file content (%zu bytes):\n", file_read); \ + for (size_t i = 0; i < file_read; i++) { \ + unsigned char c = (unsigned char)file_buffer[i]; \ + if (c < 32 || c > 126) \ + fprintf(stdout, "\\x%02x", c); \ + else \ + fputc(c, stdout); \ + } \ + fprintf(stdout, "\n"); \ + \ + if (strstr(file_buffer, MESSAGE) == NULL) { \ + fprintf(stderr, "Error: message not found in file.\n"); \ + fprintf(stderr, "Expected message: %s\n", MESSAGE); \ + fprintf(stderr, "File content: %s\n", file_buffer); \ + assert(0); \ + } \ + if (strstr(stderr_buffer, MESSAGE) == NULL) { \ + fprintf(stderr, "Error: message not found in stderr.\n"); \ + fprintf(stderr, "Expected message: %s\n", MESSAGE); \ + fprintf(stderr, "stderr content: %s\n", stderr_buffer); \ + assert(0); \ + } \ + \ + if (strstr(stderr_buffer, "\033") == NULL) { \ + fprintf(stdout, "Note: ANSI color codes not found in stderr.\n"); \ + fprintf(stdout, "This is normal if the test is run without color support.\n");\ + } \ + \ + if (strstr(file_buffer, "\033") != NULL) { \ + fprintf(stderr, "Error: ANSI color codes found in file.\n"); \ + fprintf(stderr, "File content: %s\n", file_buffer); \ + assert(0); \ + } \ + \ + logger_free(); \ +} while(0) + +#define TEST_MODE_SWITCHING() do { \ + char log_path[256]; \ + snprintf(log_path, sizeof(log_path), "/tmp/hectic-mode-switch-%d.log", getpid()); \ + \ + logger_init(); \ + assert(logger_set_file(log_path) == 0); \ + \ + logger_set_output_mode(LOG_OUTPUT_FILE_ONLY); \ + raise_info("File only message"); \ + \ + logger_set_output_mode(LOG_OUTPUT_BOTH); \ + raise_info("Both stderr and file message"); \ + \ + logger_set_output_mode(LOG_OUTPUT_STDERR_ONLY); \ + raise_info("Stderr only message"); \ + \ + FILE *log_file = fopen(log_path, "r"); \ + assert(log_file != NULL); \ + char buffer[1024]; \ + size_t bytes_read = fread(buffer, 1, sizeof(buffer)-1, log_file); \ + buffer[bytes_read] = '\0'; \ + fclose(log_file); \ + unlink(log_path); \ + \ + if (strstr(buffer, "File only message") == NULL) { \ + fprintf(stderr, "Error: 'File only message' not found in file.\n"); \ + fprintf(stderr, "File content:\n%s\n", buffer); \ + assert(0); \ + } \ + if (strstr(buffer, "Both stderr and file message") == NULL) { \ + fprintf(stderr, "Error: 'Both stderr and file message' not found in file.\n"); \ + fprintf(stderr, "File content:\n%s\n", buffer); \ + assert(0); \ + } \ + if (strstr(buffer, "Stderr only message") != NULL) { \ + fprintf(stderr, "Error: 'Stderr only message' found in file but should not be there.\n");\ + fprintf(stderr, "File content:\n%s\n", buffer); \ + assert(0); \ + } \ + \ + logger_free(); \ } while(0) int main(void) { + debug_color_mode = COLOR_MODE_DISABLE; printf("%sRunning %s%s%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_CYAN), __FILE__, OPTIONAL_COLOR(COLOR_RESET)); TEST_RAISE_GENERIC(raise_debug, LOG_LEVEL_DEBUG, "DEBUG"); @@ -35,6 +199,19 @@ int main(void) { TEST_RAISE_GENERIC(raise_warn, LOG_LEVEL_WARN, "WARN"); TEST_RAISE_GENERIC(raise_exception, LOG_LEVEL_EXCEPTION, "EXCEPTION"); + printf("%sTesting file logging functionality...%s\n", OPTIONAL_COLOR(COLOR_CYAN), OPTIONAL_COLOR(COLOR_RESET)); + + logger_init(); + logger_level(LOG_LEVEL_DEBUG); + + TEST_FILE_LOGGING(raise_info, "INFO", "File output test"); + TEST_FILE_LOGGING(raise_debug, "DEBUG", "Debug message to file"); + TEST_FILE_LOGGING(raise_warn, "WARN", "Warning message to file"); + + TEST_DUAL_LOGGING("Dual output test message"); + + TEST_MODE_SWITCHING(); + printf("%sall tests passed.%s%s%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_CYAN), __FILE__, OPTIONAL_COLOR(COLOR_RESET)); return 0; -} +} \ No newline at end of file