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 420 : static const char* level_to_str(LogLevel level) {
25 420 : switch (level) {
26 79 : case LOG_DEBUG: return "DEBUG";
27 218 : case LOG_INFO: return "INFO";
28 85 : case LOG_WARN: return "WARN";
29 38 : 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 98 : 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 98 : if (g_log_fp) {
67 5 : if (fclose(g_log_fp) != 0) {
68 0 : fprintf(stderr, "logger_init: previous fclose failed\n");
69 : }
70 5 : g_log_fp = NULL;
71 : }
72 98 : free(g_log_path);
73 98 : g_log_path = NULL;
74 :
75 98 : g_log_level = level;
76 98 : g_log_path = strdup(log_file_path);
77 98 : if (!g_log_path) return -1;
78 :
79 : // Check size and rotate if necessary
80 : struct stat st;
81 98 : 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 98 : int fd = open(g_log_path,
89 : O_CREAT | O_WRONLY | O_APPEND,
90 : (mode_t)0600);
91 98 : 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 97 : fchmod(fd, (mode_t)0600);
98 :
99 97 : g_log_fp = fdopen(fd, "a");
100 97 : 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 97 : logger_log(LOG_INFO, "Logging initialized. Level: %s", level_to_str(level));
108 97 : return 0;
109 : }
110 :
111 2484 : void logger_log(LogLevel level, const char *format, ...) {
112 2484 : if (level < g_log_level || !g_log_fp) return;
113 :
114 323 : time_t now = time(NULL);
115 323 : struct tm *tm_info = localtime(&now);
116 : char time_str[26];
117 323 : strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);
118 :
119 : va_list args;
120 :
121 : // Log to file
122 323 : fprintf(g_log_fp, "[%s] [%s] ", time_str, level_to_str(level));
123 323 : va_start(args, format);
124 323 : vfprintf(g_log_fp, format, args);
125 323 : va_end(args);
126 323 : fprintf(g_log_fp, "\n");
127 323 : fflush(g_log_fp);
128 :
129 : // Also log to stderr if ERROR and enabled
130 323 : if (level == LOG_ERROR && g_log_stderr) {
131 36 : fprintf(stderr, "ERROR: ");
132 36 : va_start(args, format);
133 36 : vfprintf(stderr, format, args);
134 36 : va_end(args);
135 36 : fprintf(stderr, "\n");
136 : }
137 : }
138 :
139 84 : void logger_close(void) {
140 84 : if (g_log_fp) {
141 84 : if (fclose(g_log_fp) != 0) {
142 0 : fprintf(stderr, "logger: fclose failed\n");
143 : }
144 84 : g_log_fp = NULL;
145 : }
146 84 : if (g_log_path) {
147 84 : free(g_log_path);
148 84 : g_log_path = NULL;
149 : }
150 84 : }
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 : }
|