couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dav...@apache.org
Subject [3/5] couch-log commit: updated refs/heads/master to da0e489
Date Fri, 22 Jul 2016 09:49:42 GMT
Make couch_log smarter

This drops the lager and goldrush dependencies and instead moves all
logic to couch_log so that we can make our logging work more closely
with the existing configuration system.

COUCHDB-3067


Project: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/commit/b6b766dd
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/tree/b6b766dd
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/diff/b6b766dd

Branch: refs/heads/master
Commit: b6b766ddfbff8db789723689419ee6f634e752b8
Parents: cc55404
Author: Paul J. Davis <paul.joseph.davis@gmail.com>
Authored: Tue Jul 19 19:14:33 2016 -0500
Committer: Paul J. Davis <paul.joseph.davis@gmail.com>
Committed: Thu Jul 21 18:10:15 2016 -0500

----------------------------------------------------------------------
 include/couch_log.hrl             |  19 +
 rebar.config                      |  15 -
 src/couch_log.app.src             |   7 -
 src/couch_log.erl                 | 163 ++-----
 src/couch_log_config.erl          | 100 ++++
 src/couch_log_config_dyn.erl      |  28 ++
 src/couch_log_config_listener.erl |  74 ++-
 src/couch_log_error_logger_h.erl  |  57 +++
 src/couch_log_formatter.erl       | 417 ++++++++++++++++
 src/couch_log_monitor.erl         |  66 +++
 src/couch_log_server.erl          | 106 +++++
 src/couch_log_stderr.erl          |  57 ---
 src/couch_log_sup.erl             |  26 +-
 src/couch_log_trunc_io.erl        | 838 +++++++++++++++++++++++++++++++++
 src/couch_log_trunc_io_fmt.erl    | 547 +++++++++++++++++++++
 src/couch_log_util.erl            | 149 ++++++
 src/couch_log_writer.erl          |  83 ++++
 src/couch_log_writer_file.erl     | 140 ++++++
 src/couch_log_writer_stderr.erl   |  54 +++
 19 files changed, 2711 insertions(+), 235 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/include/couch_log.hrl
----------------------------------------------------------------------
diff --git a/include/couch_log.hrl b/include/couch_log.hrl
new file mode 100644
index 0000000..a472c0c
--- /dev/null
+++ b/include/couch_log.hrl
@@ -0,0 +1,19 @@
+% 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
+}).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/rebar.config
----------------------------------------------------------------------
diff --git a/rebar.config b/rebar.config
deleted file mode 100644
index 7104d3b..0000000
--- a/rebar.config
+++ /dev/null
@@ -1,15 +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.
-
-{deps, [
-    {meck, ".*", {git, "https://git-wip-us.apache.org/repos/asf/couchdb-meck.git", {tag, "0.8.2"}}}
-]}.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log.app.src
----------------------------------------------------------------------
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, []}}

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log.erl
----------------------------------------------------------------------
diff --git a/src/couch_log.erl b/src/couch_log.erl
index 678559f..0ce4739 100644
--- a/src/couch_log.erl
+++ b/src/couch_log.erl
@@ -12,163 +12,64 @@
 
 -module(couch_log).
 
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--endif.
-
--export([debug/2, info/2, notice/2, warning/2, error/2, critical/2, alert/2,
-         emergency/2]).
--export([set_level/1]).
-
--callback debug(Fmt::string(), Args::list()) -> ok.
--callback info(Fmt::string(), Args::list()) -> ok.
--callback notice(Fmt::string(), Args::list()) -> ok.
--callback warning(Fmt::string(), Args::list()) -> ok.
--callback error(Fmt::string(), Args::list()) -> ok.
--callback critical(Fmt::string(), Args::list()) -> ok.
--callback alert(Fmt::string(), Args::list()) -> ok.
--callback emergency(Fmt::string(), Args::list()) -> ok.
--callback set_level(Level::atom()) -> ok.
-
--spec level_integer(atom()) -> integer().
-level_integer(debug)             -> 1;
-level_integer(info)              -> 2;
-level_integer(notice)            -> 3;
-level_integer(warning)           -> 4;
-level_integer(error)             -> 5;
-level_integer(critical)          -> 6;
-level_integer(alert)             -> 7;
-level_integer(emergency)         -> 8;
-level_integer(none)              -> 9.
-
--spec level_to_atom(string() | integer()) -> atom().
-level_to_atom("1")                  -> debug;
-level_to_atom("debug")              -> debug;
-level_to_atom("2")                  -> info;
-level_to_atom("info")               -> info;
-level_to_atom("3")                  -> notice;
-level_to_atom("notice")             -> notice;
-level_to_atom("4")                  -> warning;
-level_to_atom("warning")            -> warning;
-level_to_atom("5")                  -> error;
-level_to_atom("error")              -> error;
-level_to_atom("6")                  -> critical;
-level_to_atom("critical")           -> critical;
-level_to_atom("7")                  -> alert;
-level_to_atom("alert")              -> alert;
-level_to_atom("8")                  -> emergency;
-level_to_atom("emergency")          -> emergency;
-level_to_atom("9")                  -> none;
-level_to_atom("none")               -> none;
-level_to_atom(V) when is_integer(V) -> level_to_atom(integer_to_list(V));
-level_to_atom(V) when is_list(V)    -> notice.
+
+-export([
+    debug/2,
+    info/2,
+    notice/2,
+    warning/2,
+    error/2,
+    critical/2,
+    alert/2,
+    emergency/2,
+
+    set_level/1
+]).
+
 
 -spec debug(string(), list()) -> ok.
 debug(Fmt, Args) -> log(debug, Fmt, Args).
 
