How to output the relevant debugging information in the code to the corresponding log file

1. Output debugging information to the screen

1.1 General writing

When we usually write code, there will definitely be some output of debugging information:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char szFileName[] = "test.txt";
    FILE *fp = fopen(szFileName, "r");
    if (fp == NULL)
    {
        // 文件打开失败,提示错误并退出
        printf("open file(%s) error.\n", szFileName);
        exit(0);
    }
    else
    {
        // 文件打开成功,进行相应的文件读/写操作
    }

    return 0;
}

Suppose there is no test.txt file in the current directory. When the program executes to line 7, it must return NULL. At this time, through the debugging information on line 11, we can help us accurately troubleshoot the reason for the program exit: it turns out that the file failed to open.

 What if there is a test.txt file in the current directory, but it is not readable?

 Also output open file(test.txt) error

In this case, how to quickly locate the cause of the failure to open the file? We can consider using  errno .

1.2 Using errno

errno is the last error code for the logging system. The error code is an int value defined in errno.h.

#include <errno.h>	// errno 头文件
#include <string.h>	// strerror 头文件

// 文件打开失败,提示错误并退出
printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));

Run main.exe again after modification:

What if the code contains a lot of debugging information? We can't know where this information is printed out at once, so we thought, can we also print out the file name and source code line location where the current debugging information is located, so that it is clear at a glance. Based on this, there is the content of 1.3.

1.3 Compiler built-in macros

There are several standard predefined macros in the ANSI C standard:

  • __LINE__: Insert the current source code line number in the source code
  • __FILE__: Insert the current source file name in the source file
  • __FUNCTION_: Insert the current function name in the source file
  • __DATE__: insert the current compilation date in the source file
  • __TIME__: insert the current compilation time in the source file
  • __STDC__: When the program is required to strictly follow the ANSI C standard, the flag is assigned a value of 1
  • __cplusplus: This identifier is defined when writing a C++ program

So we modify the output statement like this:

// 文件打开失败,提示错误并退出
printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n", 
                                                    __FILE__,
                                                    __FUNCTION__,
                                                    __LINE__,
                                                    szFileName, 
                                                    errno, strerror(errno));

 From the log information, we can accurately obtain: the 16th line of the main function in the main.c file reported an error, and the cause of the error was Permission denied

Compared with before, it can indeed help us to accurately locate the problem, but we can't write such a long printf every time, right? Is there a way to be lazy?

1.4 Use variable macros to output debugging information

1.4.1 Introduction to variable macros

Use variadic macros to pass variadic arguments. You may be familiar with using variadic arguments in functions, such as:

void printf(const char* format, ...);

In the 1999 version of the ISO C standard, macros can be defined with varargs like functions. The syntax of a macro is similar to that of a function, as follows:

#define DEBUG(...) printf(__VA_ARGS__)

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
    
    return 0;
}
  • The default number ( ...) refers to variable parameters
  • __VA_ARGS__Macros are used to accept a variable number of arguments

When this kind of macro is invoked, it (here refers to the default number ...) is expressed as zero or more symbols (including commas inside), until the end of the right parenthesis. When invoked, in the macro body (macro body), these collections of symbol sequences will replace the _VA_ARGS_ identifiers inside . When the call to the macro is expanded, the actual arguments are passed to  printf .

Compared to the ISO C standard, GCC has always supported complex macros, using a different syntax that allows you to give variadic parameters a name like any other parameter. For example the following example:

#define DEBUG(format, args...) printf(format, args)

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
    
    return 0;
}
  • This is exactly the same as the "ISO C" macro example above, but it is more readable and easier to describe

In standard C, you can't omit a variadic parameter, but you can pass it an empty parameter. For example, the following macro call is illegal in "ISO C" because there is no comma after the string:

#define DEBUG(...) printf(__VA_ARGS__)

int main()
{
    DEBUG("hello world.\n"); // 非法调用
}

GCC lets you ignore varargs entirely in this case. In the above example, there will still be problems with compilation, because after the macro is expanded, there will be an extra comma after the string inside. To solve this problem, GCC uses a special ##operation. The writing format is:

#define DEBUG(format, args...) printf(format, ##args)
  • Here, if the variable parameter is omitted or empty, ##the action will cause the preprocessor to strip the comma preceding it

  • If you do provide some variable parameters when calling the macro, the macro definition will work fine, it will put these variable parameters after the comma

1.4.2 Using variable macros to output debugging information

With the basics of 1.4.1, we can modify the code like this:

#define DEBUG(format, args...) \
            printf("[%s][%s:%d] "format"\n", \
                        		__FILE__, \
                        		__FUNCTION__, \
                        		__LINE__, \
                        		##args)

// 文件打开失败,提示错误并退出
DEBUG("open file(%s) error, errno[%d](%s).", szFileName, errno, strerror(errno));
  • Through variable macros, the problem of writing too long debugging information is perfectly solved

After the problem of writing too long is solved, there is a new problem. What if I want to know when a certain debugging information is printed?

Let us learn about time-related content in Linux.

2. Time-related functions in Linux

2.1 A structure representing time

By looking at the header files "/usr/include/time.h" and "/usr/include/bits/time.h", we can find the following four structures representing "time":

/* Returned by `time'. */
typedef __time_t time_t;
/* A time value that is accurate to the nearest
   microsecond but also has a range of years. */
struct timeval
{
    __time_t tv_sec;       /* Seconds. */
    __suseconds_t tv_usec; /* Microseconds. */
};
struct timespec
{
    __time_t tv_sec;  /* Seconds. */
    long int tv_nsec; /* Nanoseconds. */
};
struct tm
{
    int tm_sec;   /* Seconds.		[0-59] (1 leap second) */
    int tm_min;   /* Minutes.		[0-59] */
    int tm_hour;  /* Hours.    		[0-23] */
    int tm_mday;  /* Day.			[1-31] */
    int tm_mon;   /* Month.			[0-11] */
    int tm_year;  /* Year.			自 1900 起的年数 */
    int tm_wday;  /* Day of week.	[0-6] */
    int tm_yday;  /* Days in year.	[0-365] */
    int tm_isdst; /* DST.			夏令时 */

#ifdef __USE_BSD
    long int tm_gmtoff;    /* Seconds east of UTC. */
    __const char *tm_zone; /* Timezone abbreviation. */
#else
    long int __tm_gmtoff;    /* Seconds east of UTC. */
    __const char *__tm_zone; /* Timezone abbreviation. */
#endif
};
  1. time_t Is a long integer used to represent "seconds"
  2. struct timeval The structure uses "seconds and microseconds" to represent time
  3. struct timespec The structure uses "seconds and nanoseconds" to represent time
  4. struct tm Directly use "seconds, minutes, hours, days, months, years" to express time

2.2 Get the current time

// 可以获取精确到秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的秒数
time_t time(time_t *t); 
// 可以获取精确到微秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的微秒数
int gettimeofday(struct timeval *tv, struct timezone *tz);
// 可以获取精确到纳秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的纳秒数
int clock_gettime(clockid_t clk_id, struct timespec *tp)

The way to use it is as follows:

#include <stdio.h>
#include <time.h>
#include <sys/time.h>

int main()
{
    time_t lTime;
    time(&lTime);
    printf("lTime       : %ld\n", lTime);

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    printf("stTimeVal   : %ld\n", stTimeVal.tv_sec);

    struct timespec stTimeSpec;
    clock_gettime(CLOCK_REALTIME, &stTimeSpec);
    printf("stTimeSpec  : %ld\n", stTimeSpec.tv_sec);

    return 0;
}
  • We can get the current time with three different precisions through the above three functions

Notes:

  1. POSIX.1-2008 marks gettimeofday() as obsolete, recommending the use of clock_gettime(2) instead.
  2. Moreover, someone has done a test. When using gettimeofday twice in a row, there will be a phenomenon of "backward time" with a small probability. The time obtained by the second function call is less than or earlier than the time obtained by the first call. .
  3. The gettimeofday function is not as stable and not as accurate as times or clock, but they are used similarly.
  4. The clock has a timing limit, which is said to be 596.5+ hours, which is generally enough to cope.
  5. Processes such as ntpd may modify the system time, causing timing errors.
  6. According to online discussions, things like TSC and HPET interruptions may cause the system's wall time to roll back. This should be related to the specific system implementation. In short, the gettimeofday function does not guarantee the accuracy provided, nor does it guarantee the accurate time of the system. The result it returns is "the system's best guess at wall time".
  7. If possible, try to use clock_gettime (CLOCK_MONOTONIC), but not all systems implement posix realtime, such as mac os x.
  8. 所以现在应该用:int clock_gettime(CLOCK_MONOTONIC, struct timespec *tp);
    CLOCK_MONOTONIC:Clock that cannot be set and represents monotonic time since some unspecified starting point.

2.3 Conversion between seconds, milliseconds, microseconds, nanoseconds

  • 1 second = 1000 milliseconds
  • 1 millisecond = 1000 microseconds
  • 1 microsecond = 1000 nanoseconds

so:

  • 1 second = 1000,000 microseconds (one million microseconds)
  • 1 second = 1000,000,000 nanoseconds (billion nanoseconds)

From seconds to milliseconds, milliseconds to microseconds, and microseconds to nanoseconds are all times of 1000, that is, the relationship of three more 0s.

Another: It takes about 2 to 4 nanoseconds for the microprocessor of a personal computer to execute an instruction (such as adding two numbers), so the program only needs to be accurate to nanoseconds.

2.4 Formatted output of time

  1. First  convert struct timeval or  struct timespec to seconds represented by time_t:

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    
    time_t lTime = stTimeVal.tv_sec;
  2. Use system functions to convert time_t to  struct tm:

    struct tm stTime;
    localtime_r(&lTime, &stTime); // 注意,localtime_r 的第二个参数是入参
  3. Formatted output:

    char buf[128];
    // 自定义输出格式:YYYY-MM-DD hh:mm:ss
    snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d", 
                            stTime.tm_year + 1900,
                            stTime.tm_mon + 1,
                            stTime.tm_mday,
                            stTime.tm_hour,
                            stTime.tm_min,
                            stTime.tm_sec);
    puts(buf);

