1%% @author Bob Ippolito <bob@mochimedia.com> 2%% @copyright 2010 Mochi Media, Inc. 3 4%% @doc Write newline delimited log files, ensuring that if a truncated 5%% entry is found on log open then it is fixed before writing. Uses 6%% delayed writes and raw files for performance. 7-module(mochilogfile2). 8-author('bob@mochimedia.com'). 9 10-export([open/1, write/2, close/1, name/1]). 11 12%% @spec open(Name) -> Handle 13%% @doc Open the log file Name, creating or appending as necessary. All data 14%% at the end of the file will be truncated until a newline is found, to 15%% ensure that all records are complete. 16open(Name) -> 17 {ok, FD} = file:open(Name, [raw, read, write, delayed_write, binary]), 18 fix_log(FD), 19 {?MODULE, Name, FD}. 20 21%% @spec name(Handle) -> string() 22%% @doc Return the path of the log file. 23name({?MODULE, Name, _FD}) -> 24 Name. 25 26%% @spec write(Handle, IoData) -> ok 27%% @doc Write IoData to the log file referenced by Handle. 28write({?MODULE, _Name, FD}, IoData) -> 29 ok = file:write(FD, [IoData, $\n]), 30 ok. 31 32%% @spec close(Handle) -> ok 33%% @doc Close the log file referenced by Handle. 34close({?MODULE, _Name, FD}) -> 35 ok = file:sync(FD), 36 ok = file:close(FD), 37 ok. 38 39fix_log(FD) -> 40 {ok, Location} = file:position(FD, eof), 41 Seek = find_last_newline(FD, Location), 42 {ok, Seek} = file:position(FD, Seek), 43 ok = file:truncate(FD), 44 ok. 45 46%% Seek backwards to the last valid log entry 47find_last_newline(_FD, N) when N =< 1 -> 48 0; 49find_last_newline(FD, Location) -> 50 case file:pread(FD, Location - 1, 1) of 51 {ok, <<$\n>>} -> 52 Location; 53 {ok, _} -> 54 find_last_newline(FD, Location - 1) 55 end. 56 57%% 58%% Tests 59%% 60-ifdef(TEST). 61-include_lib("eunit/include/eunit.hrl"). 62name_test() -> 63 D = mochitemp:mkdtemp(), 64 FileName = filename:join(D, "open_close_test.log"), 65 H = open(FileName), 66 ?assertEqual( 67 FileName, 68 name(H)), 69 close(H), 70 file:delete(FileName), 71 file:del_dir(D), 72 ok. 73 74open_close_test() -> 75 D = mochitemp:mkdtemp(), 76 FileName = filename:join(D, "open_close_test.log"), 77 OpenClose = fun () -> 78 H = open(FileName), 79 ?assertEqual( 80 true, 81 filelib:is_file(FileName)), 82 ok = close(H), 83 ?assertEqual( 84 {ok, <<>>}, 85 file:read_file(FileName)), 86 ok 87 end, 88 OpenClose(), 89 OpenClose(), 90 file:delete(FileName), 91 file:del_dir(D), 92 ok. 93 94write_test() -> 95 D = mochitemp:mkdtemp(), 96 FileName = filename:join(D, "write_test.log"), 97 F = fun () -> 98 H = open(FileName), 99 write(H, "test line"), 100 close(H), 101 ok 102 end, 103 F(), 104 ?assertEqual( 105 {ok, <<"test line\n">>}, 106 file:read_file(FileName)), 107 F(), 108 ?assertEqual( 109 {ok, <<"test line\ntest line\n">>}, 110 file:read_file(FileName)), 111 file:delete(FileName), 112 file:del_dir(D), 113 ok. 114 115fix_log_test() -> 116 D = mochitemp:mkdtemp(), 117 FileName = filename:join(D, "write_test.log"), 118 file:write_file(FileName, <<"first line good\nsecond line bad">>), 119 F = fun () -> 120 H = open(FileName), 121 write(H, "test line"), 122 close(H), 123 ok 124 end, 125 F(), 126 ?assertEqual( 127 {ok, <<"first line good\ntest line\n">>}, 128 file:read_file(FileName)), 129 file:write_file(FileName, <<"first line bad">>), 130 F(), 131 ?assertEqual( 132 {ok, <<"test line\n">>}, 133 file:read_file(FileName)), 134 F(), 135 ?assertEqual( 136 {ok, <<"test line\ntest line\n">>}, 137 file:read_file(FileName)), 138 ok. 139 140-endif. 141