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