ToB企服应用市场:ToB评测及商务社交产业平台

标题: 将代码中的调试信息输出到日志文件中 [打印本页]

作者: 一给    时间: 2023-4-5 22:27
标题: 将代码中的调试信息输出到日志文件中
一、将调试信息输出到屏幕中

1.1 一般写法

我们平常在写代码时,肯定会有一些调试信息的输出:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main()
  4. {
  5.     char szFileName[] = "test.txt";
  6.     FILE *fp = fopen(szFileName, "r");
  7.     if (fp == NULL)
  8.     {
  9.         // 文件打开失败,提示错误并退出
  10.         printf("open file(%s) error.\n", szFileName);
  11.         exit(0);
  12.     }
  13.     else
  14.     {
  15.         // 文件打开成功,进行相应的文件读/写操作
  16.     }
  17.     return 0;
  18. }
复制代码
假设当前目录下没有 test.txt 文件。当程序执行到第 7 行时,必然返回 NULL,这时候通过第 11 行的调试信息,我们可以帮助我们精确排查到程序退出的原因:原来是文件打开失败了。

那如果当前目录下存在 test.txt 文件,只是不可读呢?

在这种情况下如何快速定位文件打开失败的原因呢?我们可以考虑使用 errno
1.2 使用 errno

errno 是记录系统的最后一次错误代码。错误代码是一个 int 型的值,在 errno.h 中定义。
  1. #include <errno.h>        // errno 头文件
  2. #include <string.h>        // strerror 头文件
  3. // 文件打开失败,提示错误并退出
  4. printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));
复制代码
修改后再次运行 main.exe:

如果代码中包含很多的调试信息呢?我们并不能一下子知道这条信息到底是在哪里打印出来的,于是,我们又想,能不能把当前调试信息所在的文件名和源码行位置也打印出来呢,这样不就一目了然了吗。基于此,便有了 1.3 的内容。
1.3 编译器内置宏

ANSI C 标准中有几个标准预定义宏:
于是我们这么修改输出语句:
  1. // 文件打开失败,提示错误并退出
  2. printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n",
  3.                                                     __FILE__,
  4.                                                     __FUNCTION__,
  5.                                                     __LINE__,
  6.                                                     szFileName,
  7.                                                     errno, strerror(errno));
复制代码

相比于之前,确实是能帮助我们精准的定位问题,但是,总不能每次都要写这么长的 printf 吧,有没有偷懒的办法呢?
1.4 使用可变宏输出调试信息

1.4.1 可变宏介绍

用可变参数宏(variadic macros)传递可变参数表,你可能很熟悉在函数中使用可变参数表,如:
void printf(const char* format, ...);在 1999 年版本的 ISO C 标准中,宏可以像函数一样,定义时可以带有可变参数。宏的语法和函数的语法类似,如下所示:
  1. #define DEBUG(...) printf(__VA_ARGS__)
  2. int main()
  3. {
  4.     int x = 10;
  5.     DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
  6.    
  7.     return 0;
  8. }
复制代码
这类宏在被调用时,它(这里指缺省号...)被表示成零个或多个符号(包括里面的逗号),一直到右括弧结束为止。当被调用时,在宏体( macro body )中,这些符号序列集合将代替里面的 _VA_ARGS_ 标识符。当宏的调用展开时,实际的参数就传递给 printf 了。
相比于 ISO C 标准,GCC 始终支持复杂的宏,它使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。例如下面的例子:
  1. #define DEBUG(format, args...) printf(format, args)
  2. int main()
  3. {
  4.     int x = 10;
  5.     DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
  6.    
  7.     return 0;
  8. }
复制代码
在标准 C 里,你不能省略可变参数,但是你却可以给它传递一个空的参数。例如,下面的宏调用在「ISO C」里是非法的,因为字符串后面没有逗号:
  1. #define DEBUG(...) printf(__VA_ARGS__)
  2. int main()
  3. {
  4.     DEBUG("hello world.\n"); // 非法调用
  5. }
