defmodule LoggerTelegramBackend do
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
@doc """
Adds the LoggerTelegramBackend backend.
## Options
* `:flush` - when `true`, guarantees all messages currently sent
to `Logger` are processed before the backend is added
"""
@spec attach(keyword) :: Supervisor.on_start_child()
def attach(opts \\ [])
case System.version() >= "1.15.0" do
true -> def attach(opts), do: LoggerBackends.add(__MODULE__, opts)
false -> def attach(opts), do: Logger.add_backend(__MODULE__, opts)
end
@doc """
Removes the LoggerTelegramBackend backend.
## Options
* `:flush` - when `true`, guarantees all messages currently sent
to `Logger` are processed before the backend is removed
"""
@spec detach(keyword) :: :ok | {:error, term}
def detach(opts \\ [])
case System.version() >= "1.15.0" do
true -> def detach(opts), do: LoggerBackends.remove(__MODULE__, opts)
false -> def detach(opts), do: Logger.remove_backend(__MODULE__, opts)
end
@doc """
Applies runtime configuration.
See the module doc for more information.
"""
@spec configure(keyword) :: term
case System.version() >= "1.15.0" do
true -> def configure(opts), do: LoggerBackends.configure(__MODULE__, opts)
false -> def configure(opts), do: Logger.configure_backend(__MODULE__, opts)
end
@behaviour :gen_event
alias LoggerTelegramBackend.Formatter
alias LoggerTelegramBackend.Sender
@default_metadata [:line, :function, :module, :application, :file]
@impl :gen_event
def init(__MODULE__) do
config = Application.get_env(:logger, __MODULE__, [])
{:ok, initialize(config)}
end
@impl :gen_event
def handle_call({:configure, config}, _state) do
config = Keyword.merge(Application.get_env(:logger, __MODULE__), config)
:ok = Application.put_env(:logger, __MODULE__, config)
state = initialize(config)
{:ok, :ok, 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 initialize(config) do
metadata = config[:metadata] || @default_metadata
sender_opts = Keyword.take(config, [:token, :chat_id, :client_request_opts])
%{
level: config[:level],
metadata: configure_metadata(metadata),
metadata_filter: config[:metadata_filter],
sender_opts: 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) do
metadata = take_metadata(metadata, state.metadata)
message = Formatter.format_event(message, level, metadata)
with {:error, reason} <- Sender.send_message(message, state.sender_opts) 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