defmodule LoggerTelegramBackend do
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
@behaviour :gen_event
defmodule State do
@moduledoc false
defstruct [:level, :metadata, :metadata_filter, :send_message]
end
alias LoggerTelegramBackend.{Formatter, Sender}
@default_sender {Sender.Telegram, [:token, :chat_id]}
@default_metadata [:line, :function, :module, :application, :file]
@impl :gen_event
def init(__MODULE__) do
config = Application.get_env(:logger, __MODULE__, [])
{:ok, initialize(config, %State{})}
end
@impl :gen_event
def handle_call({:configure, options}, state) do
{:ok, :ok, configure(options, state)}
end
@impl :gen_event
def handle_event({_level, gl, _event}, state) when node(gl) != node() do
{:ok, state}
end
def handle_event({level, _gl, {Logger, message, timestamp, metadata}}, state) do
if meet_level?(level, state.level) and metadata_matches?(metadata, state.metadata_filter) do
log_event(level, message, timestamp, metadata, state)
end
{:ok, state}
end
def handle_event(_event, state) do
{:ok, state}
end
@impl :gen_event
def handle_info(_message, state) do
{:ok, state}
end
defp meet_level?(_lvl, nil), do: true
defp meet_level?(:warn, min), do: meet_level?(:warning, min)
defp meet_level?(lvl, min), do: Logger.compare_levels(lvl, min) != :lt
defp metadata_matches?(_metadata, nil), do: true
defp metadata_matches?(_metadata, []), do: true
defp metadata_matches?(metadata, [{key, value} | rest]) do
case Keyword.fetch(metadata, key) do
{:ok, ^value} -> metadata_matches?(metadata, rest)
_ -> false
end
end
defp configure(options, %State{} = state) do
config = Keyword.merge(Application.get_env(:logger, __MODULE__), options)
Application.put_env(:logger, __MODULE__, config)
initialize(config, state)
end
defp initialize(config, %State{} = state) do
metadata = config[:metadata] || @default_metadata
{sender, sender_opts} = config[:sender] || @default_sender
sender_opts = Keyword.take(config, sender_opts)
%State{
state
| level: config[:level],
metadata: configure_metadata(metadata),
metadata_filter: config[:metadata_filter],
send_message: &sender.send_message(&1, sender_opts)
}
end
defp configure_metadata(:all), do: :all
defp configure_metadata(metadata), do: Enum.reverse(metadata)
defp log_event(level, message, _ts, metadata, %State{} = state) do
metadata = take_metadata(metadata, state.metadata)
message = Formatter.format_event(message, level, metadata)
with {:error, reason} <- state.send_message.(message) do
IO.warn("#{__MODULE__} failed to send message: #{inspect(reason)}")
end
end
defp take_metadata(metadata, :all), do: metadata
defp take_metadata(metadata, keys) do
Enum.reduce(keys, [], fn key, acc ->
case Keyword.fetch(metadata, key) do
{:ok, val} -> [{key, val} | acc]
:error -> acc
end
end)
end
end