复制代码
GCC 在这种情况下可以让你完全的忽略可变参数。在上面的例子中,编译是仍然会有问题,因为宏展开后,里面的字符串后面会有个多余的逗号。为了解决这个问题, GCC 使用了一个特殊的##操作。书写格式为:
  1. #define DEBUG(format, args...) printf(format, ##args)
复制代码
1.4.2 使用可变宏输出调试信息

有了 1.4.1 的基础知识,我们可以这么修改代码:
  1. #define DEBUG(format, args...) \
  2.             printf("[%s][%s:%d] "format"\n", \
  3.                                         __FILE__, \
  4.                                         __FUNCTION__, \
  5.                                         __LINE__, \
  6.                                         ##args)
  7. // 文件打开失败,提示错误并退出
  8. DEBUG("open file(%s) error, errno[%d](%s).", szFileName, errno, strerror(errno));
复制代码
书写过长的问题解决后,又来新问题了,如果我想知道某一调试信息是何时被打印的呢?
下面让我们学习一下 Linux 中与时间相关的内容。
二、Linux 中与时间相关的函数

2.1 表示时间的结构体

通过查看头文件「/usr/include/time.h」和「/usr/include/bits/time.h」,我们可以找到下列四种表示「时间」的结构体:
  1. /* Returned by `time'. */
  2. typedef __time_t time_t;
复制代码
  1. /* A time value that is accurate to the nearest
  2.    microsecond but also has a range of years. */
  3. struct timeval
  4. {
  5.     __time_t tv_sec;       /* Seconds. */
  6.     __suseconds_t tv_usec; /* Microseconds. */
  7. };
复制代码
  1. struct timespec
  2. {
  3.     __time_t tv_sec;  /* Seconds. */
  4.     long int tv_nsec; /* Nanoseconds. */
  5. };
复制代码
  1. struct tm
  2. {
  3.     int tm_sec;   /* Seconds.                [0-59] (1 leap second) */
  4.     int tm_min;   /* Minutes.                [0-59] */
  5.     int tm_hour;  /* Hours.                    [0-23] */
  6.     int tm_mday;  /* Day.                        [1-31] */
  7.     int tm_mon;   /* Month.                        [0-11] */
  8.     int tm_year;  /* Year.                        自 1900 起的年数 */
  9.     int tm_wday;  /* Day of week.        [0-6] */
  10.     int tm_yday;  /* Days in year.        [0-365] */
  11.     int tm_isdst; /* DST.                        夏令时 */
  12. #ifdef __USE_BSD
  13.     long int tm_gmtoff;    /* Seconds east of UTC. */
  14.     __const char *tm_zone; /* Timezone abbreviation. */
  15. #else
  16.     long int __tm_gmtoff;    /* Seconds east of UTC. */
  17.     __const char *__tm_zone; /* Timezone abbreviation. */
  18. #endif
  19. };
复制代码
2.2 获取当前时间
  1. // 可以获取精确到秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的秒数
  2. time_t time(time_t *t);
复制代码
  1. // 可以获取精确到微秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的微秒数
  2. int gettimeofday(struct timeval *tv, struct timezone *tz);
复制代码
  1. // 可以获取精确到纳秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的纳秒数
  2. int clock_gettime(clockid_t clk_id, struct timespec *tp)
复制代码
使用方式如下所示:
  1. #include <stdio.h>
  2. #include <time.h>
  3. #include <sys/time.h>
  4. int main()
  5. {
  6.     time_t lTime;
  7.     time(&lTime);
  8.     printf("lTime       : %ld\n", lTime);
  9.     struct timeval stTimeVal;
  10.     gettimeofday(&stTimeVal, NULL);
  11.     printf("stTimeVal   : %ld\n", stTimeVal.tv_sec);
  12.     struct timespec stTimeSpec;
  13.     clock_gettime(CLOCK_REALTIME, &stTimeSpec);
  14.     printf("stTimeSpec  : %ld\n", stTimeSpec.tv_sec);
  15.     return 0;
  16. }
复制代码
Notes:
2.3 秒、毫秒、微秒、纳秒之间的转换

so:
从秒到毫秒,毫秒到微秒,微秒到纳秒都是 1000 的倍关系,也就是多 3 个 0 的关系。
另:个人电脑的微处理器执行一道指令(如将两数相加)约需 2~4 纳秒,所以程序只要精确到纳秒就够了。
2.4 对时间进行格式化输出

将 time_t 转换成 struct tm 的函数一共有 4 个,分别为:
形如 localtime 和形如 localtime_r 函数的区别是:localtime 获得的返回值存在于一个 static 的 struct tm 型的变量中,可能被后面的 localtime 调用覆盖掉。如果要防止覆盖,我们可以自己提供一个 struct tm 型的变量,利用 localtime_r 函数,将我们自己定义的变量的地址传进去,将结果保存在其中,这样就可以避免覆盖。
因此可知,函数 gmtime 和 localtime 是线程不安全的,多线程编程中要慎用!
2.5 获取毫秒时间
  1. #include <stdio.h>
  2. #include <time.h>
  3. #include <sys/time.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. char *GetMsecTime()
  7. {
  8.     static char buf[128];
  9.     time_t lTime = 0;
  10.     struct timeval stTimeVal = {0};
  11.     struct tm stTime = {0};
  12.     gettimeofday(&stTimeVal, NULL);
  13.     lTime = stTimeVal.tv_sec;
  14.     localtime_r(&lTime, &stTime);
  15.     snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d.%.3d",
  16.              stTime.tm_year + 1900,
  17.              stTime.tm_mon + 1,
  18.              stTime.tm_mday,
  19.              stTime.tm_hour,
  20.              stTime.tm_min,
  21.              stTime.tm_sec,
  22.              stTimeVal.tv_usec / 1000); // 微秒 -> 毫秒
  23.     return buf;
  24. }
  25. int main()
  26. {
  27.     puts(GetMsecTime());
  28.     return 0;
  29. }
