分享一个多线程的日志库

思路部分参考了陈硕的 muduo,主要就是多线程写的安全性,以及一些日志分级,自动分割等常用功能,线上几十近百台机器一两年的运行没有发现什么问题

/*
 * encoding: gbk
 * created: 2013-09-05 10:33:59
 * 
 * 日志工具
 * 工具在初始化的时候从堆上分配一片内存,当作二维 char 数组来用
 * 每个写入线程在写入前对数组的一维下标原子加 1,作为本次写入
 * 的位置,写入的位置偏移 1 个字节,第 1 个字节用作标志位,写
 * 入完成后标志位置 1
 * 磁盘文件落地线程循环的检查是否有未读取的日志行,并将新的
 * 内存中日志写入磁盘
 * 
 * 使用方法:
 * #include "logger.h"
 * assert(Logger::Init() == 0);
 * LOG_INFO("sample log message %d", 123);
 */

#ifndef LOGGER_H
#define LOGGER_H

#include <errno.h>
#include <string>
#include <pthread.h>
#include <sys/syscall.h>
#include <libgen.h>
#include <stdlib.h>

#ifdef DEBUG
#define LOG_DEBUG(format, ...) do { if (Logger::m_pBeforeLogHook==NULL || (Logger::m_pBeforeLogHook!=NULL && Logger::m_pBeforeLogHook(__FILE__, __FUNCTION__)!=0)) { Logger::m_pLog(Logger::LOG_LEVEL_DEBUG, "[%s][%d][DEBUG][%25s@%25s,%04d]  " format "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex], syscall(__NR_gettid), __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__); } } while (0)
#define LOG_INFO(format, ...)  do { if (Logger::m_pBeforeLogHook==NULL || (Logger::m_pBeforeLogHook!=NULL && Logger::m_pBeforeLogHook(__FILE__, __FUNCTION__)!=0)) { Logger::m_pLog(Logger::LOG_LEVEL_INFO,  "[%s][%d] [INFO][%25s@%25s,%04d]  " format "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex], syscall(__NR_gettid), __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__); } } while (0)
#define LOG_WARN(format, ...)  do { if (Logger::m_pBeforeLogHook==NULL || (Logger::m_pBeforeLogHook!=NULL && Logger::m_pBeforeLogHook(__FILE__, __FUNCTION__)!=0)) { Logger::m_pLog(Logger::LOG_LEVEL_WARN,  "\033[1;33m" "[%s][%d] [WARN][%25s@%25s,%04d]  " format "\033[0m" "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex], syscall(__NR_gettid), __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__); } } while (0)
#define LOG_ERR(format, ...)   do { if (Logger::m_pBeforeLogHook==NULL || (Logger::m_pBeforeLogHook!=NULL && Logger::m_pBeforeLogHook(__FILE__, __FUNCTION__)!=0)) { Logger::m_pLog(Logger::LOG_LEVEL_ERROR, "\033[1;31m" "[%s][%d][ERROR][%25s@%25s,%04d]  " format "\033[0m" "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex], syscall(__NR_gettid), __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__); } } while (0)
#else
#define LOG_DEBUG(format, ...) Logger::m_pLog(Logger::LOG_LEVEL_DEBUG, "[%s][DEBUG][%25s@%25s,%04d]  " format "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex], __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_INFO(format, ...)  Logger::m_pLog(Logger::LOG_LEVEL_INFO,  "[%s] [INFO][%25s@%25s,%04d]  " format "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex],  __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_WARN(format, ...)  Logger::m_pLog(Logger::LOG_LEVEL_WARN,  "[%s] [WARN][%25s@%25s,%04d]  " format "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex],  __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_ERR(format, ...)   Logger::m_pLog(Logger::LOG_LEVEL_ERROR, "[%s][ERROR][%25s@%25s,%04d]  " format "\n", Logger::m_szLogTimestampStr[Logger::m_iLogTimestampIndex], __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__)
#endif

