程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 一個簡單又高效的日志系統

一個簡單又高效的日志系統

編輯:關於VC++

一、程序日志是商品程序中必不可少的部分。在正式商用的程序中一般對於日志都會有一些類似的要求:

性能要求

運行時日志級別可調整

日志文件空間使用安全性問題

下面逐一針對上面的問題一起分析程序實現。

二、性能問題。

客戶對程序的要求當然是越高越好。如果對於日志打印采用普通的方法,來一條日志就寫一條日志到文件中,這樣性能是很低的。因為程序不斷的與磁盤進行交付,對系統的沖擊很大,有可能會影響到正常的磁盤IO請求。

對於這個問題,一般的,都是采用批量寫入的方法來解決。每寫一條日志,並不是把日志立即寫入文件中,而是先寫到一個緩沖區中。當這個緩沖區達到一定的量時,再一次批量寫入到文件中。見如下代碼實現:

if (!strLog.IsEmpty())
  {
    m_strWriteStrInfo += GetCurTimeStr();
    // 增加日志級別信息
    if (enLevel == ENUM_LOG_LEVEL_ERROR)
    {
      m_strWriteStrInfo += _T("Error! ");
    }
    m_strWriteStrInfo += strLog;
    m_strWriteStrInfo += _T("\r\n");
  }

  if ( bForce
    || m_strWriteStrInfo.GetLength() > MAX_STR_LOG_INFO_LEN
    || m_iWriteBinLogLen > MAX_BIN_LOG_INFO_LEN/10)
  {
    // write info,達到一定量時才提交到文件中
    WriteLogToFile();
  }

但這樣會帶來一個問題,如果日志量比較少,很可能要很久才能達到批量提交的量,這樣就會造成程序寫了日志,但是日志寫入器還是把消息寫在緩沖區裡,文件中沒有及時體現出來。我們可以采用定時又定時的辦法來輸出日志。程序對緩沖區內的日志消息定時強制刷新到文件中去。為了體現程序的使用簡單性,把這個功能放在日志模塊中實現了,從而調用日志的程序就不用考慮定時來刷新文件了。見如下程序實現:

CSuperLog::CSuperLog(void)
{
  // 初始化臨界區變量
  InitializeCriticalSection(&m_csWriteLog); 
// 啟動信息
 m_strWriteStrInfo = WELCOME_LOG_INFO;
  // Create the Logger thread.
  m_hThread = (HANDLE)_beginthreadex( NULL, 0, &LogProcStart, NULL, 0, &m_uiThreadID );

}
unsigned __stdcall CSuperLog::LogProcStart( void* pArguments )
{
  int nCount = 1;
   do 
  {
    Sleep(300);
    if (++nCount % 10 == 0 )
    {
      WriteLog(strTemp, ENUM_LOG_LEVEL_ERROR, true); // 每隔三秒寫一次日志
    }
  } while (m_bRun);
}

采有一個全局日志類變量,在構造函數中啟動線程,線程每隔三秒去刷新一次文件。

二、日志級別可動態調整

程序的日志一般會進行日志分類,比如說日志級別一般會有調試日志,運行日志,錯誤日志等分類。在程序發布後運行時一般都會設置在運行日志級別,這時程序中的調試日志就不會被打印出來。如果程序運行中需要定位分析問題時,又需要把日志級別調低,把一些調試信息打印出來。見如下程序實現:

int CSuperLog::WriteLog(CString &strLog,enLogInfoLevel enLevel/* = ENUM_LOG_LEVEL_RUN*/, bool bForce /*= false*/)
{
  if (enLevel < m_iLogLevel)
  {
    return -1;
  }
  。。。
}

對於調整日志級別,我沒有把實現放在調用者去設置。而是把這個日志級別信息存放在共享內存中,如果要調整日志級別,則需要一個小工具去改那一個共享內存。實際上在整個設計中我一直想把日志系統設計得更獨立一點,盡量不和外部調用程序有更多牽連。

//創建共享文件。
    m_hMapLogFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,1024, _T("SuperLogShareMem"));
    if (m_hMapLogFile != NULL)
    {
      //拷貝數據到共享文件裡。
      m_psMapAddr = (LPTSTR)MapViewOfFile(m_hMapLogFile,FILE_MAP_ALL_ACCESS, 0,0,0);
      if (m_psMapAddr != NULL)
      {
        _tcscpy_s(m_psMapAddr, 1024, g_pszLogLevel[m_iLogLevel]);         
        FlushViewOfFile(m_psMapAddr, _tcslen(g_pszLogLevel[m_iLogLevel]));
        WriteLog(_T("設置默認日志級別到共享內存中成功。"), ENUM_LOG_LEVEL_RUN);
      }
    }

在線程中定時去檢查這個日志級別有否有變化,有變化則立即調整當前的級別設置。

三、日志文件空間使用安全性問題

對於長期運行的商品程序來說,一定會要考慮到文件系統安全性的問題。如果程序不停的打印垃圾信息,用不了多太,日志文件可能會變得很大。如果把用戶空間占滿了,那有可能會引起更嚴重的問題。所以一定要限制日志文件的大小。程序中考慮到日志文件更換,采用了三個文件輪換寫,寫滿一個時,更換一個文件再寫,不用考慮到日志文件會耗盡磁盤。

