操作系统版本:Debian 12.5_x64
FreeSWITCH版本: 1.10.11
apr库版本:apr-1.7.4 & apr-util-1.6.3
gcc版本: 12.2.0
日志功能在python等脚本里面是标准库提供的,使用起来非常方便,如果在新开发的C程序里面实现该功能,比如将系统时间、文件名称、代码行数都打印出来,该如何实现呢?
最近就遇到了这个问题,是通过参考freeswitch代码实现的。
今天整理下这方面的内容,我将从以下几个方面进行展开:
- freeswitch日志功能及相关源码分析
- 日志功能实现可行性分析
- 使用示例及运行效果
- 资源获取
一、功能说明及源码分析
在freeswitch中,可通过如下方式打印日志:
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "ENUM Reloaded\n");
日志配置文件路径:
/usr/local/freeswitch/conf/autoload_configs/logfile.conf.xml
日志文件路径:
/usr/local/freeswitch/log/freeswitch.log
日志效果:
由图可以看出,添加简单的日志代码,可将系统时间、文件名称、代码行数都打印出来。
这些功能在python等脚本里面很好实现,如果在c程序里面实现该功能,freeswitch的代码值得参考。
下面就结合上述示例,分析下freeswitch日志功能源码。
1、switch_log_printf函数分析
文件: switch_log.c
函数实现如下:
SWITCH_DECLARE(void) switch_log_printf(switch_text_channel_t channel, const char *file, const char *func, int line,
const char *userdata, switch_log_level_t level, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
switch_log_meta_vprintf(channel, file, func, line, userdata, level, NULL, fmt, ap);
va_end(ap);
}
从实现代码可以看出,switch_log_printf函数的参数列表里面有channel、file、func、line等参数,但调用时并未传入。
接下来看下调用时使用的参数。
1)SWITCH_CHANNEL_LOG宏
定义如下:
#define SWITCH_CHANNEL_LOG SWITCH_CHANNEL_ID_LOG, __FILE__, __SWITCH_FUNC__, __LINE__, NULL
从代码实现来看,这个宏把需要的参数都传入了。
2)SWITCH_LOG_INFO参数
这个参数是enum类型,定义如下:
2、switch_log_meta_vprintf函数分析
文件: switch_log.c
函数实现如下:
SWITCH_DECLARE(void) switch_log_meta_vprintf(switch_text_channel_t channel, const char *file, const char *func, int line,
const char *userdata, switch_log_level_t level, cJSON **meta, const char *fmt, va_list ap)
{
cJSON *log_meta = NULL;
char *data = NULL;
char *new_fmt = NULL;
int ret = 0;
FILE *handle;
const char *filep = (file ? switch_cut_path(file) : "");
const char *funcp = (func ? func : "");
char *content = NULL;
switch_time_t now = switch_micro_time_now();
uint32_t len;
#ifdef SWITCH_FUNC_IN_LOG
const char *extra_fmt = "%s [%s] %s:%d %s()%c%s";
#else
const char *extra_fmt = "%s [%s] %s:%d%c%s";
#endif
switch_log_level_t limit_level = runtime.hard_log_level;
switch_log_level_t special_level = SWITCH_LOG_UNINIT;
if (meta && *meta) {
log_meta = *meta;
*meta = NULL;
}
if (limit_level == SWITCH_LOG_DISABLE) {
goto end;
}
if (channel == SWITCH_CHANNEL_ID_SESSION && userdata) {
switch_core_session_t *session = (switch_core_session_t *) userdata;
special_level = session->loglevel;
if (limit_level < session->loglevel) {
limit_level = session->loglevel;
}
}
if (level > 100) {
if ((uint32_t) (level - 100) > runtime.debug_level) {
goto end;
}
level = 1;
}
if (level > limit_level) {
goto end;
}
switch_assert(level < SWITCH_LOG_INVALID);
handle = switch_core_data_channel(channel);
if (channel != SWITCH_CHANNEL_ID_LOG_CLEAN) {
char date[80] = "";
//switch_size_t retsize;
switch_time_exp_t tm;
switch_time_exp_lt(&tm, now);
switch_snprintf(date, sizeof(date), "%0.4d-%0.2d-%0.2d %0.2d:%0.2d:%0.2d.%0.6d %0.2f%%%%",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tm.tm_usec, switch_core_idle_cpu());
//switch_strftime_nocheck(date, &retsize, sizeof(date), "%Y-%m-%d %T", &tm);
#ifdef SWITCH_FUNC_IN_LOG
len = (uint32_t) (strlen(extra_fmt) + strlen(date) + strlen(filep) + 32 + strlen(funcp) + strlen(fmt));
#else
len = (uint32_t) (strlen(extra_fmt) + strlen(date) + strlen(filep) + 32 + strlen(fmt));
#endif
new_fmt = malloc(len + 1);
switch_assert(new_fmt);
#ifdef SWITCH_FUNC_IN_LOG
switch_snprintf(new_fmt, len, extra_fmt, date, switch_log_level2str(level), filep, line, funcp, 128, fmt);
#else
switch_snprintf(new_fmt, len, extra_fmt, date, switch_log_level2str(level), filep, line, 128, fmt);
#endif
fmt = new_fmt;
}
ret = switch_vasprintf(&data, fmt, ap);
if (ret == -1) {
fprintf(stderr, "Memory Error\n");
goto end;
}
if (channel == SWITCH_CHANNEL_ID_LOG_CLEAN) {
content = data;
} else {
if ((content = strchr(data, 128))) {
*content = ' ';
}
}
if (channel == SWITCH_CHANNEL_ID_EVENT) {
switch_event_t *event;
if (switch_event_running() == SWITCH_STATUS_SUCCESS && switch_event_create(&event, SWITCH_EVENT_LOG) == SWITCH_STATUS_SUCCESS) {
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Log-Data", data);
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Log-File", filep);
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Log-Function", funcp);
switch_event_add_header(event, SWITCH_STACK_BOTTOM, "Log-Line", "%d", line);
switch_event_add_header(event, SWITCH_STACK_BOTTOM, "Log-Level", "%d", (int) level);
if (!zstr(userdata)) {
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "User-Data", userdata);
}
switch_event_fire(&event);
data = NULL;
}
goto end;
}
if (console_mods_loaded == 0 || !do_mods) {
if (handle) {
int aok = 1;
#ifndef WIN32
fd_set can_write;
int fd;
struct timeval to;
fd = fileno(handle);
memset(&to, 0, sizeof(to));
FD_ZERO(&can_write);
FD_SET(fd, &can_write);
to.tv_sec = 0;
to.tv_usec = 100000;
if (select(fd + 1, NULL, &can_write, NULL, &to) > 0) {
aok = FD_ISSET(fd, &can_write);
} else {
aok = 0;
}
#endif
if (aok) {
if (COLORIZE) {
#ifdef WIN32
SetConsoleTextAttribute(hStdout, COLORS[level]);
WriteFile(hStdout, data, (DWORD) strlen(data), NULL, NULL);
SetConsoleTextAttribute(hStdout, wOldColorAttrs);
#else
fprintf(handle, "%s%s%s", COLORS[level], data, SWITCH_SEQ_DEFAULT_COLOR);
#endif
} else {
fprintf(handle, "%s", data);
}
}
}
}
if (do_mods && level <= MAX_LEVEL) {
switch_log_node_t *node = switch_log_node_alloc();
node->data = data;
data = NULL;
switch_set_string(node->file, filep);
switch_set_string(node->func, funcp);
node->line = line;
node->level = level;
node->slevel = special_level;
node->content = content;
node->timestamp = now;
node->channel = channel;
node->tags = NULL;
node->meta = log_meta;
log_meta = NULL;
if (channel == SWITCH_CHANNEL_ID_SESSION) {
switch_core_session_t *session = (switch_core_session_t *) userdata;
node->userdata = userdata ? strdup(switch_core_session_get_uuid(session)) : NULL;
if (session) {
switch_channel_get_log_tags(switch_core_session_get_channel(session), &node->tags);
}
} else {
node->userdata = !zstr(userdata) ? strdup(userdata) : NULL;
}
if (switch_queue_trypush(LOG_QUEUE, node) != SWITCH_STATUS_SUCCESS) {
switch_log_node_free(&node);
}
}
end:
cJSON_Delete(log_meta);
switch_safe_free(data);
switch_safe_free(new_fmt);
}
View Code
会使用 switch_queue_trypush 函数进行入队操作:
说明:
MAX_LEVEL的值会变,在 switch_log_bind_logger 时修改该值。
3、mod_logfile_load函数分析
文件: mod_logfile.c
freeswitch的日志是通过mod_logfile来配置的,在模块加载时,主要做以下事项:
1)解析配置文件;
2)通过switch_log_bind_logger函数来注册日志回调函数;
4、switch_log_bind_logger函数分析
文件: switch_log.c
主要用于注册回调函数,供后续流程使用。
5、log_thread函数
文件: switch_log.c
主要用于注册回调函数,供后续流程使用。
日志线程在初始化时就启动了。
6、mod_logfile_raw_write函数
文件: mod_logfile.c
功能:
1)写日志内容到文件;
2)日志文件轮转(rotate);
函数调用链如下:
mod_logfile_load
=> mod_logfile_logger
=> process_node
=> mod_logfile_raw_write
7、mod_logfile_rotate函数
文件: mod_logfile.c
功能:
通过修改文件名的方式,实现日志文件轮转(rotate)。
二、实现可行性分析
1、配置文件
参考freeswitch,使用xml作为配置文件,可借助libxml2来解析。
该库的GitHub地址: https://github.com/GNOME/libxml2
可以直接使用软件源安装。
debian下:
apt install libxml2-dev
依赖安装(centos7):
yum install libxml2-devel.x86_64
结论:配置功能可行,可基于libxml2实现。
2、基于队列实现日志功能
apr队列是个线程安全的FIFO队列,arp库编译、队列接口及使用示例,可参考如下文章:
https://www.cnblogs.com/MikeZhang/p/18378340/aprQueueTest20240824
可基于apr队列实现日志功能:
1)日志线程提供队列用于存储日志数据;
2)工作线程向队列中添加日志;
3)日志线程输出日志内容到文件;
结论:基于队列实现日志功能可行。
3、日志轮转
可仿照freeswitch的实现,使用apr库进行文件名称修改操作。
综上,使用apr模拟freeswitch的日志功能可行。
三、使用示例及运行效果
1、配置文件添加及解析
配置文件示例(conf.xml):
<setting>
<server>192.168.137.100:5060</server>
<log>
<dirPath>/tmp/log</dirPath>
<fileName>test.log</fileName>
<rollSize>100</rollSize> MB
<rollCount>5</rollCount>
</log>
</setting>
对应的解析代码:
完整代码可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。
2、logger实现
头文件内容如下(logger.h):
完整代码可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。
日志功能内容如下(logger.c):
完整代码可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。
3、主程序实现
头文件内容如下(testMain.h):
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/prctl.h>
#include <arpa/inet.h>
#include <apr_portable.h>
#include "apr_queue.h"
#include "apr_thread_pool.h"
#include "apr_time.h"
#include "apr_hash.h"
#include "apr_thread_mutex.h"
#include <pthread.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
typedef struct logger_vars_t {
apr_pool_t *apr_pool;
char server[256];
char log_dirname[256];
char log_filename[128];
int log_rollsize;
int log_rollcount;
}logger_vars_t;
主程序内容如下(testMain.c):
完整代码可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。
4、编译脚本及Makefile
编译脚本内容如下(doBuild.sh):
#! /bin/bash
BASEDIR=${PWD}
APRDIR=${BASEDIR}/libs/apr-1.7.4
APRUTILDIR=${BASEDIR}/libs/apr-util-1.6.3
echo ${BASEDIR}
#exit 0
cd ${APRDIR}
./configure --enable-static
make
cd ${APRUTILDIR}
./buildconf --with-apr=${APRDIR}
./configure --with-apr=${APRDIR}
make
cd ${BASEDIR}
make testMain
Makefile文件内容如下:
CC=gcc
CFLAGS=-g -gstabs+ -I/usr/include/libks -Ilibs/apr-1.7.4/include -Ilibs/apr-util-1.6.3/include -I/usr/include/libxml2/
LIBS=libs/apr-util-1.6.3/.libs/libaprutil-1.a libs/apr-1.7.4/.libs/libapr-1.a -lpthread -lxml2
#
BASEDIR=${PWD}
APRDIR=${BASEDIR}/libs/apr-1.7.4
APRUTILDIR=${BASEDIR}/libs/apr-util-1.6.3
all:
#make apr
#make apr-util
make testMain
apr:
cd $(APRDIR) && ./configure --enable-static && make
apr-util:
cd $(APRUTILDIR) && ./buildconf --with-apr=$(APRDIR) && ./configure --with-apr=$(APRDIR) && make
testMain: testMain.o logger.o
$(CC) -o testMain testMain.o logger.o $(LIBS)
clean:
rm -f testMain
rm -f *.o
.c.o:
$(CC) $(CFLAGS) -c -o $*.o $<
执行 doBuild.sh 即可编译。
5、示例效果
运行效果如下:
打包的工程文件,可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。
四、其它
1、添加core dump支持
当程序crash时产生core dump文件,便于程序调试使用。
void core_setrlimits(void)
{
// set core dump
struct rlimit rlp;
memset(&rlp, 0, sizeof(rlp));
rlp.rlim_cur = 999999;
rlp.rlim_max = 999999;
setrlimit(RLIMIT_NOFILE, &rlp);
memset(&rlp, 0, sizeof(rlp));
rlp.rlim_cur = RLIM_INFINITY;
rlp.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU, &rlp);
setrlimit(RLIMIT_DATA, &rlp);
setrlimit(RLIMIT_FSIZE, &rlp);
setrlimit(RLIMIT_CORE, &rlp);
return;
}
在main函数中调用 core_setrlimits 函数即可。
2、日志乱码问题及队列操作注意事项
可从如下渠道获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。
五、资源获取
本文涉及源码及相关文件,可从如下途径获取:
关注微信公众号(聊聊博文,文末可扫码)后回复 20250102 获取。