/** * IEC104_Log.c —— IEC104 通用日志模块实现。 * * 日志处理流程: * 级别过滤 -> 时间戳/级别前缀 -> 正文格式化 -> 回调或标准流输出 * * 模块不依赖 IEC104 报文结构,不使用动态内存。输出缓冲区位于调用栈, * 用户回调不得在返回后继续引用收到的字符串地址。 */ /* localtime_r 和 clock_gettime 是 POSIX 接口。特性宏必须先于所有头文件。 */ #if !defined(_WIN32) && !defined(_POSIX_C_SOURCE) # define _POSIX_C_SOURCE 200809L #endif #include "IEC104_Log.h" #include #include #include #ifdef _WIN32 /* 减少 引入的非必要声明,并避免旧版 Winsock 头文件干扰。 */ # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif # include #else /* 非 Windows 平台使用 POSIX 实时时钟和线程安全的本地时间转换接口。 */ # include #endif /* 包含结尾 '\0';时间戳、级别与正文最多合计511字节,不等同于511个多字节字符。 */ #define IEC104_LOG_BUFFER_SIZE 512U /* * 日志配置状态。 * * 默认级别为 INFO,默认回调为空。Set_* 应在开始并发输出之前调用; * 本模块不为配置读写增加锁。 */ static IEC104_LogCb_t g_log_callback = NULL; static int g_min_log_level = IEC104_LOG_LEVEL_INFO; /** * 功能:把格式化函数报告的长度限制在缓冲区实际可存储范围内。 * 输入参数:length——snprintf/vsnprintf 的返回值;capacity——缓冲区容量。 * 输出参数:无。 * 返回值:截断后占用的字节数,不包含结尾 '\0';格式化失败或容量为0时返回0。 */ static size_t Log_Clamp_Length(int length, size_t capacity) { if (length <= 0 || capacity == 0U) { return 0U; } if ((size_t)length >= capacity) { return capacity - 1U; } return (size_t)length; } /** * 功能:将日志级别转换为时间戳后显示的级别名称。 * 输入参数:level——日志级别。 * 输出参数:无。 * 返回值:指向静态只读字符串;未知级别返回 "???"。 */ static const char *Log_Level_Name(int level) { switch (level) { case IEC104_LOG_LEVEL_DEBUG: return "DBG "; case IEC104_LOG_LEVEL_INFO: return "INFO"; case IEC104_LOG_LEVEL_WARN: return "WARN"; case IEC104_LOG_LEVEL_ERR: return "ERR "; default: return "???"; } } /** * 功能:格式化本地时间戳和日志级别前缀。 * 输入参数:buffer——目标缓冲区;capacity——缓冲区容量;level——日志级别。 * 输出参数:buffer——形如 "[yyyy-mm-dd hh:mm:ss.mmm][INFO] " 的前缀。 * 返回值:缓冲区内实际占用的字节数,不包含结尾 '\0'。 * 说明:POSIX 时间接口失败时输出全零时间戳,避免使用未初始化数据。 */ static size_t Log_Format_Prefix(char *buffer, size_t capacity, int level) { int length; #ifdef _WIN32 SYSTEMTIME local_time; /* GetLocalTime 直接返回含毫秒字段的本地时间。 */ GetLocalTime(&local_time); length = snprintf( buffer, capacity, "[%04d-%02d-%02d %02d:%02d:%02d.%03d][%s] ", local_time.wYear, local_time.wMonth, local_time.wDay, local_time.wHour, local_time.wMinute, local_time.wSecond, local_time.wMilliseconds, Log_Level_Name(level)); #else struct timespec timestamp; struct tm local_time; /* clock_gettime 提供毫秒来源,localtime_r 负责线程安全的时区转换。 */ if (clock_gettime(CLOCK_REALTIME, ×tamp) != 0 || localtime_r(×tamp.tv_sec, &local_time) == NULL) { length = snprintf(buffer, capacity, "[0000-00-00 00:00:00.000][%s] ", Log_Level_Name(level)); } else { length = snprintf( buffer, capacity, "[%04d-%02d-%02d %02d:%02d:%02d.%03d][%s] ", local_time.tm_year + 1900, local_time.tm_mon + 1, local_time.tm_mday, local_time.tm_hour, local_time.tm_min, local_time.tm_sec, (int)(timestamp.tv_nsec / 1000000L), Log_Level_Name(level)); } #endif return Log_Clamp_Length(length, capacity); } /** * 功能:使用默认通道输出一条日志。 * 输入参数:level——日志级别;message——不含行末换行的完整日志字符串。 * 输出参数:无。 * 返回值:无。 * 说明:WARN/ERR 写 stderr,DEBUG/INFO 写 stdout;每条日志立即刷新。 */ static void Log_Write_Default(int level, const char *message) { FILE *output = (level >= IEC104_LOG_LEVEL_WARN) ? stderr : stdout; fputs(message, output); fputc('\n', output); fflush(output); } /** * 功能:设置日志输出回调。 * 输入参数:cb——新的日志回调;传入空指针时恢复默认标准流输出。 * 输出参数:无。 * 返回值:无。 * 说明:该配置没有内部同步,应在开始并发输出日志之前完成。 */ void IEC104_Set_Log_Callback(IEC104_LogCb_t cb) { /* 空回调表示恢复 Log_Write_Default()。 */ g_log_callback = cb; } /** * 功能:设置允许输出的最低日志级别。 * 输入参数:min_level——IEC104_LOG_LEVEL_* 之一。 * 输出参数:无。 * 返回值:无。 * 说明:低于该级别的消息会在格式化之前被丢弃。 */ void IEC104_Set_Log_Level(int min_level) { /* 保留 int 接口,调用方通常传入 IEC104_LOG_LEVEL_*。 */ g_min_log_level = min_level; } /** * 功能:格式化并输出一条带时间戳和级别前缀的日志。 * 输入参数:level——日志级别;fmt——非空且不含行末换行的 printf 格式串; * 其后为格式化参数。 * 输出参数:无;优先交给用户回调,否则写入默认标准流。 * 返回值:无。 * 说明:低级别消息直接忽略;超出固定缓冲区的内容按字节截断,可能截断多字节字符。 */ void IEC104_Log_Emit(int level, const char *fmt, ...) { char buffer[IEC104_LOG_BUFFER_SIZE]; size_t prefix_length; size_t remaining_capacity; size_t total_length; int body_length; va_list args; /* 被过滤的消息不执行时间和正文格式化。 */ if (fmt == NULL || level < g_min_log_level) { return; } /* 先生成前缀,再把正文写入同一个固定长度缓冲区。 */ prefix_length = Log_Format_Prefix(buffer, sizeof(buffer), level); remaining_capacity = sizeof(buffer) - prefix_length; va_start(args, fmt); body_length = vsnprintf(buffer + prefix_length, remaining_capacity, fmt, args); va_end(args); /* 格式化失败时仍可输出已经生成的前缀。 */ if (body_length < 0) { total_length = prefix_length; } else { total_length = prefix_length + Log_Clamp_Length(body_length, remaining_capacity); } buffer[total_length] = '\0'; /* 回调优先;未配置回调时使用模块默认输出通道。 */ if (g_log_callback != NULL) { g_log_callback(level, buffer); } else { Log_Write_Default(level, buffer); } }