C基礎 多用戶分級日志庫 sclog,分級sclog
引言 - sclog 總的設計思路
sclog在之前已經內置到simplec 簡易c開發框架中一個日志庫. 最近對其重新設計了一下. 減少了對外暴露的接口.
也是C開發中一個輪子. 比較簡單, 非常適合學習理解,最後自己寫一個自己喜歡的日志庫.
首先分析分級設計的總的思路.

主要是圍繞上面思路設計. 分6個等級. 2中類型的日志文件. sc.log 普通文件, 什麼信息都接受, sc.log.wf只接受異常信息. 需要緊急處理的.
繼續說明日志消息體的設計思路

到這裡設計的總思路已經清楚了. 後面會介紹詳細的實現的設計思路.
前言 - sclog 實現設計思路
到這裡我們需要看一下實現方面的思路了. 向上面的基准時間, 客戶端ip, 全局id, 模塊名稱. 用戶一次請求的所有流程都是必須一樣的.
詳細設計如下
struct slinfo {
unsigned logid; //請求的logid,唯一id
char reqip[_INT_LITTLE]; //請求方ip
char times[_INT_LITTLE]; //當前時間串
struct timeval timev; //處理時間,保存值,統一用毫秒
char mod[_INT_LITTLE]; //當前線程的模塊名稱,不能超過_INT_LITTLE - 1
};
並且上面數據 是每個線程(進程)都必須保存一份. 同樣這裡核心設計使用線程的私有變量pthread_key_t 類型.
其中對於輸出字符串輸出格式,采用宏控制如下
//
//關於日志切分,需要用第三方插件例如crontab , 或者下次我自己寫一個監測程序.
#define _INT_LITTLE (64) //保存時間或IP長度
#define _INT_LOG (1024<<3) //最多8k日志
#define _STR_SCLOG_DIR "logs" //日志相對路徑目錄,如果不需要需要配置成""
#define _STR_SCLOG_LOG "sc.log" //普通log日志 DEBUG,INFO,NOTICE,WARNING,FATAL都會輸出
#define _STR_SCLOG_WFLOG "sc.log.wf" //級別比較高的日志輸出 FATAL和WARNING
#define _STR_SCLOG_FATAL "FATAL" //錯誤,後端使用
#define _STR_SCLOG_WARNING "WARNING" //警告,前端使用錯誤,用這個
#define _STR_SCLOG_NOTICE "NOTICE" //系統使用,一般標記一條請求完成,使用這個日志
#define _STR_SCLOG_INFO "INFO" //普通的日志打印
#define _STR_SCLOG_TRACE "TRACE" //測試用的日志標記當前日志的開始和結束
#define _STR_SCLOG_DEBUG "DEBUG" //測試用的日志打印,在發布版這些日志會被清除掉
/**
* fstr : 為標識串 例如 _STR_SCLOG_FATAL, 必須是雙引號括起來的串
**
** 拼接一個 printf 輸出格式串
**/
#define SCLOG_PUTS(fstr) "[" fstr "][%s:%d:%s][logid:%u][reqip:%s][mod:%s]"
其中 SCLOG_PUTS 前面還缺少 [%s %u] 輸出時間信息. 這個內置後面函數中實現. 因為C庫提供的 time 返回時間函數不是線程安全的.
後面自己封裝一個. 主要是圍繞下面函數
/**
* 獲取 當前時間串,並塞入tstr中C長度並返回
** 使用舉例
char tstr[64];
puts(gettimes(tstr, LEN(tstr)));
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
int
sh_times(char tstr[], int len) {
struct tm st;
time_t t = time(NULL);
localtime_r(&t, &st);
return (int)strftime(tstr, len, "%F %X", &st);
}
對於localtime_r 是linux上函數套路. 對於window封裝如下
//為了解決 不通用功能
#define localtime_r(t, tm) localtime_s(tm, t)
改變參數行為, 完成了上面函數統一.
還有一個統一設計思路, 需要支持微妙級別時間量. linux 自帶 gettimeofday供支持. 但是為了跨品台 在M$上實現如下
#if defined(_MSC_VER)
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
int
gettimeofday(struct timeval * tv, void * tz) {
time_t clock;
struct tm tm;
SYSTEMTIME wtm;
GetLocalTime(&wtm);
tm.tm_year = wtm.wYear - 1900;
tm.tm_mon = wtm.wMonth - 1; //window的計數更好寫
tm.tm_mday = wtm.wDay;
tm.tm_hour = wtm.wHour;
tm.tm_min = wtm.wMinute;
tm.tm_sec = wtm.wSecond;
tm.tm_isdst = -1; //不考慮夏令時
clock = mktime(&tm);
tv->tv_sec = (long)clock; //32位使用,接口已經老了
tv->tv_usec = wtm.wMilliseconds * 1000;
return _RT_OK;
}
#endif
這些基本工作完成後. 普通跨品台的前戲操作基本就完成了.
linux 上文件結構

window上文件結構

而我們需要使用這個日志庫, 對外暴露的接口是
/**
* FATAL... 日志打印宏
** fmt : 輸出的格式串,需要""包裹起來
** ... : 後面的參數,服務於fmt
**/
#define SL_FATAL(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_FATAL, fmt, ##__VA_ARGS__)
#define SL_WARNING(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_WARNING, fmt, ##__VA_ARGS__)
#define SL_NOTICE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_NOTICE, fmt, ##__VA_ARGS__)
#define SL_INFO(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_INFO, fmt, ##__VA_ARGS__)
// 發布狀態下,關閉SL_DEBUG 宏,需要重新編譯,沒有改成運行時的判斷,這個框架主要圍繞單機部分多服務器
#if defined(_DEBUG)
# define SL_TRACE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_TRACE, fmt, ##__VA_ARGS__)
# define SL_DEBUG(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_DEBUG, fmt, ##__VA_ARGS__)
#else
# define SL_TRACE(fmt, ...) /* 人生難道就是123*/
# define SL_DEBUG(fmt, ...) /* 愛過哎 */
#endif
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** logid : 分配的唯一標識id, 默認0
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
extern int sl_init(const char mod[_INT_LITTLE], const char reqip[_INT_LITTLE], unsigned logid);
到這裡基本對外接口, 大致分析清楚裡, 理解更仔細推薦直接看附帶的源碼文件.
文件附錄:
sclog.h 對外暴露的接口

