|
|
- -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.
|