CSuperLog::enLogStatus CSuperLog::OpenLogFile(void)
{
  EnterCriticalSection(&m_csWriteLog); 
   for (int iRunCount = 0; iRunCount < MAX_LOG_FILE_COUNT; iRunCount++)
  {
    if (m_pFile == NULL)
    {
      m_pFile = new CStdioFile;
      if (m_pFile == NULL)
      {
        LeaveCriticalSection(&m_csWriteLog);
        return m_enStatus = ENUM_LOG_INVALID;
      }

        BOOL bRet = m_pFile->Open(
        g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT],
        CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone | CFile::modeNoTruncate);
      if (bRet)
      {
        WriteUnicodeHeadToFile(m_pFile);
      }
      else
      {
  
    delete m_pFile;
        m_pFile = NULL;
        LeaveCriticalSection(&m_csWriteLog);
        return m_enStatus = ENUM_LOG_INVALID;
      }
    }

    if (m_pFile->GetLength() > MAX_LOG_FILE_LEN)
    {
      m_pFile->Close();
      BOOL bRet = FALSE;
      // 上一個文件是最大的那個文件或是寫過一遍了的。
      if (m_iCurLogFileSeq >= MAX_LOG_FILE_COUNT) 
      {
        // 所有文件都是寫滿了,則強制從第一個文件開始寫,同時先清空文件
        bRet = m_pFile->Open(
          g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT],
          CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone); 
      }
      else
      {
        // 打開第二個文件,再檢查是否過了最大值
        bRet = m_pFile->Open(
          g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT],
          CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone | CFile::modeNoTruncate);
      }

      if (bRet)
      {
        WriteUnicodeHeadToFile(m_pFile);
      }
      else
      {
        delete m_pFile;
        m_pFile = NULL;
        LeaveCriticalSection(&m_csWriteLog);
        return m_enStatus = ENUM_LOG_INVALID;
      }
    }
    else
    {
      break;
    }
  }

  m_pFile->SeekToEnd();
  LeaveCriticalSection(&m_csWriteLog);
  return m_enStatus = ENUM_LOG_RUN;
}

四、其它部分

程序中使用了CStdioFile來處理文件寫入,在實現中如果使用text模式打開文件寫入,會發現無法寫入中文字符的問題。查找了一些資料,發現是字符編碼的問題。有一種解決方法是用二進制方式打開,在文件的開頭處寫入unicode頭部標識。

int CSuperLog::WriteUnicodeHeadToFile(CFile * pFile)
{
  if (pFile == NULL)
  {
    return -1;
  }
  try
  {
  if (pFile->GetLength() == 0)
    {
      m_pFile->Write("\377\376", 2); // 就是FF FE 
      if (m_enStatus == ENUM_LOG_RUN)
      {
        m_pFile->WriteString(WELCOME_LOG_INFO);
      }
      m_pFile->Flush();
    }
  }
  catch (...)
  {
    return -1;
  }
  return 0;
}

為了保證調用者盡可能的簡單,程序把類接口都實現為靜態方法,調用都可以直接使用。

#define  WRITE_LOG      CSuperLog::WriteLog
#define  LOG_LEVEL_DEBUG   CSuperLog::ENUM_LOG_LEVEL_DEBUG
#define  LOG_LEVEL_RUN    CSuperLog::ENUM_LOG_LEVEL_RUN
#define  LOG_LEVEL_ERROR   CSuperLog::ENUM_LOG_LEVEL_ERROR

調用者使用如下:

// 包含頭文件
#include "common/SuperLog.h"
WRITE_LOG(_T("短信發送失敗,重試一次。"), LOG_LEVEL_ERROR);

日志線程是在全局變量的析構函數中通知退出的。這時有可能還要會打印日志。為了保證性能,在取得當前時間的字符串時使用了兩個靜態局部變量

CString& CSuperLog::GetCurTimeStr()
{
  static CTime g_tmCurTime;
  g_tmCurTime = CTime::GetCurrentTime();// time(NULL);
  CString g_strTime;
  g_strTime = g_tmCurTime.Format(_T("%Y-%m-%d %H:%M:%S "));
  return g_strTime;
}

在使用中發現,每次退出時,如果還有日志打印,程序總會異常。後來分析發現,靜態全局變量每次都會先於全局變量析構,導致strTime析構後無效訪問。只好把這個變量變成了全局變量規避。

CString& CSuperLog::GetCurTimeStr()
{
  g_tmCurTime = CTime::GetCurrentTime();// time(NULL);
  g_strTime = g_tmCurTime.Format(_T("%Y-%m-%d %H:%M:%S "));
  return g_strTime;
}

四、結束語

程序實現倉促,基本的功能都調試完畢,但目前還有帶參數的寫日志接口沒有寫,二進制內容日志信息的接口也沒有實現。後續作者會及時完成。有興趣的同不學可以發郵件聯系。Email:[email protected]

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved