1%% ``Licensed under the Apache License, Version 2.0 (the "License");
2%% you may not use this file except in compliance with the License.
3%% You may obtain a copy of the License at
4%%
5%%     http://www.apache.org/licenses/LICENSE-2.0
6%%
7%% Unless required by applicable law or agreed to in writing, software
8%% distributed under the License is distributed on an "AS IS" BASIS,
9%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10%% See the License for the specific language governing permissions and
11%% limitations under the License.
12%%
13%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
14%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
15%% AB. All Rights Reserved.''
16%%
17%%     $Id: mod_disk_log.erl,v 1.1 2008/12/17 09:53:35 mikpe Exp $
18%%
19-module(mod_disk_log).
20-export([do/1,error_log/5,security_log/2,load/2,store/2,remove/1]).
21
22-export([report_error/2]).
23
24-define(VMODULE,"DISK_LOG").
25-include("httpd_verbosity.hrl").
26
27-include("httpd.hrl").
28
29%% do
30
31do(Info) ->
32    AuthUser  = auth_user(Info#mod.data),
33    Date      = custom_date(),
34    log_internal_info(Info,Date,Info#mod.data),
35    LogFormat = get_log_format(Info#mod.config_db),
36    case httpd_util:key1search(Info#mod.data,status) of
37	%% A status code has been generated!
38	{StatusCode,PhraseArgs,Reason} ->
39	    transfer_log(Info, "-", AuthUser, Date, StatusCode, 0, LogFormat),
40	    if
41		StatusCode >= 400 ->
42		    error_log(Info, Date, Reason, LogFormat);
43		true ->
44		    not_an_error
45	    end,
46	    {proceed,Info#mod.data};
47	%% No status code has been generated!
48	undefined ->
49	    case httpd_util:key1search(Info#mod.data,response) of
50		{already_sent,StatusCode,Size} ->
51		    transfer_log(Info, "-", AuthUser, Date, StatusCode,
52				 Size, LogFormat),
53		    {proceed,Info#mod.data};
54
55		{response, Head, Body} ->
56		    Size = httpd_util:key1search(Head, content_length, 0),
57		    Code = httpd_util:key1search(Head, code, 200),
58		    transfer_log(Info, "-", AuthUser, Date, Code,
59				 Size, LogFormat),
60		    {proceed,Info#mod.data};
61
62		{StatusCode,Response} ->
63		    transfer_log(Info, "-", AuthUser, Date, 200,
64				 httpd_util:flatlength(Response), LogFormat),
65		    {proceed,Info#mod.data};
66		undefined ->
67		    transfer_log(Info, "-", AuthUser, Date, 200,
68				 0, LogFormat),
69		    {proceed,Info#mod.data}
70	    end
71    end.
72
73custom_date() ->
74    LocalTime     = calendar:local_time(),
75    UniversalTime = calendar:universal_time(),
76    Minutes       = round(diff_in_minutes(LocalTime,UniversalTime)),
77    {{YYYY,MM,DD},{Hour,Min,Sec}} = LocalTime,
78    Date =
79	io_lib:format("~.2.0w/~.3s/~.4w:~.2.0w:~.2.0w:~.2.0w ~c~.2.0w~.2.0w",
80		      [DD,httpd_util:month(MM),YYYY,Hour,Min,Sec,sign(Minutes),
81		       abs(Minutes) div 60,abs(Minutes) rem 60]),
82    lists:flatten(Date).
83
84diff_in_minutes(L,U) ->
85    (calendar:datetime_to_gregorian_seconds(L) -
86     calendar:datetime_to_gregorian_seconds(U))/60.
87
88sign(Minutes) when Minutes > 0 ->
89    $+;
90sign(Minutes) ->
91    $-.
92
93auth_user(Data) ->
94    case httpd_util:key1search(Data,remote_user) of
95	undefined ->
96	    "-";
97	RemoteUser ->
98	    RemoteUser
99    end.
100
101%% log_internal_info
102
103log_internal_info(Info,Date,[]) ->
104    ok;
105log_internal_info(Info,Date,[{internal_info,Reason}|Rest]) ->
106    Format = get_log_format(Info#mod.config_db),
107    error_log(Info,Date,Reason,Format),
108    log_internal_info(Info,Date,Rest);
109log_internal_info(Info,Date,[_|Rest]) ->
110    log_internal_info(Info,Date,Rest).
111
112
113%% transfer_log
114
115transfer_log(Info,RFC931,AuthUser,Date,StatusCode,Bytes,Format) ->
116    case httpd_util:lookup(Info#mod.config_db,transfer_disk_log) of
117	undefined ->
118	    no_transfer_log;
119	TransferDiskLog ->
120	    {PortNumber,RemoteHost}=(Info#mod.init_data)#init_data.peername,
121	    Entry = io_lib:format("~s ~s ~s [~s] \"~s\" ~w ~w~n",
122				  [RemoteHost,RFC931,AuthUser,Date,
123				   Info#mod.request_line,StatusCode,Bytes]),
124	    write(TransferDiskLog, Entry, Format)
125    end.
126
127
128%% error_log
129
130error_log(Info, Date, Reason, Format) ->
131    Format=get_log_format(Info#mod.config_db),
132    case httpd_util:lookup(Info#mod.config_db,error_disk_log) of
133	undefined ->
134	    no_error_log;
135	ErrorDiskLog ->
136	    {PortNumber,RemoteHost}=(Info#mod.init_data)#init_data.peername,
137	    Entry =
138		io_lib:format("[~s] access to ~s failed for ~s, reason: ~p~n",
139			      [Date, Info#mod.request_uri,
140			       RemoteHost, Reason]),
141	    write(ErrorDiskLog, Entry, Format)
142    end.
143
144error_log(SocketType, Socket, ConfigDB, {PortNumber, RemoteHost}, Reason) ->
145    Format = get_log_format(ConfigDB),
146    case httpd_util:lookup(ConfigDB,error_disk_log) of
147	undefined ->
148	    no_error_log;
149	ErrorDiskLog ->
150	    Date  = custom_date(),
151	    Entry =
152		io_lib:format("[~s] server crash for ~s, reason: ~p~n",
153			      [Date,RemoteHost,Reason]),
154	    write(ErrorDiskLog, Entry, Format),
155	    ok
156    end.
157
158
159%% security_log
160
161security_log(ConfigDB, Event) ->
162    Format = get_log_format(ConfigDB),
163    case httpd_util:lookup(ConfigDB,security_disk_log) of
164	undefined ->
165	    no_error_log;
166	DiskLog ->
167	    Date  = custom_date(),
168	    Entry = io_lib:format("[~s] ~s ~n", [Date, Event]),
169	    write(DiskLog, Entry, Format),
170	    ok
171    end.
172
173report_error(ConfigDB, Error) ->
174    Format = get_log_format(ConfigDB),
175    case httpd_util:lookup(ConfigDB, error_disk_log) of
176	undefined ->
177	    no_error_log;
178	ErrorDiskLog ->
179	    Date  = custom_date(),
180	    Entry = io_lib:format("[~s] reporting error: ~s",[Date,Error]),
181	    write(ErrorDiskLog, Entry, Format),
182	    ok
183    end.
184
185%%----------------------------------------------------------------------
186%% Get the current format of the disklog
187%%----------------------------------------------------------------------
188get_log_format(ConfigDB)->
189    httpd_util:lookup(ConfigDB,disk_log_format,external).
190
191
192%%
193%% Configuration
194%%
195
196%% load
197
198load([$T,$r,$a,$n,$s,$f,$e,$r,$D,$i,$s,$k,$L,$o,$g,$S,$i,$z,$e,$ |
199      TransferDiskLogSize],[]) ->
200    case regexp:split(TransferDiskLogSize," ") of
201	{ok,[MaxBytes,MaxFiles]} ->
202	    case httpd_conf:make_integer(MaxBytes) of
203		{ok,MaxBytesInteger} ->
204		    case httpd_conf:make_integer(MaxFiles) of
205			{ok,MaxFilesInteger} ->
206			    {ok,[],{transfer_disk_log_size,
207				    {MaxBytesInteger,MaxFilesInteger}}};
208			{error,_} ->
209			    {error,
210			     ?NICE(httpd_conf:clean(TransferDiskLogSize)++
211				   " is an invalid TransferDiskLogSize")}
212		    end;
213		{error,_} ->
214		    {error,?NICE(httpd_conf:clean(TransferDiskLogSize)++
215				 " is an invalid TransferDiskLogSize")}
216	    end
217    end;
218load([$T,$r,$a,$n,$s,$f,$e,$r,$D,$i,$s,$k,$L,$o,$g,$ |TransferDiskLog],[]) ->
219    {ok,[],{transfer_disk_log,httpd_conf:clean(TransferDiskLog)}};
220
221load([$E,$r,$r,$o,$r,$D,$i,$s,$k,$L,$o,$g,$S,$i,$z,$e,$ | ErrorDiskLogSize],[]) ->
222    case regexp:split(ErrorDiskLogSize," ") of
223	{ok,[MaxBytes,MaxFiles]} ->
224	    case httpd_conf:make_integer(MaxBytes) of
225		{ok,MaxBytesInteger} ->
226		    case httpd_conf:make_integer(MaxFiles) of
227			{ok,MaxFilesInteger} ->
228			    {ok,[],{error_disk_log_size,
229				    {MaxBytesInteger,MaxFilesInteger}}};
230			{error,_} ->
231			    {error,?NICE(httpd_conf:clean(ErrorDiskLogSize)++
232					 " is an invalid ErrorDiskLogSize")}
233		    end;
234		{error,_} ->
235		    {error,?NICE(httpd_conf:clean(ErrorDiskLogSize)++
236				 " is an invalid ErrorDiskLogSize")}
237	    end
238    end;
239load([$E,$r,$r,$o,$r,$D,$i,$s,$k,$L,$o,$g,$ |ErrorDiskLog],[]) ->
240    {ok, [], {error_disk_log, httpd_conf:clean(ErrorDiskLog)}};
241
242load([$S,$e,$c,$u,$r,$i,$t,$y,$D,$i,$s,$k,$L,$o,$g,$S,$i,$z,$e,$ |SecurityDiskLogSize],[]) ->
243    case regexp:split(SecurityDiskLogSize, " ") of
244	{ok, [MaxBytes, MaxFiles]} ->
245	    case httpd_conf:make_integer(MaxBytes) of
246		{ok, MaxBytesInteger} ->
247		    case httpd_conf:make_integer(MaxFiles) of
248			{ok, MaxFilesInteger} ->
249			    {ok, [], {security_disk_log_size,
250				      {MaxBytesInteger, MaxFilesInteger}}};
251			{error,_} ->
252			    {error, ?NICE(httpd_conf:clean(SecurityDiskLogSize)++
253					  " is an invalid SecurityDiskLogSize")}
254		    end;
255		{error, _} ->
256		    {error, ?NICE(httpd_conf:clean(SecurityDiskLogSize)++
257				  " is an invalid SecurityDiskLogSize")}
258	    end
259    end;
260load([$S,$e,$c,$u,$r,$i,$t,$y,$D,$i,$s,$k,$L,$o,$g,$ |SecurityDiskLog],[]) ->
261    {ok, [], {security_disk_log, httpd_conf:clean(SecurityDiskLog)}};
262
263load([$D,$i,$s,$k,$L,$o,$g,$F,$o,$r,$m,$a,$t,$ |Format],[]) ->
264    case httpd_conf:clean(Format) of
265	"internal" ->
266	    {ok, [], {disk_log_format,internal}};
267	"external" ->
268	    {ok, [], {disk_log_format,external}};
269	_Default ->
270	    {ok, [], {disk_log_format,external}}
271    end.
272
273%% store
274
275store({transfer_disk_log,TransferDiskLog},ConfigList) ->
276    case create_disk_log(TransferDiskLog, transfer_disk_log_size, ConfigList) of
277	{ok,TransferDB} ->
278	    {ok,{transfer_disk_log,TransferDB}};
279	{error,Reason} ->
280	    {error,Reason}
281    end;
282store({security_disk_log,SecurityDiskLog},ConfigList) ->
283    case create_disk_log(SecurityDiskLog, security_disk_log_size, ConfigList) of
284	{ok,SecurityDB} ->
285	    {ok,{security_disk_log,SecurityDB}};
286	{error,Reason} ->
287	    {error,Reason}
288    end;
289store({error_disk_log,ErrorDiskLog},ConfigList) ->
290    case create_disk_log(ErrorDiskLog, error_disk_log_size, ConfigList) of
291	{ok,ErrorDB} ->
292	    {ok,{error_disk_log,ErrorDB}};
293	{error,Reason} ->
294	    {error,Reason}
295    end.
296
297
298%%----------------------------------------------------------------------
299%% Open or creates the disklogs
300%%----------------------------------------------------------------------
301log_size(ConfigList, Tag) ->
302    httpd_util:key1search(ConfigList, Tag, {500*1024,8}).
303
304create_disk_log(LogFile, SizeTag, ConfigList) ->
305    Filename = httpd_conf:clean(LogFile),
306    {MaxBytes, MaxFiles} = log_size(ConfigList, SizeTag),
307    case filename:pathtype(Filename) of
308	absolute ->
309	    create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList);
310	volumerelative ->
311	    create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList);
312	relative ->
313	    case httpd_util:key1search(ConfigList,server_root) of
314		undefined ->
315		    {error,
316		     ?NICE(Filename++
317			   " is an invalid ErrorLog beacuse ServerRoot is not defined")};
318		ServerRoot ->
319		    AbsoluteFilename = filename:join(ServerRoot,Filename),
320		    create_disk_log(AbsoluteFilename, MaxBytes, MaxFiles,
321				     ConfigList)
322	    end
323    end.
324
325create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList) ->
326    Format = httpd_util:key1search(ConfigList, disk_log_format, external),
327    open(Filename, MaxBytes, MaxFiles, Format).
328
329
330
331%% remove
332remove(ConfigDB) ->
333    lists:foreach(fun([DiskLog]) -> close(DiskLog) end,
334		  ets:match(ConfigDB,{transfer_disk_log,'$1'})),
335    lists:foreach(fun([DiskLog]) -> close(DiskLog) end,
336		  ets:match(ConfigDB,{error_disk_log,'$1'})),
337    ok.
338
339
340%%
341%% Some disk_log wrapper functions:
342%%
343
344%%----------------------------------------------------------------------
345%% Function:    open/4
346%% Description: Open a disk log file.
347%% Control which format the disk log will be in. The external file
348%% format is used as default since that format was used by older
349%% implementations of inets.
350%%
351%% When the internal disk log format is used, we will do some extra
352%% controls. If the files are valid, try to repair them and if
353%% thats not possible, truncate.
354%%----------------------------------------------------------------------
355
356open(Filename, MaxBytes, MaxFiles, internal) ->
357    Opts = [{format, internal}, {repair, truncate}],
358    open1(Filename, MaxBytes, MaxFiles, Opts);
359open(Filename, MaxBytes, MaxFiles, _) ->
360    Opts = [{format, external}],
361    open1(Filename, MaxBytes, MaxFiles, Opts).
362
363open1(Filename, MaxBytes, MaxFiles, Opts0) ->
364    Opts1 = [{name, Filename}, {file, Filename}, {type, wrap}] ++ Opts0,
365    case open2(Opts1, {MaxBytes, MaxFiles}) of
366        {ok, LogDB} ->
367            {ok, LogDB};
368        {error, Reason} ->
369            ?vlog("failed opening disk log with args:"
370                  "~n   Filename: ~p"
371                  "~n   MaxBytes: ~p"
372                  "~n   MaxFiles: ~p"
373                  "~n   Opts0:    ~p"
374                  "~nfor reason:"
375                  "~n   ~p", [Filename, MaxBytes, MaxFiles, Opts0, Reason]),
376            {error,
377             ?NICE("Can't create " ++ Filename ++
378                   lists:flatten(io_lib:format(", ~p",[Reason])))};
379        _ ->
380            {error, ?NICE("Can't create "++Filename)}
381    end.
382
383open2(Opts, Size) ->
384    case disk_log:open(Opts) of
385        {error, {badarg, size}} ->
386            %% File did not exist, add the size option and try again
387            disk_log:open([{size, Size} | Opts]);
388        Else ->
389            Else
390    end.
391
392
393%%----------------------------------------------------------------------
394%% Actually writes the entry to the disk_log. If the log is an
395%% internal disk_log write it with log otherwise with blog.
396%%----------------------------------------------------------------------
397write(Log, Entry, internal) ->
398    disk_log:log(Log, Entry);
399
400write(Log, Entry, _) ->
401    disk_log:blog(Log, Entry).
402
403%% Close the log file
404close(Log) ->
405    disk_log:close(Log).
406