#!/usr/bin/env swipl

% You can make this program executable by  removing the % from the first
% line and update the path the SWI-Prolog interpreter

% Convert Prolog logs to apache's Combined Log Format
% http://httpd.apache.org/docs/1.3/logs.html#combined

:- use_module(library(http/logstat)).
:- use_module(library(http/http_header)).
:- use_module(library(main)).

:- dynamic
	outfile/1,
	logfile/1.

:- initialization main.

main([]) :- !,
	usage.
main(Argv) :-
	retractall(outfile(_)),
	retractall(logfile(_)),
	(   process_argv(Argv, Options)
	->  true
	;   usage
	),
	(   outfile(Out)
	->  write_clf(Out, Options)
	;   write_records(current_output, Options)
	).

usage :-
	format(user_error,
	       'Usage: log2clf [-o out] [-a date] [-b date] file ...~n', []),
	halt(1).

process_argv([], []).
process_argv(['-o', OutFile|T], Options) :-
	assert(outfile(OutFile)), !,
        process_argv(T, Options).
process_argv(['-a', After|T0], [after(Date)|T]) :- !,
	text_to_data(After, Date),
	process_argv(T0, T).
process_argv(['-b', Before|T0], [before(Date)|T]) :- !,
	text_to_data(Before, Date),
	process_argv(T0, T).
process_argv([F|T], Options) :-
	assert(logfile(F)),
	read_log(F),!,
        process_argv(T, Options).

text_to_data(Text, Y/M/D) :-
	atomic_list_concat([YA, MA, DA], /, Text),
	atom_number(YA, Y),
	atom_number(MA, M),
	atom_number(DA, D).


writerecord(Out, LogRecord) :-
	memberchk(ip(IP), LogRecord),
	memberchk(time(Time), LogRecord),
	memberchk(path(Path), LogRecord),
	memberchk(referer(Referer), LogRecord),
	memberchk(result(Result), LogRecord),
	memberchk(extra(Extra), LogRecord),
	(memberchk(user_agent(Agent), Extra) -> true; Agent='-'),
	memberchk(http_version(Http1-Http2), Extra),
	format_time(atom(TimeStamp), "[%d/%b/%Y:%H:%M:%S %z]",Time),
	result_code(LogRecord, Extra, Result, ResultCode),
	(memberchk(bytes(Bytes), Extra) -> true; Bytes='-'),
	format(Out, '~w - - ~w "GET ~w HTTP/~w.~w" ~w ~w "~w" "~w"~n',
	       [IP, TimeStamp, Path, Http1, Http2, ResultCode, Bytes, Referer, Agent]),
	!.

writerecord(_, LogRecord) :-
	format(user_error, "Could not convert log record ~w~n", [LogRecord]).

write_clf(Out, Options):-
	open(Out, write, OutS, [encoding(utf8)]),
	call_cleanup(write_records(OutS, Options), close(OutS)).

%%	write_records(+Stream) is det.
%
%	Write the records. If we have multiple files, we must sort them.
%	Possibly this should be an option. It  was designed to deal with
%	multiple files from a load-balancer, but it could of course also
%	be used with multiple files from a single server.

write_records(OutS, Options) :-
	Queries = [ ip(_),
		    path(_),
		    referer(_),
		    result(_),
		    code(_),
		    extra(_)
		  | Options
		  ],
	findall(F, logfile(F), Fs),
	length(Fs, NFiles),
	(   NFiles =< 1
	->  Params = [time(_Time)|Queries],
	    forall(logrecord(Params),
		   writerecord(OutS,Params))
	;   findall(Time-logrecord([time(Time)|Queries]),
		    logrecord([time(Time)|Queries]),
		    Logrecords),
	    keysort(Logrecords, Sorted),
	    forall(member(_-logrecord(Params),Sorted),
		   writerecord(OutS,Params))
	).


result_code(Record, _, _, Code) :-
	memberchk(code(Code), Record), !.
result_code(_, Extra, _, Code) :-
	memberchk(code(Code), Extra), !.
result_code(_, _, Status, Code) :-
	status_to_code(Status, Code).

status_to_code(ok,200)			:- !.
status_to_code(true,200)		:- !.
status_to_code(file(_,_), 200)		:- !.
status_to_code(tmp_file(_,_), 200)	:- !.
status_to_code(moved(_), 301)		:- !.
status_to_code(not_modified, 304)	:- !.
status_to_code(moved_temporary(_), 307)	:- !.
status_to_code(error(404,_), 404)	:- !.
status_to_code(forbidden(_), 403)	:- !.
status_to_code(error(_R), 400)		:- !.
status_to_code(unavailable(_), 503)	:- !.
status_to_code(no_reply, 500)		:- !.
status_to_code(busy, 503)		:- !.
status_to_code(R,-)			:-
	format('Error: No map for result ~w~n', [R]),!.