+
 -spec info(string(), list()) -> ok.
 info(Fmt, Args) -> log(info, Fmt, Args).
 
+
 -spec notice(string(), list()) -> ok.
 notice(Fmt, Args) -> log(notice, Fmt, Args).
 
+
 -spec warning(string(), list()) -> ok.
 warning(Fmt, Args) -> log(warning, Fmt, Args).
 
+
 -spec error(string(), list()) -> ok.
 error(Fmt, Args) -> log(error, Fmt, Args).
 
+
 -spec critical(string(), list()) -> ok.
 critical(Fmt, Args) -> log(critical, Fmt, Args).
 
+
 -spec alert(string(), list()) -> ok.
 alert(Fmt, Args) -> log(alert, Fmt, Args).
 
+
 -spec emergency(string(), list()) -> ok.
 emergency(Fmt, Args) -> log(emergency, Fmt, Args).
 
+
+-spec set_level(atom() | string() | integer()) -> true.
+set_level(Level) ->
+    config:set("log", "level", couch_log_util:level_to_string(Level)).
+
+
 -spec log(atom(), string(), list()) -> ok.
 log(Level, Fmt, Args) ->
-    case is_active_level(Level) of
-        false -> ok;
+    case couch_log_util:should_log(Level) of
         true ->
-            {ok, Backend} = get_backend(),
-            catch couch_stats:increment_counter([couch_log, level, Level]),
-            apply(Backend, Level, [Fmt, Args])
+            Entry = couch_log_formatter:format(Level, self(), Fmt, Args),
+            ok = couch_log_server:log(Entry);
+        false ->
+            ok
     end.
-
--spec is_active_level(atom()) -> boolean().
-is_active_level(Level) ->
-    CurrentLevel = level_to_atom(config:get("log", "level", "notice")),
-    level_integer(Level) >= level_integer(CurrentLevel).
-
--spec set_level(atom() | string() | integer()) -> ok.
-set_level(Level) when is_atom(Level) ->
-    {ok, Backend} = get_backend(),
-    Backend:set_level(Level);
-set_level(Level) ->
-    set_level(level_to_atom(Level)).
-
--spec get_backend() -> {ok, atom()}.
-get_backend() ->
-    BackendName = "couch_log_" ++ config:get("log", "backend", "stderr"),
-    {ok, list_to_existing_atom(BackendName)}.
-
--ifdef(TEST).
-
-callbacks_test_() ->
-    {setup,
-        fun setup/0,
-        fun cleanup/1,
-        [
-            ?_assertEqual({ok, couch_log_eunit}, get_backend()),
-            ?_assertEqual(ok, couch_log:set_level(info)),
-            ?_assertEqual(ok, couch_log:debug("debug", [])),
-            ?_assertEqual(ok, couch_log:info("info", [])),
-            ?_assertEqual(ok, couch_log:notice("notice", [])),
-            ?_assertEqual(ok, couch_log:warning("warning", [])),
-            ?_assertEqual(ok, couch_log:error("error", [])),
-            ?_assertEqual(ok, couch_log:critical("critical", [])),
-            ?_assertEqual(ok, couch_log:alert("alert", [])),
-            ?_assertEqual(ok, couch_log:emergency("emergency", [])),
-            ?_assertEqual(stats_calls(), meck:history(couch_stats, self())),
-            ?_assertEqual(log_calls(), meck:history(couch_log_eunit, self()))
-        ]
-    }.
-
-setup() ->
-    ok = meck:new(config),
-    ok = meck:expect(config, get,
-        fun("log", "backend", _) -> "eunit";
-           ("log", "level", _)   -> "debug" end),
-    meck:new([couch_stats, couch_log_eunit], [non_strict]),
-    meck:expect(couch_stats, increment_counter, 1, ok),
-    setup_couch_log_eunit().
-
-cleanup(_) ->
-    meck:unload(config),
-    meck:unload([couch_stats, couch_log_eunit]).
-
-setup_couch_log_eunit() ->
-    meck:expect(couch_log_eunit, set_level, 1, ok),
-    Levels = [debug, info, notice, warning, error, critical, alert, emergency],
-    lists:foreach(fun(Fun) ->
-        meck:expect(couch_log_eunit, Fun, 2, ok)
-    end, Levels).
-
-stats_calls() ->
-    Levels = [debug, info, notice, warning, error, critical, alert, emergency],
-    lists:map(fun(Level) ->
-        MFA = {couch_stats, increment_counter, [[couch_log, level, Level]]},
-        {self(), MFA, ok}
-    end, Levels).
-
-log_calls() ->
-    Levels = [debug, info, notice, warning, error, critical, alert, emergency],
-    Calls = lists:map(fun(Level) ->
-        MFA = {couch_log_eunit, Level, [atom_to_list(Level),[]]},
-        {self(), MFA, ok}
-    end, Levels),
-    [{self(), {couch_log_eunit, set_level, [info]}, ok}|Calls].
-
--endif.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_config.erl
----------------------------------------------------------------------
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

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_config_dyn.erl
----------------------------------------------------------------------
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.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_config_listener.erl
----------------------------------------------------------------------
diff --git a/src/couch_log_config_listener.erl b/src/couch_log_config_listener.erl
index 6dc7ea6..287f79d 100644
--- a/src/couch_log_config_listener.erl
+++ b/src/couch_log_config_listener.erl
@@ -11,36 +11,64 @@
 % the License.
 
 -module(couch_log_config_listener).
