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 400 : static const char* level_to_str(LogLevel level) {
25 400 : switch (level) {
26 73 : case LOG_DEBUG: return "DEBUG";
27 216 : case LOG_INFO: return "INFO";
28 75 : case LOG_WARN: return "WARN";
29 36 : 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 3 : static void rotate_logs(void) {
38 3 : 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 3 : snprintf(old_name, sizeof(old_name), "%s.%d", g_log_path, MAX_ROTATED_LOGS);
49 3 : unlink(old_name);
50 :
51 : // Rotate existing logs
52 15 : for (int i = MAX_ROTATED_LOGS - 1; i >= 1; i--) {
53 12 : snprintf(old_name, sizeof(old_name), "%s.%d", g_log_path, i);
54 12 : snprintf(new_name, sizeof(new_name), "%s.%d", g_log_path, i + 1);
55 12 : rename(old_name, new_name);
56 : }
57 :
58 : // Move current log
59 3 : snprintf(new_name, sizeof(new_name), "%s.1", g_log_path);
60 3 : rename(g_log_path, new_name);
61 : }
62 :
63 92 : 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 92 : if (g_log_fp) {
67 1 : if (fclose(g_log_fp) != 0) {
68 0 : fprintf(stderr, "logger_init: previous fclose failed\n");
69 : }
70 1 : g_log_fp = NULL;
71 : }
72 92 : free(g_log_path);
73 92 : g_log_path = NULL;
74 :
75 92 : g_log_level = level;
76 92 : g_log_path = strdup(log_file_path);
77 92 : if (!g_log_path) return -1;
78 :
79 : // Check size and rotate if necessary
80 : struct stat st;
81 92 : if (stat(g_log_path, &st) == 0 && st.st_size > MAX_LOG_SIZE) {
82 3 : 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 92 : int fd = open(g_log_path,
89 : O_CREAT | O_WRONLY | O_APPEND,
90 : (mode_t)0600);
91 92 : if (fd == -1) {
92 1 : free(g_log_path);
93 1 : g_log_path = NULL;
94 1 : return -1;
95 : }
96 : /* Enforce 0600 on a pre-existing file that may have wrong permissions. */
97 91 : fchmod(fd, (mode_t)0600);
98 :
99 91 : g_log_fp = fdopen(fd, "a");
100 91 : 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 91 : logger_log(LOG_INFO, "Logging initialized. Level: %s", level_to_str(level));
108 91 : return 0;
109 : }
110 :
111 2262 : void logger_log(LogLevel level, const char *format, ...) {
112 2262 : if (level < g_log_level || !g_log_fp) return;
113 :
114 309 : time_t now = time(NULL);
115 309 : struct tm *tm_info = localtime(&now);
116 : char time_str[26];
117 309 : strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);
118 :
119 : va_list args;
120 :
121 : // Log to file
122 309 : fprintf(g_log_fp, "[%s] [%s] ", time_str, level_to_str(level));
123 309 : va_start(args, format);
124 309 : vfprintf(g_log_fp, format, args);
125 309 : va_end(args);
126 309 : fprintf(g_log_fp, "\n");
127 309 : fflush(g_log_fp);
128 :
129 : // Also log to stderr if ERROR and enabled
130 309 : if (level == LOG_ERROR && g_log_stderr) {
131 34 : fprintf(stderr, "ERROR: ");
132 34 : va_start(args, format);
133 34 : vfprintf(stderr, format, args);
134 34 : va_end(args);
135 34 : fprintf(stderr, "\n");
136 : }
137 : }
138 :
139 90 : void logger_close(void) {
140 90 : if (g_log_fp) {
141 90 : if (fclose(g_log_fp) != 0) {
142 0 : fprintf(stderr, "logger: fclose failed\n");
143 : }
144 90 : g_log_fp = NULL;
145 : }
146 90 : if (g_log_path) {
147 90 : free(g_log_path);
148 90 : g_log_path = NULL;
149 : }
150 90 : }
151 :
152 5 : void logger_set_stderr(int enable) {
153 5 : g_log_stderr = enable;
154 5 : }
155 :
156 6 : int logger_clean_logs(const char *log_dir) {
157 12 : RAII_DIR DIR *dir = opendir(log_dir);
158 6 : if (!dir) return -1;
159 :
160 : struct dirent *entry;
161 19 : while ((entry = readdir(dir)) != NULL) {
162 16 : if (!strstr(entry->d_name, "session.log")) continue;
163 :
164 : char path[1024];
165 7 : 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 7 : if (stat(path, &st) != 0) continue;
172 7 : if (!S_ISREG(st.st_mode)) continue;
173 :
174 7 : unlink(path);
175 : }
176 3 : return 0;
177 : }
|