class Logger
{
public:
    enum eLogLevel {LOG_LEVEL_DEBUG = 10, LOG_LEVEL_INFO = 20, LOG_LEVEL_WARN = 30, LOG_LEVEL_ERROR = 40, LOG_LEVEL_OFF = 50};    
    //************************************
    // Method:    Init
    // FullName:  Logger::Init
    // Access:    public static 
    // Returns:   int 0 表示初始化成功,-1 表示失败
    // Parameter: logLevel level 日志过滤等级,默认 INFO,低于该指定等级的日志将不会写入磁盘文件,也不会写入内存
    // Parameter: const char * pLogFileDir 日志文件存放的目录,默认当前目录
    // Parameter: const char * pLogFileBaseName 日志文件的文件名,不需要包括扩展名,默认当前进程名,当文件名置为 stdout 时,即时输出到屏幕
    // Parameter: uint32_t uiLogFileRollingSize 日志文件的滚动条件,文件尺寸(字节),默认 10 MB
    // Parameter: uint32_t uiMaxCharPerLine 每行最大字符数
    // Parameter: uint32_t uiLogBufferLine 缓冲区行数
    //************************************
    static int Init(eLogLevel level = LOG_LEVEL_INFO, const char* pLogFileDir = "./", const char* pLogFileBaseName = basename(getenv("_")), uint32_t uiLogFileRollingSize = 10*1024*1024, uint32_t uiMaxCharPerLine = 16384, uint32_t uiLogBufferLine = 4096);
    static void (*m_pLog)(eLogLevel level, const char* format, ...);
    static void LogToMem(eLogLevel level, const char* format, ...);
    static void LogToStdout(eLogLevel level, const char* format, ...);
    static int (*m_pBeforeLogHook)(const char* szFilename, const char* szFunctionName);
    static char m_szLogTimestampStr[2][32];
    static time_t m_stLogTimestamp;
    static int m_iLogTimestampIndex;
    // 每行最大字符数,每行的第一个字节用作标志位,为 1 表示该行写入完成可供读取,读前检查是否为 1 读取后置为 0
    static uint32_t m_uiMaxCharPerLine;
    // 最大行数,必须保证 m_uiLogBufferLines 是 2 的整数次幂
    static uint32_t m_uiLogBufferLines;
private:
    Logger();                               // 不允许实例化
    static pthread_mutex_t m_stInitLock;    // 防止多个线程同时调用 Init 函数
    static bool m_bInited;                  // 标识日志库是否初始化完毕
    static eLogLevel m_eLevel;              // 标识日志库的记录等级,大于等于这个等级的,才会写入内存,然后被写入文件
    static char *m_pLogBufferMem;           // 指向日志内存的首字节
    static uint32_t m_uiWriteLineCount;     // 日志内存写入行数累加器
};

// 磁盘文件写入线程
void* LogDumper(void *args);
struct LogDumperArgs
{
    char* pLogBufferMem;
    std::string strLogFileDir;
    std::string strLogFileName;
    uint32_t uiLogFileMaxSize; 
};

// 日志时间字符串更新线程,每 1 秒更新一次时间字符串,交替更新两个字符串数组
void* LogTimeStrGenerator(void*);

#endif
#include <string.h>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <libgen.h>
#include <unistd.h>
#include <stdint.h>
#include <pthread.h>
#include <time.h>
#include <stdarg.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <arpa/inet.h>
#include "logger.h"

