%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2006-2018. All Rights Reserved. %% %% 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. %% %% %CopyrightEnd% %% %%% Logging functionality for Common Test Master. %%% %%% This module implements a logger for the master %%% node. -module(ct_master_logs). -export([start/2, make_all_runs_index/0, log/3, nodedir/2, stop/0]). -include("ct_util.hrl"). -record(state, {log_fd, start_time, logdir, rundir, nodedir_ix_fd, nodes, nodedirs=[]}). -define(ct_master_log_name, "ct_master_log.html"). -define(all_runs_name, "master_runs.html"). -define(nodedir_index_name, "index.html"). -define(details_file_name,"details.info"). -define(table_color,"lightblue"). -define(now, os:timestamp()). %%%-------------------------------------------------------------------- %%% API %%%-------------------------------------------------------------------- start(LogDir,Nodes) -> Self = self(), Pid = spawn_link(fun() -> init(Self,LogDir,Nodes) end), MRef = erlang:monitor(process,Pid), receive {started,Pid,Result} -> erlang:demonitor(MRef, [flush]), {Pid,Result}; {'DOWN',MRef,process,_,Reason} -> exit({could_not_start_process,?MODULE,Reason}) end. log(Heading,Format,Args) -> cast({log,self(),[{int_header(),[log_timestamp(?now),Heading]}, {Format,Args}, {int_footer(),[]}]}), ok. make_all_runs_index() -> call(make_all_runs_index). nodedir(Node,RunDir) -> call({nodedir,Node,RunDir}). stop() -> case whereis(?MODULE) of Pid when is_pid(Pid) -> MRef = erlang:monitor(process,Pid), ?MODULE ! stop, receive {'DOWN',MRef,process,_,_} -> ok end; undefined -> ok end, ok. %%%-------------------------------------------------------------------- %%% Logger process %%%-------------------------------------------------------------------- init(Parent,LogDir,Nodes) -> register(?MODULE,self()), ct_util:mark_process(), Time = calendar:local_time(), RunDir = make_dirname(Time), RunDirAbs = filename:join(LogDir,RunDir), ok = make_dir(RunDirAbs), _ = write_details_file(RunDirAbs,{node(),Nodes}), case basic_html() of true -> put(basic_html, true); BasicHtml -> put(basic_html, BasicHtml), %% copy priv files to log dir (both top dir and test run %% dir) so logs are independent of Common Test installation CTPath = code:lib_dir(common_test), PrivFiles = [?css_default,?jquery_script,?tablesorter_script], PrivFilesSrc = [filename:join(filename:join(CTPath, "priv"), F) || F <- PrivFiles], PrivFilesDestTop = [filename:join(LogDir, F) || F <- PrivFiles], PrivFilesDestRun = [filename:join(RunDirAbs, F) || F <- PrivFiles], case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of {error,Src1,Dest1,Reason1} -> io:format(user, "ERROR! "++ "Priv file ~tp could not be copied to ~tp. "++ "Reason: ~tp~n", [Src1,Dest1,Reason1]), exit({priv_file_error,Dest1}); ok -> case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of {error,Src2,Dest2,Reason2} -> io:format(user, "ERROR! "++ "Priv file ~tp could not be copied to ~tp. "++ "Reason: ~tp~n", [Src2,Dest2,Reason2]), exit({priv_file_error,Dest2}); ok -> ok end end end, _ = make_all_runs_index(LogDir), CtLogFd = open_ct_master_log(RunDirAbs), NodeStr = lists:flatten(lists:map(fun(N) -> atom_to_list(N) ++ " " end,Nodes)), io:format(CtLogFd,int_header(),[log_timestamp(?now),"Test Nodes\n"]), io:format(CtLogFd,"~ts\n",[NodeStr]), io:put_chars(CtLogFd,[int_footer(),"\n"]), NodeDirIxFd = open_nodedir_index(RunDirAbs,Time), Parent ! {started,self(),{Time,RunDirAbs}}, loop(#state{log_fd=CtLogFd, start_time=Time, logdir=LogDir, rundir=RunDirAbs, nodedir_ix_fd=NodeDirIxFd, nodes=Nodes, nodedirs=lists:map(fun(N) -> {N,""} end,Nodes)}). copy_priv_files([SrcF | SrcFs], [DestF | DestFs]) -> case file:copy(SrcF, DestF) of {error,Reason} -> {error,SrcF,DestF,Reason}; _ -> copy_priv_files(SrcFs, DestFs) end; copy_priv_files([], []) -> ok. loop(State) -> receive {log,_From,List} -> Fd = State#state.log_fd, Fun = fun({Str,Args}) -> case catch io:format(Fd,Str++"\n",Args) of {'EXIT',Reason} -> io:format(Fd, "Logging fails! Str: ~tp, Args: ~tp~n", [Str,Args]), exit({logging_failed,Reason}), ok; _ -> ok end end, lists:foreach(Fun,List), loop(State); {make_all_runs_index,From} -> _ = make_all_runs_index(State#state.logdir), return(From,State#state.logdir), loop(State); {{nodedir,Node,RunDir},From} -> print_nodedir(Node,RunDir,State#state.nodedir_ix_fd), return(From,ok), loop(State); stop -> _ = make_all_runs_index(State#state.logdir), io:format(State#state.log_fd, int_header()++int_footer(), [log_timestamp(?now),"Finished!"]), _ = close_ct_master_log(State#state.log_fd), _ = close_nodedir_index(State#state.nodedir_ix_fd), ok end. %%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Master Log functions %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% open_ct_master_log(Dir) -> FullName = filename:join(Dir,?ct_master_log_name), {ok,Fd} = file:open(FullName,[write,{encoding,utf8}]), io:put_chars(Fd,header("Common Test Master Log", {[],[1,2],[]})), %% maybe add config info here later io:put_chars(Fd,config_table([])), io:put_chars(Fd, "\n"), io:put_chars(Fd, xhtml("

Progress Log

\n
\n",
		       "

Progress Log

\n
\n")),
    Fd.

close_ct_master_log(Fd) ->
    io:put_chars(Fd,["
",footer()]), file:close(Fd). config_table(Vars) -> [config_table_header()|config_table1(Vars)]. config_table_header() -> ["

Configuration

\n", xhtml(["\n", "\n"]), "\n", xhtml("", "\n\n")]. config_table1([]) -> ["\n
KeyValue
\n"]. int_header() -> "
\n
*** CT MASTER ~s *** ~ts".
int_footer() ->
    "
\n
".

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% NodeDir Index functions %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
open_nodedir_index(Dir,StartTime) ->
    FullName = filename:join(Dir,?nodedir_index_name),
    {ok,Fd} = file:open(FullName,[write,{encoding,utf8}]),
    io:put_chars(Fd,nodedir_index_header(StartTime)),
    Fd.

print_nodedir(Node,RunDir,Fd) ->
    Index = filename:join(RunDir,"index.html"),
    io:put_chars(Fd,
		 ["\n"
		  "",atom_to_list(Node),"\n",
		  "",Index,
		  "\n",
		  "\n"]),
    ok.

close_nodedir_index(Fd) ->
    io:put_chars(Fd,index_footer()),
    file:close(Fd).

nodedir_index_header(StartTime) ->
    [header("Log Files " ++ format_time(StartTime), {[],[1,2],[]}) |
     ["
\n", "

Common Test Master Log

", xhtml(["\n"], ["
\n", "\n\n"]), "\n", "\n", xhtml("", "\n\n\n")]]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% All Run Index functions %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% make_all_runs_index(LogDir) -> FullName = filename:join(LogDir,?all_runs_name), Match = filename:join(LogDir,logdir_prefix()++"*.*"), Dirs = filelib:wildcard(Match), DirsSorted = (catch sort_all_runs(Dirs)), Header = all_runs_header(), Index = [runentry(Dir) || Dir <- DirsSorted], Result = file:write_file(FullName, unicode:characters_to_binary( Header++Index++index_footer())), Result. sort_all_runs(Dirs) -> %% sort on time string, always last and on the format: %% "YYYY-MM-DD_HH.MM.SS" KeyList = lists:map(fun(Dir) -> case lists:reverse(string:lexemes(Dir,[$.,$_])) of [SS,MM,HH,Date|_] -> {{Date,HH,MM,SS},Dir}; _Other -> throw(Dirs) end end,Dirs), lists:reverse(lists:map(fun({_,Dir}) -> Dir end,lists:keysort(1,KeyList))). runentry(Dir) -> {MasterStr,NodesStr} = case read_details_file(Dir) of {Master,Nodes} when is_list(Nodes) -> [_,Host] = string:lexemes(atom_to_list(Master),"@"), {Host,lists:concat(lists:join(", ",Nodes))}; _Error -> {"unknown",""} end, Index = filename:join(Dir,?nodedir_index_name), ["\n" "\n", "\n", "\n", "\n"]. all_runs_header() -> [header("Master Test Runs", {[1],[2,3],[]}) | ["
\n", xhtml(["
NodeLog
", timestamp(Dir),"",MasterStr,"",NodesStr,"
\n"], ["
\n", "\n\n"]), "\n" "\n" "\n", xhtml("", "\n\n")]]. timestamp(Dir) -> [S,Min,H,D,M,Y|_] = lists:reverse(string:lexemes(Dir,".-_")), [S1,Min1,H1,D1,M1,Y1] = [list_to_integer(N) || N <- [S,Min,H,D,M,Y]], format_time({{Y1,M1,D1},{H1,Min1,S1}}). write_details_file(Dir,Details) -> FullName = filename:join(Dir,?details_file_name), force_write_file(FullName,term_to_binary(Details)). read_details_file(Dir) -> FullName = filename:join(Dir,?details_file_name), case file:read_file(FullName) of {ok,Bin} -> binary_to_term(Bin); Error -> Error end. %%%-------------------------------------------------------------------- %%% Internal functions %%%-------------------------------------------------------------------- header(Title, TableCols) -> CSSFile = xhtml(fun() -> "" end, fun() -> make_relative(locate_priv_file(?css_default)) end), JQueryFile = xhtml(fun() -> "" end, fun() -> make_relative(locate_priv_file(?jquery_script)) end), TableSorterFile = xhtml(fun() -> "" end, fun() -> make_relative(locate_priv_file(?tablesorter_script)) end), [xhtml(["\n", "\n"], ["\n", "\n"]), "\n", "\n", "" ++ Title ++ "\n", "\n", "\n", xhtml("", ["\n"]), xhtml("", ["\n"]), xhtml("", ["\n"]), xhtml(fun() -> "" end, fun() -> ct_logs:insert_javascript({tablesorter, ?sortable_table_name, TableCols}) end), "\n", body_tag(), "
\n", "

" ++ Title ++ "

\n", "
\n"]. index_footer() -> ["\n
HistoryMaster HostTest Nodes
\n" "
\n" | footer()]. footer() -> ["
\n", xhtml("

\n", "
\n"), xhtml("

\n", "

"), "Copyright © ", year(), " Open Telecom Platform", xhtml("
\n", "
\n"), "Updated: ", current_time(), "<--!/date-->", xhtml("
\n", "
\n"), xhtml("

\n", "
\n"), "
\n" "\n"]. body_tag() -> xhtml("\n", "\n"). current_time() -> format_time(calendar:local_time()). format_time({{Y, Mon, D}, {H, Min, S}}) -> Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)), lists:flatten(io_lib:format("~s ~s ~2.2.0w ~w ~2.2.0w:~2.2.0w:~2.2.0w", [Weekday, month(Mon), D, Y, H, Min, S])). weekday(1) -> "Mon"; weekday(2) -> "Tue"; weekday(3) -> "Wed"; weekday(4) -> "Thu"; weekday(5) -> "Fri"; weekday(6) -> "Sat"; weekday(7) -> "Sun". 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". year() -> {Y, _, _} = date(), integer_to_list(Y). make_dirname({{YY,MM,DD},{H,M,S}}) -> io_lib:format(logdir_prefix()++".~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w", [YY,MM,DD,H,M,S]). logdir_prefix() -> "ct_master_run". log_timestamp(Now) -> put(log_timestamp,Now), {_,{H,M,S}} = calendar:now_to_local_time(Now), lists:flatten(io_lib:format("~2.2.0w:~2.2.0w:~2.2.0w", [H,M,S])). basic_html() -> case application:get_env(common_test_master, basic_html) of {ok,true} -> true; _ -> false end. xhtml(HTML, XHTML) -> ct_logs:xhtml(HTML, XHTML). locate_priv_file(File) -> ct_logs:locate_priv_file(File). make_relative(Dir) -> ct_logs:make_relative(Dir). force_write_file(Name,Contents) -> _ = force_delete(Name), file:write_file(Name,Contents). force_delete(Name) -> case file:delete(Name) of {error,eacces} -> force_rename(Name,Name++".old.",0); Other -> Other end. force_rename(From,To,Number) -> Dest = [To|integer_to_list(Number)], case file:read_file_info(Dest) of {ok,_} -> force_rename(From,To,Number+1); {error,_} -> file:rename(From,Dest) end. call(Msg) -> case whereis(?MODULE) of undefined -> {error,does_not_exist}; Pid -> MRef = erlang:monitor(process,Pid), Ref = make_ref(), ?MODULE ! {Msg,{self(),Ref}}, receive {Ref, Result} -> erlang:demonitor(MRef, [flush]), Result; {'DOWN',MRef,process,_,Reason} -> {error,{process_down,?MODULE,Reason}} end end. return({To,Ref},Result) -> To ! {Ref, Result}, ok. cast(Msg) -> case whereis(?MODULE) of undefined -> io:format("Warning: ct_master_logs not started~n"), {_,_,Content} = Msg, FormatArgs = get_format_args(Content), _ = [io:format(Format, Args) || {Format, Args} <- FormatArgs], ok; _Pid -> ?MODULE ! Msg, ok end. get_format_args(Content) -> lists:map(fun(C) -> case C of {_, FA, _} -> FA; _ -> C end end, Content). make_dir(Dir) -> case file:make_dir(Dir) of {error, eexist} -> ok; Else -> Else end.