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).
+