defmodule DaProductApp.LogFormatter do @moduledoc """ Shared log formatting module for all backends (file and console). Provides Apache-style log formatting in the format: [Day Mon DD HH:MM:SS.microseconds YYYY] [level] [pid PID] [client IP:PORT] message Used by both WeeklyLogger (file backend) and ConsoleLogger (console backend) to ensure identical output across all logging destinations. """ def format_entry(level, msg, {{year, month, day}, {hour, min, sec, ms}}, metadata, pid) do day_name = get_day_name({year, month, day}) month_name = get_month_name(month) client_info = get_client_info(metadata) msg_str = msg |> format_msg() |> strip_ansi() # Apache-style format: [Day Mon DD HH:MM:SS.microseconds YYYY] [level] [pid PID] [client IP:PORT] message # Convert milliseconds to microseconds (multiply by 1000) and format as 6 digits microseconds = ms * 1000 apache_timestamp = :io_lib.format(~c"~s ~s ~2..0B ~2..0B:~2..0B:~2..0B.~6..0B ~4..0B", [day_name, month_name, day, hour, min, sec, microseconds, year]) level_str = level |> Atom.to_string() |> String.downcase() pid_str = format_pid(pid) IO.iodata_to_binary([ "[", apache_timestamp, "] ", "[", level_str, "] ", "[pid ", pid_str, "] ", "[client ", client_info, "] ", msg_str, "\n" ]) end defp get_day_name({year, month, day}) do date = Date.new!(year, month, day) days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] day_of_week = Date.day_of_week(date, :monday) Enum.at(days, day_of_week - 1) end defp get_month_name(month) do months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] Enum.at(months, month - 1) end defp get_client_info(metadata) do case Keyword.get(metadata, :client_ip) do {a, b, c, d} -> port = Keyword.get(metadata, :client_port, 0) "#{a}.#{b}.#{c}.#{d}:#{port}" _ -> case Keyword.get(metadata, :remote_ip) do {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}:0" _ -> "127.0.0.1:0" end end end defp format_pid(pid) when is_integer(pid), do: Integer.to_string(pid) defp format_pid(pid) when is_binary(pid) do case Integer.parse(pid) do {num, ""} -> Integer.to_string(num) _ -> pid end end defp format_pid(nil), do: "?" defp format_pid(pid), do: inspect(pid) defp format_msg({:string, chardata}), do: format_chardata(chardata) defp format_msg({:report, report}), do: inspect(report) defp format_msg(chardata), do: format_chardata(chardata) defp format_chardata(chardata) do IO.chardata_to_string(chardata) rescue _ -> inspect(chardata) end defp strip_ansi(str), do: Regex.replace(~r/\e\[[0-9;]*[a-zA-Z]/, str, "") end