From b6b766ddfbff8db789723689419ee6f634e752b8 Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Tue, 19 Jul 2016 19:14:33 -0500 Subject: [PATCH 1/3] Make couch_log smarter This drops the lager and goldrush dependencies and instead moves all logic to couch_log so that we can make our logging work more closely with the existing configuration system. COUCHDB-3067 --- rebar.config => include/couch_log.hrl | 10 +- src/couch_log.app.src | 7 - src/couch_log.erl | 163 +---- src/couch_log_config.erl | 100 +++ src/couch_log_config_dyn.erl | 28 + src/couch_log_config_listener.erl | 74 ++- src/couch_log_error_logger_h.erl | 57 ++ src/couch_log_formatter.erl | 417 +++++++++++++ src/couch_log_monitor.erl | 66 ++ src/couch_log_server.erl | 106 ++++ src/couch_log_stderr.erl | 57 -- src/couch_log_sup.erl | 26 +- src/couch_log_trunc_io.erl | 838 ++++++++++++++++++++++++++ src/couch_log_trunc_io_fmt.erl | 547 +++++++++++++++++ src/couch_log_util.erl | 149 +++++ src/couch_log_writer.erl | 83 +++ src/couch_log_writer_file.erl | 140 +++++ src/couch_log_writer_stderr.erl | 54 ++ 18 files changed, 2699 insertions(+), 223 deletions(-) rename rebar.config => include/couch_log.hrl (82%) create mode 100644 src/couch_log_config.erl create mode 100644 src/couch_log_config_dyn.erl create mode 100644 src/couch_log_error_logger_h.erl create mode 100644 src/couch_log_formatter.erl create mode 100644 src/couch_log_monitor.erl create mode 100644 src/couch_log_server.erl delete mode 100644 src/couch_log_stderr.erl create mode 100644 src/couch_log_trunc_io.erl create mode 100644 src/couch_log_trunc_io_fmt.erl create mode 100644 src/couch_log_util.erl create mode 100644 src/couch_log_writer.erl create mode 100644 src/couch_log_writer_file.erl create mode 100644 src/couch_log_writer_stderr.erl diff --git a/rebar.config b/include/couch_log.hrl similarity index 82% rename from rebar.config rename to include/couch_log.hrl index 7104d3b..a472c0c 100644 --- a/rebar.config +++ b/include/couch_log.hrl @@ -10,6 +10,10 @@ % License for the specific language governing permissions and limitations under % the License. -{deps, [ - {meck, ".*", {git, "https://git-wip-us.apache.org/repos/asf/couchdb-meck.git", {tag, "0.8.2"}}} -]}. +-record(log_entry, { + level, + pid, + msg, + msg_id, + time_stamp +}). diff --git a/src/couch_log.app.src b/src/couch_log.app.src index efcebec..50adfe6 100644 --- a/src/couch_log.app.src +++ b/src/couch_log.app.src @@ -13,13 +13,6 @@ {application, couch_log, [ {description, "CouchDB Log API"}, {vsn, git}, - {modules, [ - couch_log, - couch_log_app, - couch_log_config_listener, - couch_log_stderr, - couch_log_sup - ]}, {registered, [couch_log_sup]}, {applications, [kernel, stdlib, config]}, {mod, {couch_log_app, []}} diff --git a/src/couch_log.erl b/src/couch_log.erl index 678559f..0ce4739 100644 --- a/src/couch_log.erl +++ b/src/couch_log.erl @@ -12,163 +12,64 @@ -module(couch_log). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - --export([debug/2, info/2, notice/2, warning/2, error/2, critical/2, alert/2, - emergency/2]). --export([set_level/1]). - --callback debug(Fmt::string(), Args::list()) -> ok. --callback info(Fmt::string(), Args::list()) -> ok. --callback notice(Fmt::string(), Args::list()) -> ok. --callback warning(Fmt::string(), Args::list()) -> ok. --callback error(Fmt::string(), Args::list()) -> ok. --callback critical(Fmt::string(), Args::list()) -> ok. --callback alert(Fmt::string(), Args::list()) -> ok. --callback emergency(Fmt::string(), Args::list()) -> ok. --callback set_level(Level::atom()) -> ok. - --spec level_integer(atom()) -> integer(). -level_integer(debug) -> 1; -level_integer(info) -> 2; -level_integer(notice) -> 3; -level_integer(warning) -> 4; -level_integer(error) -> 5; -level_integer(critical) -> 6; -level_integer(alert) -> 7; -level_integer(emergency) -> 8; -level_integer(none) -> 9. - --spec level_to_atom(string() | integer()) -> atom(). -level_to_atom("1") -> debug; -level_to_atom("debug") -> debug; -level_to_atom("2") -> info; -level_to_atom("info") -> info; -level_to_atom("3") -> notice; -level_to_atom("notice") -> notice; -level_to_atom("4") -> warning; -level_to_atom("warning") -> warning; -level_to_atom("5") -> error; -level_to_atom("error") -> error; -level_to_atom("6") -> critical; -level_to_atom("critical") -> critical; -level_to_atom("7") -> alert; -level_to_atom("alert") -> alert; -level_to_atom("8") -> emergency; -level_to_atom("emergency") -> emergency; -level_to_atom("9") -> none; -level_to_atom("none") -> none; -level_to_atom(V) when is_integer(V) -> level_to_atom(integer_to_list(V)); -level_to_atom(V) when is_list(V) -> notice. + +-export([ + debug/2, + info/2, + notice/2, + warning/2, + error/2, + critical/2, + alert/2, + emergency/2, + + set_level/1 +]). + -spec debug(string(), list()) -> ok. debug(Fmt, Args) -> log(debug, Fmt, Args). + -spec info(string(), list()) -> ok. info(Fmt, Args) -> log(info, Fmt, Args). + -spec notice(string(), list()) -> ok. notice(Fmt, Args) -> log(notice, Fmt, Args). + -spec warning(string(), list()) -> ok. warning(Fmt, Args) -> log(warning, Fmt, Args). + -spec error(string(), list()) -> ok. error(Fmt, Args) -> log(error, Fmt, Args). + -spec critical(string(), list()) -> ok. critical(Fmt, Args) -> log(critical, Fmt, Args). + -spec alert(string(), list()) -> ok. alert(Fmt, Args) -> log(alert, Fmt, Args). + -spec emergency(string(), list()) -> ok. emergency(Fmt, Args) -> log(emergency, Fmt, Args). + +-spec set_level(atom() | string() | integer()) -> true. +set_level(Level) -> + config:set("log", "level", couch_log_util:level_to_string(Level)). + + -spec log(atom(), string(), list()) -> ok. log(Level, Fmt, Args) -> - case is_active_level(Level) of - false -> ok; + case couch_log_util:should_log(Level) of true -> - {ok, Backend} = get_backend(), - catch couch_stats:increment_counter([couch_log, level, Level]), - apply(Backend, Level, [Fmt, Args]) + Entry = couch_log_formatter:format(Level, self(), Fmt, Args), + ok = couch_log_server:log(Entry); + false -> + ok end. - --spec is_active_level(atom()) -> boolean(). -is_active_level(Level) -> - CurrentLevel = level_to_atom(config:get("log", "level", "notice")), - level_integer(Level) >= level_integer(CurrentLevel). - --spec set_level(atom() | string() | integer()) -> ok. -set_level(Level) when is_atom(Level) -> - {ok, Backend} = get_backend(), - Backend:set_level(Level); -set_level(Level) -> - set_level(level_to_atom(Level)). - --spec get_backend() -> {ok, atom()}. -get_backend() -> - BackendName = "couch_log_" ++ config:get("log", "backend", "stderr"), - {ok, list_to_existing_atom(BackendName)}. - --ifdef(TEST). - -callbacks_test_() -> - {setup, - fun setup/0, - fun cleanup/1, - [ - ?_assertEqual({ok, couch_log_eunit}, get_backend()), - ?_assertEqual(ok, couch_log:set_level(info)), - ?_assertEqual(ok, couch_log:debug("debug", [])), - ?_assertEqual(ok, couch_log:info("info", [])), - ?_assertEqual(ok, couch_log:notice("notice", [])), - ?_assertEqual(ok, couch_log:warning("warning", [])), - ?_assertEqual(ok, couch_log:error("error", [])), - ?_assertEqual(ok, couch_log:critical("critical", [])), - ?_assertEqual(ok, couch_log:alert("alert", [])), - ?_assertEqual(ok, couch_log:emergency("emergency", [])), - ?_assertEqual(stats_calls(), meck:history(couch_stats, self())), - ?_assertEqual(log_calls(), meck:history(couch_log_eunit, self())) - ] - }. - -setup() -> - ok = meck:new(config), - ok = meck:expect(config, get, - fun("log", "backend", _) -> "eunit"; - ("log", "level", _) -> "debug" end), - meck:new([couch_stats, couch_log_eunit], [non_strict]), - meck:expect(couch_stats, increment_counter, 1, ok), - setup_couch_log_eunit(). - -cleanup(_) -> - meck:unload(config), - meck:unload([couch_stats, couch_log_eunit]). - -setup_couch_log_eunit() -> - meck:expect(couch_log_eunit, set_level, 1, ok), - Levels = [debug, info, notice, warning, error, critical, alert, emergency], - lists:foreach(fun(Fun) -> - meck:expect(couch_log_eunit, Fun, 2, ok) - end, Levels). - -stats_calls() -> - Levels = [debug, info, notice, warning, error, critical, alert, emergency], - lists:map(fun(Level) -> - MFA = {couch_stats, increment_counter, [[couch_log, level, Level]]}, - {self(), MFA, ok} - end, Levels). - -log_calls() -> - Levels = [debug, info, notice, warning, error, critical, alert, emergency], - Calls = lists:map(fun(Level) -> - MFA = {couch_log_eunit, Level, [atom_to_list(Level),[]]}, - {self(), MFA, ok} - end, Levels), - [{self(), {couch_log_eunit, set_level, [info]}, ok}|Calls]. - --endif. diff --git a/src/couch_log_config.erl b/src/couch_log_config.erl new file mode 100644 index 0000000..766d068 --- /dev/null +++ b/src/couch_log_config.erl @@ -0,0 +1,100 @@ +% 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. +% +% Based on Bob Ippolitto's mochiglobal.erl + +-module(couch_log_config). + + +-export([ + init/0, + reconfigure/0, + get/1 +]). + + +-define(MOD_NAME, couch_log_config_dyn). +-define(ERL_FILE, "couch_log_config_dyn.erl"). + + +-spec init() -> ok. +init() -> + reconfigure(). + + +-spec reconfigure() -> ok. +reconfigure() -> + {ok, ?MOD_NAME, Bin} = compile:forms(forms(), [verbose, report_errors]), + code:purge(?MOD_NAME), + {module, ?MOD_NAME} = code:load_binary(?MOD_NAME, ?ERL_FILE, Bin), + ok. + + +-spec get(atom()) -> term(). +get(Key) -> + ?MOD_NAME:get(Key). + + +-spec entries() -> [string()]. +entries() -> + [ + {level, "level", "info"}, + {level_int, "level", "info"}, + {max_message_size, "max_message_size", "16000"} + ]. + + +-spec forms() -> [erl_syntax:syntaxTree()]. +forms() -> + GetFunClauses = lists:map(fun({FunKey, CfgKey, Default}) -> + FunVal = transform(FunKey, config:get("log", CfgKey, Default)), + Patterns = [erl_syntax:abstract(FunKey)], + Bodies = [erl_syntax:abstract(FunVal)], + erl_syntax:clause(Patterns, none, Bodies) + end, entries()), + + Statements = [ + % -module(?MOD_NAME) + erl_syntax:attribute( + erl_syntax:atom(module), + [erl_syntax:atom(?MOD_NAME)] + ), + + % -export([lookup/1]). + erl_syntax:attribute( + erl_syntax:atom(export), + [erl_syntax:list([ + erl_syntax:arity_qualifier( + erl_syntax:atom(get), + erl_syntax:integer(1)) + ])] + ), + + % list(Key) -> Value. + erl_syntax:function(erl_syntax:atom(get), GetFunClauses) + ], + [erl_syntax:revert(X) || X <- Statements]. + + +transform(level, LevelStr) -> + couch_log_util:level_to_atom(LevelStr); + +transform(level_int, LevelStr) -> + Level = couch_log_util:level_to_atom(LevelStr), + couch_log_util:level_to_integer(Level); + +transform(max_message_size, SizeStr) -> + try list_to_integer(SizeStr) of + Size -> Size + catch _:_ -> + 16000 + end. \ No newline at end of file diff --git a/src/couch_log_config_dyn.erl b/src/couch_log_config_dyn.erl new file mode 100644 index 0000000..f7541f6 --- /dev/null +++ b/src/couch_log_config_dyn.erl @@ -0,0 +1,28 @@ +% 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. +% +% This module gets replaced at runtime with a dynamically +% compiled version so don't rely on these default's making +% sense. They only mirror what's in the default.ini checked +% into the root Apache CouchDB Git repository. + +-module(couch_log_config_dyn). + + +-export([ + get/1 +]). + + +get(level) -> info; +get(level_int) -> 2; +get(max_message_size) -> 16000. diff --git a/src/couch_log_config_listener.erl b/src/couch_log_config_listener.erl index 6dc7ea6..287f79d 100644 --- a/src/couch_log_config_listener.erl +++ b/src/couch_log_config_listener.erl @@ -11,36 +11,64 @@ % the License. -module(couch_log_config_listener). --vsn(2). -behaviour(config_listener). -% public interface --export([subscribe/0]). -% config_listener callback --export([handle_config_change/5, handle_config_terminate/3]). +-export([ + start/0 +]). + +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + + +-ifdef(TEST). +-define(RELISTEN_DELAY, 500). +-else. +-define(RELISTEN_DELAY, 5000). +-endif. + + +start() -> + ok = config:listen_for_changes(?MODULE, nil). -subscribe() -> - Settings = [ - {backend, config:get("log", "backend", "stderr")}, - {level, config:get("log", "level", "notice")} - ], - ok = config:listen_for_changes(?MODULE, Settings), - ok. -handle_config_change("log", "backend", Value, _, Settings) -> - {level, Level} = lists:keyfind(level, 1, Settings), - couch_log:set_level(Level), - {ok, lists:keyreplace(backend, 1, Settings, {backend, Value})}; -handle_config_change("log", "level", Value, _, Settings) -> - couch_log:set_level(Value), - {ok, lists:keyreplace(level, 1, Settings, {level, Value})}; +handle_config_change("log", Key, _, _, _) -> + case Key of + "level" -> + couch_log_config:reconfigure(); + "max_message_size" -> + couch_log_config:reconfigure(); + _ -> + % Someone may have changed the config for + % the writer so we need to re-initialize. + couch_log_server:reconfigure() + end, + notify_listeners(), + {ok, nil}; + handle_config_change(_, _, _, _, Settings) -> {ok, Settings}. -handle_config_terminate(_, stop, _) -> ok; -handle_config_terminate(_Server, _Reason, State) -> + +handle_config_terminate(_, stop, _) -> + ok; +handle_config_terminate(_, _, _) -> spawn(fun() -> - timer:sleep(5000), - config:listen_for_changes(?MODULE, State) + timer:sleep(?RELISTEN_DELAY), + ok = config:listen_for_changes(?MODULE, nil) end). + + +-ifdef(TEST). +notify_listeners() -> + Listeners = application:get_env(couch_log, config_listeners, []), + lists:foreach(fun(L) -> + L ! couch_log_config_change_finished + end, Listeners). +-else. +notify_listeners() -> + ok. +-endif. diff --git a/src/couch_log_error_logger_h.erl b/src/couch_log_error_logger_h.erl new file mode 100644 index 0000000..c0765c6 --- /dev/null +++ b/src/couch_log_error_logger_h.erl @@ -0,0 +1,57 @@ +% 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. +% +% This file is primarily based on error_logger_lager_h.erl from +% https://github.com/basho/lager which is available under the +% above marked ASFL v2 license. + + +-module(couch_log_error_logger_h). + + +-behaviour(gen_event). + +-export([ + init/1, + terminate/2, + handle_call/2, + handle_event/2, + handle_info/2, + code_change/3 +]). + + +init(_) -> + {ok, undefined}. + + +terminate(_Reason, _St) -> + ok. + + +handle_call(_, St) -> + {ok, ignored, St}. + + +handle_event(Event, St) -> + Entry = couch_log_formatter:format(Event), + ok = couch_log_server:log(Entry), + {ok, St}. + + +handle_info(_, St) -> + {ok, St}. + + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + diff --git a/src/couch_log_formatter.erl b/src/couch_log_formatter.erl new file mode 100644 index 0000000..a2c5305 --- /dev/null +++ b/src/couch_log_formatter.erl @@ -0,0 +1,417 @@ +% 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. +% +% @doc The formatting functions in this module are pulled +% from lager's error_logger_lager_h.erl which is available +% under the ASFv2 license. + + +-module(couch_log_formatter). + + +-export([ + format/4, + format/3, + format/1, + + format_reason/1, + format_mfa/1, + format_trace/1, + format_args/3 +]). + + +-include("couch_log.hrl"). + + +-define(DEFAULT_TRUNCATION, 1024). + + +format(Level, Pid, Fmt, Args) -> + #log_entry{ + level = couch_log_util:level_to_atom(Level), + pid = Pid, + msg = maybe_truncate(Fmt, Args), + msg_id = couch_log_util:get_msg_id(), + time_stamp = couch_log_util:iso8601_timestamp() + }. + + +format(Level, Pid, Msg) -> + #log_entry{ + level = couch_log_util:level_to_atom(Level), + pid = Pid, + msg = maybe_truncate(Msg), + msg_id = couch_log_util:get_msg_id(), + time_stamp = couch_log_util:iso8601_timestamp() + }. + + +format({error, _GL, {Pid, "** Generic server " ++ _, Args}}) -> + %% gen_server terminate + [Name, LastMsg, State, Reason] = Args, + MsgFmt = "gen_server ~w terminated with reason: ~s~n" ++ + " last msg: ~p~n state: ~p", + MsgArgs = [Name, format_reason(Reason), LastMsg, State], + format(error, Pid, MsgFmt, MsgArgs); + +format({error, _GL, {Pid, "** State machine " ++ _, Args}}) -> + %% gen_fsm terminate + [Name, LastMsg, StateName, State, Reason] = Args, + MsgFmt = "gen_fsm ~w in state ~w terminated with reason: ~s~n" ++ + " last msg: ~p~n state: ~p", + MsgArgs = [Name, StateName, format_reason(Reason), LastMsg, State], + format(error, Pid, MsgFmt, MsgArgs); + +format({error, _GL, {Pid, "** gen_event handler" ++ _, Args}}) -> + %% gen_event handler terminate + [ID, Name, LastMsg, State, Reason] = Args, + MsgFmt = "gen_event ~w installed in ~w terminated with reason: ~s~n" ++ + " last msg: ~p~n state: ~p", + MsgArgs = [ID, Name, format_reason(Reason), LastMsg, State], + format(error, Pid, MsgFmt, MsgArgs); + +format({error, _GL, {Pid, Fmt, Args}}) -> + format(error, Pid, Fmt, Args); + +format({error_report, _GL, {Pid, std_error, D}}) -> + format(error, Pid, print_silly_list(D)); + +format({error_report, _GL, {Pid, supervisor_report, D}}) -> + case lists:sort(D) of + [{errorContext, Ctx}, {offender, Off}, + {reason, Reason}, {supervisor, Name}] -> + Offender = format_offender(Off), + MsgFmt = "Supervisor ~w had child ~s exit " ++ + "with reason ~s in context ~w", + Args = [ + supervisor_name(Name), + Offender, + format_reason(Reason), + Ctx + ], + format(error, Pid, MsgFmt, Args); + _ -> + format(error, Pid, "SUPERVISOR REPORT " ++ print_silly_list(D)) + end; + +format({error_report, _GL, {Pid, crash_report, [Report, Neighbors]}}) -> + Msg = "CRASH REPORT " ++ format_crash_report(Report, Neighbors), + format(error, Pid, Msg); + +format({warning_msg, _GL, {Pid, Fmt, Args}}) -> + format(warning, Pid, Fmt, Args); + +format({warning_report, _GL, {Pid, std_warning, Report}}) -> + format(warning, Pid, print_silly_list(Report)); + +format({info_msg, _GL, {Pid, Fmt, Args}}) -> + format(info, Pid, Fmt, Args); + +format({info_report, _GL, {Pid, std_info, D}}) when is_list(D) -> + case lists:sort(D) of + [{application, App}, {exited, Reason}, {type, _Type}] -> + MsgFmt = "Application ~w exited with reason: ~s", + format(info, Pid, MsgFmt, [App, format_reason(Reason)]); + _ -> + format(info, Pid, print_silly_list(D)) + end; + +format({info_report, _GL, {Pid, std_info, D}}) -> + format(info, Pid, "~w", [D]); + +format({info_report, _GL, {Pid, progress, D}}) -> + case lists:sort(D) of + [{application, App}, {started_at, Node}] -> + MsgFmt = "Application ~w started on node ~w", + format(info, Pid, MsgFmt, [App, Node]); + [{started, Started}, {supervisor, Name}] -> + MFA = format_mfa(get_value(mfargs, Started)), + ChildPid = get_value(pid, Started), + MsgFmt = "Supervisor ~w started ~s at pid ~w", + format(debug, Pid, MsgFmt, [supervisor_name(Name), MFA, ChildPid]); + _ -> + format(info, Pid, "PROGRESS REPORT " ++ print_silly_list(D)) + end; + +format(Event) -> + format(warning, self(), "Unexpected error_logger event ~w", [Event]). + + +format_crash_report(Report, Neighbours) -> + Pid = get_value(pid, Report), + Name = case get_value(registered_name, Report) of + undefined -> + pid_to_list(Pid); + Atom -> + io_lib:format("~s (~w)", [Atom, Pid]) + end, + {Class, Reason, Trace} = get_value(error_info, Report), + ReasonStr = format_reason({Reason, Trace}), + Type = case Class of + exit -> "exited"; + _ -> "crashed" + end, + MsgFmt = "Process ~s with ~w neighbors ~s with reason: ~s", + Args = [Name, length(Neighbours), Type, ReasonStr], + Msg = io_lib:format(MsgFmt, Args), + case filter_silly_list(Report, [pid, registered_name, error_info]) of + [] -> + Msg; + Rest -> + Msg ++ "; " ++ print_silly_list(Rest) + end. + + +format_offender(Off) -> + case get_value(mfargs, Off) of + undefined -> + %% supervisor_bridge + Args = [get_value(mod, Off), get_value(pid, Off)], + io_lib:format("at module ~w at ~w", Args); + MFArgs -> + %% regular supervisor + MFA = format_mfa(MFArgs), + + %% In 2014 the error report changed from `name' to + %% `id', so try that first. + Name = case get_value(id, Off) of + undefined -> + get_value(name, Off); + Id -> + Id + end, + Args = [Name, MFA, get_value(pid, Off)], + io_lib:format("~p started with ~s at ~w", Args) + end. + + +format_reason({'function not exported', [{M, F, A} | Trace]}) -> + ["call to unexported function ", format_mfa({M, F, A}), + " at ", format_trace(Trace)]; + +format_reason({'function not exported' = C, [{M, F, A, _Props} | Rest]}) -> + %% Drop line number from undefined function + format_reason({C, [{M, F, A} | Rest]}); + +format_reason({undef, [MFA | Trace]}) -> + ["call to undefined function ", format_mfa(MFA), + " at ", format_trace(Trace)]; + +format_reason({bad_return, {MFA, Val}}) -> + ["bad return value ", print_val(Val), " from ", format_mfa(MFA)]; + +format_reason({bad_return_value, Val}) -> + ["bad return value ", print_val(Val)]; + +format_reason({{bad_return_value, Val}, MFA}) -> + ["bad return value ", print_val(Val), " at ", format_mfa(MFA)]; + +format_reason({{badrecord, Record}, Trace}) -> + ["bad record ", print_val(Record), " at ", format_trace(Trace)]; + +format_reason({{case_clause, Val}, Trace}) -> + ["no case clause matching ", print_val(Val), " at ", format_trace(Trace)]; + +format_reason({function_clause, [MFA | Trace]}) -> + ["no function clause matching ", format_mfa(MFA), + " at ", format_trace(Trace)]; + +format_reason({if_clause, Trace}) -> + ["no true branch found while evaluating if expression at ", + format_trace(Trace)]; + +format_reason({{try_clause, Val}, Trace}) -> + ["no try clause matching ", print_val(Val), " at ", format_trace(Trace)]; + +format_reason({badarith, Trace}) -> + ["bad arithmetic expression at ", format_trace(Trace)]; + +format_reason({{badmatch, Val}, Trace}) -> + ["no match of right hand value ", print_val(Val), + " at ", format_trace(Trace)]; + +format_reason({emfile, Trace}) -> + ["maximum number of file descriptors exhausted, check ulimit -n; ", + format_trace(Trace)]; + +format_reason({system_limit, [{M, F, A} | Trace]}) -> + Limit = case {M, F} of + {erlang, open_port} -> + "maximum number of ports exceeded"; + {erlang, spawn} -> + "maximum number of processes exceeded"; + {erlang, spawn_opt} -> + "maximum number of processes exceeded"; + {erlang, list_to_atom} -> + "tried to create an atom larger than 255, or maximum atom count exceeded"; + {ets, new} -> + "maximum number of ETS tables exceeded"; + _ -> + format_mfa({M, F, A}) + end, + ["system limit: ", Limit, " at ", format_trace(Trace)]; + +format_reason({badarg, [MFA | Trace]}) -> + ["bad argument in call to ", format_mfa(MFA), + " at ", format_trace(Trace)]; + +format_reason({{badarg, Stack}, _}) -> + format_reason({badarg, Stack}); + +format_reason({{badarity, {Fun, Args}}, Trace}) -> + {arity, Arity} = lists:keyfind(arity, 1, erlang:fun_info(Fun)), + MsgFmt = "function called with wrong arity of ~w instead of ~w at ", + [io_lib:format(MsgFmt, [length(Args), Arity]), format_trace(Trace)]; + +format_reason({noproc, MFA}) -> + ["no such process or port in call to ", format_mfa(MFA)]; + +format_reason({{badfun, Term}, Trace}) -> + ["bad function ", print_val(Term), " called at ", format_trace(Trace)]; + +format_reason({Reason, [{M, F, A} | _] = Trace}) + when is_atom(M), is_atom(F), is_integer(A) -> + [format_reason(Reason), " at ", format_trace(Trace)]; + +format_reason({Reason, [{M, F, A, Props} | _] = Trace}) + when is_atom(M), is_atom(F), is_integer(A), is_list(Props) -> + [format_reason(Reason), " at ", format_trace(Trace)]; + +format_reason(Reason) -> + {Str, _} = couch_log_trunc_io:print(Reason, 500), + Str. + + +format_mfa({M, F, A}) when is_list(A) -> + {FmtStr, Args} = format_args(A, [], []), + io_lib:format("~w:~w(" ++ FmtStr ++ ")", [M, F | Args]); + +format_mfa({M, F, A}) when is_integer(A) -> + io_lib:format("~w:~w/~w", [M, F, A]); + +format_mfa({M, F, A, Props}) when is_list(Props) -> + case get_value(line, Props) of + undefined -> + format_mfa({M, F, A}); + Line -> + [format_mfa({M, F, A}), io_lib:format("(line:~w)", [Line])] + end; + +format_mfa(Trace) when is_list(Trace) -> + format_trace(Trace); + +format_mfa(Other) -> + io_lib:format("~w", [Other]). + + +format_trace([MFA]) -> + [trace_mfa(MFA)]; + +format_trace([MFA | Rest]) -> + [trace_mfa(MFA), " <= ", format_trace(Rest)]; + +format_trace(Other) -> + io_lib:format("~w", [Other]). + + +trace_mfa({M, F, A}) when is_list(A) -> + format_mfa({M, F, length(A)}); + +trace_mfa({M, F, A, Props}) when is_list(A) -> + format_mfa({M, F, length(A), Props}); + +trace_mfa(Other) -> + format_mfa(Other). + + +format_args([], FmtAcc, ArgsAcc) -> + {string:join(lists:reverse(FmtAcc), ", "), lists:reverse(ArgsAcc)}; + +format_args([H|T], FmtAcc, ArgsAcc) -> + {Str, _} = couch_log_trunc_io:print(H, 100), + format_args(T, ["~s" | FmtAcc], [Str | ArgsAcc]). + + +maybe_truncate(Fmt, Args) -> + MaxMsgSize = couch_log_config:get(max_message_size), + couch_log_trunc_io:format(Fmt, Args, MaxMsgSize). + + +maybe_truncate(Msg) -> + MaxMsgSize = couch_log_config:get(max_message_size), + case iolist_size(Msg) > MaxMsgSize of + true -> + MsgBin = iolist_to_binary(Msg), + PrefixSize = MaxMsgSize - 3, + <> = MsgBin, + [Prefix, "..."]; + false -> + Msg + end. + + +print_silly_list(L) when is_list(L) -> + case couch_log_util:string_p(L) of + true -> + couch_log_trunc_io:format("~s", [L], ?DEFAULT_TRUNCATION); + _ -> + print_silly_list(L, [], []) + end; + +print_silly_list(L) -> + {Str, _} = couch_log_trunc_io:print(L, ?DEFAULT_TRUNCATION), + Str. + + +print_silly_list([], Fmt, Acc) -> + couch_log_trunc_io:format(string:join(lists:reverse(Fmt), ", "), + lists:reverse(Acc), ?DEFAULT_TRUNCATION); + +print_silly_list([{K, V} | T], Fmt, Acc) -> + print_silly_list(T, ["~p: ~p" | Fmt], [V, K | Acc]); + +print_silly_list([H | T], Fmt, Acc) -> + print_silly_list(T, ["~p" | Fmt], [H | Acc]). + + +print_val(Val) -> + {Str, _} = couch_log_trunc_io:print(Val, 500), + Str. + + +filter_silly_list([], _) -> + []; + +filter_silly_list([{K, V} | T], Filter) -> + case lists:member(K, Filter) of + true -> + filter_silly_list(T, Filter); + false -> + [{K, V} | filter_silly_list(T, Filter)] + end; + +filter_silly_list([H | T], Filter) -> + [H | filter_silly_list(T, Filter)]. + + +get_value(Key, Value) -> + get_value(Key, Value, undefined). + +get_value(Key, List, Default) -> + case lists:keyfind(Key, 1, List) of + false -> Default; + {Key, Value} -> Value + end. + +supervisor_name({local, Name}) -> Name; +supervisor_name(Name) -> Name. diff --git a/src/couch_log_monitor.erl b/src/couch_log_monitor.erl new file mode 100644 index 0000000..236d340 --- /dev/null +++ b/src/couch_log_monitor.erl @@ -0,0 +1,66 @@ +% 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(couch_log_monitor). + +-behaviour(gen_server). +-vsn(1). + + +-export([ + start_link/0 +]). + +-export([ + init/1, + terminate/2, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3 +]). + + +-define(HANDLER_MOD, couch_log_error_logger_h). + + +start_link() -> + gen_server:start_link(?MODULE, [], []). + + +init(_) -> + ok = gen_event:add_sup_handler(error_logger, ?HANDLER_MOD, []), + {ok, nil}. + + +terminate(_, _) -> + ok. + + +handle_call(_Msg, _From, St) -> + {reply, ignored, St}. + + +handle_cast(_Msg, St) -> + {noreply, St}. + + +handle_info({gen_event_EXIT, ?HANDLER_MOD, Reason}, St) -> + {stop, Reason, St}; + + +handle_info(_Msg, St) -> + {noreply, St}. + + +code_change(_, State, _) -> + {ok, State}. diff --git a/src/couch_log_server.erl b/src/couch_log_server.erl new file mode 100644 index 0000000..be44af8 --- /dev/null +++ b/src/couch_log_server.erl @@ -0,0 +1,106 @@ +% 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(couch_log_server). +-behavior(gen_server). + + +-export([ + start_link/0, + reconfigure/0, + log/1 +]). + +-export([ + init/1, + terminate/2, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3 +]). + + +-include("couch_log.hrl"). + + +-record(st, { + writer +}). + + +-ifdef(TEST). +-define(SEND(Entry), gen_server:call(?MODULE, {log, Entry})). +-else. +-define(SEND(Entry), gen_server:cast(?MODULE, {log, Entry})). +-endif. + + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +reconfigure() -> + gen_server:call(?MODULE, reconfigure). + + +log(Entry) -> + ?SEND(Entry). + + +init(_) -> + process_flag(trap_exit, true), + {ok, #st{ + writer = couch_log_writer:init() + }}. + + +terminate(Reason, St) -> + ok = couch_log_writer:terminate(Reason, St#st.writer). + + +handle_call(reconfigure, _From, St) -> + ok = couch_log_writer:terminate(reconfiguring, St#st.writer), + {reply, ok, St#st{ + writer = couch_log_writer:init() + }}; + +handle_call({log, Entry}, _From, St) -> + % We re-check if we should log here in case an operator + % adjusted the log level and then realized it was a bad + % idea because it filled our message queue. + case couch_log_util:should_log(Entry) of + true -> + NewWriter = couch_log_writer:write(Entry, St#st.writer), + {reply, ok, St#st{writer = NewWriter}}; + false -> + {reply, ok, St} + end; + +handle_call(Ignore, From, St) -> + Args = [?MODULE, Ignore], + Entry = couch_log_formatter:format(error, ?MODULE, "~s ignored ~p", Args), + handle_call({log, Entry}, From, St). + + +handle_cast(Msg, St) -> + {reply, ok, NewSt} = handle_call(Msg, nil, St), + {noreply, NewSt}. + + +handle_info(Msg, St) -> + {reply, ok, NewSt} = handle_call(Msg, nil, St), + {noreply, NewSt}. + + +code_change(_Vsn, St, _Extra) -> + {ok, St}. diff --git a/src/couch_log_stderr.erl b/src/couch_log_stderr.erl deleted file mode 100644 index 6bf95b9..0000000 --- a/src/couch_log_stderr.erl +++ /dev/null @@ -1,57 +0,0 @@ -% 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(couch_log_stderr). - --behaviour(couch_log). - --export([ - debug/2, - info/2, - notice/2, - warning/2, - error/2, - critical/2, - alert/2, - emergency/2, - set_level/1 -]). - -debug(Fmt, Args) -> - write_log("[debug] " ++ Fmt, Args). - -info(Fmt, Args) -> - write_log("[info] " ++ Fmt, Args). - -notice(Fmt, Args) -> - write_log("[notice] " ++ Fmt, Args). - -warning(Fmt, Args) -> - write_log("[warning] " ++ Fmt, Args). - -error(Fmt, Args) -> - write_log("[error] " ++ Fmt, Args). - -critical(Fmt, Args) -> - write_log("[critical] " ++ Fmt, Args). - -alert(Fmt, Args) -> - write_log("[alert] " ++ Fmt, Args). - -emergency(Fmt, Args) -> - write_log("[emergency] " ++ Fmt, Args). - -write_log(Fmt, Args) -> - io:format(standard_error, Fmt ++ "~n", Args). - -set_level(_) -> - ok. diff --git a/src/couch_log_sup.erl b/src/couch_log_sup.erl index 9d69fd0..3106659 100644 --- a/src/couch_log_sup.erl +++ b/src/couch_log_sup.erl @@ -23,5 +23,27 @@ start_link() -> init([]) -> - ok = couch_log_config_listener:subscribe(), - {ok, {{one_for_one, 1, 1}, []}}. + ok = couch_log_config:init(), + ok = couch_log_config_listener:start(), + {ok, {{one_for_one, 1, 1}, children()}}. + + +children() -> + [ + { + couch_log_server, + {couch_log_server, start_link, []}, + permanent, + 5000, + worker, + [couch_log_server] + }, + { + couch_log_monitor, + {couch_log_monitor, start_link, []}, + permanent, + 5000, + worker, + [couch_log_monitor] + } + ]. diff --git a/src/couch_log_trunc_io.erl b/src/couch_log_trunc_io.erl new file mode 100644 index 0000000..636dfdc --- /dev/null +++ b/src/couch_log_trunc_io.erl @@ -0,0 +1,838 @@ +%% ``The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with your Erlang distribution. If not, it can be +%% retrieved via the world wide web at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% The Initial Developer of the Original Code is Corelatus AB. +%% Portions created by Corelatus are Copyright 2003, Corelatus +%% AB. All Rights Reserved.'' +%% +%% @doc Module to print out terms for logging. Limits by length rather than depth. +%% +%% The resulting string may be slightly larger than the limit; the intention +%% is to provide predictable CPU and memory consumption for formatting +%% terms, not produce precise string lengths. +%% +%% Typical use: +%% +%% trunc_io:print(Term, 500). +%% +%% Source license: Erlang Public License. +%% Original author: Matthias Lang, matthias@corelatus.se +%% +%% Various changes to this module, most notably the format/3 implementation +%% were added by Andrew Thompson `'. The module has been renamed +%% to avoid conflicts with the vanilla module. +%% +%% Module renamed to couch_log_trunc_io to avoid naming collisions with +%% the lager version. + +-module(couch_log_trunc_io). +-author('matthias@corelatus.se'). +%% And thanks to Chris Newcombe for a bug fix +-export([format/3, format/4, print/2, print/3, fprint/2, fprint/3, safe/2]). % interface functions +-version("$Id: trunc_io.erl,v 1.11 2009-02-23 12:01:06 matthias Exp $"). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +-type option() :: {'depth', integer()} + | {'lists_as_strings', boolean()} + | {'force_strings', boolean()}. +-type options() :: [option()]. + +-record(print_options, { + %% negative depth means no depth limiting + depth = -1 :: integer(), + %% whether to print lists as strings, if possible + lists_as_strings = true :: boolean(), + %% force strings, or binaries to be printed as a string, + %% even if they're not printable + force_strings = false :: boolean() + }). + +format(Fmt, Args, Max) -> + format(Fmt, Args, Max, []). + +format(Fmt, Args, Max, Options) -> + try couch_log_trunc_io_fmt:format(Fmt, Args, Max, Options) + catch + _What:_Why -> + erlang:error(badarg, [Fmt, Args]) + end. + +%% @doc Returns an flattened list containing the ASCII representation of the given +%% term. +-spec fprint(term(), pos_integer()) -> string(). +fprint(Term, Max) -> + fprint(Term, Max, []). + + +%% @doc Returns an flattened list containing the ASCII representation of the given +%% term. +-spec fprint(term(), pos_integer(), options()) -> string(). +fprint(T, Max, Options) -> + {L, _} = print(T, Max, prepare_options(Options, #print_options{})), + lists:flatten(L). + +%% @doc Same as print, but never crashes. +%% +%% This is a tradeoff. Print might conceivably crash if it's asked to +%% print something it doesn't understand, for example some new data +%% type in a future version of Erlang. If print crashes, we fall back +%% to io_lib to format the term, but then the formatting is +%% depth-limited instead of length limited, so you might run out +%% memory printing it. Out of the frying pan and into the fire. +%% +-spec safe(term(), pos_integer()) -> {string(), pos_integer()} | {string()}. +safe(What, Len) -> + case catch print(What, Len) of + {L, Used} when is_list(L) -> {L, Used}; + _ -> {"unable to print" ++ io_lib:write(What, 99)} + end. + +%% @doc Returns {List, Length} +-spec print(term(), pos_integer()) -> {iolist(), pos_integer()}. +print(Term, Max) -> + print(Term, Max, []). + +%% @doc Returns {List, Length} +-spec print(term(), pos_integer(), options() | #print_options{}) -> {iolist(), pos_integer()}. +print(Term, Max, Options) when is_list(Options) -> + %% need to convert the proplist to a record + print(Term, Max, prepare_options(Options, #print_options{})); + +print(Term, _Max, #print_options{force_strings=true}) when not is_list(Term), not is_binary(Term), not is_atom(Term) -> + erlang:error(badarg); + +print(_, Max, _Options) when Max < 0 -> {"...", 3}; +print(_, _, #print_options{depth=0}) -> {"...", 3}; + + +%% @doc We assume atoms, floats, funs, integers, PIDs, ports and refs never need +%% to be truncated. This isn't strictly true, someone could make an +%% arbitrarily long bignum. Let's assume that won't happen unless someone +%% is being malicious. +%% +print(Atom, _Max, #print_options{force_strings=NoQuote}) when is_atom(Atom) -> + L = atom_to_list(Atom), + R = case atom_needs_quoting_start(L) andalso not NoQuote of + true -> lists:flatten([$', L, $']); + false -> L + end, + {R, length(R)}; + +print(<<>>, _Max, #print_options{depth=1}) -> + {"<<>>", 4}; +print(Bin, _Max, #print_options{depth=1}) when is_binary(Bin) -> + {"<<...>>", 7}; +print(<<>>, _Max, Options) -> + case Options#print_options.force_strings of + true -> + {"", 0}; + false -> + {"<<>>", 4} + end; + +print(Binary, 0, _Options) when is_bitstring(Binary) -> + {"<<..>>", 6}; + +print(Bin, Max, _Options) when is_binary(Bin), Max < 2 -> + {"<<...>>", 7}; +print(Binary, Max, Options) when is_binary(Binary) -> + B = binary_to_list(Binary, 1, lists:min([Max, byte_size(Binary)])), + {Res, Length} = case Options#print_options.lists_as_strings orelse + Options#print_options.force_strings of + true -> + Depth = Options#print_options.depth, + MaxSize = (Depth - 1) * 4, + %% check if we need to truncate based on depth + In = case Depth > -1 andalso MaxSize < length(B) andalso + not Options#print_options.force_strings of + true -> + string:substr(B, 1, MaxSize); + false -> B + end, + MaxLen = case Options#print_options.force_strings of + true -> + Max; + false -> + %% make room for the leading doublequote + Max - 1 + end, + try alist(In, MaxLen, Options) of + {L0, Len0} -> + case Options#print_options.force_strings of + false -> + case B /= In of + true -> + {[$", L0, "..."], Len0+4}; + false -> + {[$"|L0], Len0+1} + end; + true -> + {L0, Len0} + end + catch + throw:{unprintable, C} -> + Index = string:chr(In, C), + case Index > 1 andalso Options#print_options.depth =< Index andalso + Options#print_options.depth > -1 andalso + not Options#print_options.force_strings of + true -> + %% print first Index-1 characters followed by ... + {L0, Len0} = alist_start(string:substr(In, 1, Index - 1), Max - 1, Options), + {L0++"...", Len0+3}; + false -> + list_body(In, Max-4, dec_depth(Options), true) + end + end; + _ -> + list_body(B, Max-4, dec_depth(Options), true) + end, + case Options#print_options.force_strings of + true -> + {Res, Length}; + _ -> + {["<<", Res, ">>"], Length+4} + end; + +%% bitstrings are binary's evil brother who doesn't end on an 8 bit boundary. +%% This makes printing them extremely annoying, so list_body/list_bodyc has +%% some magic for dealing with the output of bitstring_to_list, which returns +%% a list of integers (as expected) but with a trailing binary that represents +%% the remaining bits. +print({inline_bitstring, B}, _Max, _Options) when is_bitstring(B) -> + Size = bit_size(B), + <> = B, + ValueStr = integer_to_list(Value), + SizeStr = integer_to_list(Size), + {[ValueStr, $:, SizeStr], length(ValueStr) + length(SizeStr) +1}; +print(BitString, Max, Options) when is_bitstring(BitString) -> + BL = case byte_size(BitString) > Max of + true -> + binary_to_list(BitString, 1, Max); + _ -> + R = erlang:bitstring_to_list(BitString), + {Bytes, [Bits]} = lists:splitwith(fun erlang:is_integer/1, R), + %% tag the trailing bits with a special tuple we catch when + %% list_body calls print again + Bytes ++ [{inline_bitstring, Bits}] + end, + {X, Len0} = list_body(BL, Max - 4, dec_depth(Options), true), + {["<<", X, ">>"], Len0 + 4}; + +print(Float, _Max, _Options) when is_float(Float) -> + %% use the same function io_lib:format uses to print floats + %% float_to_list is way too verbose. + L = io_lib_format:fwrite_g(Float), + {L, length(L)}; + +print(Fun, Max, _Options) when is_function(Fun) -> + L = erlang:fun_to_list(Fun), + case length(L) > Max of + true -> + S = erlang:max(5, Max), + Res = string:substr(L, 1, S) ++ "..>", + {Res, length(Res)}; + _ -> + {L, length(L)} + end; + +print(Integer, _Max, _Options) when is_integer(Integer) -> + L = integer_to_list(Integer), + {L, length(L)}; + +print(Pid, _Max, _Options) when is_pid(Pid) -> + L = pid_to_list(Pid), + {L, length(L)}; + +print(Ref, _Max, _Options) when is_reference(Ref) -> + L = erlang:ref_to_list(Ref), + {L, length(L)}; + +print(Port, _Max, _Options) when is_port(Port) -> + L = erlang:port_to_list(Port), + {L, length(L)}; + +print({'$lager_record', Name, Fields}, Max, Options) -> + Leader = "#" ++ atom_to_list(Name) ++ "{", + {RC, Len} = record_fields(Fields, Max - length(Leader) + 1, dec_depth(Options)), + {[Leader, RC, "}"], Len + length(Leader) + 1}; + +print(Tuple, Max, Options) when is_tuple(Tuple) -> + {TC, Len} = tuple_contents(Tuple, Max-2, Options), + {[${, TC, $}], Len + 2}; + +print(List, Max, Options) when is_list(List) -> + case Options#print_options.lists_as_strings orelse + Options#print_options.force_strings of + true -> + alist_start(List, Max, dec_depth(Options)); + _ -> + {R, Len} = list_body(List, Max - 2, dec_depth(Options), false), + {[$[, R, $]], Len + 2} + end; + +print(Map, Max, Options) -> + case erlang:is_builtin(erlang, is_map, 1) andalso erlang:is_map(Map) of + true -> + {MapBody, Len} = map_body(Map, Max - 3, dec_depth(Options)), + {[$#, ${, MapBody, $}], Len + 3}; + false -> + error(badarg, [Map, Max, Options]) + end. + +%% Returns {List, Length} +tuple_contents(Tuple, Max, Options) -> + L = tuple_to_list(Tuple), + list_body(L, Max, dec_depth(Options), true). + +%% Format the inside of a list, i.e. do not add a leading [ or trailing ]. +%% Returns {List, Length} +list_body([], _Max, _Options, _Tuple) -> {[], 0}; +list_body(_, Max, _Options, _Tuple) when Max < 4 -> {"...", 3}; +list_body(_, _Max, #print_options{depth=0}, _Tuple) -> {"...", 3}; +list_body([H], Max, Options=#print_options{depth=1}, _Tuple) -> + print(H, Max, Options); +list_body([H|_], Max, Options=#print_options{depth=1}, Tuple) -> + {List, Len} = print(H, Max-4, Options), + Sep = case Tuple of + true -> $,; + false -> $| + end, + {[List ++ [Sep | "..."]], Len + 4}; +list_body([H|T], Max, Options, Tuple) -> + {List, Len} = print(H, Max, Options), + {Final, FLen} = list_bodyc(T, Max - Len, Options, Tuple), + {[List|Final], FLen + Len}; +list_body(X, Max, Options, _Tuple) -> %% improper list + {List, Len} = print(X, Max - 1, Options), + {[$|,List], Len + 1}. + +list_bodyc([], _Max, _Options, _Tuple) -> {[], 0}; +list_bodyc(_, Max, _Options, _Tuple) when Max < 5 -> {",...", 4}; +list_bodyc(_, _Max, #print_options{depth=1}, true) -> {",...", 4}; +list_bodyc(_, _Max, #print_options{depth=1}, false) -> {"|...", 4}; +list_bodyc([H|T], Max, #print_options{depth=Depth} = Options, Tuple) -> + {List, Len} = print(H, Max, dec_depth(Options)), + {Final, FLen} = list_bodyc(T, Max - Len - 1, dec_depth(Options), Tuple), + Sep = case Depth == 1 andalso not Tuple of + true -> $|; + _ -> $, + end, + {[Sep, List|Final], FLen + Len + 1}; +list_bodyc(X, Max, Options, _Tuple) -> %% improper list + {List, Len} = print(X, Max - 1, Options), + {[$|,List], Len + 1}. + +map_body(Map, Max, #print_options{depth=Depth}) when Max < 4; Depth =:= 0 -> + case erlang:map_size(Map) of + 0 -> {[], 0}; + _ -> {"...", 3} + end; +map_body(Map, Max, Options) -> + case maps:to_list(Map) of + [] -> + {[], 0}; + [{Key, Value} | Rest] -> + {KeyStr, KeyLen} = print(Key, Max - 4, Options), + DiffLen = KeyLen + 4, + {ValueStr, ValueLen} = print(Value, Max - DiffLen, Options), + DiffLen2 = DiffLen + ValueLen, + {Final, FLen} = map_bodyc(Rest, Max - DiffLen2, dec_depth(Options)), + {[KeyStr, " => ", ValueStr | Final], DiffLen2 + FLen} + end. + +map_bodyc([], _Max, _Options) -> + {[], 0}; +map_bodyc(_Rest, Max,#print_options{depth=Depth}) when Max < 5; Depth =:= 0 -> + {",...", 4}; +map_bodyc([{Key, Value} | Rest], Max, Options) -> + {KeyStr, KeyLen} = print(Key, Max - 5, Options), + DiffLen = KeyLen + 5, + {ValueStr, ValueLen} = print(Value, Max - DiffLen, Options), + DiffLen2 = DiffLen + ValueLen, + {Final, FLen} = map_bodyc(Rest, Max - DiffLen2, dec_depth(Options)), + {[$,, KeyStr, " => ", ValueStr | Final], DiffLen2 + FLen}. + +%% The head of a list we hope is ascii. Examples: +%% +%% [65,66,67] -> "ABC" +%% [65,0,67] -> "A"[0,67] +%% [0,65,66] -> [0,65,66] +%% [65,b,66] -> "A"[b,66] +%% +alist_start([], _Max, #print_options{force_strings=true}) -> {"", 0}; +alist_start([], _Max, _Options) -> {"[]", 2}; +alist_start(_, Max, _Options) when Max < 4 -> {"...", 3}; +alist_start(_, _Max, #print_options{depth=0}) -> {"[...]", 5}; +alist_start(L, Max, #print_options{force_strings=true} = Options) -> + alist(L, Max, Options); +%alist_start([H|_T], _Max, #print_options{depth=1}) when is_integer(H) -> {[$[, H, $|, $., $., $., $]], 7}; +alist_start([H|T], Max, Options) when is_integer(H), H >= 16#20, H =< 16#7e -> % definitely printable + try alist([H|T], Max -1, Options) of + {L, Len} -> + {[$"|L], Len + 1} + catch + throw:{unprintable, _} -> + {R, Len} = list_body([H|T], Max-2, Options, false), + {[$[, R, $]], Len + 2} + end; +alist_start([H|T], Max, Options) when is_integer(H), H >= 16#a0, H =< 16#ff -> % definitely printable + try alist([H|T], Max -1, Options) of + {L, Len} -> + {[$"|L], Len + 1} + catch + throw:{unprintable, _} -> + {R, Len} = list_body([H|T], Max-2, Options, false), + {[$[, R, $]], Len + 2} + end; +alist_start([H|T], Max, Options) when H =:= $\t; H =:= $\n; H =:= $\r; H =:= $\v; H =:= $\e; H=:= $\f; H=:= $\b -> + try alist([H|T], Max -1, Options) of + {L, Len} -> + {[$"|L], Len + 1} + catch + throw:{unprintable, _} -> + {R, Len} = list_body([H|T], Max-2, Options, false), + {[$[, R, $]], Len + 2} + end; +alist_start(L, Max, Options) -> + {R, Len} = list_body(L, Max-2, Options, false), + {[$[, R, $]], Len + 2}. + +alist([], _Max, #print_options{force_strings=true}) -> {"", 0}; +alist([], _Max, _Options) -> {"\"", 1}; +alist(_, Max, #print_options{force_strings=true}) when Max < 4 -> {"...", 3}; +alist(_, Max, #print_options{force_strings=false}) when Max < 5 -> {"...\"", 4}; +alist([H|T], Max, Options = #print_options{force_strings=false,lists_as_strings=true}) when H =:= $"; H =:= $\\ -> + %% preserve escaping around quotes + {L, Len} = alist(T, Max-1, Options), + {[$\\,H|L], Len + 2}; +alist([H|T], Max, Options) when is_integer(H), H >= 16#20, H =< 16#7e -> % definitely printable + {L, Len} = alist(T, Max-1, Options), + {[H|L], Len + 1}; +alist([H|T], Max, Options) when is_integer(H), H >= 16#a0, H =< 16#ff -> % definitely printable + {L, Len} = alist(T, Max-1, Options), + {[H|L], Len + 1}; +alist([H|T], Max, Options) when H =:= $\t; H =:= $\n; H =:= $\r; H =:= $\v; H =:= $\e; H=:= $\f; H=:= $\b -> + {L, Len} = alist(T, Max-1, Options), + case Options#print_options.force_strings of + true -> + {[H|L], Len + 1}; + _ -> + {[escape(H)|L], Len + 1} + end; +alist([H|T], Max, #print_options{force_strings=true} = Options) when is_integer(H) -> + {L, Len} = alist(T, Max-1, Options), + {[H|L], Len + 1}; +alist([H|T], Max, Options = #print_options{force_strings=true}) when is_binary(H); is_list(H) -> + {List, Len} = print(H, Max, Options), + case (Max - Len) =< 0 of + true -> + %% no more room to print anything + {List, Len}; + false -> + %% no need to decrement depth, as we're in printable string mode + {Final, FLen} = alist(T, Max - Len, Options), + {[List|Final], FLen+Len} + end; +alist(_, _, #print_options{force_strings=true}) -> + erlang:error(badarg); +alist([H|_L], _Max, _Options) -> + throw({unprintable, H}); +alist(H, _Max, _Options) -> + %% improper list + throw({unprintable, H}). + +%% is the first character in the atom alphabetic & lowercase? +atom_needs_quoting_start([H|T]) when H >= $a, H =< $z -> + atom_needs_quoting(T); +atom_needs_quoting_start(_) -> + true. + +atom_needs_quoting([]) -> + false; +atom_needs_quoting([H|T]) when (H >= $a andalso H =< $z); + (H >= $A andalso H =< $Z); + (H >= $0 andalso H =< $9); + H == $@; H == $_ -> + atom_needs_quoting(T); +atom_needs_quoting(_) -> + true. + +-spec prepare_options(options(), #print_options{}) -> #print_options{}. +prepare_options([], Options) -> + Options; +prepare_options([{depth, Depth}|T], Options) when is_integer(Depth) -> + prepare_options(T, Options#print_options{depth=Depth}); +prepare_options([{lists_as_strings, Bool}|T], Options) when is_boolean(Bool) -> + prepare_options(T, Options#print_options{lists_as_strings = Bool}); +prepare_options([{force_strings, Bool}|T], Options) when is_boolean(Bool) -> + prepare_options(T, Options#print_options{force_strings = Bool}). + +dec_depth(#print_options{depth=Depth} = Options) when Depth > 0 -> + Options#print_options{depth=Depth-1}; +dec_depth(Options) -> + Options. + +escape($\t) -> "\\t"; +escape($\n) -> "\\n"; +escape($\r) -> "\\r"; +escape($\e) -> "\\e"; +escape($\f) -> "\\f"; +escape($\b) -> "\\b"; +escape($\v) -> "\\v". + +record_fields([], _, _) -> + {"", 0}; +record_fields(_, Max, #print_options{depth=D}) when Max < 4; D == 0 -> + {"...", 3}; +record_fields([{Field, Value}|T], Max, Options) -> + {ExtraChars, Terminator} = case T of + [] -> + {1, []}; + _ -> + {2, ","} + end, + {FieldStr, FieldLen} = print(Field, Max - ExtraChars, Options), + {ValueStr, ValueLen} = print(Value, Max - (FieldLen + ExtraChars), Options), + {Final, FLen} = record_fields(T, Max - (FieldLen + ValueLen + ExtraChars), dec_depth(Options)), + {[FieldStr++"="++ValueStr++Terminator|Final], FLen + FieldLen + ValueLen + ExtraChars}. + + +-ifdef(TEST). +%%-------------------- +%% The start of a test suite. So far, it only checks for not crashing. +format_test() -> + %% simple format strings + ?assertEqual("foobar", lists:flatten(format("~s", [["foo", $b, $a, $r]], 50))), + ?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~p", [["foo", $b, $a, $r]], 50))), + ?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~P", [["foo", $b, $a, $r], 10], 50))), + ?assertEqual("[[102,111,111],98,97,114]", lists:flatten(format("~w", [["foo", $b, $a, $r]], 50))), + + %% complex ones + ?assertEqual(" foobar", lists:flatten(format("~10s", [["foo", $b, $a, $r]], 50))), + ?assertEqual("f", lists:flatten(format("~1s", [["foo", $b, $a, $r]], 50))), + ?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~22p", [["foo", $b, $a, $r]], 50))), + ?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~22P", [["foo", $b, $a, $r], 10], 50))), + ?assertEqual("**********", lists:flatten(format("~10W", [["foo", $b, $a, $r], 10], 50))), + ?assertEqual("[[102,111,111],98,97,114]", lists:flatten(format("~25W", [["foo", $b, $a, $r], 10], 50))), + % Note these next two diverge from io_lib:format; the field width is + % ignored, when it should be used as max line length. + ?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~10p", [["foo", $b, $a, $r]], 50))), + ?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~10P", [["foo", $b, $a, $r], 10], 50))), + ok. + +atom_quoting_test() -> + ?assertEqual("hello", lists:flatten(format("~p", [hello], 50))), + ?assertEqual("'hello world'", lists:flatten(format("~p", ['hello world'], 50))), + ?assertEqual("'Hello world'", lists:flatten(format("~p", ['Hello world'], 50))), + ?assertEqual("hello_world", lists:flatten(format("~p", ['hello_world'], 50))), + ?assertEqual("'node@127.0.0.1'", lists:flatten(format("~p", ['node@127.0.0.1'], 50))), + ?assertEqual("node@nohost", lists:flatten(format("~p", [node@nohost], 50))), + ?assertEqual("abc123", lists:flatten(format("~p", [abc123], 50))), + ok. + +sane_float_printing_test() -> + ?assertEqual("1.0", lists:flatten(format("~p", [1.0], 50))), + ?assertEqual("1.23456789", lists:flatten(format("~p", [1.23456789], 50))), + ?assertEqual("1.23456789", lists:flatten(format("~p", [1.234567890], 50))), + ?assertEqual("0.3333333333333333", lists:flatten(format("~p", [1/3], 50))), + ?assertEqual("0.1234567", lists:flatten(format("~p", [0.1234567], 50))), + ok. + +float_inside_list_test() -> + ?assertEqual("[97,38.233913133184835,99]", lists:flatten(format("~p", [[$a, 38.233913133184835, $c]], 50))), + ?assertError(badarg, lists:flatten(format("~s", [[$a, 38.233913133184835, $c]], 50))), + ok. + +quote_strip_test() -> + ?assertEqual("\"hello\"", lists:flatten(format("~p", ["hello"], 50))), + ?assertEqual("hello", lists:flatten(format("~s", ["hello"], 50))), + ?assertEqual("hello", lists:flatten(format("~s", [hello], 50))), + ?assertEqual("hello", lists:flatten(format("~p", [hello], 50))), + ?assertEqual("'hello world'", lists:flatten(format("~p", ['hello world'], 50))), + ?assertEqual("hello world", lists:flatten(format("~s", ['hello world'], 50))), + ok. + +binary_printing_test() -> + ?assertEqual("<<>>", lists:flatten(format("~p", [<<>>], 50))), + ?assertEqual("", lists:flatten(format("~s", [<<>>], 50))), + ?assertEqual("<<..>>", lists:flatten(format("~p", [<<"hi">>], 0))), + ?assertEqual("<<...>>", lists:flatten(format("~p", [<<"hi">>], 1))), + ?assertEqual("<<\"hello\">>", lists:flatten(format("~p", [<<$h, $e, $l, $l, $o>>], 50))), + ?assertEqual("<<\"hello\">>", lists:flatten(format("~p", [<<"hello">>], 50))), + ?assertEqual("<<104,101,108,108,111>>", lists:flatten(format("~w", [<<"hello">>], 50))), + ?assertEqual("<<1,2,3,4>>", lists:flatten(format("~p", [<<1, 2, 3, 4>>], 50))), + ?assertEqual([1,2,3,4], lists:flatten(format("~s", [<<1, 2, 3, 4>>], 50))), + ?assertEqual("hello", lists:flatten(format("~s", [<<"hello">>], 50))), + ?assertEqual("hello\nworld", lists:flatten(format("~s", [<<"hello\nworld">>], 50))), + ?assertEqual("<<\"hello\\nworld\">>", lists:flatten(format("~p", [<<"hello\nworld">>], 50))), + ?assertEqual("<<\"\\\"hello world\\\"\">>", lists:flatten(format("~p", [<<"\"hello world\"">>], 50))), + ?assertEqual("<<\"hello\\\\world\">>", lists:flatten(format("~p", [<<"hello\\world">>], 50))), + ?assertEqual("<<\"hello\\\\\world\">>", lists:flatten(format("~p", [<<"hello\\\world">>], 50))), + ?assertEqual("<<\"hello\\\\\\\\world\">>", lists:flatten(format("~p", [<<"hello\\\\world">>], 50))), + ?assertEqual("<<\"hello\\bworld\">>", lists:flatten(format("~p", [<<"hello\bworld">>], 50))), + ?assertEqual("<<\"hello\\tworld\">>", lists:flatten(format("~p", [<<"hello\tworld">>], 50))), + ?assertEqual("<<\"hello\\nworld\">>", lists:flatten(format("~p", [<<"hello\nworld">>], 50))), + ?assertEqual("<<\"hello\\rworld\">>", lists:flatten(format("~p", [<<"hello\rworld">>], 50))), + ?assertEqual("<<\"hello\\eworld\">>", lists:flatten(format("~p", [<<"hello\eworld">>], 50))), + ?assertEqual("<<\"hello\\fworld\">>", lists:flatten(format("~p", [<<"hello\fworld">>], 50))), + ?assertEqual("<<\"hello\\vworld\">>", lists:flatten(format("~p", [<<"hello\vworld">>], 50))), + ?assertEqual(" hello", lists:flatten(format("~10s", [<<"hello">>], 50))), + ?assertEqual("[a]", lists:flatten(format("~s", [<<"[a]">>], 50))), + ?assertEqual("[a]", lists:flatten(format("~s", [[<<"[a]">>]], 50))), + + ok. + +bitstring_printing_test() -> + ?assertEqual("<<1,2,3,1:7>>", lists:flatten(format("~p", + [<<1, 2, 3, 1:7>>], 100))), + ?assertEqual("<<1:7>>", lists:flatten(format("~p", + [<<1:7>>], 100))), + ?assertEqual("<<1,2,3,...>>", lists:flatten(format("~p", + [<<1, 2, 3, 1:7>>], 12))), + ?assertEqual("<<1,2,3,...>>", lists:flatten(format("~p", + [<<1, 2, 3, 1:7>>], 13))), + ?assertEqual("<<1,2,3,1:7>>", lists:flatten(format("~p", + [<<1, 2, 3, 1:7>>], 14))), + ?assertEqual("<<..>>", lists:flatten(format("~p", [<<1:7>>], 0))), + ?assertEqual("<<...>>", lists:flatten(format("~p", [<<1:7>>], 1))), + ?assertEqual("[<<1>>,<<2>>]", lists:flatten(format("~p", [[<<1>>, <<2>>]], + 100))), + ?assertEqual("{<<1:7>>}", lists:flatten(format("~p", [{<<1:7>>}], 50))), + ok. + +list_printing_test() -> + ?assertEqual("[]", lists:flatten(format("~p", [[]], 50))), + ?assertEqual("[]", lists:flatten(format("~w", [[]], 50))), + ?assertEqual("", lists:flatten(format("~s", [[]], 50))), + ?assertEqual("...", lists:flatten(format("~s", [[]], -1))), + ?assertEqual("[[]]", lists:flatten(format("~p", [[[]]], 50))), + ?assertEqual("[13,11,10,8,5,4]", lists:flatten(format("~p", [[13,11,10,8,5,4]], 50))), + ?assertEqual("\"\\rabc\"", lists:flatten(format("~p", [[13,$a, $b, $c]], 50))), + ?assertEqual("[1,2,3|4]", lists:flatten(format("~p", [[1, 2, 3|4]], 50))), + ?assertEqual("[...]", lists:flatten(format("~p", [[1, 2, 3,4]], 4))), + ?assertEqual("[1,...]", lists:flatten(format("~p", [[1, 2, 3, 4]], 6))), + ?assertEqual("[1,...]", lists:flatten(format("~p", [[1, 2, 3, 4]], 7))), + ?assertEqual("[1,2,...]", lists:flatten(format("~p", [[1, 2, 3, 4]], 8))), + ?assertEqual("[1|4]", lists:flatten(format("~p", [[1|4]], 50))), + ?assertEqual("[1]", lists:flatten(format("~p", [[1]], 50))), + ?assertError(badarg, lists:flatten(format("~s", [[1|4]], 50))), + ?assertEqual("\"hello...\"", lists:flatten(format("~p", ["hello world"], 10))), + ?assertEqual("hello w...", lists:flatten(format("~s", ["hello world"], 10))), + ?assertEqual("hello world\r\n", lists:flatten(format("~s", ["hello world\r\n"], 50))), + ?assertEqual("\rhello world\r\n", lists:flatten(format("~s", ["\rhello world\r\n"], 50))), + ?assertEqual("\"\\rhello world\\r\\n\"", lists:flatten(format("~p", ["\rhello world\r\n"], 50))), + ?assertEqual("[13,104,101,108,108,111,32,119,111,114,108,100,13,10]", lists:flatten(format("~w", ["\rhello world\r\n"], 60))), + ?assertEqual("...", lists:flatten(format("~s", ["\rhello world\r\n"], 3))), + ?assertEqual("[22835963083295358096932575511191922182123945984,...]", + lists:flatten(format("~p", [ + [22835963083295358096932575511191922182123945984, + 22835963083295358096932575511191922182123945984]], 9))), + ?assertEqual("[22835963083295358096932575511191922182123945984,...]", + lists:flatten(format("~p", [ + [22835963083295358096932575511191922182123945984, + 22835963083295358096932575511191922182123945984]], 53))), + %%improper list + ?assertEqual("[1,2,3|4]", lists:flatten(format("~P", [[1|[2|[3|4]]], 5], 50))), + ?assertEqual("[1|1]", lists:flatten(format("~P", [[1|1], 5], 50))), + ?assertEqual("[9|9]", lists:flatten(format("~p", [[9|9]], 50))), + ok. + +iolist_printing_test() -> + ?assertEqual("iolist: HelloIamaniolist", + lists:flatten(format("iolist: ~s", [[$H, $e, $l, $l, $o, "I", ["am", [<<"an">>], [$i, $o, $l, $i, $s, $t]]]], 1000))), + ?assertEqual("123...", + lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 6))), + ?assertEqual("123456...", + lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 9))), + ?assertEqual("123456789H...", + lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 13))), + ?assertEqual("123456789HellIamaniolist", + lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 30))), + + ok. + +tuple_printing_test() -> + ?assertEqual("{}", lists:flatten(format("~p", [{}], 50))), + ?assertEqual("{}", lists:flatten(format("~w", [{}], 50))), + ?assertError(badarg, lists:flatten(format("~s", [{}], 50))), + ?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 1))), + ?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 2))), + ?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 3))), + ?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 4))), + ?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 5))), + ?assertEqual("{foo,...}", lists:flatten(format("~p", [{foo,bar}], 6))), + ?assertEqual("{foo,...}", lists:flatten(format("~p", [{foo,bar}], 7))), + ?assertEqual("{foo,...}", lists:flatten(format("~p", [{foo,bar}], 9))), + ?assertEqual("{foo,bar}", lists:flatten(format("~p", [{foo,bar}], 10))), + ?assertEqual("{22835963083295358096932575511191922182123945984,...}", + lists:flatten(format("~w", [ + {22835963083295358096932575511191922182123945984, + 22835963083295358096932575511191922182123945984}], 10))), + ?assertEqual("{22835963083295358096932575511191922182123945984,...}", + lists:flatten(format("~w", [ + {22835963083295358096932575511191922182123945984, + bar}], 10))), + ?assertEqual("{22835963083295358096932575511191922182123945984,...}", + lists:flatten(format("~w", [ + {22835963083295358096932575511191922182123945984, + 22835963083295358096932575511191922182123945984}], 53))), + ok. + +map_printing_test() -> + case erlang:is_builtin(erlang, is_map, 1) of + true -> + ?assertEqual("#{}", lists:flatten(format("~p", [maps:new()], 50))), + ?assertEqual("#{}", lists:flatten(format("~p", [maps:new()], 3))), + ?assertEqual("#{}", lists:flatten(format("~w", [maps:new()], 50))), + ?assertError(badarg, lists:flatten(format("~s", [maps:new()], 50))), + ?assertEqual("#{...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 1))), + ?assertEqual("#{...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 6))), + ?assertEqual("#{bar => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 7))), + ?assertEqual("#{bar => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 9))), + ?assertEqual("#{bar => foo}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 10))), + ?assertEqual("#{bar => ...,...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 9))), + ?assertEqual("#{bar => foo,...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 10))), + ?assertEqual("#{bar => foo,...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 17))), + ?assertEqual("#{bar => foo,foo => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 18))), + ?assertEqual("#{bar => foo,foo => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 19))), + ?assertEqual("#{bar => foo,foo => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 20))), + ?assertEqual("#{bar => foo,foo => bar}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 21))), + ?assertEqual("#{22835963083295358096932575511191922182123945984 => ...}", + lists:flatten(format("~w", [ + maps:from_list([{22835963083295358096932575511191922182123945984, + 22835963083295358096932575511191922182123945984}])], 10))), + ?assertEqual("#{22835963083295358096932575511191922182123945984 => ...}", + lists:flatten(format("~w", [ + maps:from_list([{22835963083295358096932575511191922182123945984, + bar}])], 10))), + ?assertEqual("#{22835963083295358096932575511191922182123945984 => ...}", + lists:flatten(format("~w", [ + maps:from_list([{22835963083295358096932575511191922182123945984, + bar}])], 53))), + ?assertEqual("#{22835963083295358096932575511191922182123945984 => bar}", + lists:flatten(format("~w", [ + maps:from_list([{22835963083295358096932575511191922182123945984, + bar}])], 54))), + ok; + false -> + ok + end. + +unicode_test() -> + ?assertEqual([231,167,129], lists:flatten(format("~s", [<<231,167,129>>], 50))), + ?assertEqual([31169], lists:flatten(format("~ts", [<<231,167,129>>], 50))), + ok. + +depth_limit_test() -> + ?assertEqual("{...}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 1], 50))), + ?assertEqual("{a,...}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 2], 50))), + ?assertEqual("{a,[...]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 3], 50))), + ?assertEqual("{a,[b|...]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 4], 50))), + ?assertEqual("{a,[b,[...]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 5], 50))), + ?assertEqual("{a,[b,[c|...]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 6], 50))), + ?assertEqual("{a,[b,[c,[...]]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 7], 50))), + ?assertEqual("{a,[b,[c,[d]]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 8], 50))), + ?assertEqual("{a,[b,[c,[d]]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 9], 50))), + + ?assertEqual("{a,{...}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 3], 50))), + ?assertEqual("{a,{b,...}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 4], 50))), + ?assertEqual("{a,{b,{...}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 5], 50))), + ?assertEqual("{a,{b,{c,...}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 6], 50))), + ?assertEqual("{a,{b,{c,{...}}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 7], 50))), + ?assertEqual("{a,{b,{c,{d}}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 8], 50))), + + case erlang:is_builtin(erlang, is_map, 1) of + true -> + ?assertEqual("#{a => #{...}}", + lists:flatten(format("~P", + [maps:from_list([{a, maps:from_list([{b, maps:from_list([{c, d}])}])}]), 2], 50))), + ?assertEqual("#{a => #{b => #{...}}}", + lists:flatten(format("~P", + [maps:from_list([{a, maps:from_list([{b, maps:from_list([{c, d}])}])}]), 3], 50))), + ?assertEqual("#{a => #{b => #{c => d}}}", + lists:flatten(format("~P", + [maps:from_list([{a, maps:from_list([{b, maps:from_list([{c, d}])}])}]), 4], 50))), + + ?assertEqual("#{}", lists:flatten(format("~P", [maps:new(), 1], 50))), + ?assertEqual("#{...}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 1], 50))), + ?assertEqual("#{1 => 1,...}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 2], 50))), + ?assertEqual("#{1 => 1,2 => 2,...}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 3], 50))), + ?assertEqual("#{1 => 1,2 => 2,3 => 3}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 4], 50))), + + ok; + false -> + ok + end, + + ?assertEqual("{\"a\",[...]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 3], 50))), + ?assertEqual("{\"a\",[\"b\",[[...]|...]]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 6], 50))), + ?assertEqual("{\"a\",[\"b\",[\"c\",[\"d\"]]]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 9], 50))), + + ?assertEqual("[...]", lists:flatten(format("~P", [[1, 2, 3], 1], 50))), + ?assertEqual("[1|...]", lists:flatten(format("~P", [[1, 2, 3], 2], 50))), + ?assertEqual("[1,2|...]", lists:flatten(format("~P", [[1, 2, 3], 3], 50))), + ?assertEqual("[1,2,3]", lists:flatten(format("~P", [[1, 2, 3], 4], 50))), + + ?assertEqual("{1,...}", lists:flatten(format("~P", [{1, 2, 3}, 2], 50))), + ?assertEqual("{1,2,...}", lists:flatten(format("~P", [{1, 2, 3}, 3], 50))), + ?assertEqual("{1,2,3}", lists:flatten(format("~P", [{1, 2, 3}, 4], 50))), + + ?assertEqual("{1,...}", lists:flatten(format("~P", [{1, 2, 3}, 2], 50))), + ?assertEqual("[1,2|...]", lists:flatten(format("~P", [[1, 2, <<3>>], 3], 50))), + ?assertEqual("[1,2,<<...>>]", lists:flatten(format("~P", [[1, 2, <<3>>], 4], 50))), + ?assertEqual("[1,2,<<3>>]", lists:flatten(format("~P", [[1, 2, <<3>>], 5], 50))), + + ?assertEqual("<<...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 1], 50))), + ?assertEqual("<<0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 2], 50))), + ?assertEqual("<<0,0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 3], 50))), + ?assertEqual("<<0,0,0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 4], 50))), + ?assertEqual("<<0,0,0,0>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 5], 50))), + + %% this is a seriously weird edge case + ?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 2], 50))), + ?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 3], 50))), + ?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 4], 50))), + ?assertEqual("<<32,32,32,0>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 5], 50))), + ?assertEqual("<<32,32,32,0>>", lists:flatten(format("~p", [<<32, 32, 32, 0>>], 50))), + + %% depth limiting for some reason works in 4 byte chunks on printable binaries? + ?assertEqual("<<\"hell\"...>>", lists:flatten(format("~P", [<<"hello world">>, 2], 50))), + ?assertEqual("<<\"abcd\"...>>", lists:flatten(format("~P", [<<$a, $b, $c, $d, $e, 0>>, 2], 50))), + + %% I don't even know... + ?assertEqual("<<>>", lists:flatten(format("~P", [<<>>, 1], 50))), + ?assertEqual("<<>>", lists:flatten(format("~W", [<<>>, 1], 50))), + + ?assertEqual("{abc,<<\"abc\\\"\">>}", lists:flatten(format("~P", [{abc,<<"abc\"">>}, 4], 50))), + + ok. + +print_terms_without_format_string_test() -> + ?assertError(badarg, format({hello, world}, [], 50)), + ?assertError(badarg, format([{google, bomb}], [], 50)), + ?assertError(badarg, format([$h,$e,$l,$l,$o, 3594], [], 50)), + ?assertEqual("helloworld", lists:flatten(format([$h,$e,$l,$l,$o, "world"], [], 50))), + ?assertEqual("hello", lists:flatten(format(<<"hello">>, [], 50))), + ?assertEqual("hello", lists:flatten(format('hello', [], 50))), + ?assertError(badarg, format(<<1, 2, 3, 1:7>>, [], 100)), + ?assertError(badarg, format(65535, [], 50)), + ok. + +improper_io_list_test() -> + ?assertEqual(">hello", lists:flatten(format('~s', [[$>|<<"hello">>]], 50))), + ?assertEqual(">hello", lists:flatten(format('~ts', [[$>|<<"hello">>]], 50))), + ?assertEqual("helloworld", lists:flatten(format('~ts', [[<<"hello">>|<<"world">>]], 50))), + ok. + +-endif. \ No newline at end of file diff --git a/src/couch_log_trunc_io_fmt.erl b/src/couch_log_trunc_io_fmt.erl new file mode 100644 index 0000000..7f3ba37 --- /dev/null +++ b/src/couch_log_trunc_io_fmt.erl @@ -0,0 +1,547 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2011-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% fork of io_lib_format that uses trunc_io to protect against large terms +%% +%% Renamed to couch_log_format to avoid naming collision with +%% lager_Format. +-module(couch_log_trunc_io_fmt). + + +-export([format/3, format/4]). + +-record(options, { + chomp = false :: boolean() + }). + +format(FmtStr, Args, MaxLen) -> + format(FmtStr, Args, MaxLen, []). + +format([], [], _, _) -> + ""; +format(FmtStr, Args, MaxLen, Opts) when is_atom(FmtStr) -> + format(atom_to_list(FmtStr), Args, MaxLen, Opts); +format(FmtStr, Args, MaxLen, Opts) when is_binary(FmtStr) -> + format(binary_to_list(FmtStr), Args, MaxLen, Opts); +format(FmtStr, Args, MaxLen, Opts) when is_list(FmtStr) -> + case couch_log_util:string_p(FmtStr) of + true -> + Options = make_options(Opts, #options{}), + Cs = collect(FmtStr, Args), + {Cs2, MaxLen2} = build(Cs, [], MaxLen, Options), + %% count how many terms remain + {Count, StrLen} = lists:foldl( + fun({_C, _As, _F, _Adj, _P, _Pad, _Enc}, {Terms, Chars}) -> + {Terms + 1, Chars}; + (_, {Terms, Chars}) -> + {Terms, Chars + 1} + end, {0, 0}, Cs2), + build2(Cs2, Count, MaxLen2 - StrLen); + false -> + erlang:error(badarg) + end; +format(_FmtStr, _Args, _MaxLen, _Opts) -> + erlang:error(badarg). + +collect([$~|Fmt0], Args0) -> + {C,Fmt1,Args1} = collect_cseq(Fmt0, Args0), + [C|collect(Fmt1, Args1)]; +collect([C|Fmt], Args) -> + [C|collect(Fmt, Args)]; +collect([], []) -> []. + +collect_cseq(Fmt0, Args0) -> + {F,Ad,Fmt1,Args1} = field_width(Fmt0, Args0), + {P,Fmt2,Args2} = precision(Fmt1, Args1), + {Pad,Fmt3,Args3} = pad_char(Fmt2, Args2), + {Encoding,Fmt4,Args4} = encoding(Fmt3, Args3), + {C,As,Fmt5,Args5} = collect_cc(Fmt4, Args4), + {{C,As,F,Ad,P,Pad,Encoding},Fmt5,Args5}. + +encoding([$t|Fmt],Args) -> + {unicode,Fmt,Args}; +encoding(Fmt,Args) -> + {latin1,Fmt,Args}. + +field_width([$-|Fmt0], Args0) -> + {F,Fmt,Args} = field_value(Fmt0, Args0), + field_width(-F, Fmt, Args); +field_width(Fmt0, Args0) -> + {F,Fmt,Args} = field_value(Fmt0, Args0), + field_width(F, Fmt, Args). + +field_width(F, Fmt, Args) when F < 0 -> + {-F,left,Fmt,Args}; +field_width(F, Fmt, Args) when F >= 0 -> + {F,right,Fmt,Args}. + +precision([$.|Fmt], Args) -> + field_value(Fmt, Args); +precision(Fmt, Args) -> + {none,Fmt,Args}. + +field_value([$*|Fmt], [A|Args]) when is_integer(A) -> + {A,Fmt,Args}; +field_value([C|Fmt], Args) when is_integer(C), C >= $0, C =< $9 -> + field_value([C|Fmt], Args, 0); +field_value(Fmt, Args) -> + {none,Fmt,Args}. + +field_value([C|Fmt], Args, F) when is_integer(C), C >= $0, C =< $9 -> + field_value(Fmt, Args, 10*F + (C - $0)); +field_value(Fmt, Args, F) -> %Default case + {F,Fmt,Args}. + +pad_char([$.,$*|Fmt], [Pad|Args]) -> {Pad,Fmt,Args}; +pad_char([$.,Pad|Fmt], Args) -> {Pad,Fmt,Args}; +pad_char(Fmt, Args) -> {$\s,Fmt,Args}. + +%% collect_cc([FormatChar], [Argument]) -> +%% {Control,[ControlArg],[FormatChar],[Arg]}. +%% Here we collect the argments for each control character. +%% Be explicit to cause failure early. + +collect_cc([$w|Fmt], [A|Args]) -> {$w,[A],Fmt,Args}; +collect_cc([$p|Fmt], [A|Args]) -> {$p,[A],Fmt,Args}; +collect_cc([$W|Fmt], [A,Depth|Args]) -> {$W,[A,Depth],Fmt,Args}; +collect_cc([$P|Fmt], [A,Depth|Args]) -> {$P,[A,Depth],Fmt,Args}; +collect_cc([$s|Fmt], [A|Args]) -> {$s,[A],Fmt,Args}; +collect_cc([$e|Fmt], [A|Args]) -> {$e,[A],Fmt,Args}; +collect_cc([$f|Fmt], [A|Args]) -> {$f,[A],Fmt,Args}; +collect_cc([$g|Fmt], [A|Args]) -> {$g,[A],Fmt,Args}; +collect_cc([$b|Fmt], [A|Args]) -> {$b,[A],Fmt,Args}; +collect_cc([$B|Fmt], [A|Args]) -> {$B,[A],Fmt,Args}; +collect_cc([$x|Fmt], [A,Prefix|Args]) -> {$x,[A,Prefix],Fmt,Args}; +collect_cc([$X|Fmt], [A,Prefix|Args]) -> {$X,[A,Prefix],Fmt,Args}; +collect_cc([$+|Fmt], [A|Args]) -> {$+,[A],Fmt,Args}; +collect_cc([$#|Fmt], [A|Args]) -> {$#,[A],Fmt,Args}; +collect_cc([$c|Fmt], [A|Args]) -> {$c,[A],Fmt,Args}; +collect_cc([$~|Fmt], Args) when is_list(Args) -> {$~,[],Fmt,Args}; +collect_cc([$n|Fmt], Args) when is_list(Args) -> {$n,[],Fmt,Args}; +collect_cc([$i|Fmt], [A|Args]) -> {$i,[A],Fmt,Args}. + + +%% build([Control], Pc, Indentation) -> [Char]. +%% Interpret the control structures. Count the number of print +%% remaining and only calculate indentation when necessary. Must also +%% be smart when calculating indentation for characters in format. + +build([{$n, _, _, _, _, _, _}], Acc, MaxLen, #options{chomp=true}) -> + %% trailing ~n, ignore + {lists:reverse(Acc), MaxLen}; +build([{C,As,F,Ad,P,Pad,Enc}|Cs], Acc, MaxLen, O) -> + {S, MaxLen2} = control(C, As, F, Ad, P, Pad, Enc, MaxLen), + build(Cs, [S|Acc], MaxLen2, O); +build([$\n], Acc, MaxLen, #options{chomp=true}) -> + %% trailing \n, ignore + {lists:reverse(Acc), MaxLen}; +build([$\n|Cs], Acc, MaxLen, O) -> + build(Cs, [$\n|Acc], MaxLen - 1, O); +build([$\t|Cs], Acc, MaxLen, O) -> + build(Cs, [$\t|Acc], MaxLen - 1, O); +build([C|Cs], Acc, MaxLen, O) -> + build(Cs, [C|Acc], MaxLen - 1, O); +build([], Acc, MaxLen, _O) -> + {lists:reverse(Acc), MaxLen}. + +build2([{C,As,F,Ad,P,Pad,Enc}|Cs], Count, MaxLen) -> + {S, Len} = control2(C, As, F, Ad, P, Pad, Enc, MaxLen div Count), + [S|build2(Cs, Count - 1, MaxLen - Len)]; +build2([C|Cs], Count, MaxLen) -> + [C|build2(Cs, Count, MaxLen)]; +build2([], _, _) -> []. + +%% control(FormatChar, [Argument], FieldWidth, Adjust, Precision, PadChar, +%% Indentation) -> [Char] +%% This is the main dispatch function for the various formatting commands. +%% Field widths and precisions have already been calculated. + +control($e, [A], F, Adj, P, Pad, _Enc, L) when is_float(A) -> + Res = fwrite_e(A, F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($f, [A], F, Adj, P, Pad, _Enc, L) when is_float(A) -> + Res = fwrite_f(A, F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($g, [A], F, Adj, P, Pad, _Enc, L) when is_float(A) -> + Res = fwrite_g(A, F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($b, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + Res = unprefixed_integer(A, F, Adj, base(P), Pad, true), + {Res, L - lists:flatlength(Res)}; +control($B, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + Res = unprefixed_integer(A, F, Adj, base(P), Pad, false), + {Res, L - lists:flatlength(Res)}; +control($x, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A), + is_atom(Prefix) -> + Res = prefixed_integer(A, F, Adj, base(P), Pad, atom_to_list(Prefix), true), + {Res, L - lists:flatlength(Res)}; +control($x, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + true = io_lib:deep_char_list(Prefix), %Check if Prefix a character list + Res = prefixed_integer(A, F, Adj, base(P), Pad, Prefix, true), + {Res, L - lists:flatlength(Res)}; +control($X, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A), + is_atom(Prefix) -> + Res = prefixed_integer(A, F, Adj, base(P), Pad, atom_to_list(Prefix), false), + {Res, L - lists:flatlength(Res)}; +control($X, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + true = io_lib:deep_char_list(Prefix), %Check if Prefix a character list + Res = prefixed_integer(A, F, Adj, base(P), Pad, Prefix, false), + {Res, L - lists:flatlength(Res)}; +control($+, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + Base = base(P), + Prefix = [integer_to_list(Base), $#], + Res = prefixed_integer(A, F, Adj, Base, Pad, Prefix, true), + {Res, L - lists:flatlength(Res)}; +control($#, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + Base = base(P), + Prefix = [integer_to_list(Base), $#], + Res = prefixed_integer(A, F, Adj, Base, Pad, Prefix, false), + {Res, L - lists:flatlength(Res)}; +control($c, [A], F, Adj, P, Pad, unicode, L) when is_integer(A) -> + Res = char(A, F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($c, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> + Res = char(A band 255, F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($~, [], F, Adj, P, Pad, _Enc, L) -> + Res = char($~, F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($n, [], F, Adj, P, Pad, _Enc, L) -> + Res = newline(F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control($i, [_A], _F, _Adj, _P, _Pad, _Enc, L) -> + {[], L}; +control($s, [A], F, Adj, P, Pad, _Enc, L) when is_atom(A) -> + Res = string(atom_to_list(A), F, Adj, P, Pad), + {Res, L - lists:flatlength(Res)}; +control(C, A, F, Adj, P, Pad, Enc, L) -> + %% save this for later - these are all the 'large' terms + {{C, A, F, Adj, P, Pad, Enc}, L}. + +control2($w, [A], F, Adj, P, Pad, _Enc, L) -> + Term = couch_log_trunc_io:fprint(A, L, [{lists_as_strings, false}]), + Res = term(Term, F, Adj, P, Pad), + {Res, lists:flatlength(Res)}; +control2($p, [A], _F, _Adj, _P, _Pad, _Enc, L) -> + Term = couch_log_trunc_io:fprint(A, L, [{lists_as_strings, true}]), + {Term, lists:flatlength(Term)}; +control2($W, [A,Depth], F, Adj, P, Pad, _Enc, L) when is_integer(Depth) -> + Term = couch_log_trunc_io:fprint(A, L, [{depth, Depth}, {lists_as_strings, false}]), + Res = term(Term, F, Adj, P, Pad), + {Res, lists:flatlength(Res)}; +control2($P, [A,Depth], _F, _Adj, _P, _Pad, _Enc, L) when is_integer(Depth) -> + Term = couch_log_trunc_io:fprint(A, L, [{depth, Depth}, {lists_as_strings, true}]), + {Term, lists:flatlength(Term)}; +control2($s, [L0], F, Adj, P, Pad, latin1, L) -> + List = couch_log_trunc_io:fprint(iolist_to_chars(L0), L, [{force_strings, true}]), + Res = string(List, F, Adj, P, Pad), + {Res, lists:flatlength(Res)}; +control2($s, [L0], F, Adj, P, Pad, unicode, L) -> + List = couch_log_trunc_io:fprint(cdata_to_chars(L0), L, [{force_strings, true}]), + Res = uniconv(string(List, F, Adj, P, Pad)), + {Res, lists:flatlength(Res)}. + +iolist_to_chars([C|Cs]) when is_integer(C), C >= $\000, C =< $\377 -> + [C | iolist_to_chars(Cs)]; +iolist_to_chars([I|Cs]) -> + [iolist_to_chars(I) | iolist_to_chars(Cs)]; +iolist_to_chars([]) -> + []; +iolist_to_chars(B) when is_binary(B) -> + binary_to_list(B). + +cdata_to_chars([C|Cs]) when is_integer(C), C >= $\000 -> + [C | cdata_to_chars(Cs)]; +cdata_to_chars([I|Cs]) -> + [cdata_to_chars(I) | cdata_to_chars(Cs)]; +cdata_to_chars([]) -> + []; +cdata_to_chars(B) when is_binary(B) -> + case catch unicode:characters_to_list(B) of + L when is_list(L) -> L; + _ -> binary_to_list(B) + end. + +make_options([], Options) -> + Options; +make_options([{chomp, Bool}|T], Options) when is_boolean(Bool) -> + make_options(T, Options#options{chomp=Bool}). + +-ifdef(UNICODE_AS_BINARIES). +uniconv(C) -> + unicode:characters_to_binary(C,unicode). +-else. +uniconv(C) -> + C. +-endif. +%% Default integer base +base(none) -> + 10; +base(B) when is_integer(B) -> + B. + +%% term(TermList, Field, Adjust, Precision, PadChar) +%% Output the characters in a term. +%% Adjust the characters within the field if length less than Max padding +%% with PadChar. + +term(T, none, _Adj, none, _Pad) -> T; +term(T, none, Adj, P, Pad) -> term(T, P, Adj, P, Pad); +term(T, F, Adj, P0, Pad) -> + L = lists:flatlength(T), + P = case P0 of none -> erlang:min(L, F); _ -> P0 end, + if + L > P -> + adjust(chars($*, P), chars(Pad, F-P), Adj); + F >= P -> + adjust(T, chars(Pad, F-L), Adj) + end. + +%% fwrite_e(Float, Field, Adjust, Precision, PadChar) + +fwrite_e(Fl, none, Adj, none, Pad) -> %Default values + fwrite_e(Fl, none, Adj, 6, Pad); +fwrite_e(Fl, none, _Adj, P, _Pad) when P >= 2 -> + float_e(Fl, float_data(Fl), P); +fwrite_e(Fl, F, Adj, none, Pad) -> + fwrite_e(Fl, F, Adj, 6, Pad); +fwrite_e(Fl, F, Adj, P, Pad) when P >= 2 -> + term(float_e(Fl, float_data(Fl), P), F, Adj, F, Pad). + +float_e(Fl, Fd, P) when Fl < 0.0 -> %Negative numbers + [$-|float_e(-Fl, Fd, P)]; +float_e(_Fl, {Ds,E}, P) -> + case float_man(Ds, 1, P-1) of + {[$0|Fs],true} -> [[$1|Fs]|float_exp(E)]; + {Fs,false} -> [Fs|float_exp(E-1)] + end. + +%% float_man([Digit], Icount, Dcount) -> {[Chars],CarryFlag}. +%% Generate the characters in the mantissa from the digits with Icount +%% characters before the '.' and Dcount decimals. Handle carry and let +%% caller decide what to do at top. + +float_man(Ds, 0, Dc) -> + {Cs,C} = float_man(Ds, Dc), + {[$.|Cs],C}; +float_man([D|Ds], I, Dc) -> + case float_man(Ds, I-1, Dc) of + {Cs,true} when D =:= $9 -> {[$0|Cs],true}; + {Cs,true} -> {[D+1|Cs],false}; + {Cs,false} -> {[D|Cs],false} + end; +float_man([], I, Dc) -> %Pad with 0's + {string:chars($0, I, [$.|string:chars($0, Dc)]),false}. + +float_man([D|_], 0) when D >= $5 -> {[],true}; +float_man([_|_], 0) -> {[],false}; +float_man([D|Ds], Dc) -> + case float_man(Ds, Dc-1) of + {Cs,true} when D =:= $9 -> {[$0|Cs],true}; + {Cs,true} -> {[D+1|Cs],false}; + {Cs,false} -> {[D|Cs],false} + end; +float_man([], Dc) -> {string:chars($0, Dc),false}. %Pad with 0's + +%% float_exp(Exponent) -> [Char]. +%% Generate the exponent of a floating point number. Always include sign. + +float_exp(E) when E >= 0 -> + [$e,$+|integer_to_list(E)]; +float_exp(E) -> + [$e|integer_to_list(E)]. + +%% fwrite_f(FloatData, Field, Adjust, Precision, PadChar) + +fwrite_f(Fl, none, Adj, none, Pad) -> %Default values + fwrite_f(Fl, none, Adj, 6, Pad); +fwrite_f(Fl, none, _Adj, P, _Pad) when P >= 1 -> + float_f(Fl, float_data(Fl), P); +fwrite_f(Fl, F, Adj, none, Pad) -> + fwrite_f(Fl, F, Adj, 6, Pad); +fwrite_f(Fl, F, Adj, P, Pad) when P >= 1 -> + term(float_f(Fl, float_data(Fl), P), F, Adj, F, Pad). + +float_f(Fl, Fd, P) when Fl < 0.0 -> + [$-|float_f(-Fl, Fd, P)]; +float_f(Fl, {Ds,E}, P) when E =< 0 -> + float_f(Fl, {string:chars($0, -E+1, Ds),1}, P); %Prepend enough 0's +float_f(_Fl, {Ds,E}, P) -> + case float_man(Ds, E, P) of + {Fs,true} -> "1" ++ Fs; %Handle carry + {Fs,false} -> Fs + end. + +%% float_data([FloatChar]) -> {[Digit],Exponent} + +float_data(Fl) -> + float_data(float_to_list(Fl), []). + +float_data([$e|E], Ds) -> + {lists:reverse(Ds),list_to_integer(E)+1}; +float_data([D|Cs], Ds) when D >= $0, D =< $9 -> + float_data(Cs, [D|Ds]); +float_data([_|Cs], Ds) -> + float_data(Cs, Ds). + +%% fwrite_g(Float, Field, Adjust, Precision, PadChar) +%% Use the f form if Float is >= 0.1 and < 1.0e4, +%% and the prints correctly in the f form, else the e form. +%% Precision always means the # of significant digits. + +fwrite_g(Fl, F, Adj, none, Pad) -> + fwrite_g(Fl, F, Adj, 6, Pad); +fwrite_g(Fl, F, Adj, P, Pad) when P >= 1 -> + A = abs(Fl), + E = if A < 1.0e-1 -> -2; + A < 1.0e0 -> -1; + A < 1.0e1 -> 0; + A < 1.0e2 -> 1; + A < 1.0e3 -> 2; + A < 1.0e4 -> 3; + true -> fwrite_f + end, + if P =< 1, E =:= -1; + P-1 > E, E >= -1 -> + fwrite_f(Fl, F, Adj, P-1-E, Pad); + P =< 1 -> + fwrite_e(Fl, F, Adj, 2, Pad); + true -> + fwrite_e(Fl, F, Adj, P, Pad) + end. + + +%% string(String, Field, Adjust, Precision, PadChar) + +string(S, none, _Adj, none, _Pad) -> S; +string(S, F, Adj, none, Pad) -> + string_field(S, F, Adj, lists:flatlength(S), Pad); +string(S, none, _Adj, P, Pad) -> + string_field(S, P, left, lists:flatlength(S), Pad); +string(S, F, Adj, P, Pad) when F >= P -> + N = lists:flatlength(S), + if F > P -> + if N > P -> + adjust(flat_trunc(S, P), chars(Pad, F-P), Adj); + N < P -> + adjust([S|chars(Pad, P-N)], chars(Pad, F-P), Adj); + true -> % N == P + adjust(S, chars(Pad, F-P), Adj) + end; + true -> % F == P + string_field(S, F, Adj, N, Pad) + end. + +string_field(S, F, _Adj, N, _Pad) when N > F -> + flat_trunc(S, F); +string_field(S, F, Adj, N, Pad) when N < F -> + adjust(S, chars(Pad, F-N), Adj); +string_field(S, _, _, _, _) -> % N == F + S. + +%% unprefixed_integer(Int, Field, Adjust, Base, PadChar, Lowercase) +%% -> [Char]. + +unprefixed_integer(Int, F, Adj, Base, Pad, Lowercase) + when Base >= 2, Base =< 1+$Z-$A+10 -> + if Int < 0 -> + S = cond_lowercase(erlang:integer_to_list(-Int, Base), Lowercase), + term([$-|S], F, Adj, none, Pad); + true -> + S = cond_lowercase(erlang:integer_to_list(Int, Base), Lowercase), + term(S, F, Adj, none, Pad) + end. + +%% prefixed_integer(Int, Field, Adjust, Base, PadChar, Prefix, Lowercase) +%% -> [Char]. + +prefixed_integer(Int, F, Adj, Base, Pad, Prefix, Lowercase) + when Base >= 2, Base =< 1+$Z-$A+10 -> + if Int < 0 -> + S = cond_lowercase(erlang:integer_to_list(-Int, Base), Lowercase), + term([$-,Prefix|S], F, Adj, none, Pad); + true -> + S = cond_lowercase(erlang:integer_to_list(Int, Base), Lowercase), + term([Prefix|S], F, Adj, none, Pad) + end. + +%% char(Char, Field, Adjust, Precision, PadChar) -> [Char]. + +char(C, none, _Adj, none, _Pad) -> [C]; +char(C, F, _Adj, none, _Pad) -> chars(C, F); +char(C, none, _Adj, P, _Pad) -> chars(C, P); +char(C, F, Adj, P, Pad) when F >= P -> + adjust(chars(C, P), chars(Pad, F - P), Adj). + +%% newline(Field, Adjust, Precision, PadChar) -> [Char]. + +newline(none, _Adj, _P, _Pad) -> "\n"; +newline(F, right, _P, _Pad) -> chars($\n, F). + +%% +%% Utilities +%% + +adjust(Data, [], _) -> Data; +adjust(Data, Pad, left) -> [Data|Pad]; +adjust(Data, Pad, right) -> [Pad|Data]. + +%% Flatten and truncate a deep list to at most N elements. +flat_trunc(List, N) when is_integer(N), N >= 0 -> + flat_trunc(List, N, []). + +flat_trunc(L, 0, R) when is_list(L) -> + lists:reverse(R); +flat_trunc([H|T], N, R) -> + flat_trunc(T, N-1, [H|R]); +flat_trunc([], _, R) -> + lists:reverse(R). + +%% A deep version of string:chars/2,3 + +chars(_C, 0) -> + []; +chars(C, 1) -> + [C]; +chars(C, 2) -> + [C,C]; +chars(C, 3) -> + [C,C,C]; +chars(C, N) when is_integer(N), (N band 1) =:= 0 -> + S = chars(C, N bsr 1), + [S|S]; +chars(C, N) when is_integer(N) -> + S = chars(C, N bsr 1), + [C,S|S]. + +%chars(C, N, Tail) -> +% [chars(C, N)|Tail]. + +%% Lowercase conversion + +cond_lowercase(String, true) -> + lowercase(String); +cond_lowercase(String,false) -> + String. + +lowercase([H|T]) when is_integer(H), H >= $A, H =< $Z -> + [(H-$A+$a)|lowercase(T)]; +lowercase([H|T]) -> + [H|lowercase(T)]; +lowercase([]) -> + []. \ No newline at end of file diff --git a/src/couch_log_util.erl b/src/couch_log_util.erl new file mode 100644 index 0000000..c8b8e54 --- /dev/null +++ b/src/couch_log_util.erl @@ -0,0 +1,149 @@ +% 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(couch_log_util). + + +-export([ + should_log/1, + iso8601_timestamp/0, + get_msg_id/0, + + level_to_integer/1, + level_to_atom/1, + level_to_string/1, + + string_p/1 +]). + + +-include("couch_log.hrl"). + + +-spec should_log(#log_entry{} | atom()) -> boolean(). +should_log(#log_entry{level = Level}) -> + should_log(Level); + +should_log(Level) -> + level_to_integer(Level) >= couch_log_config:get(level_int). + + +-spec iso8601_timestamp() -> string(). +iso8601_timestamp() -> + {_,_,Micro} = Now = os:timestamp(), + {{Year,Month,Date},{Hour,Minute,Second}} = calendar:now_to_datetime(Now), + Format = "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~6.10.0BZ", + io_lib:format(Format, [Year, Month, Date, Hour, Minute, Second, Micro]). + + +-spec get_msg_id() -> string(). +get_msg_id() -> + case erlang:get(nonce) of + undefined -> "--------"; + MsgId -> MsgId + end. + + +-spec level_to_integer(atom() | string() | integer()) -> integer(). +level_to_integer(L) when L >= 0, L =< 9 -> L; +level_to_integer(debug) -> 1; +level_to_integer(info) -> 2; +level_to_integer(notice) -> 3; +level_to_integer(warning) -> 4; +level_to_integer(warn) -> 4; +level_to_integer(error) -> 5; +level_to_integer(err) -> 5; +level_to_integer(critical) -> 6; +level_to_integer(crit) -> 6; +level_to_integer(alert) -> 7; +level_to_integer(emergency) -> 8; +level_to_integer(emerg) -> 8; +level_to_integer(none) -> 9; +level_to_integer("debug") -> 1; +level_to_integer("info") -> 2; +level_to_integer("notice") -> 3; +level_to_integer("warning") -> 4; +level_to_integer("warn") -> 4; +level_to_integer("error") -> 5; +level_to_integer("err") -> 5; +level_to_integer("critical") -> 6; +level_to_integer("crit") -> 6; +level_to_integer("alert") -> 7; +level_to_integer("emergency") -> 8; +level_to_integer("emerg") -> 8; +level_to_integer("none") -> 9; +level_to_integer("1") -> 1; +level_to_integer("2") -> 2; +level_to_integer("3") -> 3; +level_to_integer("4") -> 4; +level_to_integer("5") -> 5; +level_to_integer("6") -> 6; +level_to_integer("7") -> 7; +level_to_integer("8") -> 8; +level_to_integer("9") -> 9. + + +-spec level_to_atom(atom() | string() | integer()) -> atom(). +level_to_atom(L) when is_atom(L) -> L; +level_to_atom("1") -> debug; +level_to_atom("debug") -> debug; +level_to_atom("2") -> info; +level_to_atom("info") -> info; +level_to_atom("3") -> notice; +level_to_atom("notice") -> notice; +level_to_atom("4") -> warning; +level_to_atom("warning") -> warning; +level_to_atom("warn") -> warning; +level_to_atom("5") -> error; +level_to_atom("error") -> error; +level_to_atom("err") -> error; +level_to_atom("6") -> critical; +level_to_atom("critical") -> critical; +level_to_atom("crit") -> critical; +level_to_atom("7") -> alert; +level_to_atom("alert") -> alert; +level_to_atom("8") -> emergency; +level_to_atom("emergency") -> emergency; +level_to_atom("emerg") -> emergency; +level_to_atom("9") -> none; +level_to_atom("none") -> none; +level_to_atom(V) when is_integer(V) -> level_to_atom(integer_to_list(V)); +level_to_atom(V) when is_list(V) -> info. + + +level_to_string(L) when is_atom(L) -> atom_to_list(L); +level_to_string(L) -> atom_to_list(level_to_atom(L)). + + + +% From error_logger_file_h via lager_stdlib.erl +string_p([]) -> + false; +string_p(Term) -> + string_p1(Term). + +string_p1([H|T]) when is_integer(H), H >= $\s, H < 256 -> + string_p1(T); +string_p1([$\n|T]) -> string_p1(T); +string_p1([$\r|T]) -> string_p1(T); +string_p1([$\t|T]) -> string_p1(T); +string_p1([$\v|T]) -> string_p1(T); +string_p1([$\b|T]) -> string_p1(T); +string_p1([$\f|T]) -> string_p1(T); +string_p1([$\e|T]) -> string_p1(T); +string_p1([H|T]) when is_list(H) -> + case string_p1(H) of + true -> string_p1(T); + _ -> false + end; +string_p1([]) -> true; +string_p1(_) -> false. diff --git a/src/couch_log_writer.erl b/src/couch_log_writer.erl new file mode 100644 index 0000000..5e28a07 --- /dev/null +++ b/src/couch_log_writer.erl @@ -0,0 +1,83 @@ +% 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. +% +% @doc Modules wishing to handle writing log +% messages should implement this behavior. + + +-module(couch_log_writer). + + +-export([ + init/0, + terminate/2, + write/2 +]). + + +-include("couch_log.hrl"). + + +-define(DEFAULT_WRITER, couch_log_writer_stderr). + + +-callback init() -> {ok, State::term()}. +-callback terminate(Reason::term(), State::term()) -> ok. +-callback write(LogEntry::#log_entry{}, State::term()) -> + {ok, NewState::term()}. + + +-spec init() -> {atom(), term()}. +init() -> + Writer = get_writer_mod(), + {ok, St} = Writer:init(), + {Writer, St}. + + +-spec terminate(term(), {atom(), term()}) -> ok. +terminate(Reason, {Writer, St}) -> + ok = Writer:terminate(Reason, St). + + +-spec write(#log_entry{}, {atom(), term()}) -> {atom(), term()}. +write(Entry, {Writer, St}) -> + {ok, NewSt} = Writer:write(Entry, St), + {Writer, NewSt}. + + +get_writer_mod() -> + WriterStr = config:get("log", "writer", "stderr"), + ModName1 = to_atom("couch_log_writer_" ++ WriterStr), + case mod_exists(ModName1) of + true -> + ModName1; + false -> + ModName2 = to_atom(WriterStr), + case mod_exists(ModName2) of + true -> + ModName2; + false -> + ?DEFAULT_WRITER + end + end. + + +to_atom(Str) -> + try list_to_existing_atom(Str) of + Atom -> Atom + catch _:_ -> + undefined + end. + + +mod_exists(ModName) -> + code:which(ModName) /= non_existing. diff --git a/src/couch_log_writer_file.erl b/src/couch_log_writer_file.erl new file mode 100644 index 0000000..fb01363 --- /dev/null +++ b/src/couch_log_writer_file.erl @@ -0,0 +1,140 @@ +% 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(couch_log_writer_file). +-behaviour(couch_log_writer). + + +-export([ + init/0, + terminate/2, + write/2 +]). + + +-include_lib("kernel/include/file.hrl"). +-include("couch_log.hrl"). + + +-record(st, { + file_path, + fd, + inode, + last_check +}). + + +-define(CHECK_INTERVAL, 30000000). + + +-ifdef(TEST). +-compile(export_all). +-endif. + + +init() -> + FilePath = config:get("log", "file", "./couch.log"), + Opts = [append, raw] ++ buffer_opt(), + case filelib:ensure_dir(FilePath) of + ok -> + case file:open(FilePath, Opts) of + {ok, Fd} -> + case file:read_file_info(FilePath) of + {ok, FInfo} -> + {ok, #st{ + file_path = FilePath, + fd = Fd, + inode = FInfo#file_info.inode, + last_check = os:timestamp() + }}; + FInfoError -> + ok = file:close(Fd), + FInfoError + end; + OpenError -> + OpenError + end; + EnsureDirError -> + EnsureDirError + end. + + +terminate(_, St) -> + % Apparently delayed_write can require two closes + file:close(St#st.fd), + file:close(St#st.fd), + ok. + + +write(Entry, St) -> + {ok, NewSt} = maybe_reopen(St), + #log_entry{ + level = Level, + pid = Pid, + msg = Msg, + msg_id = MsgId, + time_stamp = TimeStamp + } = Entry, + Fmt = "[~s] ~s ~s ~p ~s ", + Args = [ + couch_log_util:level_to_string(Level), + TimeStamp, + node(), + Pid, + MsgId + ], + MsgSize = couch_log_config:get(max_message_size), + Data = couch_log_trunc_io:format(Fmt, Args, MsgSize), + ok = file:write(NewSt#st.fd, [Data, Msg, "\n"]), + {ok, NewSt}. + + +buffer_opt() -> + WriteBuffer = config:get_integer("log", "write_buffer", 0), + WriteDelay = config:get_integer("log", "write_delay", 0), + case {WriteBuffer, WriteDelay} of + {B, D} when is_integer(B), is_integer(D), B > 0, D > 0 -> + [{delayed_write, B, D}]; + _ -> + [] + end. + + +maybe_reopen(St) -> + #st{ + last_check = LastCheck + } = St, + Now = os:timestamp(), + case timer:now_diff(Now, LastCheck) > ?CHECK_INTERVAL of + true -> reopen(St); + false -> {ok, St} + end. + + +reopen(St) -> + case file:read_file_info(St#st.file_path) of + {ok, FInfo} -> + NewINode = FInfo#file_info.inode, + case NewINode == St#st.inode of + true -> + % No rotate necessary + {ok, St}; + false -> + % File was moved and re-created + terminate(rotating, St), + init() + end; + _ -> + % File was moved or deleted + terminate(rotating, St), + init() + end. diff --git a/src/couch_log_writer_stderr.erl b/src/couch_log_writer_stderr.erl new file mode 100644 index 0000000..7c5fc6c --- /dev/null +++ b/src/couch_log_writer_stderr.erl @@ -0,0 +1,54 @@ +% 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(couch_log_writer_stderr). +-behaviour(couch_log_writer). + + +-export([ + init/0, + terminate/2, + write/2 +]). + + +-include("couch_log.hrl"). + + +init() -> + {ok, nil}. + + +terminate(_, _St) -> + ok. + + +write(Entry, St) -> + #log_entry{ + level = Level, + pid = Pid, + msg = Msg, + msg_id = MsgId, + time_stamp = TimeStamp + } = Entry, + Fmt = "[~s] ~s ~s ~p ~s ", + Args = [ + couch_log_util:level_to_string(Level), + TimeStamp, + node(), + Pid, + MsgId + ], + MsgSize = couch_log_config:get(max_message_size), + Data = couch_log_trunc_io:format(Fmt, Args, MsgSize), + io:format(standard_error, [Data, Msg, "\n"], []), + {ok, St}. From c572aae63262dd0454cb549a215aa26bb7d2339e Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Wed, 20 Jul 2016 12:01:54 -0500 Subject: [PATCH 2/3] Add a syslog writer COUCHDB-3067 --- src/couch_log_writer_syslog.erl | 155 ++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/couch_log_writer_syslog.erl diff --git a/src/couch_log_writer_syslog.erl b/src/couch_log_writer_syslog.erl new file mode 100644 index 0000000..738d162 --- /dev/null +++ b/src/couch_log_writer_syslog.erl @@ -0,0 +1,155 @@ +% 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(couch_log_writer_syslog). +-behavior(couch_log_writer). + + +-export([ + init/0, + terminate/2, + write/2 +]). + + +-include("couch_log.hrl"). + + +-record(st, { + socket, + host, + port, + hostname, + os_pid, + appid, + facility +}). + + +-define(SYSLOG_VERSION, 1). + + +-ifdef(TEST). +-compile(export_all). +-endif. + + +init() -> + {ok, Socket} = gen_udp:open(0), + + SysLogHost = config:get("log", "syslog_host"), + Host = case inet:getaddr(SysLogHost, inet) of + {ok, Address} when SysLogHost /= undefined -> + Address; + _ -> + undefined + end, + + {ok, #st{ + socket = Socket, + host = Host, + port = config:get_integer("log", "syslog_port", 514), + hostname = net_adm:localhost(), + os_pid = os:getpid(), + appid = config:get("log", "syslog_appid", "couchdb"), + facility = get_facility(config:get("log", "syslog_facility", "local2")) + }}. + + +terminate(_Reason, St) -> + gen_udp:close(St#st.socket). + + +write(Entry, St) -> + #log_entry{ + level = Level, + pid = Pid, + msg = Msg, + msg_id = MsgId, + time_stamp = TimeStamp + } = Entry, + Fmt = "<~B>~B ~s ~s ~s ~p ~s - ", + Args = [ + St#st.facility bor get_level(Level), + ?SYSLOG_VERSION, + TimeStamp, + St#st.hostname, + St#st.appid, + Pid, + MsgId + ], + Pre = io_lib:format(Fmt, Args), + ok = send(St, [Pre, Msg, $\n]), + {ok, St}. + + +send(#st{host=undefined}, Packet) -> + io:format(standard_error, "~s", [Packet]); + +send(St, Packet) -> + #st{ + socket = Socket, + host = Host, + port = Port + } = St, + gen_udp:send(Socket, Host, Port, Packet). + + +get_facility(Name) -> + FacId = case Name of + "kern" -> 0; % Kernel messages + "user" -> 1; % Random user-level messages + "mail" -> 2; % Mail system + "daemon" -> 3; % System daemons + "auth" -> 4; % Security/Authorization messages + "syslog" -> 5; % Internal Syslog messages + "lpr" -> 6; % Line printer subsystem + "news" -> 7; % Network news subsystems + "uucp" -> 8; % UUCP subsystem + "clock" -> 9; % Clock daemon + "authpriv" -> 10; % Security/Authorization messages + "ftp" -> 11; % FTP daemon + "ntp" -> 12; % NTP subsystem + "audit" -> 13; % Log audit + "alert" -> 14; % Log alert + "cron" -> 15; % Scheduling daemon + "local0" -> 16; % Local use 0 + "local1" -> 17; % Local use 1 + "local2" -> 18; % Local use 2 + "local3" -> 19; % Local use 3 + "local4" -> 20; % Local use 4 + "local5" -> 21; % Local use 5 + "local6" -> 22; % Local use 6 + "local7" -> 23; % Local use 7 + _ -> + try list_to_integer(Name) of + N when N >= 0, N =< 23 -> N; + _ -> 23 + catch _:_ -> + 23 + end + end, + FacId bsl 3. + + +get_level(Name) when is_atom(Name) -> + case Name of + debug -> 7; + info -> 6; + notice -> 5; + warning -> 4; + error -> 3; + critical -> 2; + alert -> 1; + emergency -> 0; + _ -> 3 + end. From 16a46572b92bf0b53b3aa94bc209cae0ba2257c1 Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Wed, 20 Jul 2016 12:53:50 -0500 Subject: [PATCH 3/3] Add test suite for couch_log COUCHDB-3067 --- include/couch_log.hrl | 3 + rebar.config | 2 + test/couch_log_config_listener_test.erl | 56 ++ test/couch_log_config_test.erl | 110 ++++ test/couch_log_error_logger_h_test.erl | 45 ++ test/couch_log_formatter_test.erl | 768 ++++++++++++++++++++++++ test/couch_log_monitor_test.erl | 67 +++ test/couch_log_server_test.erl | 118 ++++ test/couch_log_test.erl | 85 +++ test/couch_log_test_util.erl | 153 +++++ test/couch_log_util_test.erl | 55 ++ test/couch_log_writer_ets.erl | 49 ++ test/couch_log_writer_file_test.erl | 161 +++++ test/couch_log_writer_stderr_test.erl | 58 ++ test/couch_log_writer_syslog_test.erl | 122 ++++ test/couch_log_writer_test.erl | 54 ++ 16 files changed, 1906 insertions(+) create mode 100644 rebar.config create mode 100644 test/couch_log_config_listener_test.erl create mode 100644 test/couch_log_config_test.erl create mode 100644 test/couch_log_error_logger_h_test.erl create mode 100644 test/couch_log_formatter_test.erl create mode 100644 test/couch_log_monitor_test.erl create mode 100644 test/couch_log_server_test.erl create mode 100644 test/couch_log_test.erl create mode 100644 test/couch_log_test_util.erl create mode 100644 test/couch_log_util_test.erl create mode 100644 test/couch_log_writer_ets.erl create mode 100644 test/couch_log_writer_file_test.erl create mode 100644 test/couch_log_writer_stderr_test.erl create mode 100644 test/couch_log_writer_syslog_test.erl create mode 100644 test/couch_log_writer_test.erl diff --git a/include/couch_log.hrl b/include/couch_log.hrl index a472c0c..fa544a8 100644 --- a/include/couch_log.hrl +++ b/include/couch_log.hrl @@ -17,3 +17,6 @@ msg_id, time_stamp }). + + +-define(COUCH_LOG_TEST_TABLE, couch_log_test_table). diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..e0d1844 --- /dev/null +++ b/rebar.config @@ -0,0 +1,2 @@ +{cover_enabled, true}. +{cover_print_enabled, true}. diff --git a/test/couch_log_config_listener_test.erl b/test/couch_log_config_listener_test.erl new file mode 100644 index 0000000..9a8e16d --- /dev/null +++ b/test/couch_log_config_listener_test.erl @@ -0,0 +1,56 @@ +% 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(couch_log_config_listener_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +-define(HANDLER, {config_listener, couch_log_config_listener}). + + +couch_log_config_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_restart_listener/0, + fun check_ignore_non_log/0 + ] + }. + + +check_restart_listener() -> + Handlers1 = gen_event:which_handlers(config_event), + ?assert(lists:member(?HANDLER, Handlers1)), + + gen_event:delete_handler(config_event, ?HANDLER, testing), + + Handlers2 = gen_event:which_handlers(config_event), + ?assert(not lists:member(?HANDLER, Handlers2)), + + timer:sleep(1000), + + Handlers3 = gen_event:which_handlers(config_event), + ?assert(lists:member(?HANDLER, Handlers3)). + + +check_ignore_non_log() -> + Run = fun() -> + couch_log_test_util:with_config_listener(fun() -> + config:set("foo", "bar", "baz"), + couch_log_test_util:wait_for_config() + end) + end, + ?assertError(config_change_timeout, Run()). diff --git a/test/couch_log_config_test.erl b/test/couch_log_config_test.erl new file mode 100644 index 0000000..c4677f3 --- /dev/null +++ b/test/couch_log_config_test.erl @@ -0,0 +1,110 @@ +% 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(couch_log_config_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +couch_log_config_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_level/0, + fun check_max_message_size/0, + fun check_bad_level/0, + fun check_bad_max_message_size/0 + ] + }. + + +check_level() -> + % Default level is info + ?assertEqual(info, couch_log_config:get(level)), + ?assertEqual(2, couch_log_config:get(level_int)), + + couch_log_test_util:with_config_listener(fun() -> + config:set("log", "level", "emerg"), + couch_log_test_util:wait_for_config(), + ?assertEqual(emergency, couch_log_config:get(level)), + ?assertEqual(8, couch_log_config:get(level_int)), + + config:set("log", "level", "debug"), + couch_log_test_util:wait_for_config(), + ?assertEqual(debug, couch_log_config:get(level)), + ?assertEqual(1, couch_log_config:get(level_int)), + + config:delete("log", "level"), + couch_log_test_util:wait_for_config(), + ?assertEqual(info, couch_log_config:get(level)), + ?assertEqual(2, couch_log_config:get(level_int)) + end). + + +check_max_message_size() -> + % Default is 16000 + ?assertEqual(16000, couch_log_config:get(max_message_size)), + + couch_log_test_util:with_config_listener(fun() -> + config:set("log", "max_message_size", "1024"), + couch_log_test_util:wait_for_config(), + ?assertEqual(1024, couch_log_config:get(max_message_size)), + + config:delete("log", "max_message_size"), + couch_log_test_util:wait_for_config(), + ?assertEqual(16000, couch_log_config:get(max_message_size)) + end). + + +check_bad_level() -> + % Default level is info + ?assertEqual(info, couch_log_config:get(level)), + ?assertEqual(2, couch_log_config:get(level_int)), + + couch_log_test_util:with_config_listener(fun() -> + config:set("log", "level", "debug"), + couch_log_test_util:wait_for_config(), + ?assertEqual(debug, couch_log_config:get(level)), + ?assertEqual(1, couch_log_config:get(level_int)), + + config:set("log", "level", "this is not a valid level name"), + couch_log_test_util:wait_for_config(), + ?assertEqual(info, couch_log_config:get(level)), + ?assertEqual(2, couch_log_config:get(level_int)), + + config:delete("log", "level"), + couch_log_test_util:wait_for_config(), + ?assertEqual(info, couch_log_config:get(level)), + ?assertEqual(2, couch_log_config:get(level_int)) + end). + + +check_bad_max_message_size() -> + % Default level is 16000 + ?assertEqual(16000, couch_log_config:get(max_message_size)), + + couch_log_test_util:with_config_listener(fun() -> + config:set("log", "max_message_size", "1024"), + couch_log_test_util:wait_for_config(), + ?assertEqual(1024, couch_log_config:get(max_message_size)), + + config:set("log", "max_message_size", "this is not a valid size"), + couch_log_test_util:wait_for_config(), + ?assertEqual(16000, couch_log_config:get(max_message_size)), + + config:delete("log", "max_message_size"), + couch_log_test_util:wait_for_config(), + ?assertEqual(16000, couch_log_config:get(max_message_size)) + end). diff --git a/test/couch_log_error_logger_h_test.erl b/test/couch_log_error_logger_h_test.erl new file mode 100644 index 0000000..b78598f --- /dev/null +++ b/test/couch_log_error_logger_h_test.erl @@ -0,0 +1,45 @@ +% 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(couch_log_error_logger_h_test). + + +-include_lib("eunit/include/eunit.hrl"). + + +-define(HANDLER, couch_log_error_logger_h). + + +couch_log_error_logger_h_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun handler_ignores_unknown_messages/0, + fun coverage_test/0 + ] + }. + + +handler_ignores_unknown_messages() -> + Handlers1 = gen_event:which_handlers(error_logger), + ?assert(lists:member(?HANDLER, Handlers1)), + ?assertEqual(ignored, gen_event:call(error_logger, ?HANDLER, foo)), + + error_logger ! this_is_a_message, + Handlers2 = gen_event:which_handlers(error_logger), + ?assert(lists:member(?HANDLER, Handlers2)). + + +coverage_test() -> + Resp = couch_log_error_logger_h:code_change(foo, bazinga, baz), + ?assertEqual({ok, bazinga}, Resp). diff --git a/test/couch_log_formatter_test.erl b/test/couch_log_formatter_test.erl new file mode 100644 index 0000000..1e8457b --- /dev/null +++ b/test/couch_log_formatter_test.erl @@ -0,0 +1,768 @@ +% 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(couch_log_formatter_test). + + +-include("couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +truncate_fmt_test() -> + Msg = [0 || _ <- lists:seq(1, 1048576)], + Entry = couch_log_formatter:format(info, self(), "~w", [Msg]), + ?assert(length(Entry#log_entry.msg) =< 16000). + + +truncate_test() -> + Msg = [0 || _ <- lists:seq(1, 1048576)], + Entry = couch_log_formatter:format(info, self(), Msg), + ?assert(length(Entry#log_entry.msg) =< 16000). + + +gen_server_error_test() -> + Pid = self(), + Event = { + error, + erlang:group_leader(), + { + Pid, + "** Generic server and some stuff", + [a_gen_server, {foo, bar}, server_state, some_reason] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event) + ), + do_matches(do_format(Event), [ + "gen_server a_gen_server terminated", + "with reason: some_reason", + "last msg: {foo,bar}", + "state: server_state" + ]). + + +gen_fsm_error_test() -> + Pid = self(), + Event = { + error, + erlang:group_leader(), + { + Pid, + "** State machine did a thing", + [a_gen_fsm, {ohai,there}, state_name, curr_state, barf] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event) + ), + do_matches(do_format(Event), [ + "gen_fsm a_gen_fsm in state state_name", + "with reason: barf", + "last msg: {ohai,there}", + "state: curr_state" + ]). + + +gen_event_error_test() -> + Pid = self(), + Event = { + error, + erlang:group_leader(), + { + Pid, + "** gen_event handler did a thing", + [ + handler_id, + a_gen_event, + {ohai,there}, + curr_state, + barf + ] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event) + ), + do_matches(do_format(Event), [ + "gen_event handler_id installed in a_gen_event", + "reason: barf", + "last msg: {ohai,there}", + "state: curr_state" + ]). + + +normal_error_test() -> + Pid = self(), + Event = { + error, + erlang:group_leader(), + { + Pid, + "format thing: ~w ~w", + [ + first_arg, + second_arg + ] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid, + msg = "format thing: first_arg second_arg" + }, + do_format(Event) + ). + + +error_report_std_error_test() -> + Pid = self(), + Event = { + error_report, + erlang:group_leader(), + { + Pid, + std_error, + [foo, {bar, baz}] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid, + msg = "foo, bar: baz" + }, + do_format(Event) + ). + + +supervisor_report_test() -> + Pid = self(), + % A standard supervisor report + Event1 = { + error_report, + erlang:group_leader(), + { + Pid, + supervisor_report, + [ + {supervisor, sup_name}, + {offender, [ + {id, sup_child}, + {pid, list_to_pid("<0.1.0>")}, + {mfargs, {some_mod, some_fun, 3}} + ]}, + {reason, a_reason}, + {errorContext, some_context} + ] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event1) + ), + do_matches(do_format(Event1), [ + "Supervisor sup_name", + "had child sup_child started with some_mod:some_fun/3 at <0.1.0> exit", + "with reason a_reason", + "in context some_context" + ]), + % Slightly older using name instead of id + % in the offender blob. + Event2 = { + error_report, + erlang:group_leader(), + { + Pid, + supervisor_report, + [ + {supervisor, sup_name}, + {offender, [ + {name, sup_child}, + {pid, list_to_pid("<0.1.0>")}, + {mfargs, {some_mod, some_fun, 3}} + ]}, + {reason, a_reason}, + {errorContext, some_context} + ] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event2) + ), + do_matches(do_format(Event2), [ + "Supervisor sup_name", + "had child sup_child started with some_mod:some_fun/3 at <0.1.0> exit", + "with reason a_reason", + "in context some_context" + ]), + % A supervisor_bridge + Event3 = { + error_report, + erlang:group_leader(), + { + Pid, + supervisor_report, + [ + {supervisor, sup_name}, + {offender, [ + {mod, bridge_mod}, + {pid, list_to_pid("<0.1.0>")} + ]}, + {reason, a_reason}, + {errorContext, some_context} + ] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event3) + ), + do_matches(do_format(Event3), [ + "Supervisor sup_name", + "had child at module bridge_mod at <0.1.0> exit", + "with reason a_reason", + "in context some_context" + ]), + % Any other supervisor report + Event4 = { + error_report, + erlang:group_leader(), + { + Pid, + supervisor_report, + [foo, {a, thing}, bang] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid, + msg = "SUPERVISOR REPORT foo, a: thing, bang" + }, + do_format(Event4) + ). + + +crash_report_test() -> + Pid = self(), + % A standard crash report + Event1 = { + error_report, + erlang:group_leader(), + { + Pid, + crash_report, + [ + [ + {pid, list_to_pid("<0.2.0>")}, + {error_info, { + exit, + undef, + [{mod_name, fun_name, [a, b]}] + }} + ], + [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")] + ] + } + }, + ?assertMatch( + #log_entry{ + level = error, + pid = Pid + }, + do_format(Event1) + ), + do_matches(do_format(Event1), [ + "Process <0.2.0>", + "with 2 neighbors", + "exited", + "reason: call to undefined function mod_name:fun_name\\(a, b\\)" + ]), + % A registered process crash report + Event2 = { + error_report, + erlang:group_leader(), + { + Pid, + crash_report, + [ + [ + {pid, list_to_pid("<0.2.0>")}, + {registered_name, couch_log_server}, + {error_info, { + exit, + undef, + [{mod_name, fun_name, [a, b]}] + }} + ], + [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")] + ] + } + }, + do_matches(do_format(Event2), [ + "Process couch_log_server \\(<0.2.0>\\)" + ]), + % A non-exit crash report + Event3 = { + error_report, + erlang:group_leader(), + { + Pid, + crash_report, + [ + [ + {pid, list_to_pid("<0.2.0>")}, + {registered_name, couch_log_server}, + {error_info, { + killed, + undef, + [{mod_name, fun_name, [a, b]}] + }} + ], + [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")] + ] + } + }, + do_matches(do_format(Event3), [ + "crashed" + ]), + % A extra report info + Event4 = { + error_report, + erlang:group_leader(), + { + Pid, + crash_report, + [ + [ + {pid, list_to_pid("<0.2.0>")}, + {error_info, { + killed, + undef, + [{mod_name, fun_name, [a, b]}] + }}, + {another, entry}, + yep + ], + [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")] + ] + } + }, + do_matches(do_format(Event4), [ + "; another: entry, yep" + ]). + + +warning_report_test() -> + Pid = self(), + % A warning message + Event1 = { + warning_msg, + erlang:group_leader(), + { + Pid, + "a ~s string ~w", + ["format", 7] + } + }, + ?assertMatch( + #log_entry{ + level = warning, + pid = Pid, + msg = "a format string 7" + }, + do_format(Event1) + ), + % A warning report + Event2 = { + warning_report, + erlang:group_leader(), + { + Pid, + std_warning, + [list, 'of', {things, indeed}] + } + }, + ?assertMatch( + #log_entry{ + level = warning, + pid = Pid, + msg = "list, of, things: indeed" + }, + do_format(Event2) + ). + + +info_report_test() -> + Pid = self(), + % An info message + Event1 = { + info_msg, + erlang:group_leader(), + { + Pid, + "an info ~s string ~w", + ["format", 7] + } + }, + ?assertMatch( + #log_entry{ + level = info, + pid = Pid, + msg = "an info format string 7" + }, + do_format(Event1) + ), + % Application exit info + Event2 = { + info_report, + erlang:group_leader(), + { + Pid, + std_info, + [ + {type, no_idea}, + {application, couch_log}, + {exited, red_sox_are_on} + ] + } + }, + ?assertMatch( + #log_entry{ + level = info, + pid = Pid, + msg = "Application couch_log exited with reason: red_sox_are_on" + }, + do_format(Event2) + ), + % Any other std_info message + Event3 = { + info_report, + erlang:group_leader(), + { + Pid, + std_info, + [ + {type, no_idea}, + {application, couch_log} + ] + } + }, + ?assertMatch( + #log_entry{ + level = info, + pid = Pid, + msg = "type: no_idea, application: couch_log" + }, + do_format(Event3) + ), + % Non-list other report + Event4 = { + info_report, + erlang:group_leader(), + { + Pid, + std_info, + dang + } + }, + ?assertMatch( + #log_entry{ + level = info, + pid = Pid, + msg = "dang" + }, + do_format(Event4) + ). + + +progress_report_test() -> + Pid = self(), + % Application started + Event1 = { + info_report, + erlang:group_leader(), + { + Pid, + progress, + [{started_at, 'nonode@nohost'}, {application, app_name}] + } + }, + ?assertMatch( + #log_entry{ + level = info, + pid = Pid, + msg = "Application app_name started on node nonode@nohost" + }, + do_format(Event1) + ), + % Supervisor started child + Event2 = { + info_report, + erlang:group_leader(), + { + Pid, + progress, + [ + {supervisor, sup_dude}, + {started, [ + {mfargs, {mod_name, fun_name, 1}}, + {pid, list_to_pid("<0.5.0>")} + ]} + ] + } + }, + ?assertMatch( + #log_entry{ + level = debug, + pid = Pid, + msg = "Supervisor sup_dude started mod_name:fun_name/1" + " at pid <0.5.0>" + }, + do_format(Event2) + ), + % Other progress report + Event3 = { + info_report, + erlang:group_leader(), + { + Pid, + progress, + [a, {thing, boop}, here] + } + }, + ?assertMatch( + #log_entry{ + level = info, + pid = Pid, + msg = "PROGRESS REPORT a, thing: boop, here" + }, + do_format(Event3) + ). + + +log_unknown_event_test() -> + Pid = self(), + ?assertMatch( + #log_entry{ + level = warning, + pid = Pid, + msg = "Unexpected error_logger event an_unknown_event" + }, + do_format(an_unknown_event) + ). + + +format_reason_test_() -> + Cases = [ + { + {'function not exported', [{a, b, 2}, {c, d, 1}, {e, f, 2}]}, + "call to unexported function a:b/2 at c:d/1 <= e:f/2" + }, + { + {'function not exported', [{a, b, 2, []}, {c, d, 1}, {e, f, 2}]}, + "call to unexported function a:b/2 at c:d/1 <= e:f/2" + }, + { + {undef, [{a, b, 2, []}, {c, d, 1}, {e, f, 2}]}, + "call to undefined function a:b/2 at c:d/1 <= e:f/2" + }, + { + {bad_return, {{a, b, 2}, {'EXIT', killed}}}, + "bad return value {'EXIT',killed} from a:b/2" + }, + { + {bad_return_value, foo}, + "bad return value foo" + }, + { + {{bad_return_value, foo}, {h, i, 0}}, + "bad return value foo at h:i/0" + }, + { + {{badrecord, {foo, 1, 4}}, [{h, i, 0}, {j, k, [a, b]}]}, + "bad record {foo,1,4} at h:i/0 <= j:k/2" + }, + { + {{case_clause, bingo}, [{j, k, 3}, {z, z, 0}]}, + "no case clause matching bingo at j:k/3 <= z:z/0" + }, + { + {function_clause, [{j, k, [a, 2]}, {y, x, 1}]}, + "no function clause matching j:k(a, 2) at y:x/1" + }, + { + {if_clause, [{j, k, [a, 2]}, {y, x, 1}]}, + "no true branch found while evaluating if expression at j:k/2 <= y:x/1" + }, + { + {{try_clause, bango}, [{j, k, [a, 2]}, {y, x, 1}]}, + "no try clause matching bango at j:k/2 <= y:x/1" + }, + { + {badarith, [{j, k, [a, 2]}, {y, x, 1}]}, + "bad arithmetic expression at j:k/2 <= y:x/1" + }, + { + {{badmatch, bongo}, [{j, k, [a, 2]}, {y, x, 1}]}, + "no match of right hand value bongo at j:k/2 <= y:x/1" + }, + { + {emfile, [{j, k, [a, 2]}, {y, x, 1}]}, + "maximum number of file descriptors exhausted, check ulimit -n; j:k/2 <= y:x/1" + }, + { + {system_limit, [{erlang, open_port, []}, {y, x, 1}]}, + "system limit: maximum number of ports exceeded at y:x/1" + }, + { + {system_limit, [{erlang, spawn, []}, {y, x, 1}]}, + "system limit: maximum number of processes exceeded at y:x/1" + }, + { + {system_limit, [{erlang, spawn_opt, []}, {y, x, 1}]}, + "system limit: maximum number of processes exceeded at y:x/1" + }, + { + {system_limit, [{erlang, list_to_atom, ["foo"]}, {y, x, 1}]}, + "system limit: tried to create an atom larger than 255, or maximum atom count exceeded at y:x/1" + }, + { + {system_limit, [{ets, new, []}, {y, x, 1}]}, + "system limit: maximum number of ETS tables exceeded at y:x/1" + }, + { + {system_limit, [{couch_log, totes_logs, []}, {y, x, 1}]}, + "system limit: couch_log:totes_logs() at y:x/1" + }, + { + {badarg, [{j, k, [a, 2]}, {y, x, 1}]}, + "bad argument in call to j:k(a, 2) at y:x/1" + }, + { + {{badarg, [{j, k, [a, 2]}, {y, x, 1}]}, some_ignored_thing}, + "bad argument in call to j:k(a, 2) at y:x/1" + }, + { + {{badarity, {fun erlang:spawn/1, [a, b]}}, [{y, x, 1}]}, + "function called with wrong arity of 2 instead of 1 at y:x/1" + }, + { + {noproc, [{y, x, 1}]}, + "no such process or port in call to y:x/1" + }, + { + {{badfun, 2}, [{y, x, 1}]}, + "bad function 2 called at y:x/1" + }, + { + {a_reason, [{y, x, 1}]}, + "a_reason at y:x/1" + }, + { + {a_reason, [{y, x, 1, [{line, 4}]}]}, + "a_reason at y:x/1(line:4)" + } + ], + [ + {Msg, fun() -> ?assertEqual( + Msg, + lists:flatten(couch_log_formatter:format_reason(Reason)) + ) end} + || {Reason, Msg} <- Cases + ]. + + +coverage_test() -> + % MFA's that aren't + ?assertEqual(["foo"], couch_log_formatter:format_mfa(foo)), + + % Traces with line numbers + Trace = [{x, y, [a], [{line, 4}]}], + ?assertEqual( + "x:y/1(line:4)", + lists:flatten(couch_log_formatter:format_trace(Trace)) + ), + + % Excercising print_silly_list + ?assertMatch( + #log_entry{ + level = error, + msg = "foobar" + }, + do_format({ + error_report, + erlang:group_leader(), + {self(), std_error, "foobar"} + }) + ), + + % Excercising print_silly_list + ?assertMatch( + #log_entry{ + level = error, + msg = "dang" + }, + do_format({ + error_report, + erlang:group_leader(), + {self(), std_error, dang} + }) + ). + + +do_format(Event) -> + E = couch_log_formatter:format(Event), + E#log_entry{ + msg = lists:flatten(E#log_entry.msg), + msg_id = lists:flatten(E#log_entry.msg_id), + time_stamp = lists:flatten(E#log_entry.time_stamp) + }. + + +do_matches(_, []) -> + ok; + +do_matches(#log_entry{msg = Msg} = E, [Pattern | RestPatterns]) -> + case re:run(Msg, Pattern) of + {match, _} -> + ok; + nomatch -> + Err1 = io_lib:format("'~s' does not match '~s'", [Pattern, Msg]), + Err2 = lists:flatten(Err1), + ?assertEqual(nomatch, Err2) + end, + do_matches(E, RestPatterns). diff --git a/test/couch_log_monitor_test.erl b/test/couch_log_monitor_test.erl new file mode 100644 index 0000000..eec0085 --- /dev/null +++ b/test/couch_log_monitor_test.erl @@ -0,0 +1,67 @@ +% 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(couch_log_monitor_test). + + +-include_lib("eunit/include/eunit.hrl"). + + +-define(HANDLER, couch_log_error_logger_h). + + +couch_log_monitor_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun monitor_ignores_unknown_messages/0, + fun monitor_restarts_handler/0, + fun coverage_test/0 + ] + }. + + +monitor_ignores_unknown_messages() -> + Pid1 = get_monitor_pid(), + + ?assertEqual(ignored, gen_server:call(Pid1, do_foo_please)), + + gen_server:cast(Pid1, do_bar_please), + Pid1 ! do_baz_please, + timer:sleep(250), + ?assert(is_process_alive(Pid1)). + + +monitor_restarts_handler() -> + Pid1 = get_monitor_pid(), + error_logger:delete_report_handler(?HANDLER), + timer:sleep(250), + + ?assert(not is_process_alive(Pid1)), + + Pid2 = get_monitor_pid(), + ?assert(is_process_alive(Pid2)), + + Handlers = gen_event:which_handlers(error_logger), + ?assert(lists:member(?HANDLER, Handlers)). + + +coverage_test() -> + Resp = couch_log_monitor:code_change(foo, bazinga, baz), + ?assertEqual({ok, bazinga}, Resp). + + +get_monitor_pid() -> + Children = supervisor:which_children(couch_log_sup), + [MonPid] = [Pid || {couch_log_monitor, Pid, _, _} <- Children, is_pid(Pid)], + MonPid. diff --git a/test/couch_log_server_test.erl b/test/couch_log_server_test.erl new file mode 100644 index 0000000..7af570e --- /dev/null +++ b/test/couch_log_server_test.erl @@ -0,0 +1,118 @@ +% 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(couch_log_server_test). + + +-include("couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +couch_log_server_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_can_reconfigure/0, + fun check_can_restart/0, + fun check_can_cast_log_entry/0, + fun check_logs_ignored_messages/0 + ] + }. + + +check_can_reconfigure() -> + couch_log:error("a message", []), + ?assertEqual(0, couch_log_test_util:last_log_key()), + ?assertEqual(ok, couch_log_server:reconfigure()), + ?assertEqual('$end_of_table', couch_log_test_util:last_log_key()), + + couch_log_test_util:with_config_listener(fun() -> + couch_log:error("another message", []), + ?assertEqual(0, couch_log_test_util:last_log_key()), + config:set("log", "some_key", "some_val"), + couch_log_test_util:wait_for_config(), + ?assertEqual('$end_of_table', couch_log_test_util:last_log_key()) + end). + + +check_can_restart() -> + Pid1 = whereis(couch_log_server), + Ref = erlang:monitor(process, Pid1), + ?assert(is_process_alive(Pid1)), + + supervisor:terminate_child(couch_log_sup, couch_log_server), + supervisor:restart_child(couch_log_sup, couch_log_server), + + receive + {'DOWN', Ref, _, _, _} -> ok + after 1000 -> + erlang:error(timeout_restarting_couch_log_server) + end, + + ?assert(not is_process_alive(Pid1)), + + Pid2 = whereis(couch_log_server), + ?assertNotEqual(Pid2, Pid1), + ?assert(is_process_alive(Pid2)). + + +check_can_cast_log_entry() -> + Entry = #log_entry{ + level = critical, + pid = self(), + msg = "this will be casted", + msg_id = "----", + time_stamp = "2016-07-20-almost-my-birthday" + }, + ok = gen_server:cast(couch_log_server, {log, Entry}), + timer:sleep(500), % totes gross + ?assertEqual(Entry, couch_log_test_util:last_log()). + + +check_logs_ignored_messages() -> + gen_server:call(couch_log_server, a_call), + ?assertMatch( + #log_entry{ + level = error, + pid = couch_log_server, + msg = "couch_log_server ignored a_call" + }, + couch_log_test_util:last_log() + ), + + gen_server:cast(couch_log_server, a_cast), + timer:sleep(500), % yes gross + ?assertMatch( + #log_entry{ + level = error, + pid = couch_log_server, + msg = "couch_log_server ignored a_cast" + }, + couch_log_test_util:last_log() + ), + + couch_log_server ! an_info, + timer:sleep(500), % still gross + ?assertMatch( + #log_entry{ + level = error, + pid = couch_log_server, + msg = "couch_log_server ignored an_info" + }, + couch_log_test_util:last_log() + ). + + +coverage_test() -> + Resp = couch_log_server:code_change(foo, bazinga, baz), + ?assertEqual({ok, bazinga}, Resp). diff --git a/test/couch_log_test.erl b/test/couch_log_test.erl new file mode 100644 index 0000000..1777730 --- /dev/null +++ b/test/couch_log_test.erl @@ -0,0 +1,85 @@ +% 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(couch_log_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +couch_log_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + gen() ++ [fun check_set_level/0] + }. + + +check_set_level() -> + couch_log:set_level(crit), + ?assertEqual("crit", config:get("log", "level")). + + +levels() -> + [ + debug, + info, + notice, + warning, + error, + critical, + alert, + emergency, + none + ]. + + +gen() -> + lists:map(fun(L) -> + Name = "Test log level: " ++ couch_log_util:level_to_string(L), + {Name, fun() -> check_levels(L, levels()) end} + end, levels() -- [none]). + + +check_levels(_, []) -> + ok; + +check_levels(TestLevel, [CfgLevel | RestLevels]) -> + TestInt = couch_log_util:level_to_integer(TestLevel), + CfgInt = couch_log_util:level_to_integer(CfgLevel), + Pid = self(), + Msg = new_msg(), + LastKey = couch_log_test_util:last_log_key(), + couch_log_test_util:with_level(CfgLevel, fun() -> + couch_log:TestLevel(Msg, []), + case TestInt >= CfgInt of + true -> + ?assertMatch( + #log_entry{ + level = TestLevel, + pid = Pid, + msg = Msg + }, + couch_log_test_util:last_log() + ); + false -> + ?assertEqual(LastKey, couch_log_test_util:last_log_key()) + end + end), + check_levels(TestLevel, RestLevels). + + +new_msg() -> + random:seed(os:timestamp()), + Bin = list_to_binary([random:uniform(255) || _ <- lists:seq(1, 16)]), + couch_util:to_hex(Bin). diff --git a/test/couch_log_test_util.erl b/test/couch_log_test_util.erl new file mode 100644 index 0000000..2503669 --- /dev/null +++ b/test/couch_log_test_util.erl @@ -0,0 +1,153 @@ +% 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(couch_log_test_util). +-compile(export_all). + + +-include("couch_log.hrl"). + + +start() -> + remove_error_loggers(), + application:set_env(config, ini_files, config_files()), + application:start(config), + ignore_common_loggers(), + application:start(couch_log). + + +stop(_) -> + application:stop(config), + application:stop(couch_log). + + +with_level(Name, Fun) -> + with_config_listener(fun() -> + try + LevelStr = couch_log_util:level_to_string(Name), + config:set("log", "level", LevelStr, false), + wait_for_config(), + Fun() + after + config:delete("log", "level", false) + end + end). + + +with_config_listener(Fun) -> + Listener = self(), + try + add_listener(Listener), + Fun() + after + rem_listener(Listener) + end. + + +wait_for_config() -> + receive + couch_log_config_change_finished -> ok + after 1000 -> + erlang:error(config_change_timeout) + end. + + +with_meck(Mods, Fun) -> + lists:foreach(fun(M) -> + case M of + {Name, Opts} -> meck:new(Name, Opts); + Name -> meck:new(Name) + end + end, Mods), + try + Fun() + after + lists:foreach(fun(M) -> + case M of + {Name, _} -> meck:unload(Name); + Name -> meck:unload(Name) + end + end, Mods) + end. + + +ignore_common_loggers() -> + IgnoreSet = [ + application_controller, + config, + config_event + ], + lists:foreach(fun(Proc) -> + disable_logs_from(Proc) + end, IgnoreSet). + + +disable_logs_from(Pid) when is_pid(Pid) -> + Ignored = case application:get_env(couch_log, ignored_pids) of + {ok, L} when is_list(L) -> + lists:usort([Pid | L]); + _E -> + [Pid] + end, + IgnoredAlive = [P || P <- Ignored, is_process_alive(P)], + application:set_env(couch_log, ignored_pids, IgnoredAlive); + +disable_logs_from(Name) when is_atom(Name) -> + case whereis(Name) of + P when is_pid(P) -> + disable_logs_from(P); + undefined -> + erlang:error({unknown_pid_name, Name}) + end. + + +last_log_key() -> + ets:last(?COUCH_LOG_TEST_TABLE). + + +last_log() -> + [{_, Entry}] = ets:lookup(?COUCH_LOG_TEST_TABLE, last_log_key()), + Entry. + + +remove_error_loggers() -> + lists:foreach(fun(Handler) -> + error_logger:delete_report_handler(Handler) + end, gen_event:which_handlers(error_logger)). + + +config_files() -> + Path = filename:dirname(code:which(?MODULE)), + Name = filename:join(Path, "couch_log_test.ini"), + ok = file:write_file(Name, "[log]\nwriter = ets\n"), + [Name]. + + +add_listener(Listener) -> + Listeners = case application:get_env(couch_log, config_listeners) of + {ok, L} when is_list(L) -> + lists:usort([Listener | L]); + _ -> + [Listener] + end, + application:set_env(couch_log, config_listeners, Listeners). + + +rem_listener(Listener) -> + Listeners = case application:get_env(couch_lig, config_listeners) of + {ok, L} when is_list(L) -> + L -- [Listener]; + _ -> + [] + end, + application:set_env(couch_log, config_listeners, Listeners). + diff --git a/test/couch_log_util_test.erl b/test/couch_log_util_test.erl new file mode 100644 index 0000000..e97911a --- /dev/null +++ b/test/couch_log_util_test.erl @@ -0,0 +1,55 @@ +% 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(couch_log_util_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +get_message_id_test() -> + ?assertEqual("--------", couch_log_util:get_msg_id()), + erlang:put(nonce, "deadbeef"), + ?assertEqual("deadbeef", couch_log_util:get_msg_id()), + erlang:put(nonce, undefined). + + +level_to_atom_test() -> + lists:foreach(fun(L) -> + ?assert(is_atom(couch_log_util:level_to_atom(L))), + ?assert(is_integer(couch_log_util:level_to_integer(L))), + ?assert(is_list(couch_log_util:level_to_string(L))) + end, levels()). + + +string_p_test() -> + ?assertEqual(false, couch_log_util:string_p([])), + ?assertEqual(false, couch_log_util:string_p([[false]])), + ?assertEqual(true, couch_log_util:string_p([$\n])), + ?assertEqual(true, couch_log_util:string_p([$\r])), + ?assertEqual(true, couch_log_util:string_p([$\t])), + ?assertEqual(true, couch_log_util:string_p([$\v])), + ?assertEqual(true, couch_log_util:string_p([$\b])), + ?assertEqual(true, couch_log_util:string_p([$\f])), + ?assertEqual(true, couch_log_util:string_p([$\e])). + + +levels() -> + [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, + "1", "2", "3", "4", "5", "6", "7", "8", "9", + debug, info, notice, warning, warn, error, err, + critical, crit, alert, emergency, emerg, none, + "debug", "info", "notice", "warning", "warn", "error", "err", + "critical", "crit", "alert", "emergency", "emerg", "none" + ]. diff --git a/test/couch_log_writer_ets.erl b/test/couch_log_writer_ets.erl new file mode 100644 index 0000000..d5fd327 --- /dev/null +++ b/test/couch_log_writer_ets.erl @@ -0,0 +1,49 @@ +% 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(couch_log_writer_ets). +-behaviour(couch_log_writer). + + +-export([ + init/0, + terminate/2, + write/2 +]). + + +-include("couch_log.hrl"). + + +init() -> + ets:new(?COUCH_LOG_TEST_TABLE, [named_table, public, ordered_set]), + {ok, 0}. + + +terminate(_, _St) -> + ets:delete(?COUCH_LOG_TEST_TABLE), + ok. + + +write(Entry0, St) -> + Entry = Entry0#log_entry{ + msg = lists:flatten(Entry0#log_entry.msg), + time_stamp = lists:flatten(Entry0#log_entry.time_stamp) + }, + Ignored = application:get_env(couch_log, ignored_pids, []), + case lists:member(Entry#log_entry.pid, Ignored) of + true -> + {ok, St}; + false -> + ets:insert(?COUCH_LOG_TEST_TABLE, {St, Entry}), + {ok, St + 1} + end. diff --git a/test/couch_log_writer_file_test.erl b/test/couch_log_writer_file_test.erl new file mode 100644 index 0000000..6d3f3ec --- /dev/null +++ b/test/couch_log_writer_file_test.erl @@ -0,0 +1,161 @@ +% 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(couch_log_writer_file_test). + + +-include_lib("kernel/include/file.hrl"). +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +-define(WRITER, couch_log_writer_file). + + +couch_log_writer_file_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_init_terminate/0, + fun() -> + couch_log_test_util:with_meck( + [{filelib, [unstick]}], + fun check_ensure_dir_fail/0 + ) + end, + fun() -> + couch_log_test_util:with_meck( + [{file, [unstick, passthrough]}], + fun check_open_fail/0 + ) + end, + fun() -> + couch_log_test_util:with_meck( + [{file, [unstick, passthrough]}], + fun check_read_file_info_fail/0 + ) + end, + fun check_file_write/0, + fun check_buffered_file_write/0, + fun check_reopen/0 + ] + }. + + +check_init_terminate() -> + {ok, St} = ?WRITER:init(), + ok = ?WRITER:terminate(stop, St). + + +check_ensure_dir_fail() -> + meck:expect(filelib, ensure_dir, 1, {error, eperm}), + ?assertEqual({error, eperm}, ?WRITER:init()), + ?assert(meck:called(filelib, ensure_dir, 1)), + ?assert(meck:validate(filelib)). + + +check_open_fail() -> + meck:expect(file, open, 2, {error, enotfound}), + ?assertEqual({error, enotfound}, ?WRITER:init()), + ?assert(meck:called(file, open, 2)), + ?assert(meck:validate(file)). + + +check_read_file_info_fail() -> + RFI = fun + ("./couch.log") -> {error, enoent}; + (Path) -> meck:passthrough([Path]) + end, + meck:expect(file, read_file_info, RFI), + ?assertEqual({error, enoent}, ?WRITER:init()), + ?assert(meck:called(file, read_file_info, 1)), + ?assert(meck:validate(file)). + + +check_file_write() -> + % Make sure we have an empty log for this test + IsFile = filelib:is_file("./couch.log"), + if not IsFile -> ok; true -> + file:delete("./couch.log") + end, + + Entry = #log_entry{ + level = info, + pid = list_to_pid("<0.1.0>"), + msg = "stuff", + msg_id = "msg_id", + time_stamp = "time_stamp" + }, + {ok, St} = ?WRITER:init(), + {ok, NewSt} = ?WRITER:write(Entry, St), + ok = ?WRITER:terminate(stop, NewSt), + + {ok, Data} = file:read_file("./couch.log"), + Expect = <<"[info] time_stamp nonode@nohost <0.1.0> msg_id stuff\n">>, + ?assertEqual(Expect, Data). + + +check_buffered_file_write() -> + % Make sure we have an empty log for this test + IsFile = filelib:is_file("./couch.log"), + if not IsFile -> ok; true -> + file:delete("./couch.log") + end, + + config:set("log", "write_buffer", "1024"), + config:set("log", "write_delay", "10"), + + try + Entry = #log_entry{ + level = info, + pid = list_to_pid("<0.1.0>"), + msg = "stuff", + msg_id = "msg_id", + time_stamp = "time_stamp" + }, + {ok, St} = ?WRITER:init(), + {ok, NewSt} = ?WRITER:write(Entry, St), + ok = ?WRITER:terminate(stop, NewSt) + after + config:delete("log", "write_buffer"), + config:delete("log", "write_delay") + end, + + {ok, Data} = file:read_file("./couch.log"), + Expect = <<"[info] time_stamp nonode@nohost <0.1.0> msg_id stuff\n">>, + ?assertEqual(Expect, Data). + + +check_reopen() -> + {ok, St1} = clear_clock(?WRITER:init()), + {ok, St2} = clear_clock(couch_log_writer_file:maybe_reopen(St1)), + ?assertEqual(St1, St2), + + % Delete file + file:delete("./couch.log"), + {ok, St3} = clear_clock(couch_log_writer_file:maybe_reopen(St2)), + ?assert(element(3, St3) /= element(3, St2)), + + % Recreate file + file:delete("./couch.log"), + file:write_file("./couch.log", ""), + {ok, St4} = clear_clock(couch_log_writer_file:maybe_reopen(St3)), + ?assert(element(3, St4) /= element(3, St2)). + + +clear_clock({ok, St}) -> + {ok, clear_clock(St)}; + +clear_clock(St) -> + {st, Path, Fd, INode, _} = St, + {st, Path, Fd, INode, {0, 0, 0}}. diff --git a/test/couch_log_writer_stderr_test.erl b/test/couch_log_writer_stderr_test.erl new file mode 100644 index 0000000..1e99263 --- /dev/null +++ b/test/couch_log_writer_stderr_test.erl @@ -0,0 +1,58 @@ +% 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(couch_log_writer_stderr_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +-define(WRITER, couch_log_writer_stderr). + + +couch_log_writer_stderr_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_init_terminate/0, + fun() -> + couch_log_test_util:with_meck( + [{io, [unstick]}], + fun check_write/0 + ) + end + ] + }. + + +check_init_terminate() -> + {ok, St} = ?WRITER:init(), + ok = ?WRITER:terminate(stop, St). + + +check_write() -> + meck:expect(io, format, 3, ok), + + Entry = #log_entry{ + level = debug, + pid = list_to_pid("<0.1.0>"), + msg = "stuff", + msg_id = "msg_id", + time_stamp = "time_stamp" + }, + {ok, St} = ?WRITER:init(), + {ok, NewSt} = ?WRITER:write(Entry, St), + ok = ?WRITER:terminate(stop, NewSt), + + ?assert(meck:validate(io)). diff --git a/test/couch_log_writer_syslog_test.erl b/test/couch_log_writer_syslog_test.erl new file mode 100644 index 0000000..c32b5c6 --- /dev/null +++ b/test/couch_log_writer_syslog_test.erl @@ -0,0 +1,122 @@ +% 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(couch_log_writer_syslog_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +-define(WRITER, couch_log_writer_syslog). + + +couch_log_writer_syslog_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_init_terminate/0, + fun() -> + couch_log_test_util:with_meck( + [{io, [unstick]}], + fun check_stderr_write/0 + ) + end, + fun() -> + couch_log_test_util:with_meck( + [{gen_udp, [unstick]}], + fun check_udp_send/0 + ) + end + ] + }. + + +check_init_terminate() -> + {ok, St} = ?WRITER:init(), + ok = ?WRITER:terminate(stop, St). + + +check_stderr_write() -> + meck:expect(io, format, 3, ok), + + Entry = #log_entry{ + level = debug, + pid = list_to_pid("<0.1.0>"), + msg = "stuff", + msg_id = "msg_id", + time_stamp = "time_stamp" + }, + {ok, St} = ?WRITER:init(), + {ok, NewSt} = ?WRITER:write(Entry, St), + ok = ?WRITER:terminate(stop, NewSt), + + ?assert(meck:called(io, format, 3)), + ?assert(meck:validate(io)). + + +check_udp_send() -> + meck:expect(gen_udp, open, 1, {ok, socket}), + meck:expect(gen_udp, send, 4, ok), + meck:expect(gen_udp, close, fun(socket) -> ok end), + + config:set("log", "syslog_host", "localhost"), + try + Entry = #log_entry{ + level = debug, + pid = list_to_pid("<0.1.0>"), + msg = "stuff", + msg_id = "msg_id", + time_stamp = "time_stamp" + }, + {ok, St} = ?WRITER:init(), + {ok, NewSt} = ?WRITER:write(Entry, St), + ok = ?WRITER:terminate(stop, NewSt) + after + config:delete("log", "syslog_host") + end, + + ?assert(meck:called(gen_udp, open, 1)), + ?assert(meck:called(gen_udp, send, 4)), + ?assert(meck:called(gen_udp, close, 1)), + ?assert(meck:validate(gen_udp)). + + +facility_test() -> + Names = [ + "kern", "user", "mail", "daemon", "auth", "syslog", "lpr", + "news", "uucp", "clock", "authpriv", "ftp", "ntp", "audit", + "alert", "cron", "local0", "local1", "local2", "local3", + "local4", "local5", "local6", "local7" + ], + lists:foldl(fun(Name, Id) -> + IdStr = lists:flatten(io_lib:format("~w", [Id])), + ?assertEqual(Id bsl 3, couch_log_writer_syslog:get_facility(Name)), + ?assertEqual(Id bsl 3, couch_log_writer_syslog:get_facility(IdStr)), + Id + 1 + end, 0, Names), + ?assertEqual(23 bsl 3, couch_log_writer_syslog:get_facility("foo")), + ?assertEqual(23 bsl 3, couch_log_writer_syslog:get_facility("-1")), + ?assertEqual(23 bsl 3, couch_log_writer_syslog:get_facility("24")). + + +level_test() -> + Levels = [ + emergency, alert, critical, error, + warning, notice, info, debug + ], + lists:foldl(fun(Name, Id) -> + ?assertEqual(Id, couch_log_writer_syslog:get_level(Name)), + Id + 1 + end, 0, Levels), + ?assertEqual(3, couch_log_writer_syslog:get_level(foo)). diff --git a/test/couch_log_writer_test.erl b/test/couch_log_writer_test.erl new file mode 100644 index 0000000..d0bb347 --- /dev/null +++ b/test/couch_log_writer_test.erl @@ -0,0 +1,54 @@ +% 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(couch_log_writer_test). + + +-include_lib("couch_log/include/couch_log.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +couch_log_writer_test_() -> + {setup, + fun couch_log_test_util:start/0, + fun couch_log_test_util:stop/1, + [ + fun check_writer_change/0 + ] + }. + + +check_writer_change() -> + % Change to file and back + couch_log_test_util:with_config_listener(fun() -> + config:set("log", "writer", "file"), + couch_log_test_util:wait_for_config(), + ?assertEqual(undefined, ets:info(?COUCH_LOG_TEST_TABLE)), + ?assert(is_pid(whereis(couch_log_server))), + + config:set("log", "writer", "couch_log_writer_ets"), + couch_log_test_util:wait_for_config(), + ?assertEqual(0, ets:info(?COUCH_LOG_TEST_TABLE, size)) + end), + + % Using a bad setting doesn't break things + couch_log_test_util:with_config_listener(fun() -> + config:set("log", "writer", "hopefully not an atom or module"), + couch_log_test_util:wait_for_config(), + ?assertEqual(undefined, ets:info(?COUCH_LOG_TEST_TABLE)), + ?assert(is_pid(whereis(couch_log_server))), + + config:set("log", "writer", "couch_log_writer_ets"), + couch_log_test_util:wait_for_config(), + ?assertEqual(0, ets:info(?COUCH_LOG_TEST_TABLE, size)) + end). +