defmodule RamoopsLogger do @moduledoc """ This is an in-memory backend for the Elixir Logger that can survive reboots. Install it by adding it to your `config.exs`: ```elixir use Mix.Config config :logger, backends: [:console, RamoopsLogger] # The defaults config :logger, RamoopsLogger, pmsg_path: "/dev/pmsg1", recovered_log_path: "/sys/fs/pstore/pmsg-ramoops-1" ``` Or add manually: ```elixir iex> Logger.add_backend(RamoopsLogger) :ok # Configure only if the defaults don't work on your system iex> Logger.configure(RamoopsLogger, pmsg_path: "/dev/pmsg1") ``` After a reboot, you can check if a log exists by calling `available_log?/0`. """ @behaviour :gen_event alias RamoopsLogger.Server @default_pmsg_log_path "/sys/fs/pstore/pmsg-ramoops-0" @typedoc """ Options for configuring the backend: * `:pmsg_path` - Path to pmsg device (default is `/dev/pmsg0`) * `:recovered_log_path` - Path to recovered log files from previous boots (default is `/sys/fs/pstore/pmsg-ramoops-0`) These are either specified in the Application config (e.g., `config.exs`) like this: ```elixir config :logger, RamoopsLogger, pmsg_path: "/dev/pmsg1", recovered_log_path: "/sys/fs/pstore/pmsg-ramoops-1" ``` Or configured at runtime like: ```elixir iex> Logger.configure(RamoopsLogger, pmsg_path: "/dev/pmsg1") ``` """ @type backend_option :: {:pmsg_path, Path.t()} | {:recovered_log_path, Path.t()} @doc """ Dump the contents of the ramoops pstore file to the console """ @spec dump() :: :ok | {:error, File.posix()} def dump() do case File.read(recovered_log_path()) do {:ok, contents} -> IO.binwrite(contents) error -> error end end @doc """ Read the file contents from the ramoops pstore file. This is useful if you want to pragmatically do something with the file contents, like post to an external server. """ @spec read() :: {:ok, binary()} | {:error, File.posix()} def read() do File.read(recovered_log_path()) end @doc """ Check to see if there a log """ @spec available_log?() :: boolean() def available_log?() do File.exists?(recovered_log_path()) end @doc """ Return the path to the recovered log The path won't exist if there was nothing to recover on boot. """ @spec recovered_log_path() :: Path.t() def recovered_log_path() do env = Application.get_env(:logger, __MODULE__, []) Keyword.get(env, :recovered_log_path, @default_pmsg_log_path) end # # Logger backend callbacks # @impl :gen_event def init(__MODULE__) do init({__MODULE__, []}) end def init({__MODULE__, opts}) when is_list(opts) do env = Application.get_env(:logger, __MODULE__, []) opts = Keyword.merge(env, opts) Application.put_env(:logger, __MODULE__, opts) case Server.start_link(opts) do {:ok, pid} -> {:ok, pid} :ignore -> {:error, :ignore} error -> error end end @impl :gen_event def handle_event({_level, gl, _event}, state) when node(gl) != node() do # Ignore per Elixir Logger documentation {:ok, state} end def handle_event({level, _gl, message}, state) do Server.log(state, level, message) {:ok, state} end def handle_event(:flush, state) do # No flushing needed for RamoopsLogger {:ok, state} end @impl :gen_event def handle_call({:configure, opts}, state) do {:ok, Server.configure(state, opts), state} end def handle_call(_request, state) do # Ignore to avoid crashing on bad messages {:ok, {:error, :unimplemented}, state} end @impl :gen_event def handle_info(_, state) do {:ok, state} end @impl :gen_event def code_change(_old_vsn, state, _extra) do {:ok, state} end @impl :gen_event def terminate(_reason, state) do Server.stop(state) end end