复制代码
2.6 调试信息中新增时间信息
  1. #define DEBUG(format, args...) \
  2.             printf("%s [%s][%s:%d] "format"\n", \
  3.                                         GetMsecTime(), \
  4.                                         __FILE__, \
  5.                                         __FUNCTION__, \
  6.                                         __LINE__, \
  7.                                         ##args)
复制代码
至此,我们已经将调试信息的输出格式完善了,接下来就要考虑怎么将调试信息输出到日志文件中了。
三、将调试信息输出到日志文件中

3.1 日志等级

Log4J 定义了 8 个级别的 Log(除去 OFF 和 ALL,可以说分为 6 个级别),优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。
Log4J 建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG。我们下面的程序也将围绕这四个日志等级来进行编码。
先贴上源码,后续有时间在详细解释~
3.2 源码

3.2.1 log.h
  1. #ifndef __LOG_H__
  2. #define __LOG_H__
  3. #ifdef __cplusplus
  4. extern "C"
  5. {
  6. #endif
  7. // 日志路径
  8. #define LOG_PATH       "./Log/"
  9. #define LOG_ERROR             "log.error"
  10. #define LOG_WARN              "log.warn"
  11. #define LOG_INFO              "log.info"
  12. #define LOG_DEBUG             "log.debug"
  13. #define LOG_OVERFLOW_SUFFIX             "00"    // 日志溢出后的文件后缀,如 log.error00
  14. #define LOG_FILE_SIZE  (5*1024*1024)            // 单个日志文件的大小,5M
  15. // 日志级别
  16. typedef enum tagLogLevel
  17. {
  18.     LOG_LEVEL_ERROR    = 1,                             /* error级别 */
  19.     LOG_LEVEL_WARN     = 2,                             /* warn级别  */
  20.     LOG_LEVEL_INFO     = 3,                             /* info级别  */
  21.     LOG_LEVEL_DEBUG    = 4,                             /* debug级别 */
  22. } LOG_LEVEL_E;
  23. typedef struct tagLogFile
  24. {
  25.     char szCurLog[64];
  26.     char szPreLog[64];
  27. } LOG_FILE_S;
  28. #define PARSE_LOG_ERROR(format, args...)  \
  29.     WriteLog(LOG_LEVEL_ERROR, __FILE__, __FUNCTION__, __LINE__, format, ##args)
  30. #define PARSE_LOG_WARN(format, args...)  \
  31.     WriteLog(LOG_LEVEL_WARN, __FILE__, __FUNCTION__, __LINE__, format, ##args)
  32. #define PARSE_LOG_INFO(format, args...)  \
  33.     WriteLog(LOG_LEVEL_INFO, __FILE__, __FUNCTION__, __LINE__, format, ##args)
  34. #define PARSE_LOG_DEBUG(format, args...)  \
  35.     WriteLog(LOG_LEVEL_DEBUG, __FILE__, __FUNCTION__, __LINE__, format, ##args)
  36. extern void WriteLog
  37. (
  38.     LOG_LEVEL_E enLogLevel,
  39.     const char *pcFileName,
  40.     const char *pcFuncName,
  41.     int iFileLine,
  42.     const char *format,
  43.     ...
  44. );
  45. #ifdef __cplusplus
  46. }
  47. #endif
  48. #endif
复制代码
3.2.2 log.c
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <stdarg.h>     // va_stat 头文件
  5. #include <errno.h>      // errno 头文件
  6. #include <time.h>       // 时间结构体头文件
  7. #include <sys/time.h>   // 时间函数头文件
  8. #include <sys/stat.h>   // stat 头文件
  9. #include "log.h"
  10. static LOG_FILE_S gstLogFile[5] =
  11. {
  12.     {"", ""},
  13.     {
  14.         /* error级别 */
  15.         LOG_PATH LOG_ERROR,                     // ./Log/log.error
  16.         LOG_PATH LOG_ERROR LOG_OVERFLOW_SUFFIX  // ./Log/log.error00
  17.     },
  18.     {
  19.         /* warn级别 */
  20.         LOG_PATH LOG_WARN,                      // ./Log/log.warn
  21.         LOG_PATH LOG_WARN LOG_OVERFLOW_SUFFIX   // ./Log/log.warn00
  22.     },
  23.     {
  24.         /* info级别 */
  25.         LOG_PATH LOG_INFO,                      // ./Log/log.info
  26.         LOG_PATH LOG_INFO LOG_OVERFLOW_SUFFIX   // ./Log/log/info00
  27.     },
  28.     {
  29.         /* debug级别 */
  30.         LOG_PATH LOG_DEBUG,                     // ./Log/log.debug
  31.         LOG_PATH LOG_DEBUG LOG_OVERFLOW_SUFFIX  // ./Log/log.debug00
  32.     },
  33. };
  34. static void __Run_Log
  35. (
  36.     LOG_LEVEL_E enLogLevel,
  37.     const char *pcFileName,
  38.     const char *pcFuncName,
  39.     int iFileLine,
  40.     const char *format,
  41.     va_list vargs
  42. )
  43. {
  44.     FILE *logfile = NULL;
  45.     logfile = fopen(gstLogFile[enLogLevel].szCurLog, "a");
  46.     if (logfile == NULL)
  47.     {
  48.         printf("open %s error[%d](%s).\n", gstLogFile[enLogLevel].szCurLog, errno, strerror(errno));
  49.         return;
  50.     }
  51.     /* 获取时间信息 */
  52.     struct timeval stTimeVal = {0};
  53.     struct tm stTime = {0};
  54.     gettimeofday(&stTimeVal, NULL);
  55.     localtime_r(&stTimeVal.tv_sec, &stTime);
  56.     char buf[768];
  57.     snprintf(buf, 768, "%.2d-%.2d %.2d:%.2d:%.2d.%.3lu [%s][%s:%d] ",
  58.                                             stTime.tm_mon,
  59.                                             stTime.tm_mday,
  60.                                             stTime.tm_hour,
  61.                                             stTime.tm_min,
  62.                                             stTime.tm_sec,
  63.                                             (unsigned long)(stTimeVal.tv_usec / 1000),
  64.                                             pcFileName,
  65.                                             pcFuncName,
  66.                                             iFileLine);
  67.     fprintf(logfile, "%s", buf);
  68.     vfprintf(logfile, format, vargs);
  69.     fprintf(logfile, "%s", "\r\n");
  70.     fflush(logfile);
  71.     fclose(logfile);
  72.     return;
  73. }
  74. static void __LogCoverStrategy(char *pcPreLog) // 日志满后的覆盖策略
  75. {
  76.     int iLen = strlen(pcPreLog);
  77.     int iNum = (pcPreLog[iLen - 2] - '0') * 10 + (pcPreLog[iLen - 1] - '0');
  78.     iNum = (iNum + 1) % 10;
  79.     pcPreLog[iLen - 2] = iNum / 10 + '0';
  80.     pcPreLog[iLen - 1] = iNum % 10 + '0';
  81. }
  82. void WriteLog
  83. (
  84.     LOG_LEVEL_E enLogLevel,
  85.     const char *pcFileName,
  86.     const char *pcFuncName,
  87.     int iFileLine,
  88.     const char *format,
  89.     ...
  90. )
  91. {
  92.     char szCommand[64]; // system函数中的指令
  93.     struct stat statbuff;
  94.     if (stat(gstLogFile[enLogLevel].szCurLog, &statbuff) >= 0) // 如果存在
  95.     {
  96.         if (statbuff.st_size > LOG_FILE_SIZE) // 如果日志文件超出限制
  97.         {
  98.             printf("LOGFILE(%s) > 5M, del it.\n", gstLogFile[enLogLevel].szCurLog);
  99.             snprintf(szCommand, 64, "cp -f %s %s", gstLogFile[enLogLevel].szCurLog, gstLogFile[enLogLevel].szPreLog);
  100.             puts(szCommand);
  101.             system(szCommand);      // 将当前超出限制的日志保存到 log.error00 中
  102.             snprintf(szCommand, 64, "rm -f %s", gstLogFile[enLogLevel].szCurLog);
  103.             system(szCommand);      // 删掉 log.error
  104.             printf("%s\n\n", szCommand);
  105.             
  106.             // 如果 log.error 超出 5M 后,将依次保存在 log.error00、log.error01、... 中
  107.             __LogCoverStrategy(gstLogFile[enLogLevel].szPreLog);
  108.         }
  109.     }
  110.     else // 如果不存在,则创建
  111.     {
  112.         printf("LOGFILE(%s) is not found, create it.\n\n", gstLogFile[enLogLevel].szCurLog);
  113.         snprintf(szCommand, 64, "touch %s", gstLogFile[enLogLevel].szCurLog);
  114.         system(szCommand);
  115.     }
  116.     va_list argument_list;
  117.     va_start(argument_list, format);
  118.     if (format)
  119.     {
  120.         __Run_Log(enLogLevel, pcFileName, pcFuncName, iFileLine, format, argument_list);
  121.     }
  122.     va_end(argument_list);
  123.     return;
  124. }
复制代码
3.3.3 main.c
  1. #include <stdio.h>
  2. #include <unistd.h> // sleep 头文件
  3. #include "log.h"
  4. int main()
  5. {
  6.     for (int i = 0; i < 5; i++)
  7.     {
  8.         PARSE_LOG_ERROR("我是第 %d 条日志", i+1);
  9.     }
  10.     return 0;
  11. }
复制代码
3.3.4 Tutorial

参考资料


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4