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