%% Copyright (c) 2011 Basho Technologies, Inc. All Rights Reserved. %% %% This file is provided to you under the Apache License, %% Version 2.0 (the "License"); you may not use this file %% except in compliance with the License. You may obtain %% a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% @doc File backend for lager, with multiple file support. %% Multiple files are supported, each with the path and the loglevel being %% configurable. The configuration paramter for this backend is a list of %% 2-tuples of the form `{FileName, Level}'. This backend supports external log %% rotation and will re-open handles to files if the inode changes. -module(lager_file_backend). -include("lager.hrl"). -behaviour(gen_event). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. -include_lib("kernel/include/file.hrl"). -export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, code_change/3]). -record(state, {files}). -record(file, { name :: string(), level :: integer(), fd :: file:io_device(), inode :: integer(), flap=false :: boolean() }). %% @private -spec init([{string(), lager:log_level()},...]) -> {ok, #state{}}. init(LogFiles) -> Files = [begin case lager_util:open_logfile(Name, true) of {ok, {FD, Inode}} -> #file{name=Name, level=lager_util:level_to_num(Level), fd=FD, inode=Inode}; {error, Reason} -> ?INT_LOG(error, "Failed to open log file ~s with error ~s", [Name, file:format_error(Reason)]), #file{name=Name, level=lager_util:level_to_num(Level), flap=true} end end || {Name, Level} <- LogFiles], {ok, #state{files=Files}}. %% @private handle_call({set_loglevel, _}, State) -> {ok, {error, missing_identifier}, State}; handle_call({set_loglevel, Ident, Level}, #state{files=Files} = State) -> case lists:keyfind(Ident, 2, Files) of false -> %% no such file exists {ok, {error, bad_identifier}, State}; _ -> NewFiles = lists:map( fun(#file{name=Name} = File) when Name == Ident -> ?INT_LOG(notice, "Changed loglevel of ~s to ~p", [Ident, Level]), File#file{level=lager_util:level_to_num(Level)}; (X) -> X end, Files), {ok, ok, State#state{files=NewFiles}} end; handle_call(get_loglevel, #state{files=Files} = State) -> Result = lists:foldl(fun(#file{level=Level}, L) -> erlang:max(Level, L); (_, L) -> L end, -1, Files), {ok, Result, State}; handle_call(_Request, State) -> {ok, ok, State}. %% @private handle_event({log, Level, {Date, Time}, Message}, #state{files=Files} = State) -> NewFiles = lists:map( fun(#file{level=L} = File) when Level =< L -> write(File, Level, [Date, " ", Time, " ", Message, "\n"]); (File) -> File end, Files), {ok, State#state{files=NewFiles}}; handle_event(_Event, State) -> {ok, State}. %% @private handle_info(_Info, State) -> {ok, State}. %% @private terminate(_Reason, State) -> %% flush and close any file handles lists:foreach( fun({_, _, FD, _}) -> file:datasync(FD), file:close(FD); (_) -> ok end, State#state.files). %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. write(#file{name=Name, fd=FD, inode=Inode, flap=Flap} = File, Level, Msg) -> case lager_util:ensure_logfile(Name, FD, Inode, true) of {ok, {NewFD, NewInode}} -> file:write(NewFD, Msg), case Level of _ when Level =< ?ERROR -> %% force a sync on any message at error severity or above Flap2 = case file:datasync(NewFD) of {error, Reason2} when Flap == false -> ?INT_LOG(error, "Failed to write log message to file ~s: ~s", [Name, file:format_error(Reason2)]), true; ok -> false; _ -> Flap end, File#file{fd=NewFD, inode=NewInode, flap=Flap2}; _ -> File#file{fd=NewFD, inode=NewInode} end; {error, Reason} -> case Flap of true -> File; _ -> ?INT_LOG(error, "Failed to reopen logfile ~s with error ~s", [Name, file:format_error(Reason)]), File#file{flap=true} end end. -ifdef(TEST). get_loglevel_test() -> {ok, Level, _} = handle_call(get_loglevel, #state{files=[ #file{name="foo", level=lager_util:level_to_num(warning), fd=0, inode=0}, #file{name="bar", level=lager_util:level_to_num(info), fd=0, inode=0}]}), ?assertEqual(Level, lager_util:level_to_num(info)), {ok, Level2, _} = handle_call(get_loglevel, #state{files=[ #file{name="foo", level=lager_util:level_to_num(warning), fd=0, inode=0}, #file{name="foo", level=lager_util:level_to_num(critical), fd=0, inode=0}, #file{name="bar", level=lager_util:level_to_num(error), fd=0, inode=0}]}), ?assertEqual(Level2, lager_util:level_to_num(warning)). rotation_test() -> {ok, {FD, Inode}} = lager_util:open_logfile("test.log", true), ?assertMatch(#file{name="test.log", level=?DEBUG, fd=FD, inode=Inode}, write(#file{name="test.log", level=?DEBUG, fd=FD, inode=Inode}, 0, "hello world")), file:delete("test.log"), Result = write(#file{name="test.log", level=?DEBUG, fd=FD, inode=Inode}, 0, "hello world"), %% assert file has changed ?assert(#file{name="test.log", level=?DEBUG, fd=FD, inode=Inode} =/= Result), ?assertMatch(#file{name="test.log", level=?DEBUG}, Result), file:rename("test.log", "test.log.1"), Result2 = write(Result, 0, "hello world"), %% assert file has changed ?assert(Result =/= Result2), ?assertMatch(#file{name="test.log", level=?DEBUG}, Result2), ok. -endif.