├── test ├── test_helper.exs └── exsyslog_test.exs ├── .gitignore ├── mix.lock ├── lib ├── exsyslog.ex └── ex_syslog │ ├── exsyslog_message.ex │ ├── supervisor.ex │ ├── exsyslog_monitor.ex │ ├── exsyslog.ex │ ├── exsyslog_util.ex │ └── exsyslog_event_handler.ex ├── mix.exs ├── LICENSE ├── config └── config.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"exactor": {:package, "0.3.3"}, 2 | "exprintf": {:package, "0.1.2"}} 3 | -------------------------------------------------------------------------------- /lib/exsyslog.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | ExSyslog.Supervisor.start_link 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/ex_syslog/exsyslog_message.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog.Message do 2 | defstruct level: 0, msg: "", msgid: :erlang.get(:nonce), pid: nil 3 | 4 | def new, do: %ExSyslog.Message{} 5 | def new(opts), do: struct(new, opts) 6 | end 7 | -------------------------------------------------------------------------------- /test/exsyslog_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExsyslogTest do 2 | use ExUnit.Case 3 | alias ExSyslog.Logger, as: ExLog 4 | 5 | defmodule MyStruct do 6 | defstruct one: 1 7 | end 8 | 9 | test "logs inspected structs" do 10 | str = inspect(%MyStruct{}) 11 | ExLog.log :none, str 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ex_syslog/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog.Supervisor do 2 | use Supervisor.Behaviour 3 | 4 | def start_link do 5 | :supervisor.start_link(__MODULE__, []) 6 | end 7 | 8 | def init([]) do 9 | children = [ 10 | worker(ExSyslog.Monitor, []) 11 | ] 12 | 13 | # See http://elixir-lang.org/docs/stable/Supervisor.Behaviour.html 14 | # for other strategies and supported options 15 | supervise(children, strategy: :one_for_one) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ex_syslog/exsyslog_monitor.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog.Monitor do 2 | 3 | use ExActor.GenServer 4 | 5 | definit do 6 | :gen_event.add_sup_handler :error_logger, ExSyslog.EventHandler, [] 7 | initial_state [] 8 | end 9 | 10 | def handle_info({:gen_event_EXIT, ExSyslog.EventHandler, reason} = msg, state) do 11 | :io.format('~p~n', [msg]) 12 | {:stop, reason, state} 13 | end 14 | 15 | def terminate(_reason, _state), do: :ok 16 | def code_change(_, state, _), do: {:ok, state} 17 | 18 | end 19 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Exsyslog.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :exsyslog, 6 | version: "0.0.6", 7 | elixir: "~> 0.13.2", 8 | deps: deps] 9 | end 10 | 11 | # Configuration for the OTP application 12 | def application do 13 | [applications: [], 14 | mod: {ExSyslog, []}] 15 | end 16 | 17 | # Dependencies can be hex.pm packages: 18 | defp deps do 19 | [ 20 | {:exprintf, "~>0.1.0"}, 21 | {:exactor, "~>0.3.3"}, 22 | ] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 E-MetroTel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application and 2 | # its dependencies. It must return a keyword list containing the 3 | # application name and have as value another keyword list with 4 | # the application key-value pairs. 5 | 6 | # Note this configuration is loaded before any dependency and is 7 | # restricted to this project. If another project depends on this 8 | # project, this file won't be loaded nor affect the parent project. 9 | 10 | # You can customize the configuration path by setting :config_path 11 | # in your mix.exs file. For example, you can emulate configuration 12 | # per environment by setting: 13 | # 14 | # config_path: "config/#{Mix.env}.exs" 15 | # 16 | # Changing any file inside the config directory causes the whole 17 | # project to be recompiled. 18 | 19 | # Sample configuration: 20 | # 21 | # [dep1: [key: :value], 22 | # dep2: [key: :value]] 23 | 24 | 25 | # [exsyslog: [ 26 | # level: :info, 27 | # host: '127.0.0.1', 28 | # facility: :local2, 29 | # appid: "exsyslog", 30 | # max_term_size: 8192, 31 | # max_message_size: 16000 32 | # ]] 33 | [exsyslog: [level: :info, host: '127.0.0.1', facility: :local1]] 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exsyslog 2 | 3 | ExSysylog is an Elixir port of the erlang [Twig](https://github.com/cloudant/twig) logger. 4 | 5 | ExSyslog is an Elixir/OTP logger. It installs a gen_event handler in the error_logger event manager, 6 | where it consumes standards OTP reports and messages as well as events generated by the ExSyslog.Logger.log. 7 | Log messages are written to a syslog server over UDP using the format specified in RFC 5424. 8 | 9 | ## Configuration 10 | 11 | ExSyslog's behavior is controlled using the application configuration environment: 12 | 13 | * __host__ (undefined): the hostname of the syslog server 14 | * __port__ (514): the port of the syslog server 15 | * __facility__ (local2): syslog facility to be used 16 | * __level__ (info): logging threshold. Messages "above" this threshold (in syslog parlance) will be discarded. Acceptable values are debug, info, notice, warn, err, crit, alert, and emerg. 17 | * __appid__ ("exsyslog"): inserted as the APPID in the syslog message 18 | * __max_term_size__ (8192): raw data size below which we format normally 19 | * __max_message_size__ (16000): approx. max size of truncated string 20 | 21 | ## License 22 | 23 | exrm_rpm is copyright (c) 2014 E-MetroTel. 24 | 25 | The source code is released under the MIT License. 26 | 27 | Check [LICENSE](LICENSE) for more information. 28 | -------------------------------------------------------------------------------- /lib/ex_syslog/exsyslog.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog.Logger do 2 | 3 | import ExPrintf 4 | import ExSyslog.Util, only: [get_env: 2] 5 | 6 | @info_level ExSyslog.Util.level(:info) 7 | 8 | def level(level_atom) do 9 | level = ExSyslog.Util.level(level_atom) 10 | ExSyslog.EventHandler.level level 11 | end 12 | 13 | def level() do 14 | get_env(:level, :undefined) 15 | end 16 | 17 | def log(level_atom, string), do: log(level_atom, String.replace(string, "%", "%%"), []) 18 | 19 | def log(level_atom, category, format, data) when is_atom(category) do 20 | category = String.upcase "#{category}" 21 | log level_atom, sprintf("<<%s>> -- %s", [category, format]), data 22 | end 23 | 24 | def log(level_atom, category, string) when is_atom(category) do 25 | log(level_atom, category, String.replace(string, "%", "%%"), []) 26 | end 27 | 28 | def log(level_atom, format, data) do 29 | level = ExSyslog.Util.level(level_atom) 30 | case ExSyslog.Util.level(Application.get_env :exsyslog, :level) do 31 | nil when level <= @info_level -> 32 | send_message(level_atom, format, data) 33 | threshold when level <= threshold -> 34 | send_message(level_atom, format, data) 35 | _ -> 36 | :ok 37 | end 38 | end 39 | 40 | def send_message(level, format, data) do 41 | :gen_event.notify(:error_logger, format(level, format, data)) 42 | end 43 | 44 | def format(level, format, data) when is_list(data) do 45 | [ level: ExSyslog.Util.level(level), msg: ExSyslog.Util.format(level, format, data), pid: self ] 46 | |> ExSyslog.Message.new 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/ex_syslog/exsyslog_util.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog.Util do 2 | import ExPrintf 3 | use Bitwise 4 | 5 | def level(:debug), do: 7 6 | def level(:info), do: 6 7 | def level(:notice), do: 5 8 | def level(:warn), do: 4 9 | def level(:warning), do: 4 10 | def level(:err), do: 3 11 | def level(:error), do: 3 12 | def level(:crit), do: 2 13 | def level(:alert), do: 1 14 | def level(:emerg), do: 0 15 | def level(:panic), do: 0 16 | def level(:none), do: 0 17 | 18 | def level(i) when is_integer(i) when i >= 0 and i <= 7, do: i 19 | def level(_bad), do: 3 20 | 21 | def format(level, format, data) when is_list(data) do 22 | level = String.upcase "#{level}" 23 | sprintf("%s: %s", [level, format]) 24 | |> sprintf(data) 25 | |> format([]) 26 | end 27 | 28 | def format(format, data) do 29 | max_term_size = get_env :max_term_size, 8192 30 | if :erts_debug.flat_size(data) > max_term_size do 31 | max_string = get_env :max_message_size, 16000 32 | {truncated, _} = :trunc_io.print(data, max_string) 33 | ["*Truncated* ", format, " - ", truncated] 34 | else 35 | :io_lib.format(format, data) 36 | end 37 | end 38 | 39 | def iso8601_timestamp do 40 | {{_year,month,date},{hour,minute,second}} = :calendar.local_time() 41 | mstr = elem({"Jan","Feb","Mar","Apr","May","Jun","Jul", "Aug","Sep","Oct","Nov","Dec"}, month-1) 42 | sprintf("%s %02d %02d:%02d:%02d", [mstr, date, hour, minute, second]) 43 | end 44 | 45 | def get_env(key, default), do: Application.get_env(:exsyslog, key, default) 46 | def put_env(key, value), do: Application.put_env(:exsyslog, key, value) 47 | 48 | def facility(:kern), do: (0 <<< 3) # % kernel messages 49 | def facility(:user), do: (1 <<< 3) # % random user-level messages 50 | def facility(:mail), do: (2 <<< 3) # % mail system 51 | def facility(:daemon), do: (3 <<< 3) # % system daemons 52 | def facility(:auth), do: (4 <<< 3) # % security/authorization messages 53 | def facility(:syslog), do: (5 <<< 3) # % messages generated internally by syslogd 54 | def facility(:lpr), do: (6 <<< 3) # % line printer subsystem 55 | def facility(:news), do: (7 <<< 3) # % network news subsystem 56 | def facility(:uucp), do: (8 <<< 3) # % UUCP subsystem 57 | def facility(:cron), do: (9 <<< 3) # % clock daemon 58 | def facility(:authpriv), do: (10 <<< 3)# % security/authorization messages (private) 59 | def facility(:ftp), do: (11 <<< 3) # % ftp daemon 60 | 61 | def facility(:local0), do: (16 <<< 3) 62 | def facility(:local1), do: (17 <<< 3) 63 | def facility(:local2), do: (18 <<< 3) 64 | def facility(:local3), do: (19 <<< 3) 65 | def facility(:local4), do: (20 <<< 3) 66 | def facility(:local5), do: (21 <<< 3) 67 | def facility(:local6), do: (22 <<< 3) 68 | def facility(:local7), do: (23 <<< 3) 69 | end 70 | -------------------------------------------------------------------------------- /lib/ex_syslog/exsyslog_event_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSyslog.EventHandler do 2 | import ExPrintf 3 | import ExSyslog.Util, only: [get_env: 2, put_env: 2] 4 | 5 | use GenEvent.Behaviour 6 | use Bitwise 7 | 8 | defmodule State do 9 | defstruct level: 2, host: :undefined, socket: nil, port: 0, 10 | hostname: nil, appid: nil, facility: 0 11 | def new, do: %State{} 12 | def new(opts), do: struct(new, opts) 13 | end 14 | 15 | def level do 16 | :gen_event.call(:error_logger, ExSyslog.EventHandler, :level) 17 | end 18 | 19 | def level(level) do 20 | :gen_event.call(:error_logger, ExSyslog.EventHandler, {:set_level, level}) 21 | end 22 | 23 | def status() do 24 | :gen_event.call(:error_logger, ExSyslog.EventHandler, :status) 25 | end 26 | 27 | def init([]) do 28 | {:ok, socket} = :gen_udp.open(0) 29 | {:ok, :ok, state} = handle_call(:load_config, State.new(socket: socket)) 30 | {:ok, state} 31 | end 32 | 33 | def handle_call({:set_level, level}, state) do 34 | put_env :level, level 35 | {:ok, :ok, struct(state, level: level)} 36 | end 37 | 38 | def handle_call(:level, %State{level: level} = state) do 39 | {:ok, level, state} 40 | end 41 | def handle_call(:status, state) do 42 | {:ok, state, state} 43 | end 44 | 45 | def handle_call(:load_config, state) do 46 | host = case :inet.getaddr(get_env(:host, :undefined), :inet) do 47 | {:ok, address} -> address 48 | {:error, _} -> :undefined 49 | end 50 | [hostname | _] = String.split("#{:net_adm.localhost()}", ".") 51 | opts = [ 52 | host: host, 53 | port: get_env(:port, 514), 54 | hostname: hostname, 55 | os_pid: :os.getpid(), 56 | appid: get_env(:appid, "exsyslog"), 57 | facility: ExSyslog.Util.facility(get_env(:facility, :local2)), 58 | level: ExSyslog.Util.level(get_env(:level, :info)), 59 | ] 60 | {:ok, :ok, struct(state, opts)} 61 | end 62 | 63 | def handle_event(%ExSyslog.Message{level: level, msg: msg, msgid: msgid, pid: pid}, state) do 64 | write(level, msgid, msg, pid, state) 65 | {:ok, state} 66 | end 67 | 68 | def handle_event({class, _gl, {pid, format, args}}, %State{level: max} = state) do 69 | case otp_event_level(class, format) do 70 | :undefined -> 71 | {:ok, state} 72 | level when level > max -> 73 | {:ok, state} 74 | level -> 75 | {msgid, msg} = message(format, args) 76 | write(level, msgid, msg, pid, state) 77 | #IO.puts "event --> class: #{inspect class}, pid: #{inspect pid}, format: #{inspect format}, args: #{inspect args}" 78 | {:ok, state} 79 | end 80 | end 81 | 82 | def handle_event({:log, msg}, state) do 83 | IO.puts "--> #{msg}" 84 | {:ok, state} 85 | end 86 | 87 | def write(level, :undefined, msg, pid, state) do 88 | write(level, 'user', msg, pid, state) 89 | end 90 | 91 | def write(level, msgid, msg, pid, state) when is_list(msg) or is_binary(msg) do 92 | %State{facility: facil, appid: app, hostname: hostname, host: host, port: port, socket: socket} = state 93 | pre = :io_lib.format('<~B>~s ~s~p: ~s - ', [facil ||| level, 94 | ExSyslog.Util.iso8601_timestamp, app, pid, msgid]) 95 | split_msg(socket, host, port, [pre, msg, '\n']) 96 | end 97 | 98 | def write(_, :undefined, packet), do: IO.puts packet 99 | 100 | def split_msg(socket, host, port, [pre, msg, nl]) do 101 | if is_list msg do 102 | String.from_char_data! msg 103 | else 104 | msg 105 | end 106 | |> String.split("\n") 107 | |> Enum.each(&send_msg(socket, host, port, [pre, &1, nl])) 108 | end 109 | 110 | def send_msg(_, :undefined, _, packet) do 111 | :io.format('~s', [packet]) 112 | end 113 | 114 | def send_msg(socket, host, port, packet) do 115 | :gen_udp.send(socket, host, port, packet) 116 | end 117 | 118 | def message(type, report) when type in [:std_error, :std_info, :std_warning, :progress_report, :progress] do 119 | {type, ExSyslog.Util.format('~2048.0p', [report])} 120 | end 121 | 122 | def message(:crash_report, report) do 123 | msg = if :erts_debug.flat_size(report) > get_env(:max_term_size, 8192) do 124 | max_string = get_env(:max_message_size, 16000) 125 | ['*Truncated* - ', :trunc_io.print(report, max_string)] 126 | else 127 | :proc_lib.format(report) 128 | end 129 | {:crash_report, msg} 130 | end 131 | def message(:supervisor_report, report) do 132 | name = Keyword.get(report, :supervisor, :undefined) 133 | error = Keyword.get(report, :errorcontext, :undefined) 134 | reason = Keyword.get(report, :reason, :undefined) 135 | offender = Keyword.get(report, :offender, :undefined) 136 | childpid = Keyword.get(offender, :pid, :undefined) 137 | childname = Keyword.get(offender, :name, :undefined) 138 | case Keyword.get(offender, :mfa, :undefined) do 139 | :undefined -> 140 | {m,f,_} = Keyword.get(offender, :mfargs, :undefined) 141 | {m,f,_} -> 142 | :ok 143 | end 144 | {:supervisor_report, ExSyslog.Util.format('~p ~p (~p) child: ~p [~p] ~p:~p', 145 | [name, error, reason, childname, childpid, m, f])}; 146 | end 147 | 148 | def message(format, args) when is_list(format) do 149 | {:msg, ExSyslog.Util.format(format, args)} 150 | end 151 | 152 | def message(format, args) do 153 | {:unknown, ExSyslog.Util.format(format, "#{inspect args}", [])} 154 | end 155 | 156 | def otp_event_level(_, :crash_report), do: level_crit 157 | def otp_event_level(_, :supervisor_report), do: level_warn 158 | def otp_event_level(_, :supervisor), do: level_warn 159 | def otp_event_level(_, :progress_report), do: level_debug 160 | def otp_event_level(_, :progress), do: level_debug 161 | def otp_event_level(:error, _), do: level_err 162 | def otp_event_level(:warning_msg, _), do: level_warn 163 | def otp_event_level(:info_msg, _), do: level_notice 164 | def otp_event_level(:error_report, _), do: level_err 165 | def otp_event_level(:warning_report, _), do: level_warn 166 | def otp_event_level(:info_report, _), do: level_notice 167 | def otp_event_level(_, _), do: level_debug 168 | 169 | def level_debug, do: 7 170 | def level_info, do: 6 171 | def level_notice, do: 5 172 | def level_warn, do: 4 173 | def level_err, do: 3 174 | def level_crit, do: 2 175 | def level_alert, do: 1 176 | def level_emerg, do: 0 177 | 178 | end 179 | --------------------------------------------------------------------------------