defmodule DaProductApp.FileLogger do @behaviour :gen_event defstruct file: nil, week_end: nil, level: :debug def init(__MODULE__) do opts = Application.get_env(:logger, :file_logger, []) level = Keyword.get(opts, :level, :debug) {:ok, open_file(%__MODULE__{level: level})} end def handle_event({level, _gl, {Logger, msg, ts, _md}}, state) do state = maybe_rotate(state) if meets_level?(level, state.level), do: write(state.file, level, msg, ts) {:ok, state} end def handle_event(:flush, state), do: {:ok, state} def handle_call({:configure, opts}, state) do level = Keyword.get(opts, :level, state.level) {:ok, :ok, %{state | level: level}} end def handle_info(_msg, state), do: {:ok, state} def code_change(_old, state, _extra), do: {:ok, state} def terminate(_reason, %{file: file}) when not is_nil(file) do File.close(file) :ok end def terminate(_reason, _state), do: :ok defp maybe_rotate(%{week_end: week_end, file: file} = state) do if Date.compare(Date.utc_today(), week_end) == :gt do if file, do: File.close(file) open_file(%{state | file: nil}) else state end end defp open_file(state) do today = Date.utc_today() log_dir = Application.get_env(:logger, :file_logger, []) |> Keyword.get(:log_dir, "logs") File.mkdir_p!(log_dir) {week_start, week_end} = find_active_week(log_dir, today) filename = "app_#{Date.to_iso8601(week_start)}_to_#{Date.to_iso8601(week_end)}.log" path = Path.join(log_dir, filename) case File.open(path, [:append, :utf8]) do {:ok, file} -> %{state | file: file, week_end: week_end} {:error, reason} -> IO.puts(:stderr, "[FileLogger] Cannot open #{path}: #{inspect(reason)}") %{state | file: nil, week_end: week_end} end end defp find_active_week(log_dir, today) do pattern = ~r/^app_(\d{4}-\d{2}-\d{2})_to_(\d{4}-\d{2}-\d{2})\.log$/ existing = log_dir |> File.ls() |> case do {:ok, files} -> files _ -> [] end |> Enum.find_value(fn filename -> case Regex.run(pattern, filename, capture: :all_but_first) do [start_str, end_str] -> with {:ok, start_date} <- Date.from_iso8601(start_str), {:ok, end_date} <- Date.from_iso8601(end_str), true <- Date.compare(today, start_date) != :lt, true <- Date.compare(today, end_date) != :gt do {start_date, end_date} else _ -> nil end _ -> nil end end) existing || {today, Date.add(today, 6)} end defp meets_level?(_level, nil), do: true defp meets_level?(level, min_level) do Logger.compare_levels(level, min_level) != :lt end defp write(nil, _level, _msg, _ts), do: :ok defp write(file, level, msg, {{y, mo, d}, {h, mi, s, ms}}) do ts = "#{pad(y, 4)}-#{pad(mo, 2)}-#{pad(d, 2)} #{pad(h, 2)}:#{pad(mi, 2)}:#{pad(s, 2)}.#{pad(ms, 3)}" lvl = level |> Atom.to_string() |> String.upcase() IO.write(file, "#{ts} [#{lvl}] #{format_msg(msg)}\n") end defp format_msg({:report, report}), do: inspect(report) defp format_msg(msg), do: IO.chardata_to_string(msg) defp pad(n, size), do: n |> Integer.to_string() |> String.pad_leading(size, "0") end