int Logger::Init(eLogLevel level, const char* pLogFileDir, const char* pLogFileBaseName, uint32_t uiLogFileRollingSize, uint32_t uiMaxCharPerLine, uint32_t uiLogBufferLines)
{
    pthread_mutex_lock(&m_stInitLock);
    if (m_bInited || level == LOG_LEVEL_OFF)
    {
        pthread_mutex_unlock(&m_stInitLock);
        return 0;
    }

    m_eLevel = level;
    m_uiMaxCharPerLine = uiMaxCharPerLine;
    if (m_uiMaxCharPerLine <= 32)
    {
        fprintf(stderr, "max char per line config error, should greater than 32, now using default value 16384\n");
        m_uiMaxCharPerLine = 16384;
    }
    m_uiLogBufferLines = uiLogBufferLines;
    if (m_uiLogBufferLines & (m_uiLogBufferLines-1) != 0)
    {
        fprintf(stderr, "log buffer line config error, must be power of 2, like 2048  4096, now using default value 4096\n");
        m_uiLogBufferLines = 4096;
    }

    memset(m_szLogTimestampStr, 0, sizeof(m_szLogTimestampStr));
    time_t stRawtime = time(NULL);
    struct tm* stTimeinfo = localtime(&stRawtime);
    strftime(Logger::m_szLogTimestampStr[0], sizeof(Logger::m_szLogTimestampStr[0])-1, "%Y-%m-%d %H:%M:%S", stTimeinfo);
    m_iLogTimestampIndex = 0;

    pthread_t tid;
    if (pthread_create(&tid, NULL, LogTimeStrGenerator, NULL) != 0)
    {
        fprintf(stderr, "create log timestamp thread failed: %s\n", strerror(errno));
        pthread_mutex_unlock(&m_stInitLock);
        return -1;
    }

    if (strcmp("stdout", pLogFileBaseName) == 0)
    {
        m_pLog = &(Logger::LogToStdout);
        m_bInited = true;
        pthread_mutex_unlock(&m_stInitLock);
        return 0;
    }
    else
    {
        m_pLog = &(Logger::LogToMem);
    }    

    int iLogBufferMemSize = m_uiMaxCharPerLine * m_uiLogBufferLines;
    m_pLogBufferMem = (char *)malloc(iLogBufferMemSize);
    if (m_pLogBufferMem == NULL)
    {
        fprintf(stderr, "alloc heap memory failed: %s\n", strerror(errno));
        pthread_mutex_unlock(&m_stInitLock);
        return -1;
    }
    m_uiWriteLineCount = 0;
    memset(m_pLogBufferMem, 0, iLogBufferMemSize);
    
    struct LogDumperArgs* pLogDumperArgs = new struct LogDumperArgs;
    pLogDumperArgs->pLogBufferMem = m_pLogBufferMem;
    pLogDumperArgs->strLogFileDir.assign(pLogFileDir);
    pLogDumperArgs->strLogFileName.assign(pLogFileBaseName);
    pLogDumperArgs->uiLogFileMaxSize = uiLogFileRollingSize;
    if (pthread_create(&tid, NULL, LogDumper, (void *)pLogDumperArgs) != 0)
    {
        fprintf(stderr, "create log dump thread failed: %s\n", strerror(errno));
        pthread_mutex_unlock(&m_stInitLock);
        return -1;
    }

    m_bInited = true;
    pthread_mutex_unlock(&m_stInitLock);
    return 0;
}

void Logger::LogToMem(eLogLevel level, const char* format, ...)
{
    if (m_bInited && level >= m_eLevel)
    {
        va_list args;
        va_start(args, format);            
        char *pWritePtr = m_pLogBufferMem+((__sync_add_and_fetch(&m_uiWriteLineCount, 1) % m_uiLogBufferLines) * m_uiMaxCharPerLine);
        vsnprintf(pWritePtr+1, m_uiMaxCharPerLine-2, format, args); 
        *(pWritePtr) = 1;
        va_end(args);
    }        
}

void Logger::LogToStdout(eLogLevel level, const char* format, ...)
{    
    if (m_bInited && level >= m_eLevel)
    {
        va_list args;
        va_start(args, format);            
        vprintf(format, args);
        va_end(args);
    }        
}

pthread_mutex_t Logger::m_stInitLock = PTHREAD_MUTEX_INITIALIZER;
bool Logger::m_bInited = false;
Logger::eLogLevel Logger::m_eLevel = Logger::LOG_LEVEL_INFO;
char* Logger::m_pLogBufferMem = NULL;
uint32_t Logger::m_uiWriteLineCount = 0;
int Logger::m_iLogTimestampIndex = 0;
char Logger::m_szLogTimestampStr[2][32] = {{0}, {0}};
time_t Logger::m_stLogTimestamp = 0;
uint32_t Logger::m_uiMaxCharPerLine = 16384;
uint32_t Logger::m_uiLogBufferLines = 4096;
void (*Logger::m_pLog)(Logger::eLogLevel level, const char* format, ...) = &(Logger::LogToMem);
int (*Logger::m_pBeforeLogHook)(const char*, const char*) = NULL;

void* LogTimeStrGenerator(void*)
{    
    while (1)
    {
        Logger::m_stLogTimestamp = time(NULL);
        struct tm* stTimeinfo = localtime(&Logger::m_stLogTimestamp);
        strftime(Logger::m_szLogTimestampStr[(Logger::m_iLogTimestampIndex+1)%2], sizeof(Logger::m_szLogTimestampStr[0])-1, "%Y-%m-%d %H:%M:%S", stTimeinfo);
        Logger::m_iLogTimestampIndex = (Logger::m_iLogTimestampIndex + 1) % 2;
        sleep(1);
    }
    return NULL;
}

