Como gerar as informações de depuração relevantes no código para o arquivo de log correspondente

1. Saída de informações de depuração para a tela

1.1 Redação geral

Quando normalmente escrevemos código, definitivamente haverá alguma saída de informações de depuração:

#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;
}

Suponha que não haja nenhum arquivo test.txt no diretório atual. Quando o programa for executado na linha 7, ele deve retornar NULL.Neste momento, por meio das informações de depuração da linha 11, podemos nos ajudar a descobrir com precisão o motivo da saída do programa: verifica-se que o arquivo falhou ao abrir.

 E se houver um arquivo test.txt no diretório atual, mas não for legível?

 Também gera erro de arquivo aberto (test.txt)

Nesse caso, como localizar rapidamente a causa da falha na abertura do arquivo? Podemos considerar o uso  de errno .

1.2 Usando errno

errno é o último código de erro para o sistema de registro. O código de erro é um valor int definido em errno.h.

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

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

Execute main.exe novamente após a modificação:

E se o código contiver muitas informações de depuração? Não podemos saber onde essas informações são impressas de uma só vez, então pensamos, podemos também imprimir o nome do arquivo e o local da linha do código-fonte onde as informações de depuração atuais estão localizadas, para que fique claro à primeira vista. Com base nisso, tem-se o conteúdo de 1.3.

1.3 Macros integradas do compilador

Existem várias macros padrão predefinidas no padrão ANSI C:

  • __LINE__: Insira o número da linha do código-fonte atual no código-fonte
  • __FILE__: Insira o nome do arquivo de origem atual no arquivo de origem
  • __FUNCTION_: Insira o nome da função atual no arquivo de origem
  • __DATE__: insere a data de compilação atual no arquivo de origem
  • __TIME__: insere o tempo de compilação atual no arquivo de origem
  • __STDC__: Quando o programa é obrigado a seguir estritamente o padrão ANSI C, o sinalizador recebe um valor de 1
  • __cplusplus: Este identificador é definido ao escrever um programa C++

Portanto, modificamos a instrução de saída assim:

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

 A partir das informações do log, podemos obter com precisão: a 16ª linha da função principal no arquivo main.c relatou um erro e a causa do erro foi Permissão negada

Comparado com antes, pode realmente nos ajudar a localizar o problema com precisão, mas não podemos escrever um printf tão longo todas as vezes, certo? Existe uma maneira de ser preguiçoso?

1.4 Use macros variáveis ​​para gerar informações de depuração

1.4.1 Introdução às macros variáveis

Use macros variadic para passar argumentos variadic. Você pode estar familiarizado com o uso de argumentos variadic em funções, como:

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

Na versão de 1999 do padrão ISO C, as macros podem ser definidas com funções semelhantes a varargs. A sintaxe de uma macro é semelhante à de uma função, como segue:

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

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
    
    return 0;
}
  • O número padrão ( ...) refere-se a parâmetros variáveis
  • __VA_ARGS__As macros são usadas para aceitar um número variável de argumentos

Quando esse tipo de macro é chamado, ele (aqui se refere ao número padrão ...) é expresso como zero ou mais símbolos (incluindo vírgulas dentro), até o final do parêntese direito. Quando invocadas, no corpo da macro (corpo da macro), essas coleções de sequências de símbolos substituirão os identificadores _VA_ARGS_ dentro de . Quando a chamada para a macro é expandida, os argumentos reais são passados ​​para  printf .

Comparado ao padrão ISO C, o GCC sempre suportou macros complexas, usando uma sintaxe diferente que permite dar aos parâmetros variáveis ​​um nome como qualquer outro parâmetro. Por exemplo o seguinte exemplo:

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

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
    
    return 0;
}
  • Isso é exatamente o mesmo que o exemplo de macro "ISO C" acima, mas é mais legível e fácil de descrever

No padrão C, você não pode omitir um parâmetro variádico, mas pode passar um parâmetro vazio. Por exemplo, a seguinte chamada de macro é ilegal em "ISO C" porque não há vírgula após a string:

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

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

O GCC permite que você ignore varargs inteiramente neste caso. No exemplo acima, ainda haverá problemas com a compilação, pois após a macro ser expandida, haverá uma vírgula extra após a string dentro dela. Para resolver esse problema, o GCC usa uma ##operação especial. O formato de escrita é:

#define DEBUG(format, args...) printf(format, ##args)
  • Aqui, se o parâmetro variável for omitido ou vazio, ##a ação fará com que o pré-processador retire a vírgula que o precede

  • Se você fornecer alguns parâmetros variáveis ​​ao chamar a macro, a definição da macro funcionará bem, colocará esses parâmetros variáveis ​​após a vírgula

1.4.2 Usando macros variáveis ​​para gerar informações de depuração

Com o básico de 1.4.1, podemos modificar o código assim:

#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));
  • Por meio de macros variáveis, o problema de escrever informações de depuração muito longas é perfeitamente resolvido

Depois que o problema de escrever muito tempo é resolvido, há um novo problema: e se eu quiser saber quando uma determinada informação de depuração é impressa?

Vamos aprender sobre o conteúdo relacionado ao tempo no Linux.

2. Funções relacionadas ao tempo no Linux

2.1 Uma estrutura que representa o tempo

Observando os arquivos de cabeçalho "/usr/include/time.h" e "/usr/include/bits/time.h", podemos encontrar as quatro estruturas a seguir representando "tempo":

