Packages

GELF Logger Backend

Current section

Files

Jump to
gelf lib gelf.ex
Raw

lib/gelf.ex

defmodule Gelf do
@moduledoc """
GELF Logger Backend
## Example
config :logger
utc_log: true,
backends: [:console, Gelf]
config :logger, Gelf,
level: :debug,
host: "localhost",
port: 12201,
compress: :zlib,
app: "my_app_name",
metadata: [:file, :line],
chunk_size: 1500
## Options
* `:level` - (atom) minimum allowed log level. Defaults to
`:debug`. That is, by default everything will be logged.
* `:host` - (string) hostname of the gelf udp server. Defaults
to `"localhost"`.
* `:port` - (integer) the port on which the gelf udp server is
listening. Defaults `12201`.
* `:compress` - (atom) the compression method to be used to compress
the data. Valid values are `:gzip`, `:zlib` and `:none`. Defaults
to `:zlib`.
* `:app` - (string) name of your app. The host field in the
[message](http://docs.graylog.org/en/2.0/pages/gelf.html#gelf-format-specification)
will be set to this value. Defaluts to current node name.
* `:metadata` - ([atom]) list of metadata fields that should be
added to the message. The fields are added as [additional
field](http://docs.graylog.org/en/2.0/pages/gelf.html#gelf-format-specification)
in the message(keys will be prefixed with `_`). Defaults to `[]`.
* `:chunk_size` - (integer) Maximum size of a single message in
bytes. If the log message is bigger than `chunk_size`, it will be
split into multiple chunks. The server will construct the message
from the chunks. Set it to the maximum bytes that can be
transferred safely as a single datagram packet. Defaults to
`1500`.
All the options(except `chunk_size`) can be changed during the
runtime using `Logger.configure_backend/2`.
## Notes
Make sure to set the `utc_log` option to true in logger. The backend
just receives a tuple without any timezone information. During the
conversion to epoch, it assumes the date is in utc format. Not
enabling `utc_log` will lead to wrong timestamp value.
"""
require Logger
use GenEvent
defstruct [level: nil, port: nil, app: nil, sock: nil, address: nil, compress: nil, metadata: nil]
def init(__MODULE__) do
{:ok, sock} = :gen_udp.open(0, [active: false])
{:ok, configure([], %__MODULE__{sock: sock})}
end
def handle_call({:configure, options}, state) do
{:ok, :ok, configure(options, state)}
end
def handle_event({_level, gl, _event}, state) when node(gl) != node() do
{:ok, state}
end
def handle_event({level, _gl, _event}, %{level: :error} = state)
when level == :debug or level == :info or level == :warn do
{:ok, state}
end
def handle_event({level, _gl, _event}, %{level: :warn} = state)
when level == :debug or level == :info do
{:ok, state}
end
def handle_event({level, _gl, _event}, %{level: :info} = state)
when level == :debug do
{:ok, state}
end
def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do
log(level, msg, ts, md, state)
{:ok, state}
end
def handle_event(:flush, state) do
{:ok, state}
end
def handle_event(_, state) do
{:ok, state}
end
defp configure(options, state) do
config =
Application.get_env(:logger, __MODULE__, [])
|> Keyword.merge(options)
Application.put_env(:logger, __MODULE__, config)
level = Keyword.get(config, :level)
port = Keyword.get(config, :port, 12201)
host = Keyword.get(config, :host, 'localhost')
compress = Keyword.get(config, :compress, :zlib)
host = if is_binary(host), do: String.to_char_list(host), else: host
{:ok, address} = :inet.getaddr(host, :inet)
app = Keyword.get(config, :app, to_string(node()))
metadata = Keyword.get(config, :metadata, [])
%{state | level: level, port: port, address: address, app: app, compress: compress, metadata: metadata}
end
defp log(level, msg, ts, md, state = %{app: app, address: address, port: port, sock: sock, compress: compress_method}) do
build_message(app, level, msg, ts, filter_metadata(md, state))
|> compress(compress_method)
|> chunk
|> Enum.map(&send_message(&1, sock, address, port))
end
defp build_message(app, level, message, ts, md) do
utf_message = IO.chardata_to_string(message)
msg = Map.merge(%{
"version": "1.1",
"timestamp": epoch(ts),
"level": level_number(level),
"host": app,
"short_message": String.slice(utf_message, 0..79),
}, md)
msg = if byte_size(utf_message) > 80 do
Map.put(msg, "full_message", utf_message)
else
msg
end
Poison.encode_to_iodata!(msg)
end
defp filter_metadata(md, %{metadata: allowed}) do
Enum.filter_map(md, fn {k, _v} -> k in allowed end, fn {k, v} -> {"_" <> to_string(k), to_number_or_string(v)} end)
|> Enum.into(%{})
end
defp to_number_or_string(x) when is_integer(x) or is_float(x), do: x
defp to_number_or_string(x), do: to_string(x)
defp compress(data, :zlib), do: :zlib.compress(data)
defp compress(data, :gzip), do: :zlib.gzip(data)
defp compress(data, :none), do: IO.iodata_to_binary(data)
defp level_number(:debug), do: 7
defp level_number(:info), do: 6
defp level_number(:warn), do: 4
defp level_number(:error), do: 3
@start :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})
defp epoch({d, {h, m, s, u}}) do
Integer.to_string(:calendar.datetime_to_gregorian_seconds({d, {h, m, s}}) - @start) <> "." <> Integer.to_string(u) |> Float.parse |> elem(0)
end
defp send_message(message, sock, address, port) do
:gen_udp.send(sock, address, port, message)
end
@chunk_size Keyword.get(Application.get_env(:logger, __MODULE__, []), :chunk_size, 1500 - 48)
@part_size @chunk_size - 12
@max_message_size @part_size * 128
defp chunk(message) when byte_size(message) > @max_message_size do
Logger.warn ["Gelf: Message too large (", Integer.to_string(byte_size(message)), " btyes). Dropping it"]
[]
end
defp chunk(message) when byte_size(message) <= @chunk_size do
[message]
end
defp chunk(message) do
break(message, [])
end
defp break(<< part::binary-size(@part_size), rest::binary >> = message, parts) when byte_size(message) > @part_size do
break(rest, [part | parts])
end
defp break(message, parts) do
parts = [message | parts]
parts_count = Enum.count(parts)
message_id = :crypto.strong_rand_bytes(8)
parts
|> Enum.reverse
|> Enum.with_index
|> Enum.map(fn ({part, index}) ->
[<<0x1e, 0x0f, message_id::binary-size(8), index, parts_count>>, part]
end)
end
end