feat hectic C: logger log in file

This commit is contained in:
2025-04-23 01:02:17 +00:00
parent e25190422b
commit cc7de6c0dd
5 changed files with 557 additions and 18 deletions

View File

@@ -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.

View File

@@ -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 <stdio.h>
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;
}

View File

@@ -3,6 +3,7 @@
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <errno.h>
#include <setjmp.h>
// 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,8 +338,14 @@ 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 ",
// Format the message first
va_list args;
va_start(args, format);
// 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),
@@ -249,13 +356,39 @@ char* raise_message(
line,
OPTIONAL_COLOR(COLOR_RESET));
// Print the actual message with variable arguments
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
// 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;
}

View File

@@ -13,6 +13,29 @@
#include <ctype.h>
#include <stdbool.h>
/*
* 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);

View File

@@ -2,8 +2,26 @@
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#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;
}