There are 4 functions to convert time_t into struct tm, namely:

  1. struct tm *gmtime(const time_t *timep);
  2. struct tm *gmtime_r(const time_t *timep, struct tm *result);
  3. struct tm *localtime(const time_t *timep);
  4. struct tm *localtime_r(const time_t *timep, struct tm *result);

The difference between functions like localtime and functions like localtime_r is: the return value obtained by localtime exists in a static struct tm variable, which may be overwritten by subsequent localtime calls. If we want to prevent overwriting, we can provide a variable of type struct tm by ourselves, use the localtime_r function to pass in the address of the variable we defined, and save the result in it, so as to avoid overwriting.

Therefore, it can be seen that the functions gmtime and localtime are not thread-safe, and should be used with caution in multi-threaded programming!

2.5 Get millisecond time

#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <string.h>

char *GetMsecTime()
{
    static char buf[128];
    time_t lTime = 0;
    struct timeval stTimeVal = {0};
    struct tm stTime = {0};

    gettimeofday(&stTimeVal, NULL);
    lTime = stTimeVal.tv_sec;
    localtime_r(&lTime, &stTime);

    snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d.%.3d",
             stTime.tm_year + 1900,
             stTime.tm_mon + 1,
             stTime.tm_mday,
             stTime.tm_hour,
             stTime.tm_min,
             stTime.tm_sec,
             stTimeVal.tv_usec / 1000); // 微秒 -> 毫秒
    return buf;
}
int main()
{
    puts(GetMsecTime());

    return 0;
}
  • Note that the buf returned by this function is modified by static and is not thread-safe

2.6 Add time information in debugging information

