Current section
Files
Jump to
Current section
Files
lib/logger_logstash_backend.ex
################################################################################
# Copyright 2015 Marcelo Gornstein <marcelog@gmail.com>
#
# 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.
################################################################################
defmodule LoggerLogstashBackend do
@behaviour :gen_event
use Timex
def init({__MODULE__, name}) do
{:ok, configure(name, [])}
end
def handle_call({:configure, opts}, %{name: name}) do
{:ok, :ok, configure(name, opts)}
end
def handle_info(_, state) do
{:ok, state}
end
def handle_event(:flush, state) 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
def code_change(_old_vsn, state, _extra) do
{:ok, state}
end
def terminate(_reason, _state) do
:ok
end
defp log_event(
level,
msg,
ts,
md,
%{
host: host,
port: port,
type: type,
metadata: metadata,
socket: socket
}
) do
fields =
md
|> Keyword.merge(metadata)
|> Enum.map(fn {k, v} -> {k, to_printable(v)} end)
|> Enum.into(%{})
|> Map.put(:level, to_string(level))
|> inspect_pids
{{year, month, day}, {hour, minute, second, milliseconds}} = ts
{:ok, ts} =
NaiveDateTime.new(
year,
month,
day,
hour,
minute,
second,
milliseconds * 1000
)
ts = Timex.to_datetime(ts, Timezone.local())
{:ok, json} =
Jason.encode(%{
type: type,
"@timestamp": Timex.format!(ts, "{ISO:Extended}"),
message: to_string(msg),
fields: fields
})
:gen_udp.send(socket, host, port, json)
end
defp configure(name, opts) do
env = Application.get_env(:logger, name, [])
opts = Keyword.merge(env, opts)
Application.put_env(:logger, name, opts)
level = Keyword.get(opts, :level, :debug)
metadata = Keyword.get(opts, :metadata, [])
type = Keyword.get(opts, :type, "elixir")
host = Keyword.get(opts, :host)
port = Keyword.get(opts, :port)
{:ok, socket} = :gen_udp.open(0)
%{
name: name,
host: to_charlist(host),
port: port,
level: level,
socket: socket,
type: type,
metadata: metadata
}
end
# inspects the argument only if it is a pid
defp inspect_pid(pid) when is_pid(pid), do: inspect(pid)
defp inspect_pid(other), do: other
# inspects the field values only if they are pids
defp inspect_pids(fields) when is_map(fields) do
Enum.into(fields, %{}, fn {key, value} ->
{key, inspect_pid(value)}
end)
end
def to_printable(v) when is_binary(v), do: v
def to_printable(v) when is_atom(v), do: v
def to_printable(v) when is_number(v), do: v
def to_printable(v) when is_list(v) do
if Keyword.keyword?(v) do
v
|> Enum.into(%{})
|> to_printable()
else
Enum.map(v, &to_printable/1)
end
end
def to_printable(%_{} = v), do: to_printable(inspect(v))
def to_printable(v) when is_map(v) do
v
|> Enum.map(fn {k, v} -> {to_printable(k), to_printable(v)} end)
|> Enum.into(%{})
end
def to_printable(v), do: inspect(v)
end