defmodule Logger.Backends.Gelf do use GenEvent @max_size 1047040 @max_packet_size 8192 @max_payload_size 8180 def init({__MODULE__, name}) do if user = Process.whereis(:user) do Process.group_leader(self(), user) {:ok, configure(name, [])} else {:error, :ignore} end end def handle_call({:configure, options}, state) do {:ok, :ok, configure(state[:name], options)} end def handle_event({_level, gl, _event}, state) when node(gl) != node() do {:ok, state} end def handle_event({level, _gl, {Logger, msg, ts, md}}, %{level: min_level} = state) do if is_nil(min_level) or Logger.compare_levels(level, min_level) != :lt do log_event(level, msg, ts, md, state) end {:ok, state} end ## Helpers defp configure(name, options) do config = Keyword.merge(Application.get_env(:logger, name, []), options) Application.put_env(:logger, name, config) {:ok, socket} = :gen_udp.open(0) {host, _exit_code} = System.cmd("hostname", [], []) {:ok, gl_host } = Keyword.get(config, :host) |> to_char_list |> :inet_parse.address port = Keyword.get(config, :port) application = Keyword.get(config, :application) level = Keyword.get(config, :level) metadata = Keyword.get(config, :metadata, []) %{name: name, gl_host: gl_host, host: String.strip(host), port: port, metadata: metadata, level: level, application: application, socket: socket} end defp log_event(level, msg, ts, md, state) do int_level = case level do :debug -> 0 :info -> 1 :warn -> 2 :error -> 3 :fatal -> 4 end fields = Enum.reduce(Dict.take(md, state[:metadata]), %{}, fn({k,v}, accum) -> Map.put(accum, "_#{k}", to_string(v)) end) # TODO: fix timestamp {{year, month, day}, {hour, min, sec, milli}} = ts gelf = %{ short_message: String.slice(to_string(msg), 0..80), long_message: to_string(msg), version: "1.1", host: state[:host], level: int_level, _log_time: "#{year}-#{month}-#{day} #{hour}:#{min}:#{sec}.#{milli}", _application: state[:application] } |> Map.merge(fields) compressed = Poison.encode!(gelf) |> :zlib.gzip size = byte_size(compressed) cond do size > @max_size -> raise "Message too large" size > @max_packet_size -> num = div(size, @max_packet_size) if (num * @max_packet_size) < size do num = num + 1 end id = :crypto.rand_bytes(8) send_chunks(state[:socket], state[:gl_host], state[:port], compressed, id, :binary.encode_unsigned(num), 0, size) true -> :gen_udp.send(state[:socket], state[:gl_host], state[:port], compressed) end end defp send_chunks(socket, host, port, data, id, num, seq, size) when size > @max_payload_size do <> = data :gen_udp.send(socket, host, port, make_chunk(payload, id, num, seq)) send_chunks(socket, host, port, rest, id, num, seq + 1, byte_size(rest)) end defp send_chunks(socket, host, port, data, id, num, seq, _size) do :gen_udp.send(socket, host, port, make_chunk(data, id, num, seq)) end defp make_chunk(payload, id, num, seq) do bin = :binary.encode_unsigned(seq) <<0x1e, 0x0f, id :: binary - size(8), bin :: binary - size(1), num :: binary - size(1), payload :: binary >> end end