Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f8ab87d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+ebin
+src/.*.swp
diff --git a/rebar b/rebar
new file mode 100755
index 0000000..1f55c73
--- /dev/null
+++ b/rebar
Binary files differ
diff --git a/src/twig.app.src b/src/twig.app.src
new file mode 100644
index 0000000..7e1da74
--- /dev/null
+++ b/src/twig.app.src
@@ -0,0 +1,8 @@
+{application, twig, [
+    {description, "Logger"},
+    {vsn, git},
+    {registered, []},
+    {applications, [kernel, stdlib]},
+    {mod, {twig_app, []}},
+    {env, []}
+]}.
diff --git a/src/twig.erl b/src/twig.erl
new file mode 100644
index 0000000..a3a9c58
--- /dev/null
+++ b/src/twig.erl
@@ -0,0 +1,78 @@
+% 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(twig).
+
+-export([debug/1, info/1, notice/1, warn/1, err/1, crit/1, alert/1, emerg/1,
+         debug/2, info/2, notice/2, warn/2, err/2, crit/2, alert/2, emerg/2,
+         debug/3, info/3, notice/3, warn/3, err/3, crit/3, alert/3, emerg/3]).
+
+-export([set_level/1]).
+
+-include("twig_int.hrl").
+
+debug(Format) ->                    log(debug, Format, [], []).
+debug(Format, Data) ->              log(debug, Format, Data, []).
+debug(Format, Data, Options) ->     log(debug, Format, Data, Options).
+
+info(Format) ->                     log(info, Format, [], []).
+info(Format, Data) ->               log(info, Format, Data, []).
+info(Format, Data, Options) ->      log(info, Format, Data, Options).
+
+notice(Format) ->                   log(notice, Format, [], []).
+notice(Format, Data) ->             log(notice, Format, Data, []).
+notice(Format, Data, Options) ->    log(notice, Format, Data, Options).
+
+warn(Format) ->                     log(warn, Format, [], []).
+warn(Format, Data) ->               log(warn, Format, Data, []).
+warn(Format, Data, Options) ->      log(warn, Format, Data, Options).
+
+err(Format) ->                      log(err, Format, [], []).
+err(Format, Data) ->                log(err, Format, Data, []).
+err(Format, Data, Options) ->       log(err, Format, Data, Options).
+
+crit(Format) ->                     log(crit, Format, [], []).
+crit(Format, Data) ->               log(crit, Format, Data, []).
+crit(Format, Data, Options) ->      log(crit, Format, Data, Options).
+
+alert(Format) ->                    log(alert, Format, [], []).
+alert(Format, Data) ->              log(alert, Format, Data, []).
+alert(Format, Data, Options) ->     log(alert, Format, Data, Options).
+
+emerg(Format) ->                    log(emerg, Format, [], []).
+emerg(Format, Data) ->              log(emerg, Format, Data, []).
+emerg(Format, Data, Options) ->     log(emerg, Format, Data, Options).
+
+set_level(LevelAtom) ->
+    application:set_env(twig, {level, twig_util:level(LevelAtom)}).
+
+%% internal
+
+log(LevelAtom, Format, Data, _Options) ->
+    %% TODO do something useful with options
+    Level = twig_util:level(LevelAtom),
+    case application:get_env(twig, level) of
+        {ok, Threshold} when Level =< Threshold ->
+            send_message(Level, Format, Data);
+        undefined when Level =< ?LEVEL_INFO ->
+            send_message(Level, Format, Data);
+        _ ->
+            ok
+    end.
+
+send_message(Level, Format, Data) ->
+    gen_event:sync_notify(error_logger, format(Level, Format, Data)).
+
+format(Level, Format, Data) ->
+    %% TODO truncate large messages
+    {twig, Level, iolist_to_binary(io_lib:format(Format, Data))}.
+
diff --git a/src/twig_app.erl b/src/twig_app.erl
new file mode 100644
index 0000000..e16ad58
--- /dev/null
+++ b/src/twig_app.erl
@@ -0,0 +1,21 @@
+% 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(twig_app).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) ->
+    twig_sup:start_link().
+
+stop(_State) ->
+    ok.
diff --git a/src/twig_event_handler.erl b/src/twig_event_handler.erl
new file mode 100644
index 0000000..874789c
--- /dev/null
+++ b/src/twig_event_handler.erl
@@ -0,0 +1,147 @@
+% 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(twig_event_handler).
+
+-behaviour(gen_event).
+
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2,
+        code_change/3]).
+
+-record(state, {
+    socket,
+    host,
+    port,
+    hostname,
+    os_pid,
+    appid,
+    facility,
+    level
+}).
+
+-include("twig_int.hrl").
+
+init([]) ->
+    {ok, Socket} = gen_udp:open(0),
+    {ok, ok, State} = handle_call(load_config, #state{socket=Socket}),
+    {ok, State}.
+
+handle_event({twig, Level, Msg}, State) ->
+    write(Level, Msg, State),
+    {ok, State};
+
+% OTP standard events
+handle_event({Class, _GL, {Pid, Format, Args}}, #state{level=Max} = State) ->
+    case otp_event_level(Class, Format) of
+        undefined ->
+            {ok, State};
+        Level when Level > Max ->
+            {ok, State};
+        Level ->
+            write(Level, message(Pid, Format, Args), State),
+            {ok, State}
+    end;
+
+handle_event(_Event, State) ->
+    {ok, State}.
+
+handle_call({set_level, Level}, State) ->
+    {ok, ok, State#state{level = Level}};
+
+handle_call(load_config, State) ->
+    Host = case inet:getaddr(get_env(host, undefined), inet) of
+    {ok, Address} ->
+        Address;
+    {error, _} ->
+        undefined
+    end,
+    NewState = State#state{
+        host = Host,
+        port = get_env(port, 514),
+        hostname = net_adm:localhost(),
+        os_pid = os:getpid(),
+        appid = get_env(appid, "bigcouch"),
+        facility = twig_util:facility(get_env(facility, local2)),
+        level = twig_util:level(get_env(level, info))
+    },
+    {ok, ok, NewState};
+
+handle_call(_Call, State) ->
+    {ok, ignored, State}.
+
+handle_info(_Info, State) ->
+    {ok, State}.
+
+terminate(_Reason, State) ->
+    gen_udp:close(State#state.socket).
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+get_env(Key, Default) ->
+    case application:get_env(twig, Key) of
+        {ok, Value} ->
+            Value;
+        undefined ->
+            Default
+    end.
+
+write(_, _, #state{host=undefined}) ->
+    ok;
+write(Level, Msg, State) when is_list(Msg); is_binary(Msg) ->
+    #state{facility=Facil, appid=App, hostname=Hostname, host=Host, port=Port,
+        socket=Socket, os_pid=OsPid} = State,
+     Pre = io_lib:format("<~B>~B ~s ~s ~s ~s - - ", [Facil bor Level,
+        ?SYSLOG_VERSION, twig_util:iso8601_timestamp(), Hostname, App, OsPid]),
+    %% TODO truncate large messages
+     gen_udp:send(Socket, Host, Port, [Pre, Msg, $\n]);
+write(Level, {Format0, Args0}, State) ->
+    #state{facility=Facil, appid=App, hostname=Hostname, host=Host, port=Port,
+        socket=Socket, os_pid=OsPid} = State,
+    Format = "<~B>~B ~s ~s ~s ~s - - " ++ Format0 ++ "\n",
+    Args = [Facil bor Level, ?SYSLOG_VERSION, twig_util:iso8601_timestamp(),
+        Hostname, App, OsPid | Args0],
+    %% TODO truncate large messages
+    Packet = io_lib:format(Format, Args),
+    gen_udp:send(Socket, Host, Port, Packet).
+
+message(_Pid, crash_report, Report) ->
+    proc_lib:format(Report);
+message(Pid, supervisor_report, Report) ->
+    Name = lists:keyfind(supervisor, 1, Report),
+    Error = lists:keyfind(errorContext, Report),
+    Reason = lists:keyfind(reason, 1, Report),
+    Offender = lists:keyfind(offender, 1, Report),
+    ChildPid = lists:keyfind(pid, 1, Offender),
+    ChildName = lists:keyfind(name, 1, Offender),
+    {M,F,_} = lists:keyfind(mfa, 1, Offender),
+    {"[~p] SUPERVISOR REPORT ~p ~p (~p) child: ~p [~p] ~p:~p",
+        [Pid, Name, Error, Reason, ChildName, ChildPid, M, F]};
+message(Pid, progress_report, Report) ->
+    {"[~p] PROGRESS REPORT~n~p", [Pid, Report]};
+message(Pid, Type, Report) when Type == std_error;
+                                Type == std_info;
+                                Type == std_warning ->
+    {"[~p] ~p: ~p", [Pid, Type, Report]};
+message(Pid, Format, Args) ->
+    {"[~p] " ++ Format, [Pid|Args]}.
+
+otp_event_level(error, _) ->                        ?LEVEL_ERR;
+otp_event_level(warning_msg, _) ->                  ?LEVEL_WARN;
+otp_event_level(info_msg, _) ->                     ?LEVEL_INFO;
+otp_event_level(_, {_, crash_report, _}) ->         ?LEVEL_CRIT;
+otp_event_level(_, {_, supervisor_report, _}) ->    ?LEVEL_WARN;
+otp_event_level(_, {_, progress_report, _}) ->      ?LEVEL_DEBUG;
+otp_event_level(error_report, _) ->                 ?LEVEL_ERR;
+otp_event_level(warning_report, _) ->               ?LEVEL_WARN;
+otp_event_level(info_report, _) ->                  ?LEVEL_INFO;
+otp_event_level(_, _) ->                            undefined.
diff --git a/src/twig_int.hrl b/src/twig_int.hrl
new file mode 100644
index 0000000..27f66ad
--- /dev/null
+++ b/src/twig_int.hrl
@@ -0,0 +1,23 @@
+% 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.
+
+-define(SYSLOG_VERSION, 1).
+
+-define(LEVEL_DEBUG, 7).
+-define(LEVEL_INFO, 6).
+-define(LEVEL_NOTICE, 5).
+-define(LEVEL_WARN, 4).
+-define(LEVEL_ERR, 3).
+-define(LEVEL_CRIT, 2).
+-define(LEVEL_ALERT, 1).
+-define(LEVEL_EMERG, 0).
+
diff --git a/src/twig_monitor.erl b/src/twig_monitor.erl
new file mode 100644
index 0000000..5a4f86c
--- /dev/null
+++ b/src/twig_monitor.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(twig_monitor).
+
+-behaviour(gen_server).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
+        code_change/3]).
+
+-export([start_link/0]).
+
+start_link() ->
+    gen_server:start_link(?MODULE, [], []).
+
+init(_) ->
+    gen_event:add_sup_handler(error_logger, twig_event_handler, []).
+
+handle_call(_Call, _From, State) ->
+    {reply, ignored, State}.
+
+handle_cast(_Cast, State) ->
+    {noreply, State}.
+
+handle_info({gen_event_EXIT, twig_event_handler, Reason} = Msg, State) ->
+    io:format("~p~n", [Msg]),
+    {stop, Reason, State};
+
+handle_info(_Msg, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_, State, _) ->
+    {ok, State}.
diff --git a/src/twig_sup.erl b/src/twig_sup.erl
new file mode 100644
index 0000000..dc9aa13
--- /dev/null
+++ b/src/twig_sup.erl
@@ -0,0 +1,24 @@
+% 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(twig_sup).
+-behaviour(supervisor).
+-export([start_link/0, init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+    {ok, { {one_for_one, 5, 10}, [?CHILD(twig_monitor, gen_server)]} }.
diff --git a/src/twig_util.erl b/src/twig_util.erl
new file mode 100644
index 0000000..19ec802
--- /dev/null
+++ b/src/twig_util.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(twig_util).
+
+-export([level/1, facility/1, iso8601_timestamp/0]).
+
+level(debug) ->     7;
+level(info) ->      6;
+level(notice) ->    5;
+level(warn) ->      4;
+level(err) ->       3;
+level(crit) ->      2;
+level(alert) ->     1;
+level(emerg) ->     0;
+
+level(I) when is_integer(I), I >= 0, I =< 7 ->
+    I;
+level(_BadLevel) ->
+    3.
+
+facility(kern)     -> (0 bsl 3) ; % kernel messages
+facility(user)     -> (1 bsl 3) ; % random user-level messages
+facility(mail)     -> (2 bsl 3) ; % mail system
+facility(daemon)   -> (3 bsl 3) ; % system daemons
+facility(auth)     -> (4 bsl 3) ; % security/authorization messages
+facility(syslog)   -> (5 bsl 3) ; % messages generated internally by syslogd
+facility(lpr)      -> (6 bsl 3) ; % line printer subsystem
+facility(news)     -> (7 bsl 3) ; % network news subsystem
+facility(uucp)     -> (8 bsl 3) ; % UUCP subsystem
+facility(cron)     -> (9 bsl 3) ; % clock daemon
+facility(authpriv) -> (10 bsl 3); % security/authorization messages (private)
+facility(ftp)      -> (11 bsl 3); % ftp daemon
+
+facility(local0)   -> (16 bsl 3);
+facility(local1)   -> (17 bsl 3);
+facility(local2)   -> (18 bsl 3);
+facility(local3)   -> (19 bsl 3);
+facility(local4)   -> (20 bsl 3);
+facility(local5)   -> (21 bsl 3);
+facility(local6)   -> (22 bsl 3);
+facility(local7)   -> (23 bsl 3).
+
+
+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]).