#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 #include #include #include #include #include #include #include #include #include #include #include #include #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/-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); }