defmodule Argos.Logger do @moduledoc """ Handler de Logger para Argos con formateo basado en Aurora. Redirige eventos de `Logger` al módulo configurado (por defecto `Argos.Logger.Default`). Filtra niveles según configuración del proyecto. ## Configuración Cada proyecto puede configurar su propio logger y niveles permitidos: config :argos, :logger, MiProyecto.Logger.Custom config :argos, :logger_levels, [:debug, :info, :warning, :error] Los errores siempre se muestran, independientemente de la configuración. En modo develop, todos los niveles configurados se muestran. En modo producción, solo se muestran errores a menos que se configuren otros niveles. ## Modo Raw Los loggers pueden implementar `log_raw/3` para soportar modo raw, donde pueden calcular el tamaño del TUI y posicionar los logs debajo del TUI. """ @behaviour :gen_event @impl true def init(__MODULE__) do logger_impl = Application.get_env(:argos, :logger, Argos.Logger.Default) unless function_exported?(logger_impl, :log, 3) do raise ArgumentError, "Logger implementation #{inspect(logger_impl)} must implement Argos.Logger.Behaviour" end env = current_env() app_name = get_app_name() configured_levels = get_configured_levels(app_name, env) {:ok, %{ logger_impl: logger_impl, env: env, handler_id: Argos.Logger, app_name: app_name, configured_levels: configured_levels, raw_mode: Application.get_env(app_name, :logger_raw_mode, false) }} end @impl true def handle_event({_level, gl, _event}, state) when node(gl) != node() do {:ok, state} end @impl true def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do if should_log?(state.env, level, state.configured_levels) do try do metadata = prepare_metadata(md, ts, level) message = extract_message(msg) if state.raw_mode && function_exported?(state.logger_impl, :log_raw, 3) do state.logger_impl.log_raw(level, message, metadata) else state.logger_impl.log(level, message, metadata) end rescue e -> IO.puts(:stderr, "Argos.Logger error: #{inspect(e)}") end end {:ok, state} end @impl true def handle_event(_event, state) do {:ok, state} end @impl true def handle_call(:get_config, state) do {:ok, state, state} end @impl true def handle_call({:configure, new_config}, state) do {:ok, :ok, Map.merge(state, new_config)} end @impl true def handle_info(_msg, state) do {:ok, state} end @impl true def code_change(_old_vsn, state, _extra) do {:ok, state} end @impl true def terminate(_reason, _state) do :ok end defp should_log?(_env, level, _configured_levels) when level in [:error, :critical, :emergency, :alert], do: true defp should_log?(:prod, level, configured_levels) do level in configured_levels end defp should_log?(_env, level, configured_levels) do level in configured_levels end defp current_env do case System.get_env("ARGOS_ENV") || Application.get_env(:argos, :env, "dev") do "prod" -> :prod "dev" -> :dev "test" -> :test :prod -> :prod :dev -> :dev :test -> :test _ -> :dev end end defp get_app_name do case Application.get_env(:argos, :logger_app_name) do nil -> case Application.get_env(:argos, :logger) do nil -> :argos module when is_atom(module) -> module |> Module.split() |> List.first() |> String.downcase() |> String.to_atom() _ -> :argos end app_name -> app_name end end defp get_configured_levels(app_name, env) do case Application.get_env(app_name, :logger_levels) do nil -> case env do :prod -> [:error, :critical, :emergency, :alert] _ -> [:debug, :info, :warning, :error, :critical, :emergency, :alert] end levels when is_list(levels) -> error_levels = [:error, :critical, :emergency, :alert] (levels ++ error_levels) |> Enum.uniq() end end defp prepare_metadata(md, ts, level) do base_md = [ timestamp: ts, level: level, handler_id: Argos.Logger ] useful_md = Enum.filter(md, fn {:module, _} -> true {:function, _} -> true {:file, _} -> true {:line, _} -> true {:application, _} -> true {:pid, _} -> true _ -> false end) base_md ++ useful_md end defp extract_message(msg) when is_binary(msg), do: msg defp extract_message({:string, chardata}), do: IO.chardata_to_string(chardata) defp extract_message({:fmt, format, args}), do: :io_lib.format(format, args) |> IO.chardata_to_string() defp extract_message(other), do: inspect(other) end