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, md) {: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) log_dir = Application.get_env(:logger, :file_logger, []) |> Keyword.get(:log_dir, "logs") compress_log(log_dir, week_end) open_file(%{state | file: nil}) else state end end defp compress_log(log_dir, week_end) do week_start = Date.add(week_end, -6) filename = "app_#{Date.to_iso8601(week_start)}_to_#{Date.to_iso8601(week_end)}.log" path = Path.join(log_dir, filename) gz_path = path <> ".gz" try do if File.exists?(path) do path |> File.stream!() |> Stream.into(File.stream!(gz_path, [:write, :compressed])) |> Stream.run() case File.rm(path) do :ok -> :ok {:error, reason} -> IO.puts(:stderr, "[FileLogger] Cannot remove original log #{path}: #{inspect(reason)}") end end rescue exception -> IO.puts(:stderr, "[FileLogger] Failed to compress #{path}: #{inspect(exception)}") 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, _md), do: :ok defp write(file, level, msg, ts, md) do IO.write(file, DaProductApp.LogFormatter.format(level, msg, ts, md)) end end