defmodule Plug.AccessLog.Logfiles do @moduledoc false use Agent require Logger @doc """ Starts the logfile agent. """ @spec start_link(any) :: Agent.on_start() def start_link(_), do: Agent.start_link(fn -> %{} end, name: __MODULE__) @doc """ Returns the logfile device for writing. """ @spec get(String.t()) :: File.io_device() | nil def get(logfile) do case get_device(logfile) do {nil, nil} -> open(logfile) {device, inode} when is_pid(device) -> get_or_reopen({device, inode}, logfile) {other_device, _} -> other_device end end @doc """ Opens a logfile for future writing. """ @spec open(String.t()) :: File.io_device() | nil def open(logfile) do case File.open(logfile, [:append, :utf8]) do {:ok, device} -> set(logfile, device) {:error, error} -> log_open_error(logfile, error) nil end end @doc """ Replaces the io_device associated with a logfile. The already registered (if any) io_device will be closed using `File.close/1`. The new io_device associated with the logfile will be returned. """ @spec replace(String.t(), File.io_device()) :: File.io_device() def replace(logfile, new_device) do case get_device(logfile) do {nil, nil} -> nil {old_device, _} -> File.close(old_device) end logstate = {new_device, inode(logfile)} Agent.update(__MODULE__, &Map.put(&1, logfile, logstate)) new_device end @doc """ Sets the io_device associated with a logfile. If a logfile is already associated with an io_device the passed one will be closed using `File.close/1`. The new io_device associated with the logfile will be returned. """ @spec set(String.t(), File.io_device()) :: File.io_device() def set(logfile, new_device) do logstate = {new_device, inode(logfile)} Agent.update(__MODULE__, &Map.put_new(&1, logfile, logstate)) case get_device(logfile) do {^new_device, _} -> new_device {old_device, _} -> File.close(new_device) old_device end end defp get_device(logfile) do Agent.get(__MODULE__, &Map.get(&1, logfile, {nil, nil})) end defp get_or_reopen({device, inode}, logfile) do case Process.alive?(device) && inode == inode(logfile) do true -> device false -> case File.open(logfile, [:append, :utf8]) do {:ok, device} -> replace(logfile, device) {:error, error} -> log_open_error(logfile, error) nil end end end defp inode(logpath) do case File.stat(logpath) do {:ok, stat} -> stat.inode _ -> nil end end defp log_open_error(logfile, error) do Logger.error(["Failed to open logfile ", inspect(logfile), " for writing: ", inspect(error)]) end end