flm01/server/api/webmachine/src/webmachine_logger.erl

225 lines
6.0 KiB
Erlang

%% @author Justin Sheehy <justin@basho.com>
%% @author Andy Gross <andy@basho.com>
%% @copyright 2007-2008 Basho Technologies
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(webmachine_logger).
-author('Justin Sheehy <justin@basho.com>').
-author('Andy Gross <andy@basho.com>').
-behaviour(gen_server).
-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([log_access/1, refresh/0]).
-include("webmachine_logger.hrl").
-record(state, {hourstamp, filename, handle}).
alog_path(BaseDir) ->
filename:join(BaseDir, "access.log").
start_link(BaseDir) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [BaseDir], []).
init([BaseDir]) ->
defer_refresh(),
FileName = alog_path(BaseDir),
DateHour = datehour(),
filelib:ensure_dir(FileName),
Handle = log_open(FileName, DateHour),
{ok, #state{filename=FileName, handle=Handle, hourstamp=DateHour}}.
refresh() ->
refresh(now()).
refresh(Time) ->
gen_server:cast(?MODULE, {refresh, Time}).
log_access(#wm_log_data{}=D) ->
gen_server:call(?MODULE, {log_access, D}).
handle_call({log_access, LogData}, _From, State) ->
NewState = maybe_rotate(State, now()),
Msg = format_req(LogData),
log_write(NewState#state.handle, Msg),
{reply, ok, NewState}.
handle_cast({refresh, Time}, State) ->
{noreply, maybe_rotate(State, Time)}.
handle_info({_Label, {From, MRef}, get_modules}, State) ->
From ! {MRef, [?MODULE]},
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
log_open(FileName, DateHour) ->
LogName = FileName ++ suffix(DateHour),
io:format("opening log file: ~p~n", [LogName]),
{ok, FD} = file:open(LogName, [read, write, raw]),
{ok, Location} = file:position(FD, eof),
fix_log(FD, Location),
file:truncate(FD),
{?MODULE, LogName, FD}.
log_write({?MODULE, _Name, FD}, IoData) ->
file:write(FD, lists:flatten(IoData)).
log_close({?MODULE, Name, FD}) ->
io:format("~p: closing log file: ~p~n", [?MODULE, Name]),
file:close(FD).
maybe_rotate(State, Time) ->
ThisHour = datehour(Time),
if ThisHour == State#state.hourstamp ->
State;
true ->
defer_refresh(),
log_close(State#state.handle),
Handle = log_open(State#state.filename, ThisHour),
State#state{hourstamp=ThisHour, handle=Handle}
end.
format_req(#wm_log_data{method=Method,
headers=Headers,
peer=Peer,
path=Path,
version=Version,
response_code=ResponseCode,
response_length=ResponseLength}) ->
User = "-",
Time = fmtnow(),
Status = integer_to_list(ResponseCode),
Length = integer_to_list(ResponseLength),
Referer =
case mochiweb_headers:get_value("Referer", Headers) of
undefined -> "";
R -> R
end,
UserAgent =
case mochiweb_headers:get_value("User-Agent", Headers) of
undefined -> "";
U -> U
end,
fmt_alog(Time, Peer, User, fmt_method(Method), Path, Version,
Status, Length, Referer, UserAgent).
fmt_method(M) when is_atom(M) -> atom_to_list(M);
fmt_method(M) when is_list(M) -> M.
%% Seek backwards to the last valid log entry
fix_log(_FD, 0) ->
ok;
fix_log(FD, 1) ->
{ok, 0} = file:position(FD, 0),
ok;
fix_log(FD, Location) ->
case file:pread(FD, Location - 1, 1) of
{ok, [$\n | _]} ->
ok;
{ok, _} ->
fix_log(FD, Location - 1)
end.
defer_refresh() ->
{_, {_, M, S}} = calendar:universal_time(),
Time = 1000 * (3600 - ((M * 60) + S)),
timer:apply_after(Time, ?MODULE, refresh, []).
datehour() ->
datehour(now()).
datehour(Now) ->
{{Y, M, D}, {H, _, _}} = calendar:now_to_universal_time(Now),
{Y, M, D, H}.
zeropad_str(NumStr, Zeros) when Zeros > 0 ->
zeropad_str([$0 | NumStr], Zeros - 1);
zeropad_str(NumStr, _) ->
NumStr.
zeropad(Num, MinLength) ->
NumStr = integer_to_list(Num),
zeropad_str(NumStr, MinLength - length(NumStr)).
suffix({Y, M, D, H}) ->
YS = zeropad(Y, 4),
MS = zeropad(M, 2),
DS = zeropad(D, 2),
HS = zeropad(H, 2),
lists:flatten([$., YS, $_, MS, $_, DS, $_, HS]).
fmt_alog(Time, Ip, User, Method, Path, {VM,Vm},
Status, Length, Referrer, UserAgent) ->
[fmt_ip(Ip), " - ", User, [$\s], Time, [$\s, $"], Method, " ", Path,
" HTTP/", integer_to_list(VM), ".", integer_to_list(Vm), [$",$\s],
Status, [$\s], Length, [$\s,$"], Referrer,
[$",$\s,$"], UserAgent, [$",$\n]].
month(1) ->
"Jan";
month(2) ->
"Feb";
month(3) ->
"Mar";
month(4) ->
"Apr";
month(5) ->
"May";
month(6) ->
"Jun";
month(7) ->
"Jul";
month(8) ->
"Aug";
month(9) ->
"Sep";
month(10) ->
"Oct";
month(11) ->
"Nov";
month(12) ->
"Dec".
zone() ->
Time = erlang:universaltime(),
LocalTime = calendar:universal_time_to_local_time(Time),
DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) - calendar:datetime_to_gregorian_seconds(Time),
zone((DiffSecs/3600)*100).
%% Ugly reformatting code to get times like +0000 and -1300
zone(Val) when Val < 0 ->
io_lib:format("-~4..0w", [trunc(abs(Val))]);
zone(Val) when Val >= 0 ->
io_lib:format("+~4..0w", [trunc(abs(Val))]).
fmt_ip(IP) when is_tuple(IP) ->
inet_parse:ntoa(IP);
fmt_ip(undefined) ->
"0.0.0.0";
fmt_ip(HostName) ->
HostName.
fmtnow() ->
{{Year, Month, Date}, {Hour, Min, Sec}} = calendar:local_time(),
io_lib:format("[~2..0w/~s/~4..0w:~2..0w:~2..0w:~2..0w ~s]",
[Date,month(Month),Year, Hour, Min, Sec, zone()]).