![]()
#ifndef _H_SIMPLE_SCLOG
#define _H_SIMPLE_SCLOG
#include "schead.h"
//-------------------------------------------------------------------------------------------|
// 第一部分 共用的參數宏
//-------------------------------------------------------------------------------------------|
//
//關於日志切分,需要用第三方插件例如crontab , 或者下次我自己寫一個監測程序.
#define _INT_LITTLE (64) //保存時間或IP長度
#define _INT_LOG (1024<<3) //最多8k日志
#define _STR_SCLOG_DIR "logs" //日志相對路徑目錄,如果不需要需要配置成""
#define _STR_SCLOG_LOG "sc.log" //普通log日志 DEBUG,INFO,NOTICE,WARNING,FATAL都會輸出
#define _STR_SCLOG_WFLOG "sc.log.wf" //級別比較高的日志輸出 FATAL和WARNING
#define _STR_SCLOG_FATAL "FATAL" //錯誤,後端使用
#define _STR_SCLOG_WARNING "WARNING" //警告,前端使用錯誤,用這個
#define _STR_SCLOG_NOTICE "NOTICE" //系統使用,一般標記一條請求完成,使用這個日志
#define _STR_SCLOG_INFO "INFO" //普通的日志打印
#define _STR_SCLOG_TRACE "TRACE" //測試用的日志標記當前日志的開始和結束
#define _STR_SCLOG_DEBUG "DEBUG" //測試用的日志打印,在發布版這些日志會被清除掉
/**
* fstr : 為標識串 例如 _STR_SCLOG_FATAL, 必須是雙引號括起來的串
**
** 拼接一個 printf 輸出格式串
**/
#define SCLOG_PUTS(fstr) "[" fstr "][%s:%d:%s][logid:%u][reqip:%s][mod:%s]"
/**
* fstr : 只能是 _STR_SCLOG_* 開頭的宏
** fmt : 必須是""括起來的宏.單獨輸出的格式宏
** ... : 對映fmt參數集
**
** 拼接這裡使用的宏,為sl_printf 打造一個模板
**/
#define SCLOG_PRINTF(fstr, fmt, ...) \
sl_printf(SCLOG_PUTS(fstr) fmt "\n", __FILE__, __LINE__, __func__, \
sl_getlogid(), sl_getreqip(), sl_getmod(), ##__VA_ARGS__)
/**
* FATAL... 日志打印宏
** fmt : 輸出的格式串,需要""包裹起來
** ... : 後面的參數,服務於fmt
**/
#define SL_FATAL(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_FATAL, fmt, ##__VA_ARGS__)
#define SL_WARNING(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_WARNING, fmt, ##__VA_ARGS__)
#define SL_NOTICE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_NOTICE, fmt, ##__VA_ARGS__)
#define SL_INFO(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_INFO, fmt, ##__VA_ARGS__)
// 發布狀態下,關閉SL_DEBUG 宏,需要重新編譯,沒有改成運行時的判斷,這個框架主要圍繞單機部分多服務器
#if defined(_DEBUG)
# define SL_TRACE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_TRACE, fmt, ##__VA_ARGS__)
# define SL_DEBUG(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_DEBUG, fmt, ##__VA_ARGS__)
#else
# define SL_TRACE(fmt, ...) /* 人生難道就是123*/
# define SL_DEBUG(fmt, ...) /* 愛過哎 */
#endif
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** logid : 分配的唯一標識id, 默認0
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
extern int sl_init(const char mod[_INT_LITTLE], const char reqip[_INT_LITTLE], unsigned logid);
/**
* 獲取日志信息體的唯一的logid
**/
unsigned sl_getlogid(void);
/**
* 獲取日志信息體的請求ip串,返回NULL表示沒有初始化
**/
const char* sl_getreqip(void);
/**
* 獲取日志信息體的名稱,返回NULL表示沒有初始化
**/
const char* sl_getmod(void);
//-------------------------------------------------------------------------------------------|
// 第三部分 對日志系統具體的輸出輸入接口部分
//-------------------------------------------------------------------------------------------|
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**/
extern void sl_start(void);
/**
* 這個函數不希望你使用,是一個內部限定死的日志輸出內容.推薦使用相應的宏
**打印相應級別的日志到對映的文件中.
**
** format : 必須是""號括起來的宏,開頭必須是 [FALTAL:%s]後端錯誤
** [WARNING:%s]前端錯誤, [NOTICE:%s]系統使用, [INFO:%s]普通信息,
** [DEBUG:%s] 開發測試用
**
** return : 返回輸出內容長度
**/
int sl_printf(const char* format, ...);
#endif // !_H_SIMPLE_SCLOG
View Code
scatom.h 跨平台的原子操作

![]()
#ifndef _H_SIMPLEC_SCATOM
#define _H_SIMPLEC_SCATOM
/*
* 作者 : wz
*
* 描述 : 簡單的原子操作,目前只考慮 VS(CL) 小端機 和 gcc
* 推薦用 posix 線程庫
*/
// 如果 是 VS 編譯器
#if defined(_MSC_VER)
#include <Windows.h>
//忽略 warning C4047: “==”:“void *”與“LONG”的間接級別不同
#pragma warning(disable:4047)
// v 和 a 都是 long 這樣數據
#define ATOM_FETCH_ADD(v, a) \
InterlockedExchangeAdd((LONG*)&(v), (LONG)(a))
#define ATOM_ADD_FETCH(v, a) \
InterlockedAdd((LONG*)&(v), (LONG)(a))
#define ATOM_SET(v, a) \
InterlockedExchange((LONG*)&(v), (LONG)(a))
#define ATOM_CMP(v, c, a) \
(c == InterlockedCompareExchange((LONG*)&(v), (LONG)(a), (LONG)c))
/*
對於 InterlockedCompareExchange(v, c, a) 等價於下面
long tmp = v ; v == a ? v = c : ; return tmp;
咱們的 ATOM_FETCH_CMP(v, c, a) 等價於下面
long tmp = v ; v == c ? v = a : ; return tmp;
*/
#define ATOM_FETCH_CMP(v, c, a) \
InterlockedCompareExchange((LONG*)&(v), (LONG)(a), (LONG)c)
#define ATOM_LOCK(v) \
while(ATOM_SET(v, 1)) \
Sleep(0)
#define ATOM_UNLOCK(v) \
ATOM_SET(v, 0)
//否則 如果是 gcc 編譯器
#elif defined(__GNUC__)
#include <unistd.h>
/*
type tmp = v ; v += a ; return tmp ;
type 可以是 8,16,32,64 bit的類型
*/
#define ATOM_FETCH_ADD(v, a) \
__sync_fetch_add_add(&(v), (a))
/*
v += a ; return v;
*/
#define ATOM_ADD_FETCH(v, a) \
__sync_add_and_fetch(&(v), (a))
/*
type tmp = v ; v = a; return tmp;
*/
#define ATOM_SET(v, a) \
__sync_lock_test_and_set(&(v), (a))
/*
bool b = v == c; b ? v=a : ; return b;
*/
#define ATOM_CMP(v, c, a) \
__sync_bool_compare_and_swap(&(v), (c), (a))
/*
type tmp = v ; v == c ? v = a : ; return v;
*/
#define ATOM_FETCH_CMP(v, c, a) \
__sync_val_compare_and_swap(&(v), (c), (a))
/*
加鎖等待,知道 ATOM_SET 返回合適的值
_INT_USLEEP 是操作系統等待納秒數,可以優化,看具體操作系統
使用方式
int lock = 0;
ATOM_LOCK(lock);
//to do think ...
ATOM_UNLOCK(lock);
*/
#define _INT_USLEEP (2)
#define ATOM_LOCK(v) \
while(ATOM_SET(v, 1)) \
usleep(_INT_USLEEP)
/*
對ATOM_LOCK 解鎖, 當然 直接調用相當於 v = 0;
*/
#define ATOM_UNLOCK(v) \
__sync_lock_release(&(v))
#endif // !_MSC_VER && !__GNUC__
#endif // !_H_SIMPLEC_SCATOM
View Code
schead.h 跨平台的基礎頭文件

![]()
#ifndef _H_SIMPLEC_SCHEAD
#define _H_SIMPLEC_SCHEAD
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <stddef.h>
/*
* 1.0 錯誤定義宏 用於判斷返回值狀態的狀態碼 _RF表示返回標志
* 使用舉例 :
int flag = scconf_get("pursue");
if(flag != _RT_OK) {
sclog_error("get config %s error! flag = %d.", "pursue", flag);
exit(EXIT_FAILURE);
}
* 這裡是內部 使用的通用返回值 標志
*/
#define _RT_OK (0) //結果正確的返回宏
#define _RT_EB (-1) //錯誤基類型,所有錯誤都可用它,在不清楚的情況下
#define _RT_EP (-2) //參數錯誤
#define _RT_EM (-3) //內存分配錯誤
#define _RT_EC (-4) //文件已經讀取完畢或表示鏈接關閉
#define _RT_EF (-5) //文件打開失敗
/*
* 1.1 定義一些 通用的函數指針幫助,主要用於基庫的封裝中
* 有構造函數, 釋放函數, 比較函數等
*/
typedef void * (*pnew_f)();
typedef void (*vdel_f)(void* node);
// icmp_f 最好 是 int cmp(const void* ln,const void* rn); 標准結構
typedef int (*icmp_f)();
/*
* c 如果是空白字符返回 true, 否則返回false
* c : 必須是 int 值,最好是 char 范圍
*/
#define sh_isspace(c) \
((c==' ')||(c>='\t'&&c<='\r'))
/*
* 2.0 如果定義了 __GNUC__ 就假定是 使用gcc 編譯器,為Linux平台
* 否則 認為是 Window 平台,不可否認宏是丑陋的
*/
#if defined(__GNUC__)
//下面是依賴 Linux 實現,等待毫秒數
#include <unistd.h>
#include <sys/time.h>
#define SLEEPMS(m) \
usleep(m * 1000)
// 屏幕清除宏, 依賴系統腳本
#define CONSOLE_CLEAR() \
system("printf '\ec'")
#else
// 這裡創建等待函數 以毫秒為單位 , 需要依賴操作系統實現
#include <Windows.h>
#include <direct.h> // 加載多余的頭文件在 編譯階段會去掉
#define rmdir _rmdir
#define CONSOLE_CLEAR() \
system("cls")
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
extern int gettimeofday(struct timeval* tv, void* tz);
//為了解決 不通用功能
#define localtime_r(t, tm) localtime_s(tm, t)
#define SLEEPMS(m) \
Sleep(m)
#endif // !__GNUC__ 跨平台的代碼都很丑陋
//3.0 浮點數據判斷宏幫助, __開頭表示不希望你使用的宏
#define __DIFF(x, y) ((x)-(y)) //兩個表達式做差宏
#define __IF_X(x, z) ((x)<z && (x)>-z) //判斷宏,z必須是宏常量
#define EQ(x, y, c) EQ_ZERO(__DIFF(x,y), c) //判斷x和y是否在誤差范圍內相等
//3.1 float判斷定義的宏
#define _FLOAT_ZERO (0.000001f) //float 0的誤差判斷值
#define EQ_FLOAT_ZERO(x) __IF_X(x, _FLOAT_ZERO) //float 判斷x是否為零是返回true
#define EQ_FLOAT(x, y) EQ(x, y, _FLOAT_ZERO) //判斷表達式x與y是否相等
//3.2 double判斷定義的宏
#define _DOUBLE_ZERO (0.000000000001) //double 0誤差判斷值
#define EQ_DOUBLE_ZERO(x) __IF_X(x, _DOUBLE_ZERO) //double 判斷x是否為零是返回true
#define EQ_DOUBLE(x,y) EQ(x, y, _DOUBLE_ZERO) //判斷表達式x與y是否相等
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#ifndef CERR
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__)
#endif // !CERR
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#ifndef CERR_EXIT
#define CERR_EXIT(fmt,...) \
CERR(fmt, ##__VA_ARGS__),exit(EXIT_FAILURE)
#endif // !CERR_EXIT
//4.2 執行後檢測,如果有錯誤直接退出
#ifndef IF_CHECK
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
#endif // !IF_CHECK
//5.0 獲取數組長度,只能是數組類型或""字符串常量,後者包含'\0'
#ifndef LEN
#define LEN(arr) \
(sizeof(arr)/sizeof(*(arr)))
#endif/* !ARRLEN */
//7.0 置空操作
#ifndef BZERO
//v必須是個變量
#define BZERO(v) \
memset(&v,0,sizeof(v))
#endif/* !BZERO */
//9.0 scanf 健壯的
#ifndef SAFETY_SCANF
#define _STR_SAFETY_SCANF "Input error, please according to the prompt!"
#define SAFETY_SCANF(scanf_code, ...) \
while(printf(__VA_ARGS__), scanf_code){\
while('\n' != getchar()) \
;\
puts(_STR_SAFETY_SCANF);\
}\
while('\n' != getchar())
#endif /*!SAFETY_SCANF*/
//10.0 簡單的time幫助宏
#ifndef TIME_PRINT
#define _STR_TIME_PRINT "The current code block running time:%lf seconds\n"
#define TIME_PRINT(code) \
do{\
clock_t __st, __et;\
__st=clock();\
code\
__et=clock();\
printf(_STR_TIME_PRINT, (0.0 + __et - __st) / CLOCKS_PER_SEC);\
} while(0)
#endif // !TIME_PRINT
/*
* 10.1 這裡是一個 在 DEBUG 模式下的測試宏
*
* 用法 :
* DEBUG_CODE({
* puts("debug start...");
* });
*/
#ifndef DEBUG_CODE
# ifdef _DEBUG
# define DEBUG_CODE(code) code
# else
# define DEBUG_CODE(code)
# endif // ! _DEBUG
#endif // ! DEBUG_CODE
//11.0 等待的宏 是個單線程沒有加鎖
#define _STR_PAUSEMSG "請按任意鍵繼續. . ."
extern void sh_pause(void);
#ifndef INIT_PAUSE
# ifdef _DEBUG
# define INIT_PAUSE() atexit(sh_pause)
# else
# define INIT_PAUSE() /* 別說了,都重新開始吧 */
# endif
#endif // !INIT_PAUSE
//12.0 判斷是大端序還是小端序,大端序返回true
extern bool sh_isbig(void);
/**
* sh_free - 簡單的釋放內存函數,對free再封裝了一下
**可以避免野指針
**pobj:指向待釋放內存的指針(void*)
**/
extern void sh_free(void ** pobj);
/**
* 獲取 當前時間串,並塞入tstr中長度並返回 need tstr >= 20, 否則返回NULL
** 使用舉例
char tstr[64];
sh_times(tstr, LEN(tstr));
puts(tstr);
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
extern int sh_times(char tstr[], int len);
/*
* 比較兩個結構體棧上內容是否相等,相等返回true,不等返回false
* a : 第一個結構體值
* b : 第二個結構體值
* : 相等返回true, 否則false
*/
#define STRUCTCMP(a, b) \
(!memcmp(&a, &b, sizeof(a)))
#endif// ! _H_SIMPLEC_SCHEAD
View Code
schead.c 跨平台的基礎頭文件實現文件

![]()
#include "schead.h"
//簡單通用的等待函數
void
sh_pause(void) {
rewind(stdin);
printf(_STR_PAUSEMSG);
getchar();
}
//12.0 判斷是大端序還是小端序,大端序返回true
bool
sh_isbig(void) {
static union {
unsigned short _s;
unsigned char _c;
} __u = { 1 };
return __u._c == 0;
}
/**
* sh_free - 簡單的釋放內存函數,對free再封裝了一下
**可以避免野指針
**@pobj:指向待釋放內存的指針(void*)
**/
void
sh_free(void ** pobj) {
if (pobj == NULL || *pobj == NULL)
return;
free(*pobj);
*pobj = NULL;
}
#if defined(_MSC_VER)
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
int
gettimeofday(struct timeval * tv, void * tz) {
time_t clock;
struct tm tm;
SYSTEMTIME wtm;
GetLocalTime(&wtm);
tm.tm_year = wtm.wYear - 1900;
tm.tm_mon = wtm.wMonth - 1; //window的計數更好寫
tm.tm_mday = wtm.wDay;
tm.tm_hour = wtm.wHour;
tm.tm_min = wtm.wMinute;
tm.tm_sec = wtm.wSecond;
tm.tm_isdst = -1; //不考慮夏令時
clock = mktime(&tm);
tv->tv_sec = (long)clock; //32位使用,接口已經老了
tv->tv_usec = wtm.wMilliseconds * 1000;
return _RT_OK;
}
#endif
/**
* 獲取 當前時間串,並塞入tstr中C長度並返回
** 使用舉例
char tstr[64];
puts(gettimes(tstr, LEN(tstr)));
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
int
sh_times(char tstr[], int len) {
struct tm st;
time_t t = time(NULL);
localtime_r(&t, &st);
return (int)strftime(tstr, len, "%F %X", &st);
}
View Code
正文 - 最後的實現細節
在看之前推薦下載demo文件 .
linux 上 項目 http://files.cnblogs.com/files/life2refuel/sclog_linux.zip
window 上項目 http://files.cnblogs.com/files/life2refuel/sclog_window.zip

window 因為沒有 pthread 庫. 需要自己下載 pthread for window 處理. 編譯的時候遇到問題再去處理.
我遇到的問題是 文件太老了, 刪除一個無效的pthread宏, 結構 添加上面window 導入 lib的代碼.
具體的下載安裝可以參照下面博文, 翻到最最後面.
http://www.cnblogs.com/life2refuel/p/5135899.html
本文也是對於上面博文的日志庫部分升級版.
前戲部分完畢, 進入核心層
先看需要的私有數據和私有函數
//錯誤重定向宏 具體應用 於 "mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR
#define _STR_TOOUT "__out__"
#define _STR_TOERR "__err__"
#define _STR_LOGID "__lgd__" //保存logid,持久化
static struct {
pthread_key_t key; //全局線程私有變量
pthread_once_t once; //全局初始化用的類型
unsigned logid; //默認的全局logid, 唯一標識
FILE * log; //log文件指針
FILE * wf; //wf文件指針
} _slmain = { (pthread_key_t)0, PTHREAD_ONCE_INIT, 0, NULL, NULL };
//內部簡單的釋放函數,服務於pthread_key_create 防止線程資源洩露
static void _slinfo_destroy(void* slinfo) {
free(slinfo);
}
static void _gkey(void) {
pthread_key_create(&_slmain.key, _slinfo_destroy);
}
上面 私有靜態全局變量我放入一個結構中保存. 簡單封裝吧. 後面兩個函數是為了線程私有變量初始化,銷毀必須要的.
私有變量是線程''獨有的'' 具體需要在各自線程或進程中 初始化
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** logid : 分配的唯一標識id, 默認0
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
int
sl_init(const char mod[_INT_LITTLE], const char reqip[_INT_LITTLE], unsigned logid) {
struct slinfo* pl;
//保證 _gkey只被執行一次
pthread_once(&_slmain.once, _gkey);
if((pl = pthread_getspecific(_slmain.key)) == NULL){
//重新構建
if ((pl = malloc(sizeof(struct slinfo))) == NULL)
return _RT_EM;
}
gettimeofday(&pl->timev, NULL);
//設置日志logid, 有設置, 沒有默認原子自增
pl->logid = logid ? logid : ATOM_ADD_FETCH(_slmain.logid, 1);
strncpy(pl->mod, mod, _INT_LITTLE); //復制一些數據
strncpy(pl->reqip, reqip, _INT_LITTLE);
//設置私有變量
pthread_setspecific(_slmain.key, pl);
return _RT_OK;
}
其中logid = 0的時候是默認的, 系統分配一個給它. 否則的話表示設置的, 外部傳入的. 應用場景是.
進程A將這個請求處理完畢轉給進程B, 順帶把logid也傳過去.
後面是sclog 日志庫的開啟和銷毀
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**/
void
sl_start(void)
{
FILE *lid;
//單例只執行一次
if (NULL == _slmain.log) {
//先多級創建目錄,簡易不借助宏實現跨平台,system返回值是很復雜,默認成功!
system("mkdir -p \"" _STR_SCLOG_DIR "\" >" _STR_TOOUT " 2>" _STR_TOERR);
rmdir("-p");
remove(_STR_TOOUT);
remove(_STR_TOERR);
}
if (NULL == _slmain.log) {
_slmain.log = fopen(_STR_SCLOG_DIR "/" _STR_SCLOG_LOG, "a+");
if (NULL == _slmain.log)
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_LOG);
}
//繼續打開 wf 文件
if (NULL == _slmain.wf) {
_slmain.wf = fopen(_STR_SCLOG_DIR "/" _STR_SCLOG_WFLOG, "a+");
if (!_slmain.wf) {
fclose(_slmain.log); //其實這都沒有必要,圖個心安
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_WFLOG);
}
}
//讀取文件內容,讀取文件內容,持久化
if ((lid = fopen(_STR_LOGID, "r")) != NULL)
fscanf(lid, "%u", &_slmain.logid);
//簡單判斷是否有初始化的必要
if (_slmain.log && _slmain.wf) {
//這裡可以單獨開啟一個線程或進程,處理日志整理但是 這個模塊可以讓運維做,按照規則搞
sl_init("main thread", "0.0.0.0", 0);
//注冊退出操作
atexit(_sl_end);
}
}
主要打開文件對象, 注冊退出清理操作.
/**
* 日志關閉時候執行,這個接口,關閉打開的文件句柄
**/
static void _sl_end(void)
{
FILE* lid;
void* pl;
// 在簡單地方多做安全操作值得,在核心地方用算法優化的才能穩固
if (NULL == _slmain.log)
return;
//重置當前系統打開文件結構體
fclose(_slmain.log);
fclose(_slmain.wf);
//寫入文件
lid = fopen(_STR_LOGID, "w");
if (NULL != lid) {
fprintf(lid, "%u", _slmain.logid);
fclose(lid);
}
BZERO(_slmain);
//主動釋放私有變量,其實主進程 相當於一個線程是不合理的!還是不同的生存周期的
pl = pthread_getspecific(_slmain.key);
_slinfo_destroy(pl);
pthread_setspecific(_slmain.key, NULL);
}
清理主要清楚句柄, 私有變量等. 後面 是文本輸出操作.
int
sl_printf(const char* format, ...)
{
int len;
va_list ap;
char logs[_INT_LOG]; //這個不是一個好的設計,最新c 中支持 int a[n];
char tstr[_INT_LITTLE];// [%s] => [2016-01-08 23:59:59]
if (NULL == _slmain.log) {
CERR("%s fopen %s | %s error!",_STR_SCLOG_DIR, _STR_SCLOG_LOG, _STR_SCLOG_WFLOG);
return _RT_EF;
}
//初始化時間參數
sh_times(tstr, _INT_LITTLE);
len = snprintf(logs, LEN(logs), "[%s %s]", tstr, _sl_gettimes());
va_start(ap, format);
vsnprintf(logs + len, LEN(logs) - len, format, ap);
va_end(ap);
// 寫普通文件 log
fputs(logs, _slmain.log); //把鎖機制去掉了,fputs就是線程安全的
// 寫警告文件 wf
if (format[1] == 'F' || format[1] == 'W') //當為FATAL或WARNING需要些寫入到警告文件中
fputs(logs, _slmain.wf);
return _RT_OK;
}
很實在 中間 打印了 [%s, %s] 操作. [當前時間, 經過的時間]. 這裡使用的一個技巧是. 用空間換時間.
char logs[_INT_LOG]; 聲明了8k的棧空間保存數據.
附 sclog.c 實現文件

