defmodule SlackLog do @moduledoc """ `SlackLog` is a custom backend for the elixir `:logger` application. Based on the configuration it will post errors to specified Slack hooks """ @behaviour :gen_event @type level ::Logger.level() @type metadata :: [atom] @default_format "$time $metadata[$level] $message\n" alias SlackLog.Messenger def init({__MODULE__, name}) do {:ok, configure(name, [])} end def handle_call({:configure, opts}, %{name: name} = state) do {:ok, :ok, configure(name, opts, state)} end def handle_event({level, _pid, {Logger, msg, ts, meta}}, %{level: min_level, metadata_filter: metadata_filter} = state) do if is_nil(min_level) or Logger.compare_levels(level, min_level) != :lt and metadata_matches?(meta, metadata_filter) do log_event(level, msg, ts, meta, state) else {:ok, state} end end def handle_event(:flush, state) do {:ok, state} end def handle_info(_message, state) do {:ok, state} end defp log_event(_level, _msg, _ts, _meta, %{slack_url: nil} = state) do {:ok, state} end defp log_event(level, msg, ts, meta, %{slack_url: slack_url, metadata: metadata} = state) when is_binary(slack_url) do message = SlackLog.Formatter.format_message({level, msg, ts, meta}, metadata) case Jason.encode(message) do {:ok, json} -> Messenger.send(slack_url, json) {:error, _error} -> :error # Fail silently in this case - search for a solution to catch this end {:ok, state} end @doc false @spec metadata_matches?(Keyword.t(), nil | Keyword.t()) :: true | false def metadata_matches?(_md, nil), do: true # all of the filter keys are present def metadata_matches?(_md, []), do: true def metadata_matches?(md, [{key, [_ | _] = val} | rest]) do case Keyword.fetch(md, key) do {:ok, md_val} -> (md_val in val) && metadata_matches?(md, rest) # fail on first mismatch _ -> false end end def metadata_matches?(md, [{key, val} | rest]) do case Keyword.fetch(md, key) do {:ok, ^val} -> metadata_matches?(md, rest) # fail on first mismatch _ -> false end end defp configure(name, opts) do state = %{ name: nil, format: nil, level: nil, metadata: nil, metadata_filter: nil, slack_url: nil } configure(name, opts, state) end defp configure(name, opts, state) do env = Application.get_env(:logger, name, []) opts = Keyword.merge(env, opts) Application.put_env(:logger, name, opts) level = Keyword.get(opts, :level) metadata = Keyword.get(opts, :metadata, []) format_opts = Keyword.get(opts, :format, @default_format) format = Logger.Formatter.compile(format_opts) metadata_filter = Keyword.get(opts, :metadata_filter) slack_url = Keyword.get(opts, :slack_url) %{ state | name: name, format: format, level: level, metadata: metadata, metadata_filter: metadata_filter, slack_url: slack_url } end end