-module(rumBkdFile). %% @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 key-value 2-tuples. See the init() function for the available options. %% This backend supports external and internal log rotation and will re-open handles to files if the inode changes. %% It will also rotate the files itself if the size of the file exceeds the `size' and keep `count' rotated files. %% `date' is an alternate rotation trigger, based on time. See the README for documentation. %% For performance, the file backend does delayed writes, although it will sync at specific log levels, configured via the `sync_on' option. %% By default the error level or above will trigger a sync. -include("rumDef.hrl"). -include_lib("kernel/include/file.hrl"). -behaviour(gen_emm). -export([configToId/1]). -export([ init/1 , handleCall/2 , handleEvent/2 , handleInfo/2 , terminate/2 , code_change/3 ]). -record(state, { fileName :: string(), level :: rumMaskLevel(), fd :: file:io_device() | undefined, inode :: integer() | undefined, ctime :: file:date_time() | undefined, flap = false :: boolean(), size = 0 :: integer(), date :: undefined | string(), count = 10 :: integer(), rotator = lager_util :: atom(), shaper :: rumShaper(), formatter :: atom(), formatterConfig :: any(), syncOn :: integer(), checkInterval = ?RumDefCheckInterval :: non_neg_integer(), %% 单位毫秒 syncInterval = ?RumDefSyncInterval :: non_neg_integer(), syncSize = ?RumDefSyncSize :: non_neg_integer(), lastCheck = rumTime:nowMs() :: erlang:timestamp(), %% 单位毫秒 osType :: atom() }). -spec init([rumFileOpt(), ...]) -> {ok, #state{}} | {error, atom()}. init(Opts) -> true = checkOpts(Opts, false), RelName = rumUtil:get_opt(file, Opts, undefined), CfgLevel = rumUtil:get_opt(level, Opts, ?RumDefLogLevel), CfgDate = rumUtil:get_opt(date, Opts, ?RumDefRotateDate), Size = rumUtil:get_opt(size, Opts, ?RumDefRotateSize), Count = rumUtil:get_opt(count, Opts, ?RumDefRotateCnt), Rotator = rumUtil:get_opt(rotator, Opts, ?RumDefRotateMod), HighWaterMark = rumUtil:get_opt(high_water_mark, Opts, ?RumDefCheckHWM), Flush = rumUtil:get_opt(flush_queue, Opts, ?RumDefFlushQueue), FlushThr = rumUtil:get_opt(flush_threshold, Opts, ?RumDefFlushThreshold), SyncInterval = rumUtil:get_opt(sync_interval, Opts, ?RumDefSyncInterval), CfgCheckInterval = rumUtil:get_opt(check_interval, Opts, ?RumDefCheckInterval), SyncSize = rumUtil:get_opt(sync_size, Opts, ?RumDefSyncSize), CfgSyncOn = rumUtil:get_opt(sync_on, Opts, ?RumDefSyncLevel), Formatter = rumUtil:get_opt(formatter, Opts, ?RumDefFormatter), FormatterConfig = rumUtil:get_opt(formatter_config, Opts, ?RumDefFormatterCfg), %% 需要二次转换的配置在这里处理 Level = rumUtil:configToMask(CfgLevel), SyncOn = rumUtil:configToMask(CfgSyncOn), CheckInterval = ?IIF(CfgCheckInterval == always, 0, CfgCheckInterval), {ok, Date} = rumUtil:parseRotateSpec(CfgDate), FileName = rumUtil:parsePath(RelName), scheduleRotation(Date, FileName), Shaper = rumUtil:maybeFlush(Flush, #rumShaper{hwm = HighWaterMark, flushThreshold = FlushThr, id = FileName}), TemState = #state{ fileName = FileName, level = Level, size = Size, date = Date , count = Count, rotator = Rotator, shaper = Shaper , formatter = Formatter, formatterConfig = FormatterConfig , syncOn = SyncOn, syncInterval = SyncInterval , syncSize = SyncSize, checkInterval = CheckInterval }, case Rotator:createLogFile(FileName, {SyncSize, SyncInterval}) of {ok, Fd, Inode, CTime, _Size} -> {ok, TemState#state{fd = Fd, inode = Inode, ctime = CTime}}; {error, Reason} -> ?INT_LOG(error, "Failed to open log file ~ts with error ~s", [FileName, file:format_error(Reason)]), {ok, TemState#state{flap = true}} end. handleCall(mGetLogLevel, #state{level = Level} = State) -> {reply, Level, State}; handleCall({mSetLogLevel, Level}, #state{fileName = Ident} = State) -> case rumUtil:validateLogLevel(Level) of false -> {reply, {error, bad_loglevel}, State}; LevelMask -> ?INT_LOG(notice, "Changed loglevel of ~s to ~p", [Ident, Level]), {reply, ok, State#state{level = LevelMask}} end; handleCall({mSetLogHwm, Hwm}, #state{shaper = Shaper, fileName = FileName} = State) -> case checkOpts([{high_water_mark, Hwm}], true) of false -> {reply, {error, bad_log_hwm}, State}; _ -> NewShaper = Shaper#rumShaper{hwm = Hwm}, ?INT_LOG(notice, "Changed loghwm of ~ts to ~p", [FileName, Hwm]), {reply, {last_loghwm, Shaper#rumShaper.hwm}, State#state{shaper = NewShaper}} end; handleCall(mRotate, State = #state{fileName = File}) -> {ok, NewState} = handleInfo({mRotate, File}, State), {reply, ok, NewState}; handleCall(_Msg, State) -> ?ERR("~p call receive unexpect msg ~p ~n ", [?MODULE, _Msg]), {reply, ok, State}. handleEvent({mWriteLog, Message}, #state{fileName = FileName, level = Level, shaper = Shaper, formatter = Formatter, formatterConfig = FormatConfig} = State) -> case rumUtil:isLoggAble(Message, Level, {rumBkdFile, FileName}) of true -> case rumUtil:checkHwm(Shaper) of {true, _Drop, NewShaper} -> {ok, writeLog(State#state{shaper = NewShaper}, rumMsg:timestamp(Message), rumMsg:severity_as_int(Message), Formatter:format(Message, FormatConfig))}; {drop, Drop, NewShaper} -> TemState = case Drop =< 0 of true -> State; _ -> Report = eFmt:format(<<"rumBkdFile dropped ~p messages in the last second that exceeded the limit of ~p messages/sec">>, [Drop, NewShaper#rumShaper.hwm]), ReportMsg = rumMsg:new(Report, warning, [], []), writeLog(State, rumMsg:timestamp(ReportMsg), rumMsg:severity_as_int(ReportMsg), Formatter:format(ReportMsg, FormatConfig)) end, {ok, writeLog(TemState#state{shaper = NewShaper}, rumMsg:timestamp(Message), rumMsg:severity_as_int(Message), Formatter:format(Message, FormatConfig))}; {false, _, NewShaper} -> {ok, State#state{shaper = NewShaper}} end; _ -> kpS end; handleEvent(_Msg, _State) -> ?ERR("~p event receive unexpect msg ~p ~n ", [?MODULE, _Msg]), kpS. handleInfo({mRotate, File}, #state{fileName = File, count = Count, date = Date, rotator = Rotator} = State) -> NewState = closeFile(State), _ = Rotator:rotateLogFile(File, Count), scheduleRotation(File, Date), {ok, NewState}; handleInfo({mShaperExpired, Name}, #state{shaper = Shaper, fileName = Name, formatter = Formatter, formatterConfig = FormatConfig} = State) -> case Shaper#rumShaper.dropped of 0 -> ignore; Dropped -> Report = eFmt:format(<<"rumBkdFile dropped ~p messages in the last second that exceeded the limit of ~p messages/sec">>, [Dropped, Shaper#rumShaper.hwm]), ReportMsg = rumMsg:new(Report, warning, [], []), writeLog(State, rumMsg:timestamp(ReportMsg), rumMsg:severity_as_int(ReportMsg), Formatter:format(ReportMsg, FormatConfig)) end, {ok, State#state{shaper = Shaper#rumShaper{dropped = 0, mps = 0, lastTime = rumTime:now()}}}; handleInfo(_Msg, _State) -> ?ERR("~p info receive unexpect msg ~p ~n ", [?MODULE, _Msg]), kpS. terminate(_Reason, State) -> %% leaving this function call unmatched makes dialyzer cranky _ = closeFile(State), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. writeLog(#state{fileName = FileName, fd = Fd, inode = Inode, ctime = CTime, flap = Flap, size = RotSize, count = Count, rotator = Rotator, lastCheck = LastCheck, checkInterval = CheckInterval, syncSize = SyncSize, syncInterval = SyncInterval} = State, Timestamp, Level, Msg) -> case isWriteCheck(Fd, LastCheck, CheckInterval, FileName, Inode, CTime, Timestamp) of true -> %% need to check for rotation case Rotator:ensureLogFile(FileName, Fd, Inode, CTime, {SyncSize, SyncInterval}) of {ok, NewFD, NewInode, NewCTime, FileSize} -> case RotSize > 0 andalso FileSize > RotSize of true -> TemState = closeFile(State), case Rotator:rotateLogFile(FileName, Count) of ok -> %% go around the loop again, we'll do another rotation check and hit the next clause of ensureLogFile writeLog(TemState, Timestamp, Level, Msg); {error, Reason} -> ?IIF(Flap, State, begin ?INT_LOG(error, "Failed to rotate log file ~ts with error ~s", [FileName, file:format_error(Reason)]), State#state{flap = true} end) end; _ -> %% update our last check and try again TemState = State#state{lastCheck = Timestamp, fd = NewFD, inode = NewInode, ctime = NewCTime}, writeFile(TemState, Level, Msg) end; {error, Reason} -> ?IIF(Flap, State, begin ?INT_LOG(error, "Failed to reopen log file ~ts with error ~s", [FileName, file:format_error(Reason)]), State#state{flap = true} end) end; _ -> writeFile(State, Level, Msg) end. writeFile(#state{fd = Fd, fileName = FileName, flap = Flap, syncOn = SyncOn} = State, Level, Msg) -> %% delayed_write doesn't report errors _ = file:write(Fd, unicode:characters_to_binary(Msg)), case (Level band SyncOn) =/= 0 of true -> %% force a sync on any message that matches the 'syncOn' bitmask NewFlap = case file:datasync(Fd) of {error, Reason} when Flap == false -> ?INT_LOG(error, "Failed to write log message to file ~ts: ~s", [FileName, file:format_error(Reason)]), true; ok -> false; _ -> Flap end, State#state{flap = NewFlap}; _ -> State end. isWriteCheck(Fd, LastCheck, CheckInterval, Name, Inode, CTime, Timestamp) -> DiffTime = abs(Timestamp - LastCheck), case DiffTime >= CheckInterval orelse Fd == undefined of true -> true; _ -> % We need to know if the file has changed "out from under lager" so we don't write to an invalid Fd {Result, _FInfo} = rumUtil:isFileChanged(Name, Inode, CTime), Result end. %% Convert the config into a gen_event handler ID configToId(Config) -> case rumUtil:get_opt(file, Config, undefined) of undefined -> erlang:error(no_file); File -> {?MODULE, File} end. checkOpts([], IsFile) -> ?IIF(IsFile, true, {error, no_file_name}); checkOpts([{file, _File} | Tail], _IsFile) -> checkOpts(Tail, true); checkOpts([{level, Level} | Tail], IsFile) -> ?IIF(rumUtil:validateLogLevel(Level) /= false, checkOpts(Tail, IsFile), {error, {invalid_log_level, Level}}); checkOpts([{size, Size} | Tail], IsFile) when is_integer(Size), Size >= 0 -> checkOpts(Tail, IsFile); checkOpts([{count, Count} | Tail], IsFile) when is_integer(Count), Count >= 0 -> checkOpts(Tail, IsFile); checkOpts([{rotator, Rotator} | Tail], IsFile) when is_atom(Rotator) -> checkOpts(Tail, IsFile); checkOpts([{high_water_mark, HighWaterMark} | Tail], IsFile) when is_integer(HighWaterMark), HighWaterMark >= 0 -> checkOpts(Tail, IsFile); checkOpts([{date, _Date} | Tail], IsFile) -> checkOpts(Tail, IsFile); checkOpts([{sync_interval, SyncInt} | Tail], IsFile) when is_integer(SyncInt), SyncInt >= 0 -> checkOpts(Tail, IsFile); checkOpts([{sync_size, SyncSize} | Tail], IsFile) when is_integer(SyncSize), SyncSize >= 0 -> checkOpts(Tail, IsFile); checkOpts([{check_interval, CheckInt} | Tail], IsFile) when is_integer(CheckInt), CheckInt >= 0; CheckInt == always -> checkOpts(Tail, IsFile); checkOpts([{sync_on, Level} | Tail], IsFile) -> ?IIF(rumUtil:validateLogLevel(Level) /= false, checkOpts(Tail, IsFile), {error, {invalid_sync_on, Level}}); checkOpts([{formatter, Fmt} | Tail], IsFile) when is_atom(Fmt) -> checkOpts(Tail, IsFile); checkOpts([{formatter_config, FmtCfg} | Tail], IsFile) when is_list(FmtCfg) -> checkOpts(Tail, IsFile); checkOpts([{flush_queue, FlushCfg} | Tail], IsFile) when is_boolean(FlushCfg) -> checkOpts(Tail, IsFile); checkOpts([{flush_threshold, Thr} | Tail], IsFile) when is_integer(Thr), Thr >= 0 -> checkOpts(Tail, IsFile); checkOpts([Other | _Tail], _IsFile) -> {error, {invalid_opt, Other}}. scheduleRotation(undefined, _FileName) -> ok; scheduleRotation(Date, Name) -> erlang:send_after(rumUtil:calcNextRotateMs(Date), self(), {mRotate, Name}), ok. closeFile(#state{fd = Fd} = State) -> case Fd of undefined -> State; _ -> %% Flush and close any file handles. %% delayed write can cause file:close not to do a close _ = file:datasync(Fd), _ = file:close(Fd), _ = file:close(Fd), State#state{fd = undefined} end.