void* LogDumper(void *args)
{    
    struct LogDumperArgs* pLogDumperArgs = (struct LogDumperArgs*)args;
    char* pLogBufferMem = pLogDumperArgs->pLogBufferMem;
    const std::string strLogFileDir = pLogDumperArgs->strLogFileDir;
    const std::string strLogFileName = pLogDumperArgs->strLogFileName;
    const uint32_t uiLogFileMaxSize = pLogDumperArgs->uiLogFileMaxSize;
    delete pLogDumperArgs;
    pLogDumperArgs = NULL;
    args = NULL;

    uint32_t iReadLineCount = 1;

    // 获取 IP 地址,用于文件名        
    char* pSzIpAddr = "0.0.0.0";
    char szIpAddrBuffer[32] = {0};
    struct ifaddrs* pIfAddrs;
    if (getifaddrs(&pIfAddrs) == 0)
    {
        for (struct ifaddrs* pIfAddr = pIfAddrs; pIfAddr; pIfAddr = pIfAddr->ifa_next)
        {
            if (pIfAddr->ifa_addr == NULL || !(pIfAddr->ifa_flags & IFF_UP) || strncmp(pIfAddr->ifa_name, "eth1", strlen("eth1")) != 0)
            {
                continue;
            }
            if (inet_ntop(pIfAddr->ifa_addr->sa_family, &((struct sockaddr_in *)pIfAddr->ifa_addr)->sin_addr, szIpAddrBuffer, sizeof(szIpAddrBuffer)))
            {   
                pSzIpAddr = szIpAddrBuffer;
                break;                
            }
        }
        freeifaddrs(pIfAddrs);
    }
    // 获取进程 ID,用于文件名
    pid_t pid = getpid();

    // 检查目录是否存在,不存在则自动创建
    char szMkdirpCmd[512] = {0};
    snprintf(szMkdirpCmd, sizeof(szMkdirpCmd), "mkdir -p %s", strLogFileDir.c_str());
    system(szMkdirpCmd);

    // 开始写入文件
    while (1)
    {
        // 获取当前时间,用于文件名
        time_t stRawtime = time(NULL);
        struct tm* stTimeinfo = localtime(&stRawtime);
        char pTimeBuffer[80] = {0}, pLogFileFullName[512] = {0}, pLogFileLinkName[512] = {0};
        strftime(pTimeBuffer, sizeof(pTimeBuffer)-1, "%Y%m%d%H%M%S", stTimeinfo);
        snprintf(pLogFileFullName, sizeof(pLogFileFullName)-1, "%s/%s_%s_%s_%d.log", strLogFileDir.c_str(), strLogFileName.c_str(), pTimeBuffer, pSzIpAddr, pid);
        snprintf(pLogFileLinkName, sizeof(pLogFileLinkName)-1, "%s.log", strLogFileName.c_str());
        FILE* fp = fopen(pLogFileFullName, "a+");
        if (fp == NULL)
        {
            fprintf(stderr, "open log file %s failed: %s\n", pLogFileFullName, strerror(errno));
            return NULL;
        }
        // 建立软链接
        unlink(pLogFileLinkName);
        symlink(pLogFileFullName, pLogFileLinkName);

        uint32_t uiWriteByteCount = 0;
        time_t stLastFlushTimestamp = 0;
        while (1)
        {        
            char* pReadPtr = pLogBufferMem+((iReadLineCount % Logger::m_uiLogBufferLines) * Logger::m_uiMaxCharPerLine);
            if (*pReadPtr == 1)
            {
                uiWriteByteCount += fprintf(fp, "%s", pReadPtr + 1);
                *pReadPtr = 0;
                iReadLineCount++;
                if (uiWriteByteCount > uiLogFileMaxSize)
                {
                    break;
                }
            } 
            else
            {
                if (Logger::m_stLogTimestamp - stLastFlushTimestamp >= 3)
                {
                    stLastFlushTimestamp = Logger::m_stLogTimestamp;
                    fflush(fp);
                }
                usleep(5000);
            }    
        }        

        fclose(fp);
    }
      
    return NULL;
}

One thought on “分享一个多线程的日志库

  1. Pingback: 动态链接库中的全局变量 | ZRJ

Leave a Reply

Your email address will not be published. Required fields are marked *