/* 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 É um inteiro longo usado para representar "segundos"
  2. struct timeval A estrutura usa "segundos e microssegundos" para representar o tempo
  3. struct timespec A estrutura usa "segundos e nanossegundos" para representar o tempo
  4. struct tm Use diretamente "segundos, minutos, horas, dias, meses, anos" para expressar o tempo

2.2 Obter a hora atual

// 可以获取精确到秒的当前距离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)

A forma de usar é a seguinte:

#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;
}
  • Podemos obter a hora atual com três precisões diferentes através das três funções acima

Notas:

  1. POSIX.1-2008 marca gettimeofday() como obsoleto, recomendando o uso de clock_gettime(2).
  2. Além disso, alguém fez um teste. Ao usar gettimeofday duas vezes seguidas, haverá um fenômeno de "tempo para trás" com uma pequena probabilidade. O tempo obtido pela segunda chamada da função é menor ou anterior ao tempo obtido pelo primeira chamada. .
  3. A função gettimeofday não é tão estável e nem tão precisa quanto as horas ou o relógio, mas são usadas de maneira semelhante.
  4. O relógio tem um limite de tempo, que é de 596,5+ horas, o que geralmente é suficiente para lidar.
  5. Processos como o ntpd podem modificar a hora do sistema, causando erros de temporização.
  6. De acordo com discussões on-line, coisas como interrupções de TSC e HPET podem fazer com que o tempo de parede do sistema seja revertido. Isso deve estar relacionado à implementação específica do sistema. Resumindo, a função gettimeofday não garante a precisão fornecida, nem garante a hora exata do sistema. O resultado que ela retorna é "o melhor palpite do sistema no tempo de parede".
  7. Se possível, tente usar clock_gettime (CLOCK_MONOTONIC), mas nem todos os sistemas implementam posix realtime, como mac os x.
  8. 所以现在应该用:int clock_gettime(CLOCK_MONOTONIC, struct timespec *tp);
    CLOCK_MONOTONIC:Relógio que não pode ser ajustado e representa tempo monotônico desde algum ponto inicial não especificado.

2.3 Conversão entre segundos, milissegundos, microssegundos, nanossegundos

  • 1 segundo = 1000 milissegundos
  • 1 milissegundo = 1000 microssegundos
  • 1 microssegundo = 1000 nanossegundos

então:

  • 1 segundo = 1000.000 microssegundos (um milhão de microssegundos)
  • 1 segundo = 1000.000.000 nanossegundos (bilhões de nanossegundos)

De segundos a milissegundos, milissegundos a microssegundos e microssegundos a nanossegundos são tempos de 1000, ou seja, a relação de mais três 0s.

Outra: leva cerca de 2 a 4 nanossegundos para o microprocessador de um computador pessoal executar uma instrução (como somar dois números), portanto, o programa só precisa ter precisão de nanossegundos.

2.4 Saída formatada de tempo

  1. Primeiro  converta struct timeval ou  struct timespec para segundos representados por time_t:

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    
    time_t lTime = stTimeVal.tv_sec;
  2. Use as funções do sistema para converter time_t para  struct tm:

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

    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);

Existem 4 funções para converter time_t em struct tm, a saber:

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

A diferença entre funções como localtime e funções como localtime_r é: o valor de retorno obtido por localtime existe em uma variável struct tm estática, que pode ser sobrescrita por chamadas de localtime subsequentes. Se quisermos evitar a substituição, podemos fornecer uma variável do tipo struct tm por nós mesmos, usar a função localtime_r para passar o endereço da variável que definimos e salvar o resultado nela, para evitar a substituição.

Portanto, pode-se ver que as funções gmtime e localtime não são thread-safe, e devem ser usadas com cautela em programação multi-thread!

2.5 Obter tempo em milissegundos

#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;
}
  • Observe que o buf retornado por esta função é modificado por static e não é thread-safe

2.6 Adicionar informações de tempo nas informações de depuração

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

Até agora, aperfeiçoamos o formato de saída das informações de depuração e precisamos considerar como enviar as informações de depuração para o arquivo de log.

3. Envie as informações de depuração para o arquivo de log

3.1 Nível de registro

Log4J define 8 níveis de Log (excluindo OFF e ALL, pode ser dividido em 6 níveis), e a prioridade de alto a baixo é: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL.

  • OFF: o nível mais alto, usado para desligar todos os logs

  • FATAL: Indica que cada evento de erro grave fará com que o aplicativo seja encerrado. Este nível é relativamente alto, erros graves, você pode parar o programa diretamente neste nível

  • ERRO: Indica que, embora ocorra um evento de erro, ele ainda não afeta a operação contínua do sistema. Imprimir informações de erro e exceção, se você não quiser gerar muitos logs, pode usar este nível

  • WARN: Indica que um possível erro irá ocorrer.Algumas informações não são uma mensagem de erro, mas algumas dicas também são dadas ao programador.

  • INFO: Imprima algumas informações de seu interesse ou importantes. Isso pode ser usado para exibir algumas informações importantes sobre a execução do programa no ambiente de produção, mas não pode ser abusado para evitar a impressão de muitos logs

  • DEBUG: Usado principalmente para imprimir algumas informações em execução durante o processo de desenvolvimento

  • TRACE:  nível de log muito baixo, geralmente não usado

  • ALL:  o nível mais baixo, usado para abrir todos os logs

Log4J recomenda usar apenas quatro níveis, a prioridade de alta para baixa é ERROR, WARN, INFO, DEBUG. Nosso programa abaixo também será codificado em torno desses quatro níveis de log.

Cole o código-fonte primeiro e explique-o em detalhes quando tiver tempo~

3.2 Código fonte

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 principal.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

Coloque log.h, log.c, main.c no mesmo diretório

E crie um novo diretório de Log

compilar, executar

 

Acho que você gosta

Origin blog.csdn.net/weixin_55305220/article/details/131200293
Recomendado
Clasificación