Merge branch '3067-improve-couch-log'
diff --git a/include/couch_log.hrl b/include/couch_log.hrl
new file mode 100644
index 0000000..fa544a8
--- /dev/null
+++ b/include/couch_log.hrl
@@ -0,0 +1,22 @@
+% 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.
+
+-record(log_entry, {
+ level,
+ pid,
+ msg,
+ msg_id,
+ time_stamp
+}).
+
+
+-define(COUCH_LOG_TEST_TABLE, couch_log_test_table).
diff --git a/rebar.config b/rebar.config
index 7104d3b..e0d1844 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,15 +1,2 @@
-% 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.
-
-{deps, [
- {meck, ".*", {git, "https://git-wip-us.apache.org/repos/asf/couchdb-meck.git", {tag, "0.8.2"}}}
-]}.
+{cover_enabled, true}.
+{cover_print_enabled, true}.
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]).
+-export([
+ debug/2,
+ info/2,
+ notice/2,
+ warning/2,
+ error/2,
+ critical/2,
+ alert/2,
+ emergency/2,
--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.
+ set_level/1
+]).
--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.
-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
+]).
-subscribe() ->
- Settings = [
- {backend, config:get("log", "backend", "stderr")},
- {level, config:get("log", "level", "notice")}
- ],
- ok = config:listen_for_changes(?MODULE, Settings),
- ok.
+-export([
+ handle_config_change/5,
+ handle_config_terminate/3
+]).
-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})};
+
+-ifdef(TEST).
+-define(RELISTEN_DELAY, 500).
+-else.
+-define(RELISTEN_DELAY, 5000).
+-endif.
+
+
+start() ->
+ ok = config:listen_for_changes(?MODULE, nil).
+
+
+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,
+ <<Prefix:PrefixSize/binary, _/binary>> = 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 @@
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, <tt>matthias@corelatus.se</tt>
+%%
+%% Various changes to this module, most notably the format/3 implementation
+%% were added by Andrew Thompson `<andrew@basho.com>'. 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),
+ <<Value:Size>> = 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}.
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.
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).
+