222 lines
7.2 KiB
C
222 lines
7.2 KiB
C
/**
|
||
* 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 <stdarg.h>
|
||
#include <stddef.h>
|
||
#include <stdio.h>
|
||
|
||
#ifdef _WIN32
|
||
/* 减少 <windows.h> 引入的非必要声明,并避免旧版 Winsock 头文件干扰。 */
|
||
# ifndef WIN32_LEAN_AND_MEAN
|
||
# define WIN32_LEAN_AND_MEAN
|
||
# endif
|
||
# include <windows.h>
|
||
#else
|
||
/* 非 Windows 平台使用 POSIX 实时时钟和线程安全的本地时间转换接口。 */
|
||
# include <time.h>
|
||
#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);
|
||
}
|
||
}
|