defmodule Argos.Log do @moduledoc """ Sistema de logging estructurado con soporte para modo TUI y standalone. Proporciona logging con colores ANSI, limpieza automática de códigos de escape, y manejo especial para modo interfaz de usuario (TUI). ## Características - Evita logs en entornos de producción - Limpia códigos ANSI automáticamente - Muestra timestamp, nivel y origen del log - Pretty print de structs y mapas - Detección automática de modo TUI - Almacenamiento de logs para lectura asíncrona en TUI ## Uso # Logging simple Argos.Log.log(:info, "Operación completada") # Logging con metadata Argos.Log.log(:info, "Tarea completada", task_id: 123, duration: 1500) # Logging de comandos Argos.Log.log_command_execution("ls -la", 0, 150, "total 8...") # Logging de tareas Argos.Log.log_task_execution("compile", true, 2500, "Success") ## Niveles Soportados - `:debug` - Información de depuración (cyan) - `:info` - Información general (azul) - `:notice` - Notificaciones (magenta) - `:warn`, `:warning` - Advertencias (amarillo) - `:success` - Operaciones exitosas (verde) - `:error` - Errores (rojo) - `:critical` - Errores críticos (rojo) - `:emergency` - Emergencias (fondo rojo) ## Configuración del Entorno Define la variable `ARGOS_ENV` para controlar el comportamiento: - `dev` - Logging completo (default) - `test` - Logging completo - `prod` - Sin logging """ @behaviour :gen_event require Logger # ---------------- Callbacks ------------------ def init(__MODULE__), do: {:ok, %{env: current_env()}} def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do if state.env != :prod do try do formatted_msg = msg |> format_message() formatted_lines = String.split(formatted_msg, "\n", trim: true) # Verificar si estamos en modo TUI (buscando si hay un proceso TUI corriendo) if in_tui_mode?() do # En modo TUI, almacenar logs para mostrarlos de forma no intrusiva store_log_messages(level, formatted_lines, ts, md) else # Fuera de TUI, comportamiento normal Enum.each(formatted_lines, &IO.puts(pretty_line(&1, level, ts, md))) end rescue e -> if in_tui_mode?() do store_log_messages(:error, ["[Argos.Log Error] #{inspect(e)}"], ts, md) else IO.puts("[Argos.Log Error] #{inspect(e)}") end end end {:ok, state} end def handle_event(_, state), do: {:ok, state} def handle_call(_, state), do: {:ok, :ok, state} # ---------------- Helpers ------------------ defp current_env do case System.get_env("ARGOS_ENV") do "prod" -> :prod "dev" -> :dev "test" -> :test _ -> :dev end end defp format_message(msg) when is_binary(msg), do: clean_ansi(msg) defp format_message(msg), do: msg |> inspect(pretty: true) |> clean_ansi() # ---------------- Pretty line ------------------ defp pretty_line(line, level, ts, md) do ts_str = format_timestamp(ts) module = Keyword.get(md, :module, "Unknown") fun = Keyword.get(md, :function, "unknown/0") params = Keyword.get(md, :params, nil) header = "#{ts_str} #{level_icon(level)} [#{module}.#{fun}]" params_str = case params do nil -> "" _ -> " Params: #{inspect(params, limit: 3)}" end "#{colorize(level, header)} #{line}#{params_str}" end # ---------------- Icons ------------------ defp level_icon(:warn), do: "[!]" defp level_icon(:warning), do: "[!]" defp level_icon(:info), do: "[i]" defp level_icon(:debug), do: "[d]" defp level_icon(:notice), do: "[n]" defp level_icon(:success), do: "[✓]" defp level_icon(:error), do: "[X]" defp level_icon(:alert), do: "[!A]" defp level_icon(:critical), do: "[!C]" defp level_icon(:emergency), do: "[!!!]" defp level_icon(_), do: "[?]" # ---------------- Colors ANSI ------------------ defp colorize(:info, text), do: "\e[34m#{text}\e[0m" defp colorize(:debug, text), do: "\e[36m#{text}\e[0m" defp colorize(:warn, text), do: "\e[33m#{text}\e[0m" defp colorize(:warning, text), do: "\e[33m#{text}\e[0m" defp colorize(:error, text), do: "\e[31m#{text}\e[0m" defp colorize(:success, text), do: "\e[32m#{text}\e[0m" defp colorize(:notice, text), do: "\e[35m#{text}\e[0m" defp colorize(:alert, text), do: "\e[31m#{text}\e[0m" defp colorize(:critical, text), do: "\e[31m#{text}\e[0m" defp colorize(:emergency, text), do: "\e[41;37m#{text}\e[0m" defp colorize(_, text), do: text # ---------------- Timestamp ------------------ defp format_timestamp({{y, mo, d}, {h, m, s, ms}}) do "#{pad2(y)}-#{pad2(mo)}-#{pad2(d)} #{pad2(h)}:#{pad2(m)}:#{pad2(s)}.#{pad3(ms)}" end defp format_timestamp(_), do: "unknown_time" defp pad2(n) when n < 10, do: "0#{n}" defp pad2(n), do: "#{n}" defp pad3(n) when n < 10, do: "00#{n}" defp pad3(n) when n < 100, do: "0#{n}" defp pad3(n), do: "#{n}" # ---------------- ANSI cleanup ------------------ defp clean_ansi(str) do str |> String.replace(~r/\e\[[\d;?]*[a-zA-Z]/, "") |> String.replace(~r/\eP.*?\e\\/, "") |> String.trim() end # ---------------- Public API for Argos ------------------ def log(level, message, metadata \\ []) do Logger.log(level, message, metadata) end def log_command_execution(command, exit_code, duration, output) do Logger.info( "Command executed: #{command} (Exit code: #{exit_code}, Duration: #{duration}ms, Output: #{inspect(output) |> clean_ansi()})" ) end def log_task_execution(task_name, success?, duration, result) do status = if success?, do: "SUCCESS", else: "FAILED" Logger.info( "Task executed: #{task_name} (Status: #{status}, Duration: #{duration}ms, Result: #{inspect(result) |> clean_ansi()})" ) end # ---------------- TUI Mode Detection ------------------ defp in_tui_mode? do # Verificar si hay un proceso del TUI activo Process.whereis(:aegis_tui) != nil or Enum.any?(Process.registered(), fn name -> Atom.to_string(name) |> String.starts_with?("Aegis.Tui") end) end # ---------------- Log Storage for TUI Mode ------------------ defp store_log_messages(level, lines, ts, _md) do # Añadir logs a una cola o archivo temporal # En modo TUI, los logs se mostrarán en una sección específica de la UI log_entries = Enum.map(lines, fn line -> "#{format_timestamp(ts)} #{level_icon(level)} [#{inspect(level)}] #{line} " end) # Escribir logs a un archivo temporal para que la UI pueda leerlos log_dir = Path.join(System.tmp_dir!(), "aegis_logs") File.mkdir_p!(log_dir) log_file = Path.join(log_dir, "tui_logs.txt") Enum.each(log_entries, fn entry -> File.write(log_file, entry, [:append]) end) end end