-module(lager_rotator_default).
|
|
|
|
-include_lib("kernel/include/file.hrl").
|
|
|
|
-behaviour(lager_rotator_behaviour).
|
|
|
|
-export([
|
|
create_logfile/2, open_logfile/2, ensure_logfile/5, rotate_logfile/2
|
|
]).
|
|
|
|
-ifdef(TEST).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-endif.
|
|
|
|
create_logfile(Name, Buffer) ->
|
|
open_logfile(Name, Buffer).
|
|
|
|
open_logfile(Name, Buffer) ->
|
|
case filelib:ensure_dir(Name) of
|
|
ok ->
|
|
Options = [append, raw] ++
|
|
case Buffer of
|
|
{Size0, Interval} when is_integer(Interval), Interval >= 0, is_integer(Size0), Size0 >= 0 ->
|
|
[{delayed_write, Size0, Interval}];
|
|
_ -> []
|
|
end,
|
|
case file:open(Name, Options) of
|
|
{ok, FD} ->
|
|
case file:read_file_info(Name, [raw]) of
|
|
{ok, FInfo0} ->
|
|
Inode = FInfo0#file_info.inode,
|
|
{ok, Ctime} = maybe_update_ctime(Name, FInfo0),
|
|
Size1 = FInfo0#file_info.size,
|
|
{ok, {FD, Inode, Ctime, Size1}};
|
|
X -> X
|
|
end;
|
|
Y -> Y
|
|
end;
|
|
Z -> Z
|
|
end.
|
|
|
|
ensure_logfile(Name, undefined, _Inode, _Ctime, Buffer) ->
|
|
open_logfile(Name, Buffer);
|
|
ensure_logfile(Name, FD, Inode0, Ctime0, Buffer) ->
|
|
case lager_util:has_file_changed(Name, Inode0, Ctime0) of
|
|
{true, _FInfo} ->
|
|
reopen_logfile(Name, FD, Buffer);
|
|
{_, FInfo} ->
|
|
{ok, {FD, Inode0, Ctime0, FInfo#file_info.size}}
|
|
end.
|
|
|
|
reopen_logfile(Name, FD0, Buffer) ->
|
|
%% delayed write can cause file:close not to do a close
|
|
_ = file:close(FD0),
|
|
_ = file:close(FD0),
|
|
case open_logfile(Name, Buffer) of
|
|
{ok, {_FD1, _Inode, _Size, _Ctime}=FileInfo} ->
|
|
%% inode changed, file was probably moved and
|
|
%% recreated
|
|
{ok, FileInfo};
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
%% renames failing are OK
|
|
rotate_logfile(File, 0) ->
|
|
%% open the file in write-only mode to truncate/create it
|
|
case file:open(File, [write]) of
|
|
{ok, FD} ->
|
|
_ = file:close(FD),
|
|
{ok, _Ctime} = maybe_update_ctime(File),
|
|
ok;
|
|
Error ->
|
|
Error
|
|
end;
|
|
rotate_logfile(File0, 1) ->
|
|
File1 = File0 ++ ".0",
|
|
_ = file:rename(File0, File1),
|
|
rotate_logfile(File0, 0);
|
|
rotate_logfile(File0, Count) ->
|
|
File1 = File0 ++ "." ++ integer_to_list(Count - 2),
|
|
File2 = File0 ++ "." ++ integer_to_list(Count - 1),
|
|
_ = file:rename(File1, File2),
|
|
rotate_logfile(File0, Count - 1).
|
|
|
|
maybe_update_ctime(Name) ->
|
|
case file:read_file_info(Name, [raw]) of
|
|
{ok, FInfo} ->
|
|
maybe_update_ctime(Name, FInfo);
|
|
_ ->
|
|
{ok, calendar:local_time()}
|
|
end.
|
|
|
|
maybe_update_ctime(Name, FInfo) ->
|
|
{OsType, _} = os:type(),
|
|
do_update_ctime(OsType, Name, FInfo).
|
|
|
|
do_update_ctime(win32, Name, FInfo0) ->
|
|
% Note: we force the creation time to be the current time.
|
|
% On win32 this may prevent the ctime from being updated:
|
|
% https://stackoverflow.com/q/8804342/1466825
|
|
NewCtime = calendar:local_time(),
|
|
FInfo1 = FInfo0#file_info{ctime = NewCtime},
|
|
ok = file:write_file_info(Name, FInfo1, [raw]),
|
|
{ok, NewCtime};
|
|
do_update_ctime(_, _Name, FInfo) ->
|
|
{ok, FInfo#file_info.ctime}.
|
|
|
|
-ifdef(TEST).
|
|
|
|
rotate_file_test() ->
|
|
RotCount = 10,
|
|
{ok, TestDir} = lager_util:create_test_dir(),
|
|
TestLog = filename:join(TestDir, "rotation.log"),
|
|
Outer = fun(N) ->
|
|
?assertEqual(ok, lager_util:safe_write_file(TestLog, erlang:integer_to_list(N))),
|
|
Inner = fun(M) ->
|
|
File = lists:flatten([TestLog, $., erlang:integer_to_list(M)]),
|
|
?assert(filelib:is_regular(File)),
|
|
%% check the expected value is in the file
|
|
Number = erlang:list_to_binary(integer_to_list(N - M - 1)),
|
|
?assertEqual({ok, Number}, file:read_file(File))
|
|
end,
|
|
Count = erlang:min(N, RotCount),
|
|
% The first time through, Count == 0, so the sequence is empty,
|
|
% effectively skipping the inner loop so a rotation can occur that
|
|
% creates the file that Inner looks for.
|
|
% Don't shoot the messenger, it was worse before this refactoring.
|
|
lists:foreach(Inner, lists:seq(0, Count-1)),
|
|
rotate_logfile(TestLog, RotCount)
|
|
end,
|
|
lists:foreach(Outer, lists:seq(0, (RotCount * 2))),
|
|
lager_util:delete_test_dir(TestDir).
|
|
|
|
rotate_file_zero_count_test() ->
|
|
%% Test that a rotation count of 0 simply truncates the file
|
|
{ok, TestDir} = lager_util:create_test_dir(),
|
|
TestLog = filename:join(TestDir, "rotation.log"),
|
|
?assertMatch(ok, rotate_logfile(TestLog, 0)),
|
|
?assertNot(filelib:is_regular(TestLog ++ ".0")),
|
|
?assertEqual(true, filelib:is_regular(TestLog)),
|
|
?assertEqual(1, length(filelib:wildcard(TestLog++"*"))),
|
|
%% assert the new file is 0 size:
|
|
case file:read_file_info(TestLog, [raw]) of
|
|
{ok, FInfo} ->
|
|
?assertEqual(0, FInfo#file_info.size);
|
|
_ ->
|
|
?assert(false)
|
|
end,
|
|
lager_util:delete_test_dir(TestDir).
|
|
|
|
rotate_file_fail_test() ->
|
|
{ok, TestDir} = lager_util:create_test_dir(),
|
|
TestLog = filename:join(TestDir, "rotation.log"),
|
|
|
|
%% set known permissions on it
|
|
ok = lager_util:set_dir_permissions("u+rwx", TestDir),
|
|
|
|
%% write a file
|
|
?assertEqual(ok, lager_util:safe_write_file(TestLog, "hello")),
|
|
|
|
case os:type() of
|
|
{win32, _} -> ok;
|
|
_ ->
|
|
%% hose up the permissions
|
|
ok = lager_util:set_dir_permissions("u-w", TestDir),
|
|
?assertMatch({error, _}, rotate_logfile(TestLog, 10))
|
|
end,
|
|
|
|
%% check we still only have one file, rotation.log
|
|
?assertEqual([TestLog], filelib:wildcard(TestLog++"*")),
|
|
?assert(filelib:is_regular(TestLog)),
|
|
|
|
%% fix the permissions
|
|
ok = lager_util:set_dir_permissions("u+w", TestDir),
|
|
|
|
?assertMatch(ok, rotate_logfile(TestLog, 10)),
|
|
?assert(filelib:is_regular(TestLog ++ ".0")),
|
|
?assertEqual(true, filelib:is_regular(TestLog)),
|
|
?assertEqual(2, length(filelib:wildcard(TestLog++"*"))),
|
|
|
|
%% assert the new file is 0 size:
|
|
case file:read_file_info(TestLog, [raw]) of
|
|
{ok, FInfo} ->
|
|
?assertEqual(0, FInfo#file_info.size);
|
|
_ ->
|
|
?assert(false)
|
|
end,
|
|
|
|
%% check that the .0 file now has the contents "hello"
|
|
?assertEqual({ok, <<"hello">>}, file:read_file(TestLog++".0")),
|
|
lager_util:delete_test_dir(TestDir).
|
|
|
|
-endif.
|