#define DEBUG(format, args...) \
            printf("%s [%s][%s:%d] "format"\n", \
                        		GetMsecTime(), \
                        		__FILE__, \
                        		__FUNCTION__, \
                        		__LINE__, \
                        		##args)

So far, we have perfected the output format of the debugging information, and then we need to consider how to output the debugging information to the log file.

3. Output the debugging information to the log file

3.1 Log level

Log4J defines 8 levels of Log (excluding OFF and ALL, it can be divided into 6 levels), and the priority from high to low is: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL.

  • OFF: the highest level, used to turn off all logging

  • FATAL: Indicates that each serious error event will cause the application to exit. This level is relatively high, major errors, you can stop the program directly at this level

  • ERROR: Indicates that although an error event occurs, it still does not affect the continuous operation of the system. Print error and exception information, if you don't want to output too many logs, you can use this level

  • WARN: Indicates that a potential error will occur. Some information is not an error message, but some hints are also given to the programmer.

  • INFO: Print some information that you are interested in or important. This can be used to output some important information about the running of the program in the production environment, but it cannot be abused to avoid printing too many logs

  • DEBUG: Mainly used to print some running information during the development process

  • TRACE:  Very low log level, generally not used

  • ALL:  the lowest level, used to open all logging

Log4J recommends using only four levels, the priority from high to low is ERROR, WARN, INFO, DEBUG. Our program below will also be coded around these four log levels.

Paste the source code first, and then explain it in detail when you have time~

3.2 Source code

3.2.1 log.h

#ifndef __LOG_H__
#define __LOG_H__

#ifdef __cplusplus
extern "C"
{
#endif

// 日志路径
#define LOG_PATH       "./Log/"
#define LOG_ERROR             "log.error"
#define LOG_WARN              "log.warn"
#define LOG_INFO              "log.info"
#define LOG_DEBUG             "log.debug"
#define LOG_OVERFLOW_SUFFIX             "00"    // 日志溢出后的文件后缀,如 log.error00

#define LOG_FILE_SIZE  (5*1024*1024)            // 单个日志文件的大小,5M

// 日志级别
typedef enum tagLogLevel
{
    LOG_LEVEL_ERROR    = 1,                             /* error级别 */
    LOG_LEVEL_WARN     = 2,                             /* warn级别  */
    LOG_LEVEL_INFO     = 3,                             /* info级别  */
    LOG_LEVEL_DEBUG    = 4,                             /* debug级别 */
} LOG_LEVEL_E;

typedef struct tagLogFile
{
    char szCurLog[64];
    char szPreLog[64];
} LOG_FILE_S;

#define PARSE_LOG_ERROR(format, args...)  \
    WriteLog(LOG_LEVEL_ERROR, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_WARN(format, args...)  \
    WriteLog(LOG_LEVEL_WARN, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_INFO(format, args...)  \
    WriteLog(LOG_LEVEL_INFO, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_DEBUG(format, args...)  \
    WriteLog(LOG_LEVEL_DEBUG, __FILE__, __FUNCTION__, __LINE__, format, ##args)

extern void WriteLog
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format, 
    ...
);

#ifdef __cplusplus
}
#endif

#endif

3.2.2 log.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>     // va_stat 头文件
#include <errno.h>      // errno 头文件
#include <time.h>       // 时间结构体头文件
#include <sys/time.h>   // 时间函数头文件
#include <sys/stat.h>   // stat 头文件
#include "log.h"

static LOG_FILE_S gstLogFile[5] = 
{
    {"", ""},
    {
        /* error级别 */
        LOG_PATH LOG_ERROR,                     // ./Log/log.error
        LOG_PATH LOG_ERROR LOG_OVERFLOW_SUFFIX  // ./Log/log.error00
    },
    {
        /* warn级别 */
        LOG_PATH LOG_WARN,                      // ./Log/log.warn
        LOG_PATH LOG_WARN LOG_OVERFLOW_SUFFIX   // ./Log/log.warn00
    }, 
    {
        /* info级别 */
        LOG_PATH LOG_INFO,                      // ./Log/log.info
        LOG_PATH LOG_INFO LOG_OVERFLOW_SUFFIX   // ./Log/log/info00
    }, 
    {
        /* debug级别 */
        LOG_PATH LOG_DEBUG,                     // ./Log/log.debug
        LOG_PATH LOG_DEBUG LOG_OVERFLOW_SUFFIX  // ./Log/log.debug00
    }, 
};

static void __Run_Log
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format,
    va_list vargs
)
{
    FILE *logfile = NULL;
    logfile = fopen(gstLogFile[enLogLevel].szCurLog, "a");
    if (logfile == NULL)
    {
        printf("open %s error[%d](%s).\n", gstLogFile[enLogLevel].szCurLog, errno, strerror(errno));
        return;
    }

    /* 获取时间信息 */
    struct timeval stTimeVal = {0};
    struct tm stTime = {0};
    gettimeofday(&stTimeVal, NULL);
    localtime_r(&stTimeVal.tv_sec, &stTime);

    char buf[768];
    snprintf(buf, 768, "%.2d-%.2d %.2d:%.2d:%.2d.%.3lu [%s][%s:%d] ",
                                            stTime.tm_mon + 1,
                                            stTime.tm_mday,
                                            stTime.tm_hour,
                                            stTime.tm_min,
                                            stTime.tm_sec,
                                            (unsigned long)(stTimeVal.tv_usec / 1000),
                                            pcFileName,
                                            pcFuncName,
                                            iFileLine);

    fprintf(logfile, "%s", buf);
    vfprintf(logfile, format, vargs);
    fprintf(logfile, "%s", "\r\n");
    fflush(logfile);

    fclose(logfile);

    return;
}
static void __LogCoverStrategy(char *pcPreLog) // 日志满后的覆盖策略
{
    int iLen = strlen(pcPreLog);
    int iNum = (pcPreLog[iLen - 2] - '0') * 10 + (pcPreLog[iLen - 1] - '0');
    iNum = (iNum + 1) % 10;

    pcPreLog[iLen - 2] = iNum / 10 + '0';
    pcPreLog[iLen - 1] = iNum % 10 + '0';
}

void WriteLog
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format, 
    ...
)
{
    char szCommand[64]; // system函数中的指令
    struct stat statbuff;
    if (stat(gstLogFile[enLogLevel].szCurLog, &statbuff) >= 0) // 如果存在
    {
        if (statbuff.st_size > LOG_FILE_SIZE) // 如果日志文件超出限制
        {
            printf("LOGFILE(%s) > 5M, del it.\n", gstLogFile[enLogLevel].szCurLog);
            snprintf(szCommand, 64, "cp -f %s %s", gstLogFile[enLogLevel].szCurLog, gstLogFile[enLogLevel].szPreLog); 
            puts(szCommand);
            system(szCommand);      // 将当前超出限制的日志保存到 log.error00 中

            snprintf(szCommand, 64, "rm -f %s", gstLogFile[enLogLevel].szCurLog);
            system(szCommand);      // 删掉 log.error
            printf("%s\n\n", szCommand);
            
            // 如果 log.error 超出 5M 后,将依次保存在 log.error00、log.error01、... 中
            __LogCoverStrategy(gstLogFile[enLogLevel].szPreLog); 
        }
    }
    else // 如果不存在,则创建
    {
        printf("LOGFILE(%s) is not found, create it.\n\n", gstLogFile[enLogLevel].szCurLog);
        snprintf(szCommand, 64, "touch %s", gstLogFile[enLogLevel].szCurLog);
        system(szCommand);
    }

    va_list argument_list;
    va_start(argument_list, format);

    if (format)
    {
        __Run_Log(enLogLevel, pcFileName, pcFuncName, iFileLine, format, argument_list);
    }

    va_end(argument_list);

    return;
}

3.3.3 main.c

#include <stdio.h>
#include <unistd.h> // sleep 头文件
#include "log.h"

int main()
{
    for (int i = 0; i < 5; i++)
    {
        PARSE_LOG_ERROR("我是第 %d 条日志", i+1);
    }

    return 0;
}

3.3.4 Tutorial

Put log.h, log.c, main.c in the same directory

And create a new Log directory

compile, run

 

Guess you like

Origin blog.csdn.net/weixin_55305220/article/details/131200293