You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

194 lines
6.5 KiB

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