summaryrefslogtreecommitdiff
path: root/src/core/log.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /src/core/log.c
#2
Diffstat (limited to 'src/core/log.c')
-rw-r--r--src/core/log.c694
1 files changed, 694 insertions, 0 deletions
diff --git a/src/core/log.c b/src/core/log.c
new file mode 100644
index 0000000..7812c00
--- /dev/null
+++ b/src/core/log.c
@@ -0,0 +1,694 @@
+#if defined(__linux__)
+#define _DEFAULT_SOURCE
+#endif
+
+#include "hybbx/log.h"
+#if !defined(HYBBX_CLIENT_BUILD)
+#include "hybbx/config.h"
+#endif
+#include "hybbx/limits.h"
+#include "hybbx/util.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#define HYBBX_LOG_LINE_MAX 1024u
+
+/*
+ * Weekly HyBBX file log (replaces stuck month-keyed "daily" open handle).
+ *
+ * Naming: YYYYMMDD-week-hybbx.log
+ * YYYYMMDD = ISO week start (Monday, local time). One file per 7-day week.
+ *
+ * Cycle (forever):
+ * Days 1–7 → append to the current week file.
+ * Day 8 → switch to the new week file (new Monday), then pack every
+ * closed/legacy packable log under [log] dir into
+ * logs/arc/<YYYYMMDD>-week-hybbx.tar.bz2 (bzip2) and delete
+ * those sources only after tar succeeds.
+ * Pack batches of up to 7 members per archive (oldest first); repeat until
+ * fewer than 1 packable remain. Legacy daily names (YYYYMMDD-hybbx.log)
+ * are packable and are swept on day-8 switch (and on startup when ≥7).
+ *
+ * security.log is never packed or deleted here.
+ */
+
+#define HYBBX_LOG_ARC_SUBDIR "arc"
+#define HYBBX_LOG_PACK_BATCH 7
+#define HYBBX_LOG_PACKABLE_MAX 64
+
+static hybbx_log_config_t g_log_config;
+static int g_log_ready;
+static FILE *g_log_file;
+/* ISO-Monday yyyymmdd of the file currently open (0 = none). */
+static int g_log_open_week_yyyymmdd;
+static char g_log_open_name[64];
+static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER;
+
+static int mkdir_p(const char *path)
+{
+ char buf[HYBBX_PATH_MAX];
+ size_t len;
+ size_t i;
+
+ if (path == NULL || path[0] == '\0') {
+ return -1;
+ }
+
+ hybbx_strlcpy(buf, path, sizeof(buf));
+ len = strlen(buf);
+ while (len > 0 && buf[len - 1] == '/') {
+ buf[--len] = '\0';
+ }
+
+ for (i = 1; i < len; i++) {
+ if (buf[i] != '/') {
+ continue;
+ }
+ buf[i] = '\0';
+ if (buf[0] != '\0' && mkdir(buf, 0755) != 0 && errno != EEXIST) {
+ return -1;
+ }
+ buf[i] = '/';
+ }
+
+ if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
+ return -1;
+ }
+
+ return 0;
+}
+
+void hybbx_log_config_defaults(hybbx_log_config_t *cfg)
+{
+ if (cfg == NULL) {
+ return;
+ }
+
+ cfg->enabled = 1;
+ cfg->dir[0] = '\0';
+ cfg->level = HYBBX_LOG_WARN;
+}
+
+hybbx_log_level_t hybbx_log_parse_level(const char *value)
+{
+ if (value == NULL || value[0] == '\0') {
+ return HYBBX_LOG_WARN;
+ }
+
+ if (strcasecmp(value, "debug") == 0) {
+ return HYBBX_LOG_DEBUG;
+ }
+ if (strcasecmp(value, "stats") == 0) {
+ return HYBBX_LOG_STATS;
+ }
+ if (strcasecmp(value, "info") == 0) {
+ return HYBBX_LOG_INFO;
+ }
+ if (strcasecmp(value, "warn") == 0) {
+ return HYBBX_LOG_WARN;
+ }
+
+ return HYBBX_LOG_WARN;
+}
+
+const char *hybbx_log_level_name(hybbx_log_level_t level)
+{
+ switch (level) {
+ case HYBBX_LOG_DEBUG:
+ return "debug";
+ case HYBBX_LOG_STATS:
+ return "stats";
+ case HYBBX_LOG_INFO:
+ return "info";
+ case HYBBX_LOG_WARN:
+ return "warn";
+ default:
+ return "?";
+ }
+}
+
+int hybbx_log_level_visible(hybbx_log_level_t level)
+{
+ if (!g_log_ready) {
+ return 1;
+ }
+
+ return level >= g_log_config.level;
+}
+
+const hybbx_log_config_t *hybbx_log_config_get(void)
+{
+ return g_log_ready ? &g_log_config : NULL;
+}
+
+int hybbx_log_enabled(void)
+{
+ return g_log_ready && g_log_config.enabled && g_log_file != NULL;
+}
+
+static void log_close_file(void)
+{
+ if (g_log_file != NULL) {
+ fclose(g_log_file);
+ g_log_file = NULL;
+ }
+ g_log_open_week_yyyymmdd = 0;
+ g_log_open_name[0] = '\0';
+}
+
+/* Monday of the ISO week containing @p tm (local wall calendar). */
+static int log_week_start_tm(const struct tm *tm, struct tm *out)
+{
+ time_t t;
+ int from_monday;
+
+ if (tm == NULL || out == NULL) {
+ return -1;
+ }
+
+ *out = *tm;
+ /* tm_wday: 0=Sun … 6=Sat → days since Monday */
+ from_monday = (out->tm_wday + 6) % 7;
+ out->tm_mday -= from_monday;
+ out->tm_hour = 12; /* avoid DST midnight edge when normalizing */
+ out->tm_min = 0;
+ out->tm_sec = 0;
+ out->tm_isdst = -1;
+ t = mktime(out);
+ if (t == (time_t)-1) {
+ return -1;
+ }
+ if (localtime_r(&t, out) == NULL) {
+ return -1;
+ }
+ return 0;
+}
+
+static int log_week_yyyymmdd(const struct tm *tm)
+{
+ struct tm week;
+
+ if (log_week_start_tm(tm, &week) != 0) {
+ return 0;
+ }
+ return (week.tm_year + 1900) * 10000
+ + (week.tm_mon + 1) * 100
+ + week.tm_mday;
+}
+
+static void log_week_basename(int week_yyyymmdd, char *name, size_t name_len)
+{
+ snprintf(name, name_len, "%08d-week-hybbx.log", week_yyyymmdd);
+}
+
+static int log_build_path_for_week(char *out, size_t out_len, int week_yyyymmdd)
+{
+ char name[64];
+
+ log_week_basename(week_yyyymmdd, name, sizeof(name));
+ return hybbx_path_join(out, out_len, g_log_config.dir, name) == HYBBX_OK
+ ? 0
+ : -1;
+}
+
+static int log_is_eight_digits(const char *s)
+{
+ size_t i;
+
+ for (i = 0; i < 8; i++) {
+ if (s[i] < '0' || s[i] > '9') {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/*
+ * Packable: current-scheme week files, or legacy daily YYYYMMDD-hybbx.log.
+ * Never security.log, arc/, or the active week file.
+ */
+static int log_name_is_packable(const char *name)
+{
+ size_t len;
+
+ if (name == NULL || name[0] == '\0' || name[0] == '.') {
+ return 0;
+ }
+ if (strcmp(name, g_log_open_name) == 0) {
+ return 0;
+ }
+
+ len = strlen(name);
+ if (len == 22 && log_is_eight_digits(name) &&
+ strcmp(name + 8, "-week-hybbx.log") == 0) {
+ return 1;
+ }
+ /* Legacy daily: 20260720-hybbx.log (8 digits + 10). */
+ if (len == 18 && log_is_eight_digits(name) &&
+ strcmp(name + 8, "-hybbx.log") == 0) {
+ return 1;
+ }
+ return 0;
+}
+
+static int log_cmp_names(const void *a, const void *b)
+{
+ return strcmp(*(const char *const *)a, *(const char *const *)b);
+}
+
+static int log_run_tar_bz2(const char *arc_path, char **basenames, int count)
+{
+ pid_t pid;
+ int status;
+ int i;
+ char *argv[HYBBX_LOG_PACKABLE_MAX + 8];
+ int argc = 0;
+
+ if (arc_path == NULL || basenames == NULL || count <= 0 ||
+ count > HYBBX_LOG_PACKABLE_MAX) {
+ return -1;
+ }
+
+ argv[argc++] = "tar";
+ argv[argc++] = "-C";
+ argv[argc++] = (char *)g_log_config.dir;
+ argv[argc++] = "-cjf";
+ argv[argc++] = (char *)arc_path;
+ for (i = 0; i < count; i++) {
+ argv[argc++] = basenames[i];
+ }
+ argv[argc] = NULL;
+
+ pid = fork();
+ if (pid < 0) {
+ return -1;
+ }
+ if (pid == 0) {
+ execvp("tar", argv);
+ _exit(127);
+ }
+
+ if (waitpid(pid, &status, 0) < 0) {
+ return -1;
+ }
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ return -1;
+ }
+ return 0;
+}
+
+static int log_collect_packable(char namebuf[][64], char **names, int max)
+{
+ DIR *dir;
+ struct dirent *ent;
+ int count = 0;
+
+ dir = opendir(g_log_config.dir);
+ if (dir == NULL) {
+ return 0;
+ }
+
+ while ((ent = readdir(dir)) != NULL && count < max) {
+ if (!log_name_is_packable(ent->d_name)) {
+ continue;
+ }
+ if (strlen(ent->d_name) >= 64) {
+ continue;
+ }
+ hybbx_strlcpy(namebuf[count], ent->d_name, 64);
+ names[count] = namebuf[count];
+ count++;
+ }
+ closedir(dir);
+ return count;
+}
+
+/* Pack up to HYBBX_LOG_PACK_BATCH oldest packable files. Returns packed count. */
+static int log_archive_one_batch(void)
+{
+ char *names[HYBBX_LOG_PACKABLE_MAX];
+ char namebuf[HYBBX_LOG_PACKABLE_MAX][64];
+ int count;
+ int pack_n;
+ int i;
+ char arc_dir[HYBBX_PATH_MAX];
+ char arc_path[HYBBX_PATH_MAX];
+ char arc_name[80];
+ struct stat st;
+
+ count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX);
+ if (count <= 0) {
+ return 0;
+ }
+
+ qsort(names, (size_t)count, sizeof(names[0]), log_cmp_names);
+ pack_n = count < HYBBX_LOG_PACK_BATCH ? count : HYBBX_LOG_PACK_BATCH;
+
+ if (hybbx_path_join(arc_dir, sizeof(arc_dir), g_log_config.dir,
+ HYBBX_LOG_ARC_SUBDIR) != HYBBX_OK) {
+ fprintf(stderr, "[log] archive path too long\n");
+ return -1;
+ }
+ if (mkdir_p(arc_dir) != 0) {
+ fprintf(stderr, "[log] cannot create %s\n", arc_dir);
+ return -1;
+ }
+
+ snprintf(arc_name, sizeof(arc_name), "%.8s-week-hybbx.tar.bz2", names[0]);
+ if (hybbx_path_join(arc_path, sizeof(arc_path), arc_dir, arc_name) !=
+ HYBBX_OK) {
+ fprintf(stderr, "[log] archive file path too long\n");
+ return -1;
+ }
+ if (stat(arc_path, &st) == 0) {
+ snprintf(arc_name, sizeof(arc_name),
+ "%.8s-week-hybbx-%ld.tar.bz2", names[0], (long)time(NULL));
+ if (hybbx_path_join(arc_path, sizeof(arc_path), arc_dir, arc_name) !=
+ HYBBX_OK) {
+ return -1;
+ }
+ }
+
+ if (log_run_tar_bz2(arc_path, names, pack_n) != 0) {
+ fprintf(stderr, "[log] tar.bz2 failed for %s — sources kept\n",
+ arc_path);
+ return -1;
+ }
+
+ for (i = 0; i < pack_n; i++) {
+ char full[HYBBX_PATH_MAX];
+ if (hybbx_path_join(full, sizeof(full), g_log_config.dir, names[i]) !=
+ HYBBX_OK) {
+ continue;
+ }
+ if (unlink(full) != 0) {
+ fprintf(stderr, "[log] cannot remove packed %s\n", full);
+ }
+ }
+
+ fprintf(stderr, "[log] archived %d file(s) → %s\n", pack_n, arc_path);
+ return pack_n;
+}
+
+/*
+ * @p on_week_switch: day-8 path — pack every closed/legacy file (batches of 7).
+ * Otherwise (startup): pack only when ≥7 packable (legacy daily sweep).
+ */
+static void log_archive_packable(int on_week_switch)
+{
+ char *names[HYBBX_LOG_PACKABLE_MAX];
+ char namebuf[HYBBX_LOG_PACKABLE_MAX][64];
+ int count;
+ int guard;
+
+ if (g_log_config.dir[0] == '\0') {
+ return;
+ }
+
+ count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX);
+ if (count <= 0) {
+ return;
+ }
+ if (!on_week_switch && count < HYBBX_LOG_PACK_BATCH) {
+ return;
+ }
+
+ for (guard = 0; guard < HYBBX_LOG_PACKABLE_MAX; guard++) {
+ int packed = log_archive_one_batch();
+ if (packed <= 0) {
+ break;
+ }
+ count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX);
+ if (count <= 0) {
+ break;
+ }
+ if (!on_week_switch && count < HYBBX_LOG_PACK_BATCH) {
+ break;
+ }
+ }
+}
+
+hybbx_result_t hybbx_log_current_path(char *out, size_t out_len)
+{
+ time_t now;
+ struct tm tm_buf;
+ struct tm *tm;
+ int week;
+
+ if (out == NULL || out_len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ out[0] = '\0';
+ if (!g_log_ready || !g_log_config.enabled || g_log_config.dir[0] == '\0') {
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ now = time(NULL);
+ tm = localtime_r(&now, &tm_buf);
+ if (tm == NULL) {
+ return HYBBX_ERR_IO;
+ }
+
+ week = log_week_yyyymmdd(tm);
+ if (week == 0 || log_build_path_for_week(out, out_len, week) != 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ return HYBBX_OK;
+}
+
+static int log_open_for_time(const struct tm *tm)
+{
+ char path[HYBBX_PATH_MAX];
+ FILE *fp;
+ int week;
+ int rotated = 0;
+
+ if (tm == NULL) {
+ return -1;
+ }
+
+ week = log_week_yyyymmdd(tm);
+ if (week == 0) {
+ return -1;
+ }
+
+ if (g_log_file != NULL && g_log_open_week_yyyymmdd == week) {
+ return 0;
+ }
+
+ if (g_log_file != NULL) {
+ rotated = 1;
+ }
+
+ log_close_file();
+
+ if (mkdir_p(g_log_config.dir) != 0) {
+ fprintf(stderr, "[log] cannot create directory %s\n", g_log_config.dir);
+ return -1;
+ }
+
+ if (log_build_path_for_week(path, sizeof(path), week) != 0) {
+ fprintf(stderr, "[log] path too long for log file\n");
+ return -1;
+ }
+
+ fp = fopen(path, "a");
+ if (fp == NULL) {
+ fprintf(stderr, "[log] cannot open %s\n", path);
+ return -1;
+ }
+
+ g_log_file = fp;
+ g_log_open_week_yyyymmdd = week;
+ log_week_basename(week, g_log_open_name, sizeof(g_log_open_name));
+
+ /* Day-8 switch packs closed week (+ legacy). Startup sweeps if ≥7. */
+ log_archive_packable(rotated);
+
+ return 0;
+}
+
+static int log_ensure_open(void)
+{
+ time_t now;
+ struct tm tm_buf;
+ struct tm *tm;
+
+ if (!g_log_config.enabled || g_log_config.dir[0] == '\0') {
+ return -1;
+ }
+
+ now = time(NULL);
+ tm = localtime_r(&now, &tm_buf);
+ if (tm == NULL) {
+ return -1;
+ }
+
+ return log_open_for_time(tm);
+}
+
+#if !defined(HYBBX_CLIENT_BUILD)
+void hybbx_log_config_apply(const struct hybbx_config *config)
+{
+ const char *dir_raw;
+ const char *level_raw;
+ hybbx_log_level_t parsed;
+
+ hybbx_log_shutdown();
+ hybbx_log_config_defaults(&g_log_config);
+
+ if (config != NULL) {
+ g_log_config.enabled =
+ hybbx_config_get_bool(config, "log", "enabled", 1);
+ dir_raw = hybbx_config_get(config, "log", "dir", NULL);
+ level_raw = hybbx_config_get(config, "log", "level", "warn");
+
+ parsed = hybbx_log_parse_level(level_raw);
+ if (level_raw != NULL && level_raw[0] != '\0' &&
+ strcmp(level_raw, hybbx_log_level_name(parsed)) != 0 &&
+ strcasecmp(level_raw, hybbx_log_level_name(parsed)) != 0) {
+ fprintf(stderr,
+ "[log] unknown level '%s' — using warn (debug|stats|info|warn)\n",
+ level_raw);
+ }
+ g_log_config.level = parsed;
+
+ if (dir_raw != NULL && dir_raw[0] != '\0') {
+ if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir),
+ dir_raw) != HYBBX_OK) {
+ fprintf(stderr, "[log] invalid dir path\n");
+ g_log_config.enabled = 0;
+ }
+ } else if (g_log_config.enabled) {
+ if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir),
+ HYBBX_DIR_LOGS) != HYBBX_OK) {
+ fprintf(stderr, "[log] cannot resolve default log directory\n");
+ g_log_config.enabled = 0;
+ }
+ }
+ } else {
+ if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir),
+ HYBBX_DIR_LOGS) != HYBBX_OK) {
+ g_log_config.enabled = 0;
+ }
+ }
+
+ g_log_ready = 1;
+
+ if (g_log_config.enabled) {
+ if (log_ensure_open() != 0) {
+ g_log_config.enabled = 0;
+ }
+ }
+
+ printf("[log] enabled=%s dir=%s level=%s week=%s\n",
+ hybbx_bool_to_string(g_log_config.enabled),
+ g_log_config.dir[0] != '\0' ? g_log_config.dir : "-",
+ hybbx_log_level_name(g_log_config.level),
+ g_log_open_name[0] != '\0' ? g_log_open_name : "-");
+}
+#else
+void hybbx_log_config_apply(const struct hybbx_config *config)
+{
+ (void)config;
+ hybbx_log_config_defaults(&g_log_config);
+ g_log_ready = 1;
+}
+#endif
+
+static void log_write_console(hybbx_log_level_t level, const char *message)
+{
+ FILE *out = (level == HYBBX_LOG_WARN) ? stderr : stdout;
+
+ fputs(message, out);
+ fputc('\n', out);
+ fflush(out);
+}
+
+static void log_write_file(hybbx_log_level_t level, const char *message)
+{
+ char line[HYBBX_LOG_LINE_MAX + 64];
+ time_t now;
+ struct tm tm_buf;
+ struct tm *tm;
+
+ if (!g_log_config.enabled) {
+ return;
+ }
+
+ pthread_mutex_lock(&g_log_lock);
+
+ if (log_ensure_open() != 0) {
+ pthread_mutex_unlock(&g_log_lock);
+ return;
+ }
+
+ now = time(NULL);
+ tm = localtime_r(&now, &tm_buf);
+ if (tm == NULL) {
+ pthread_mutex_unlock(&g_log_lock);
+ return;
+ }
+
+ if (hybbx_time_format_stamp(line, sizeof(line), tm, NULL) != HYBBX_OK) {
+ pthread_mutex_unlock(&g_log_lock);
+ return;
+ }
+
+ {
+ size_t stamp_len = strlen(line);
+ snprintf(line + stamp_len, sizeof(line) - stamp_len,
+ " [%s] %s\n", hybbx_log_level_name(level), message);
+ }
+
+ fputs(line, g_log_file);
+ fflush(g_log_file);
+
+ pthread_mutex_unlock(&g_log_lock);
+}
+
+void hybbx_log_write(hybbx_log_level_t level, const char *fmt, ...)
+{
+ char message[HYBBX_LOG_LINE_MAX];
+ va_list ap;
+
+ if (fmt == NULL) {
+ return;
+ }
+
+ if (g_log_ready && !hybbx_log_level_visible(level)) {
+ return;
+ }
+
+ va_start(ap, fmt);
+ vsnprintf(message, sizeof(message), fmt, ap);
+ va_end(ap);
+
+ log_write_console(level, message);
+ if (g_log_ready) {
+ log_write_file(level, message);
+ }
+}
+
+void hybbx_log_shutdown(void)
+{
+ pthread_mutex_lock(&g_log_lock);
+ log_close_file();
+ g_log_ready = 0;
+ hybbx_log_config_defaults(&g_log_config);
+ pthread_mutex_unlock(&g_log_lock);
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com