lager源码阅读
一. lager简单介绍
lager是一开源erlang 日志框架。地址:https://github.com/basho/lager, 在erlang中得到了广泛的使用。那么:
1. 一个好的日志框架应该是什么样子?
2. lager是一个好的日志框架吗?
3. lager 对比erlang自带的日志系统有什么优点?
4. lager有什么缺点?
由于没有对比其他语言的日志框架,我并不能回答第一个问题。而其他几个问题或多或少都能在余下部分找到答案。
二. lager的特点
2.1 作者介绍lager
链接:http://basho.com/posts/technical/introducing-lager-a-new-logging-framework-for-erlangotp/
lager的特点:
1. 可读性更高。
2. 安全的输出crash日志。
3. 能在消息日志中得到当前模块名,当前函数,当前进程,行号等信息。
4. 使用parse transform 解析模块,函数名,而不是使用宏得到?MODULE,?LINE。这样更容易扩展。能做更多的事情,比如编译时间计算。
5. 能够安装多handler,目前有控制台handler,和文件handler。
6. 文件handler支持多文件,所以你可以输出不同等级的消息到不同的文件。
7. 日志级别能够在运行时改变。
8. lager会保持的对各个日志等级消费情况的跟踪。This means that if you have no backends consuming debug messages, you can log a million debug messages in less than half a second; they’re effectively free. Therefore you can add lots of debug messages to your code and not have to worry they’re slowing things down if you’re not looking at them.
9. 支持日志切换能够随时删除和移动日志文件,lager会再创建它。
以上是从作者文章中提炼出来的,在 github上也能看到这些特点。
2.2 其他人评价lager
链接:https://pdincau.wordpress.com/2012/08/11/how-to-log-your-stuff-with-lager/
1. 容易使用,容易跟项目整合。
2. lager的log文件会精简消息,以至于不会混淆你,但是也会存储详细消息到crash 文件,如果你需要可以从这里读取。
3. lager有不同的日志等级 :debug, info, notice, warning, error, critical, alert, emergency等。
4. lager能够添加多个handler。能够自动换行。
有了上面这些特点,那么我们就可以看看是如何实现这些特点的。
三. lager安装与使用
下载:
git clone https://github.com/basho/lager
编译:
./rebar get-deps
./rebar compile
使用:
1. 使用方法可根据自己项目是手动复制,还是通过-pa参数,还是使用./rebar generate.都可以达到目的。
2. 在自己的代码中,需要添加编译选项 {parse_transform, lager_transform},此选项会使得lager修改你代码的abstract code。
3. 然后可以调用lager:start(), 然后lager:info等,输出日志。
四. lager的源码
4.1 parse transform技术
lager代码中最有趣的就是使用了parse transform 得到调用地方的行号,模块名,函数名等信息,而不是使用宏。这主要使用了{parse_transform, lager_transform}编译选项,
在erlang文档中是这么解释的:
{parse_transform,Module}
Causes the parse transformation function Module:parse_transform/2 to be applied to the parsed code before the code is checked for errors.
大概意思就是 在检查代码错误之前会调用此模块。
在lager源码中你可以看到lager模块并没有 lager:info, lager:error, lager:debug等函数。这就是因为在生成目标二进制的使用,编译器会调用lager_transform:parse_transform/2修改你的代码的abstract code 然后再进行编译,所以生成的beam文件并不是你原先代码的beam文件。我们可以尝试修改lager_transform的parse_transform函数,如下:
parse_transform(AST, Options) ->
TruncSize = proplists:get_value(lager_truncation_size, Options, ?DEFAULT_TRUNCATION),
Enable = proplists:get_value(lager_print_records_flag, Options, true),
Sinks = [lager] ++ proplists:get_value(lager_extra_sinks, Options, []),
put(print_records_flag, Enable),
put(truncation_size, TruncSize),
put(sinks, Sinks),
erlang:put(records, []),
%% .app file should either be in the outdir, or the same dir as the source file
guess_application(proplists:get_value(outdir, Options), hd(AST)),
AST2 = walk_ast([], AST),
%%添加此行代码输出
io:format("ttt:~n~p", [erl_prettypr:format(erl_syntax:form_list(AST2))]),
AST2.
在这里我把修改后的AST转换再重新转换为代码,进行输出。下面是我的例子:
原始代码:
-module(lagertest).
-compile([{parse_transform, lager_transform}]).
%% API
-export([test/0]).
test() ->
ok = lager:start(),
lager:info("xxxxxx").
修改后的代码:
-module(lagertest).
-export([test/0]).
test() ->
ok = lager:start(),
case {whereis(lager_event), whereis(lager_event), lager_config:get({lager_event, loglevel}, {0, []})} of
{undefined, undefined, _} ->
fun() -> {error, lager_not_running}end();
{undefined, _, _} ->
fun()-> {error, {sink_not_configured, lager_event}} end();
{_Pidlagertest21 band 64 /= 0 orelse _Tracelagertest21 /= []} ->
lager:do_log(info, [{application, lager}, {module, lagertest}, {function,test}, {line, 21},
{pid, pid_to_list(self())}, {node,node()},lager:md()], "xxxxxx", none, 4096, 64, __Levellagertest21,__Traceslagertest21
,lager_event, __Pidlagertest21);
_ -> ok
end.
用代码写代码,有点元编程的意思了。
4.2 lager整体架构
lager的日志功能主要还是使用了gen_event那一套功能,简单介绍一下gen_event的作用,在gen_event中能存放多个回调模块,当你使用gen_event:notify的时候,所有的回调模块都会调用。所以lager最主要的功能还是启动一个gen_event进程,并添加不同的handler,目前支持能够输入到文件的handler,与能输出到控制台的handler。当然gen_event进程也能启动多个可以自行配置。
以下是lager启动时的代码:
start(_StartType, _StartArgs) ->
{ok, Pid} = lager_sup:start_link(),
%% Handle the default sink.
determine_async_behavior(?DEFAULT_SINK,
application:get_env(lager, async_threshold),
application:get_env(lager, async_threshold_window)),
start_handlers(?DEFAULT_SINK,
get_env(lager, handlers, ?DEFAULT_HANDLER_CONF)),
ok = add_configured_traces(),
lager:update_loglevel_config(?DEFAULT_SINK),
SavedHandlers = start_error_logger_handler(
application:get_env(lager, error_logger_redirect),
interpret_hwm(application:get_env(lager, error_logger_hwm)),
application:get_env(lager, error_logger_whitelist)
),
_ = lager_util:trace_filter(none),
%% Now handle extra sinks
configure_extra_sinks(get_env(lager, extra_sinks, [])),
clean_up_config_checks(),
{ok, Pid, SavedHandlers}.
可以看到启动了DEFAULT_SINK gen_event进程,并且安装了默认handler, 然后启动了配置文件中的gen_event。
五. 其他说明
lager的lager_backend_throttle模块有点意思,这个模块也是一个handler,所以日志消息也会发送到此模块,此模块检查消息队里的消息,如果日志压力过大,会修改配置为异步记录日志。异步发送是通过gen_event:notify发送消息,而同步通过gen_event:sync_notify发送。在gen_event模块中,我们可以看到,异步使用global:send, 同步是gen:call。
六. 疑问
lager_handler_watcher_sup: 不知道怎么模块为啥要启动一个监控树,这个模块下面挂载着lager_handler_watcher,但是lager_handler_watcher只做了一件事情:重新安装handler。但是这件事情明显不会太过频繁。个人感觉还没到使用监控树挂一堆的地步