Current section

Files

Jump to
plug_accesslog lib plug accesslog logfiles.ex
Raw

lib/plug/accesslog/logfiles.ex

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