前言:
一个 C 工程,在大多数时候我们需要定制打印函数,而不再仅仅使用printf。
对于一个工程,常见的需求有:
- 可以控制 打印级别;
- 控制分模块打印;
- 在打印头添加 __FILE__、__FUNCTION__、__LINE__ 等信息;
- 在打印头添加 时间信息;
- 日志直接输出到文件;
- 通过配置文件初始化打印级别 。。。
对于整个Log模块,我们可以使用面向对象的思想,使用 log.c 和 log.h 这两个文件来负责关于 Log 的所有工作。
以下,我们来看看怎么实现这样的定制功能:
1、控制打印级别
控制打印级别有很多方法,介绍一下其中的2种
// log.h
#define LOG_LEVEL 3
#if (LOG_LEVEL < 0 || LOG_LEVEL > 5)
#error "LOG_LEVEL should be between 0 and 5(include 0 and 5)"
#endif
/**************************App debug output control*******************************/
#define LOG_LEVEL LOG_LEVEL
#define log_p(...)
#define log_i(...)
#define log_d(...)
#define log_w(...)
#define log_e(...)
#if (LOG_LEVEL > 0)
#undef log_p
#define log_p(...) printf("[LOG_PRINT] "__VA_ARGS__)
#if (LOG_LEVEL > 1)
#undef log_i
#define log_i(...) printf("[LOG_INFO] "__VA_ARGS__)
#if (LOG_LEVEL > 2)
#undef log_d
#define log_d(...) printf("[LOG_DEBUG] "__VA_ARGS__)
#if (LOG_LEVEL > 3)
#undef log_w
#define log_w(...) printf("[LOG_WARNING] "__VA_ARGS__)
#if (LOG_LEVEL > 4)
#undef log_e
#define log_e(...) printf("[LOG_ERROR] "__VA_ARGS__)
#endif
#endif
#endif
#endif
#endif
这个方法实现了,控制大于某个级别才会输出,而且在每句打印开头都加了级别说明。
此方法只需要 log.h 就能实现控制输出级别了,这里在设置 LOG_LEVEL 的时候,用了一种宏检测技术,当值超过我们的限定的时候,#error 会中止编译。
这里通过 LOG_LEVEL 来控制打印级别,首先定义所有的打印函数为空,但是对于大于 LOG_LEVEL 的打印级别会被重新定义。
"..."是 C 语言中的变长参数表,后面使用宏 __VA_ARGS__ 来接收,相当于在每个打印前加了相应的级别说明。
不足:控制级别只能是从低到高,不可跳级设置;得使用多个不同的函数。
// log.h
#ifndef __LOG_H__
#define __LOG_H__
#include <stdio.h>
typedef enum
{
LOG_INFO = 0x01,
LOG_DEBUG = 0x02,
LOG_WARNING = 0x04,
LOG_ERROR = 0x08,
LOG_ALL = 0xff
}LOG_TAG;
void set_level(const unsigned char level); //设置打印级别
void print(const unsigned char level,const char *, ...); //带级别的打印
#endif
// log.c
#include "log.h"
#include <stdarg.h>
static unsigned char g_level = 0;
void set_level(const unsigned char level)
{
g_level = level;
}
void print(const unsigned char level,const char *va_alist, ...)
{
if(!(level & g_level))
return ;
va_list ap;
switch (level & g_level)
{
case LOG_INFO:
printf("[INFO] ");
break;
case LOG_DEBUG:
printf("[DEBUG] ");
break;
case LOG_WARNING:
printf("[WARNING] ");
break;
case LOG_ERROR:
printf("[ERROR] ");
break;
default:
return ;
}
va_start(ap, va_alist);
vfprintf(stdout, va_alist, ap);
va_end(ap);
return ;
}
这种方法实现了,可配置任意输出级别而不限于大小,带有打印级别说明。
此方法使用单个函数实现了级别控制,打印级别作为参数传入。
LOG_TAG 的数据类型为一个 8bit 的 unsigned char 型,每一个级别对应一个位。所以在可以直接通过 位与 的结果来判断是否设置为打印。
另外,在对 "..." 的解析中,使用 va_list 来解析。va_start 使 ap 指向可变参数表中的第一个参数;vfprintf 将打印的字符输出到标准输出;va_end 将 ap 赋0,使它不指向内存的变量。
不足:级别数有限,此处最高7级,依据数据位数而定。
2、分模块输出
分模块的原理和分级别的原理基本相同,可以将两者结合起来,实现更细分的控制。
// log.h
#ifndef __LOG_H__
#define __LOG_H__
#include <stdio.h>
typedef unsigned int uint;
typedef unsigned char uchar;
typedef enum
{
LOG_M_MAIN = 0,
LOG_M_INIT,
LOG_M_SEVER,
LOG_M_MEDIA,
LOG_M_LED,
LOG_M_MAX
}LOG_MODULE;
typedef enum
{
LOG_T_INFO = 0x01,
LOG_T_DEBUG = 0x02,
LOG_T_WARNING = 0x04,
LOG_T_ERROR = 0x08,
LOG_T_ALL = 0xff
}LOG_TAG;
void log_set_level(const uint mod, const uchar level); //设置打印级别
void log_p(const uint mod, const uchar level,const char *, ...); //带级别的打印
#endif
// log.c
#include "log.h"
#include <stdarg.h>
static uchar g_log_modu_str[][20] =
{
"M_MAIN",
"M_INIT",
"M_SEVER",
"M_MEDIA",
"M_LED",
"M_MAX",
};
static uchar g_level[LOG_M_MAX] = {0};
#ifdef USE_SUB_MODULE
void log_set_level(const uint mod, const uchar level)
{
if (mod >= LOG_M_MAX)
{
return ;
}
g_level[mod] = level;
return ;
}
void log_p(const uint mod, const uchar level,const char *va_alist, ...)
{
if (mod >= LOG_M_MAX || !(g_level[mod] & level))
{
return ;
}
va_list args;
uchar buf[128] = {0};
snprintf(buf, sizeof(buf), "[%s]", g_log_modu_str[mod]);
switch (level & g_level[mod])
{
case LOG_T_INFO:
strcat(buf, "[T_INFO] ");
break;
case LOG_T_DEBUG:
strcat(buf, "[T_DEBUG] ");
break;
case LOG_T_WARNING:
strcat(buf, "[T_WARNING] ");
break;
case LOG_T_ERROR:
strcat(buf, "[T_ERROR] ");
break;
default:
return ;
}
fprintf(stdout, buf);
va_start(args, va_alist);
vfprintf(stdout, va_alist, args);
va_end(args);
fflush(stdout);
return ;
}
此方法,除了设置级别外,还可以分模块输出。
3、在打印头添加 __FILE__、__FUNCTION__、__LINE__ 等信息
// log.h
#ifdef USE_LOG_MODULE
#define log_fl(MOD, LEVEL, FORMAT, ARGS...) \
log_p(MOD,LEVEL,"[%s][%s][%d] "FORMAT, __FILE__, __FUNCTION__, __LINE__, ##ARGS )
#else
#define log_fl(LEVEL, FORMAT, ARGS...) \
log_p(LEVEL,"[%s][%s][%d] "FORMAT, __FILE__, __FUNCTION__, __LINE__, ##ARGS )
#endif
在前面的基础上,如果要加上 文件名、函数名、行数 等信息的话,只需要在头文件声明一个宏即可,将信息分别对应着输入项。
对于前面的分模块和不分模块的打印,用 USE_LOG_MODULE 来区分。
4、在打印头添加 时间信息
时间大致有 墙上时钟时间、用户UTC时间、系统CPU时间。
这里以用户UTC时间为列:
// log.c
#include <sys/time.h>
#include <time.h>
#ifdef USE_LOG_TIME
struct timeval tmval;
struct tm tm_now = {0};
time_t t_now = 0;
gettimeofday(&tmval, NULL);
t_now = tmval.tv_sec;
localtime_r( &t_now, &tm_now);
snprintf( buf, sizeof(buf)-1, "[%02d:%02d:%02d.%03d]",
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec, tmval.tv_usec/1000 );
fputs(buf, stderr);
#endif
5、日志直接输出到文件
即将原来的输出到终端,转为存储到文件。
// log.c
#define LOG_FILE_NAME "log.txt"
int file_write(char* buf, size_t buf_len, char* file_name, char* type)
{
if ( buf == NULL || buf_len <= 0 ||\
file_name == NULL || file_name[0] == 0)
{
return 0;
}
FILE *fd = fopen(file_name, type);
if (fd == NULL)
{
perror("fopen");
return 0;
}
fwrite(buf, 1, buf_len, fd);
fclose(fd);
return 1;
}
int log_write_file(char* buf, size_t buf_len, char* file_name)
{
return file_write(buf, buf_len, file_name, "a+");
}
void log_to_file(const uint mod, const uchar level,const char *va_alist, ...)
{
if (mod >= LOG_M_MAX || !(g_level[mod] & level))
{
return ;
}
va_list args;
char buf[128] = {0};
snprintf(buf, sizeof(buf), "[%s]", g_log_modu_str[mod]);
switch (level & g_level[mod])
{
case LOG_T_INFO:
strcat(buf, "[T_INFO] ");
break;
case LOG_T_DEBUG:
strcat(buf, "[T_DEBUG] ");
break;
case LOG_T_WARNING:
strcat(buf, "[T_WARNING] ");
break;
case LOG_T_ERROR:
strcat(buf, "[T_ERROR] ");
break;
default:
return ;
}
log_write_file(buf, strlen(buf), LOG_FILE_NAME);
va_start(args, va_alist);
memset(buf, 0, sizeof(buf));
vsnprintf(buf, sizeof(buf), va_alist, args);
log_write_file(buf, strlen(buf), LOG_FILE_NAME);
va_end(args);
return ;
}
6、通过配置文件初始化打印
以带有模块的打印为例,配置文件命名为 log.ini:
M_MAIN = sfs
M_INIT =
M_SEVER = 8
M_MEDIA = 1
M_LED = 43
M_MAX = 2
格式是,模块名 = 等级
这个是测试用的文件,里面包含了一些错误配置,包括 等级为英文和等级非法的,缺省等级的。
// log.c
#define LOG_INI_FILE "log.ini"
void log_init(void)
{
FILE *fd = fopen(LOG_INI_FILE, "r");
if (fd == NULL)
{
perror("fopen LOG_INI_FILE");
return ;
}
int level = 0;
char buf[128] = {0};
size_t read_len = 0, i, j;
while (!feof(fd))
{
fgets(buf, sizeof(buf), fd);
read_len = strlen(buf);
for(i = 0; i <= LOG_M_MAX; i++)
{
if(strstr(buf, g_log_modu_str[i]))
{
for(j = 0; j < read_len; j++)
{
if(buf[j] == '=')
{
j++;
if(j < read_len)
{
level = atoi(buf+j);
}
else
{
break;
}
switch(level)
{
case LOG_T_INFO:
case LOG_T_DEBUG:
case LOG_T_WARNING:
case LOG_T_ERROR:
case LOG_T_ALL:
if(i == LOG_M_MAX)
{
for(i = 0; i < LOG_M_MAX; i++)
{
log_set_level(i, level);
}
return ;
}
log_set_level(i, level);
break;
default:
break;
}
break;
}
}
break;
}
}
memset(buf, 0, sizeof(buf));
}
return ;
}
可对每个模块设置自己的等级;除此之外,当 M_MAX有值,使用这个值设置所有模块。
总结:
好用的、格式标准的 Log 对于一个工程来说至关重要,定制化 Log 是必须的,上面的代码还比较粗糙,但是万变不离其宗,以上是 Log 的思想和初步实现,供各位参考。