FreeSWITCH日志功能分析及apr模拟

操作系统版本: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 获取。

相关推荐
贾宝玉的玉宝贾4 天前
FreeSWITCH 简单图形化界面39 - Windows安装FreeSWITCH For IPPBX(WSL环境)
windows·voip·freeswitch·ippbx·sip测试
贾宝玉的玉宝贾10 天前
FreeSWITCH 简单图形化界面38 - 使用uniapp中使用JsSIP进行音视频呼叫
uni-app·音视频·voip·freeswitch·ippbx·jssip
Mike_Zhang20 天前
使用pjsip封装自定义软电话sdk
voip·pjsip·pjsua
代码浪人1 个月前
docker 基于Debian镜像安装FreeSwitch1.10.7
docker·容器·debian·freeswitch
new_abc1 个月前
Sofia-SIP 使用教程
freeswitch·sofia
戴草帽的大z1 个月前
Kamailio SIP服务器的配置与运行
kamailio·voip·sip
hongkid2 个月前
docker 部署freeswitch(非编译方式)
docker·容器·freeswitch
Mike_Zhang2 个月前
pjsip编译、说明及vs2022使用示例
voip·pjsua
异域天使phy2 个月前
树莓派安装FreeSWITCH
树莓派·freeswitch