--vsn(2).
 -behaviour(config_listener).
 
-% public interface
--export([subscribe/0]).
 
-% config_listener callback
--export([handle_config_change/5, handle_config_terminate/3]).
+-export([
+    start/0
+]).
+
+-export([
+    handle_config_change/5,
+    handle_config_terminate/3
+]).
+
+
+-ifdef(TEST).
+-define(RELISTEN_DELAY, 500).
+-else.
+-define(RELISTEN_DELAY, 5000).
+-endif.
+
+
+start() ->
+    ok = config:listen_for_changes(?MODULE, nil).
 
-subscribe() ->
-    Settings = [
-        {backend, config:get("log", "backend", "stderr")},
-        {level, config:get("log", "level", "notice")}
-    ],
-    ok = config:listen_for_changes(?MODULE, Settings),
-    ok.
 
-handle_config_change("log", "backend", Value, _, Settings) ->
-    {level, Level} = lists:keyfind(level, 1, Settings),
-    couch_log:set_level(Level),
-    {ok, lists:keyreplace(backend, 1, Settings, {backend, Value})};
-handle_config_change("log", "level", Value, _, Settings) ->
-    couch_log:set_level(Value),
-    {ok, lists:keyreplace(level, 1, Settings, {level, Value})};
+handle_config_change("log", Key, _, _, _) ->
+    case Key of
+        "level" ->
+            couch_log_config:reconfigure();
+        "max_message_size" ->
+            couch_log_config:reconfigure();
+        _ ->
+            % Someone may have changed the config for
+            % the writer so we need to re-initialize.
+            couch_log_server:reconfigure()
+    end,
+    notify_listeners(),
+    {ok, nil};
+
 handle_config_change(_, _, _, _, Settings) ->
     {ok, Settings}.
 
-handle_config_terminate(_, stop, _) -> ok;
-handle_config_terminate(_Server, _Reason, State) ->
+
+handle_config_terminate(_, stop, _) ->
+    ok;
+handle_config_terminate(_, _, _) ->
     spawn(fun() ->
-        timer:sleep(5000),
-        config:listen_for_changes(?MODULE, State)
+        timer:sleep(?RELISTEN_DELAY),
+        ok = config:listen_for_changes(?MODULE, nil)
     end).
+
+
+-ifdef(TEST).
+notify_listeners() ->
+    Listeners = application:get_env(couch_log, config_listeners, []),
+    lists:foreach(fun(L) ->
+        L ! couch_log_config_change_finished
+    end, Listeners).
+-else.
+notify_listeners() ->
+    ok.
+-endif.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_error_logger_h.erl
----------------------------------------------------------------------
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}.
+

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_formatter.erl
----------------------------------------------------------------------
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.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_monitor.erl
----------------------------------------------------------------------
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}.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_server.erl
----------------------------------------------------------------------
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}.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_stderr.erl
----------------------------------------------------------------------
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.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_sup.erl
----------------------------------------------------------------------
diff --git a/src/couch_log_sup.erl b/src/couch_log_sup.erl
index 9d69fd0..3106659 100644
--- a/src/couch_log_sup.erl
+++ b/src/couch_log_sup.erl
@@ -23,5 +23,27 @@ start_link() ->
 
 
 init([]) ->
-    ok = couch_log_config_listener:subscribe(),
-    {ok, {{one_for_one, 1, 1}, []}}.
+    ok = couch_log_config:init(),
+    ok = couch_log_config_listener:start(),
+    {ok, {{one_for_one, 1, 1}, children()}}.
+
+
+children() ->
+    [
+        {
+            couch_log_server,
+            {couch_log_server, start_link, []},
+            permanent,
+            5000,
+            worker,
+            [couch_log_server]
+        },
+        {
+            couch_log_monitor,
+            {couch_log_monitor, start_link, []},
+            permanent,
+            5000,
+            worker,
+            [couch_log_monitor]
+        }
+    ].

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/b6b766dd/src/couch_log_trunc_io.erl
----------------------------------------------------------------------
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


Mime
View raw message