├── test ├── test_helper.exs ├── syslog_test.exs └── timestamp_test.exs ├── .gitignore ├── mix.exs ├── config └── config.exs ├── LICENSE ├── lib ├── utils.ex └── syslog.ex └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Syslog.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :syslog, 6 | version: "1.0.0", 7 | elixir: ">= 1.0.0", 8 | deps: deps()] 9 | end 10 | 11 | # Configuration for the OTP application 12 | # 13 | # Type `mix help compile.app` for more information 14 | def application do 15 | [applications: []] 16 | end 17 | 18 | # Type `mix help deps` for more examples and options 19 | defp deps do 20 | [] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/syslog_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SyslogTest do 2 | use ExUnit.Case 3 | 4 | import Logger.Backends.Syslog #, only: [handle_event: 2] 5 | 6 | test "the truth" do 7 | assert 1 + 1 == 2 8 | end 9 | 10 | test "handle_event" do 11 | #{:ok, sock} = :gen_udp.open(5555, []) 12 | sock = nil 13 | ts = Logger.Utils.timestamp(false) 14 | state = %{format: [], metadata: [], facility: 0x10, appid: "test", 15 | hostname: nil, host: '127.0.0.1', port: 5555, socket: sock, level_num: 6, level: :info} 16 | assert handle_event({:info, nil, {Logger, "test", ts, []}}, state) == {:ok, state} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/timestamp_test.exs: -------------------------------------------------------------------------------- 1 | alias Logger.Syslog.Utils 2 | defmodule Utils.TimestampTest do 3 | use ExUnit.Case 4 | 5 | import Utils, only: [iso8601_timestamp: 1] 6 | 7 | test "timestamp format" do 8 | ts = Logger.Utils.timestamp(false) 9 | s = iso8601_timestamp(ts) 10 | assert String.length(s) == 15 11 | [mon, day, hms] = String.split(s) 12 | [h, m, s] = String.split(hms, ":") 13 | assert mon =~ ~r/^[A-Z][a-z][a-z]$/ 14 | for x <- [h, m, s, day], do: 15 | assert x =~ ~r/^\d\d$/ 16 | [h, m, s, day] = for x <- [h, m, s, day], do: 17 | Integer.parse(x) |> elem(0) 18 | assert day >= 1 19 | assert day <= 31 20 | assert h >= 0 21 | assert h <= 23 22 | assert m >= 0 23 | assert m <= 59 24 | assert s >= 0 25 | assert s <= 59 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application, it should 9 | # be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :my_dep, 14 | # key: :value, 15 | # limit: 42 16 | 17 | # It is also possible to import configuration files, relative to this 18 | # directory. For example, you can emulate configuration per environment 19 | # by uncommenting the line below and defining dev.exs, test.exs and such. 20 | # Configuration from the imported file will override the ones defined 21 | # here (which is why it is important to import them last). 22 | # 23 | # import_config "#{Mix.env}.exs" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 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 | -------------------------------------------------------------------------------- /lib/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Logger.Syslog.Utils do 2 | use Bitwise 3 | 4 | 5 | def iso8601_timestamp({{year,month,date},{hour,minute,second,micro}}) do 6 | :io_lib.format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B.~3..0BZ", 7 | [year, month, date, hour, minute, second, micro]) 8 | |> to_string 9 | end 10 | 11 | def facility(:local0), do: (16 <<< 3) 12 | def facility(:local1), do: (17 <<< 3) 13 | def facility(:local2), do: (18 <<< 3) 14 | def facility(:local3), do: (19 <<< 3) 15 | def facility(:local4), do: (20 <<< 3) 16 | def facility(:local5), do: (21 <<< 3) 17 | def facility(:local6), do: (22 <<< 3) 18 | def facility(:local7), do: (23 <<< 3) 19 | 20 | def level(:debug), do: 7 21 | def level(:info), do: 6 22 | def level(:notice), do: 5 23 | def level(:warn), do: 4 24 | def level(:warning), do: 4 25 | def level(:err), do: 3 26 | def level(:error), do: 3 27 | def level(:crit), do: 2 28 | def level(:alert), do: 1 29 | def level(:emerg), do: 0 30 | def level(:panic), do: 0 31 | 32 | def level(i) when is_integer(i) when i >= 0 and i <= 7, do: i 33 | def level(_bad), do: 3 34 | end 35 | -------------------------------------------------------------------------------- /lib/syslog.ex: -------------------------------------------------------------------------------- 1 | defmodule Logger.Backends.Syslog do 2 | @behaviour :gen_event 3 | 4 | use Bitwise 5 | 6 | def init(_) do 7 | if user = Process.whereis(:user) do 8 | Process.group_leader(self(), user) 9 | {:ok, socket} = :gen_udp.open(0) 10 | {:ok, configure([socket: socket])} 11 | else 12 | {:error, :ignore} 13 | end 14 | end 15 | 16 | def handle_call({:configure, options}, _state) do 17 | {:ok, :ok, configure(options)} 18 | end 19 | 20 | def handle_event({_level, gl, _event}, state) when node(gl) != node() do 21 | {:ok, state} 22 | end 23 | 24 | def handle_event({level, _gl, {Logger, msg, ts, md}}, %{level: min_level} = state) do 25 | if is_nil(min_level) or Logger.compare_levels(level, min_level) != :lt do 26 | log_event(level, msg, ts, md, state) 27 | end 28 | {:ok, state} 29 | end 30 | 31 | def handle_info(_msg, state) do 32 | {:ok, state} 33 | end 34 | 35 | ## Helpers 36 | 37 | defp configure(options) do 38 | syslog = Keyword.merge(Application.get_env(:logger, :syslog, []), options) 39 | socket = Keyword.get(options, :socket) 40 | Application.put_env(:logger, :syslog, syslog) 41 | 42 | format = syslog 43 | |> Keyword.get(:format) 44 | |> Logger.Formatter.compile 45 | 46 | level = Keyword.get(syslog, :level) 47 | metadata = Keyword.get(syslog, :metadata, []) 48 | host = Keyword.get(syslog, :host, '127.0.0.1') 49 | port = Keyword.get(syslog, :port, 514) 50 | facility = Keyword.get(syslog, :facility, :local2) |> Logger.Syslog.Utils.facility 51 | appid = Keyword.get(syslog, :appid, :elixir) 52 | [hostname | _] = String.split("#{:net_adm.localhost()}", ".") 53 | %{format: format, metadata: metadata, level: level, socket: socket, 54 | host: host, port: port, facility: facility, appid: appid, hostname: hostname} 55 | end 56 | 57 | defp log_event(level, msg, ts, md, state) do 58 | %{format: format, metadata: metadata, facility: facility, appid: appid, 59 | hostname: _hostname, host: host, port: port, socket: socket} = state 60 | 61 | level_num = Logger.Syslog.Utils.level(level) 62 | pre = :io_lib.format('<~B>~s ~s~p: ', [facility ||| level_num, 63 | Logger.Syslog.Utils.iso8601_timestamp(ts), appid, self()]) 64 | packet = [pre, Logger.Formatter.format(format, level, msg, ts, Keyword.take(md, metadata))] 65 | if socket, do: :gen_udp.send(socket, host, port, packet) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Syslog 2 | ====== 3 | 4 | Syslog is an Elixir port of the erlang [Twig](https://github.com/cloudant/twig) 5 | logger. It is an Elixir logger backend providing UDP support to a syslog server. 6 | 7 | ## Configuration 8 | 9 | ### Elixir Project 10 | 11 | Syslog's behavior is controlled using the application configuration environment: 12 | 13 | * __host__ (127.0.0.1): 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__ (:elixir): inserted as the APPID in the syslog message 18 | 19 | For example, the following `config/config.exs` file sets up syslog using 20 | level debug, facility local1, and appid myproj 21 | 22 | ``` 23 | use Mix.Config 24 | config :logger, [ 25 | level: :debug, 26 | backends: [Logger.Backends.Syslog], 27 | syslog: [facility: :local1, appid: "myproj"] 28 | ] 29 | ``` 30 | 31 | ### Add the application 32 | 33 | You should also add the `syslog` application in the `mix.exs` file as shown below: 34 | 35 | ``` 36 | defmodule MyMod.Mixfile do 37 | # ... 38 | def application do 39 | [applications: [:logger, :syslog], 40 | mod: {MyMod, []}] 41 | end 42 | # ... 43 | end 44 | ``` 45 | 46 | ### Syslog Server 47 | 48 | The syslog server must be configured to support remote logging. On a Redhat based 49 | Linux distribution, you can setup remote logging by editing `/etc/sysconfig/syslog` 50 | and add the -r option as shown below: 51 | 52 | ``` 53 | # /etc/sysconfig/syslog 54 | ... 55 | SYSLOGD_OPTIONS="-m 0 -r" 56 | ... 57 | ``` 58 | 59 | If your system uses `rsyslog` you should add or uncomment the following lines in your `/etc/rsyslog.conf`: 60 | ``` 61 | $ModLoad imudp 62 | $UDPServerRun 514 63 | ``` 64 | 65 | The facility also needs to be configured. Again, for Redhat distributions, edit 66 | `/etc/syslog.conf`, edit the first line below and and add the second: 67 | 68 | ``` 69 | #/etc/syslog.conf 70 | ... 71 | *.info;local1.none;mail.none;authpriv.none;cron.none /var/log/messages 72 | ... 73 | local2.* /var/log/my_elixir_project.log 74 | ``` 75 | 76 | Then restart the `syslog` service after making the configuration changes 77 | 78 | ``` 79 | root@ucx20 ~]# service syslog restart 80 | Shutting down kernel logger: [ OK ] 81 | Shutting down system logger: [ OK ] 82 | Starting system logger: [ OK ] 83 | Starting kernel logger: [ OK ] 84 | [root@ucx20 ~]# 85 | ``` 86 | 87 | ## Example Project 88 | 89 | Checkout the following [test project](https://github.com/smpallen99/test_syslog) for a working example. 90 | 91 | ## License 92 | 93 | syslog is copyright (c) 2014-2018 E-MetroTel. 94 | 95 | The source code is released under the MIT License. 96 | 97 | Check [LICENSE](LICENSE) for more information. 98 | --------------------------------------------------------------------------------