LCOV - code coverage report
Current view: top level - src/core - logger.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 85.4 % 82 70
Test Date: 2026-04-20 19:54:24 Functions: 100.0 % 7 7

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : #include "logger.h"
       5              : #include "raii.h"
       6              : #include <stdarg.h>
       7              : #include <time.h>
       8              : #include <string.h>
       9              : #include <stdlib.h>
      10              : #include <unistd.h>
      11              : #include <fcntl.h>
      12              : #include <sys/stat.h>
      13              : #include <dirent.h>
      14              : 
      15              : #define MAX_LOG_SIZE (5 * 1024 * 1024) // 5MB
      16              : #define MAX_ROTATED_LOGS 5
      17              : 
      18              : static FILE *g_log_fp = NULL;
      19              : static LogLevel g_log_level = LOG_INFO;
      20              : static char *g_log_path = NULL;
      21              : static int g_log_stderr = 1;
      22              : 
      23              : /** @brief Converts a LogLevel enum to its string representation. */
      24          172 : static const char* level_to_str(LogLevel level) {
      25          172 :     switch (level) {
      26           35 :         case LOG_DEBUG: return "DEBUG";
      27           85 :         case LOG_INFO:  return "INFO";
      28           37 :         case LOG_WARN:  return "WARN";
      29           15 :         case LOG_ERROR: return "ERROR";
      30            0 :         default:        return "UNKNOWN";
      31              :     }
      32              : }
      33              : 
      34              : /**
      35              :  * @brief Rotates log files: session.log → session.log.1, dropping session.log.5.
      36              :  */
      37            1 : static void rotate_logs(void) {
      38            1 :     if (!g_log_path) return;
      39              : 
      40              :     // session.log.5 -> deleted
      41              :     // session.log.4 -> session.log.5
      42              :     // ...
      43              :     // session.log -> session.log.1
      44              : 
      45              :     char old_name[1024], new_name[1024];
      46              : 
      47              :     // Remove the oldest log
      48            1 :     snprintf(old_name, sizeof(old_name), "%s.%d", g_log_path, MAX_ROTATED_LOGS);
      49            1 :     unlink(old_name);
      50              : 
      51              :     // Rotate existing logs
      52            5 :     for (int i = MAX_ROTATED_LOGS - 1; i >= 1; i--) {
      53            4 :         snprintf(old_name, sizeof(old_name), "%s.%d", g_log_path, i);
      54            4 :         snprintf(new_name, sizeof(new_name), "%s.%d", g_log_path, i + 1);
      55            4 :         rename(old_name, new_name);
      56              :     }
      57              : 
      58              :     // Move current log
      59            1 :     snprintf(new_name, sizeof(new_name), "%s.1", g_log_path);
      60            1 :     rename(g_log_path, new_name);
      61              : }
      62              : 
      63           39 : int logger_init(const char *log_file_path, LogLevel level) {
      64              :     /* Idempotent: if called a second time, release the previous handle
      65              :      * and path before overwriting. Prevents QA-16 fd + heap leak. */
      66           39 :     if (g_log_fp) {
      67            0 :         if (fclose(g_log_fp) != 0) {
      68            0 :             fprintf(stderr, "logger_init: previous fclose failed\n");
      69              :         }
      70            0 :         g_log_fp = NULL;
      71              :     }
      72           39 :     free(g_log_path);
      73           39 :     g_log_path = NULL;
      74              : 
      75           39 :     g_log_level = level;
      76           39 :     g_log_path = strdup(log_file_path);
      77           39 :     if (!g_log_path) return -1;
      78              : 
      79              :     // Check size and rotate if necessary
      80              :     struct stat st;
      81           39 :     if (stat(g_log_path, &st) == 0 && st.st_size > MAX_LOG_SIZE) {
      82            1 :         rotate_logs();
      83              :     }
      84              : 
      85              :     /* Open with explicit 0600 so the file is never world-readable,
      86              :      * regardless of the process umask.  O_CREAT|O_WRONLY|O_APPEND gives
      87              :      * the same semantics as fopen(...,"a") but lets us set the mode. */
      88           39 :     int fd = open(g_log_path,
      89              :                   O_CREAT | O_WRONLY | O_APPEND,
      90              :                   (mode_t)0600);
      91           39 :     if (fd == -1) {
      92            0 :         free(g_log_path);
      93            0 :         g_log_path = NULL;
      94            0 :         return -1;
      95              :     }
      96              :     /* Enforce 0600 on a pre-existing file that may have wrong permissions. */
      97           39 :     fchmod(fd, (mode_t)0600);
      98              : 
      99           39 :     g_log_fp = fdopen(fd, "a");
     100           39 :     if (!g_log_fp) {
     101            0 :         close(fd);
     102            0 :         free(g_log_path);
     103            0 :         g_log_path = NULL;
     104            0 :         return -1;
     105              :     }
     106              : 
     107           39 :     logger_log(LOG_INFO, "Logging initialized. Level: %s", level_to_str(level));
     108           39 :     return 0;
     109              : }
     110              : 
     111          997 : void logger_log(LogLevel level, const char *format, ...) {
     112          997 :     if (level < g_log_level || !g_log_fp) return;
     113              : 
     114          133 :     time_t now = time(NULL);
     115          133 :     struct tm *tm_info = localtime(&now);
     116              :     char time_str[26];
     117          133 :     strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);
     118              : 
     119              :     va_list args;
     120              :     
     121              :     // Log to file
     122          133 :     fprintf(g_log_fp, "[%s] [%s] ", time_str, level_to_str(level));
     123          133 :     va_start(args, format);
     124          133 :     vfprintf(g_log_fp, format, args);
     125          133 :     va_end(args);
     126          133 :     fprintf(g_log_fp, "\n");
     127          133 :     fflush(g_log_fp);
     128              : 
     129              :     // Also log to stderr if ERROR and enabled
     130          133 :     if (level == LOG_ERROR && g_log_stderr) {
     131           15 :         fprintf(stderr, "ERROR: ");
     132           15 :         va_start(args, format);
     133           15 :         vfprintf(stderr, format, args);
     134           15 :         va_end(args);
     135           15 :         fprintf(stderr, "\n");
     136              :     }
     137              : }
     138              : 
     139           39 : void logger_close(void) {
     140           39 :     if (g_log_fp) {
     141           39 :         if (fclose(g_log_fp) != 0) {
     142            0 :             fprintf(stderr, "logger: fclose failed\n");
     143              :         }
     144           39 :         g_log_fp = NULL;
     145              :     }
     146           39 :     if (g_log_path) {
     147           39 :         free(g_log_path);
     148           39 :         g_log_path = NULL;
     149              :     }
     150           39 : }
     151              : 
     152            2 : void logger_set_stderr(int enable) {
     153            2 :     g_log_stderr = enable;
     154            2 : }
     155              : 
     156            2 : int logger_clean_logs(const char *log_dir) {
     157            4 :     RAII_DIR DIR *dir = opendir(log_dir);
     158            2 :     if (!dir) return -1;
     159              : 
     160              :     struct dirent *entry;
     161            7 :     while ((entry = readdir(dir)) != NULL) {
     162            6 :         if (!strstr(entry->d_name, "session.log")) continue;
     163              : 
     164              :         char path[1024];
     165            3 :         snprintf(path, sizeof(path), "%s/%s", log_dir, entry->d_name);
     166              : 
     167              :         /* struct dirent::d_type is not guaranteed by POSIX; many
     168              :          * filesystems (NFS, some FUSE mounts, ext2 without feature flags)
     169              :          * report DT_UNKNOWN. Use stat() unconditionally for portability. */
     170              :         struct stat st;
     171            3 :         if (stat(path, &st) != 0) continue;
     172            3 :         if (!S_ISREG(st.st_mode)) continue;
     173              : 
     174            3 :         unlink(path);
     175              :     }
     176            1 :     return 0;
     177              : }
        

Generated by: LCOV version 2.0-1