![]()
#include "sclog.h"
#include "scatom.h"
#include "pthread.h"
#include <stdarg.h>
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
//錯誤重定向宏 具體應用 於 "mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR
#define _STR_TOOUT "__out__"
#define _STR_TOERR "__err__"
#define _STR_LOGID "__lgd__" //保存logid,持久化
static struct {
pthread_key_t key; //全局線程私有變量
pthread_once_t once; //全局初始化用的類型
unsigned logid; //默認的全局logid, 唯一標識
FILE * log; //log文件指針
FILE * wf; //wf文件指針
} _slmain = { (pthread_key_t)0, PTHREAD_ONCE_INIT, 0, NULL, NULL };
//內部簡單的釋放函數,服務於pthread_key_create 防止線程資源洩露
static void _slinfo_destroy(void* slinfo) {
free(slinfo);
}
static void _gkey(void) {
pthread_key_create(&_slmain.key, _slinfo_destroy);
}
struct slinfo {
unsigned logid; //請求的logid,唯一id
char reqip[_INT_LITTLE]; //請求方ip
char times[_INT_LITTLE]; //當前時間串
struct timeval timev; //處理時間,保存值,統一用毫秒
char mod[_INT_LITTLE]; //當前線程的模塊名稱,不能超過_INT_LITTLE - 1
};
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** logid : 分配的唯一標識id, 默認0
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
int
sl_init(const char mod[_INT_LITTLE], const char reqip[_INT_LITTLE], unsigned logid) {
struct slinfo* pl;
//保證 _gkey只被執行一次
pthread_once(&_slmain.once, _gkey);
if((pl = pthread_getspecific(_slmain.key)) == NULL){
//重新構建
if ((pl = malloc(sizeof(struct slinfo))) == NULL)
return _RT_EM;
}
gettimeofday(&pl->timev, NULL);
//設置日志logid, 有設置, 沒有默認原子自增
pl->logid = logid ? logid : ATOM_ADD_FETCH(_slmain.logid, 1);
strncpy(pl->mod, mod, _INT_LITTLE); //復制一些數據
strncpy(pl->reqip, reqip, _INT_LITTLE);
//設置私有變量
pthread_setspecific(_slmain.key, pl);
return _RT_OK;
}
/**
* 獲取日志信息體的唯一的logid
**/
unsigned
sl_getlogid(void) {
struct slinfo* pl = pthread_getspecific(_slmain.key);
if (NULL == pl) //返回0表示沒有找見
return 0u;
return pl->logid;
}
/**
* 獲取日志信息體的請求ip串,返回NULL表示沒有初始化
**/
const char*
sl_getreqip(void) {
struct slinfo* pl = pthread_getspecific(_slmain.key);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
return pl->reqip;
}
/**
* 獲取日志信息體的名稱,返回NULL表示沒有初始化
**/
const char*
sl_getmod(void)
{
struct slinfo* pl = pthread_getspecific(_slmain.key);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
return pl->mod;
}
//-------------------------------------------------------------------------------------------|
// 第三部分 對日志系統具體的輸出輸入接口部分
//-------------------------------------------------------------------------------------------|
/**
* 日志關閉時候執行,這個接口,關閉打開的文件句柄
**/
static void _sl_end(void)
{
FILE* lid;
void* pl;
// 在簡單地方多做安全操作值得,在核心地方用算法優化的才能穩固
if (NULL == _slmain.log)
return;
//重置當前系統打開文件結構體
fclose(_slmain.log);
fclose(_slmain.wf);
//寫入文件
lid = fopen(_STR_LOGID, "w");
if (NULL != lid) {
fprintf(lid, "%u", _slmain.logid);
fclose(lid);
}
BZERO(_slmain);
//主動釋放私有變量,其實主進程 相當於一個線程是不合理的!還是不同的生存周期的
pl = pthread_getspecific(_slmain.key);
_slinfo_destroy(pl);
pthread_setspecific(_slmain.key, NULL);
}
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**/
void
sl_start(void)
{
FILE *lid;
//單例只執行一次
if (NULL == _slmain.log) {
//先多級創建目錄,簡易不借助宏實現跨平台,system返回值是很復雜,默認成功!
system("mkdir -p \"" _STR_SCLOG_DIR "\" >" _STR_TOOUT " 2>" _STR_TOERR);
rmdir("-p");
remove(_STR_TOOUT);
remove(_STR_TOERR);
}
if (NULL == _slmain.log) {
_slmain.log = fopen(_STR_SCLOG_DIR "/" _STR_SCLOG_LOG, "a+");
if (NULL == _slmain.log)
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_LOG);
}
//繼續打開 wf 文件
if (NULL == _slmain.wf) {
_slmain.wf = fopen(_STR_SCLOG_DIR "/" _STR_SCLOG_WFLOG, "a+");
if (!_slmain.wf) {
fclose(_slmain.log); //其實這都沒有必要,圖個心安
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_WFLOG);
}
}
//讀取文件內容,讀取文件內容,持久化
if ((lid = fopen(_STR_LOGID, "r")) != NULL)
fscanf(lid, "%u", &_slmain.logid);
//簡單判斷是否有初始化的必要
if (_slmain.log && _slmain.wf) {
//這裡可以單獨開啟一個線程或進程,處理日志整理但是 這個模塊可以讓運維做,按照規則搞
sl_init("main thread", "0.0.0.0", 0);
//注冊退出操作
atexit(_sl_end);
}
}
/**
* 獲取日志信息體的時間串,返回NULL表示沒有初始化
**/
static const char* _sl_gettimes(void) {
struct timeval et; //記錄時間
unsigned td;
struct slinfo* pl = pthread_getspecific(_slmain.key);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
gettimeofday(&et, NULL);
//同一用微秒記
td = 1000000 * (et.tv_sec - pl->timev.tv_sec) + et.tv_usec - pl->timev.tv_usec;
snprintf(pl->times, LEN(pl->times), "%u", td);
return pl->times;
}
int
sl_printf(const char* format, ...)
{
int len;
va_list ap;
char logs[_INT_LOG]; //這個不是一個好的設計,最新c 中支持 int a[n];
char tstr[_INT_LITTLE];// [%s] => [2016-01-08 23:59:59]
if (NULL == _slmain.log) {
CERR("%s fopen %s | %s error!",_STR_SCLOG_DIR, _STR_SCLOG_LOG, _STR_SCLOG_WFLOG);
return _RT_EF;
}
//初始化時間參數
sh_times(tstr, _INT_LITTLE);
len = snprintf(logs, LEN(logs), "[%s %s]", tstr, _sl_gettimes());
va_start(ap, format);
vsnprintf(logs + len, LEN(logs) - len, format, ap);
va_end(ap);
// 寫普通文件 log
fputs(logs, _slmain.log); //把鎖機制去掉了,fputs就是線程安全的
// 寫警告文件 wf
if (format[1] == 'F' || format[1] == 'W') //當為FATAL或WARNING需要些寫入到警告文件中
fputs(logs, _slmain.wf);
return _RT_OK;
}
View Code
到這裡基本設計都已經完成了.
我們最後來個測試demo main.c
#include "sclog.h"
#include "pthread.h"
static void * _test_one(void * arg) {
sl_init("test_one", "8.8.8.8", 0);
SL_TRACE("test_one log test start!");
for (int i = 0; i < 100; ++i) {
SL_FATAL("pthread test one fatal is at %d, It's %s.", i, "OK");
SL_WARNING("pthread test one warning is at %d, It's %s.", i, "OK");
SL_INFO("pthread test one info is at %d, It's %s.", i, "OK");
SL_DEBUG("pthread test one debug is at %d, It's %s.", i, "OK");
SLEEPMS(1); //等待1s
}
SL_TRACE("test_one log test end!");
return NULL;
}
// 線程二測試函數
static void * _test_two(void * arg) {
//線程分離,自回收
pthread_detach(pthread_self());
sl_init("test_two", "8.8.8.8", 0);
SL_TRACE("test_two log test start!");
for (int i = 0; i < 3; ++i) {
SL_FATAL("pthread test two fatal is at %d, It's %s.", i, "OK");
SL_WARNING("pthread test two warning is at %d, It's %s.", i, "OK");
SL_INFO("pthread test two info is at %d, It's %s.", i, "OK");
SL_DEBUG("pthread test two debug is at %d, It's %s.", i, "OK");
SLEEPMS(2); //等待1s
}
SL_TRACE("test_two SL_TRACE test end!");
return NULL;
}
int main(int argc, char * argv[]) {
pthread_t tone, ttwo;
//注冊等待函數
INIT_PAUSE();
sl_start();
SL_NOTICE("main log test start!");
pthread_create(&tone, NULL, _test_one, NULL);
pthread_create(&ttwo, NULL, _test_two, NULL);
pthread_join(tone, NULL);
SL_NOTICE("main log test end!");
return 0;
}
linux 上編譯 Makefile 如下
CC = gcc
DEBUG = -Wall -ggdb2
LIB = -lpthread
RUN = $(CC) $(DEBUG) -o $@ $^
RUO = $(CC) -c -o $@ $^
# create main.out run file
main.out:main.o sclog.o schead.o
$(RUN) $(LIB)
# create object file
%.o:%.c
$(RUO)
# clean file
clean:
rm -rf *.o *.i *.s *.out *~ __* logs ; ls -la
最終測試結果

一切正常. 這裡沒有附加 -I_DEBUG, 默認采用Release發布版所以沒有DEBUG日志.

window測試結果也很正常. 到這裡基本就結束了.
後記
這是一個sclog小日志庫升級的介紹報文, 錯誤是難免的歡迎指正. C/C++ 寫多了, 越發覺得上層語言的可貴. 希望它們蓬勃發展, 將老的低效的都頂下來.
這種開發, 坑太多, 青黃不接, 生產率低下, 悶!
