lib/appsignal/integration_logger.ex

defmodule Appsignal.IntegrationLogger do
  require Appsignal.Utils

  @io Appsignal.Utils.compile_env(:appsignal, :io, IO)
  @file_module Appsignal.Utils.compile_env(:appsignal, :file, File)

  @log_levels [:trace, :debug, :info, :warn, :error]

  @type log_level :: :trace | :debug | :info | :warn | :error
  @type device :: :stdio | :stderr | :file

  @spec trace(String.t()) :: :ok
  def trace(message) do
    log(:trace, message, [])
  end

  @spec debug(String.t()) :: :ok
  def debug(message) do
    log(:debug, message, [])
  end

  @spec info(String.t()) :: :ok
  def info(message) do
    log(:info, message, [])
  end

  @spec warn(String.t()) :: :ok
  def warn(message, options \\ []) do
    log(:warn, message, options)
  end

  @spec error(String.t()) :: :ok
  def error(message, options \\ []) do
    log(:error, message, options)
  end

  @spec log(log_level(), String.t(), keyword()) :: :ok
  defp log(level, message, options) do
    threshold = Appsignal.Config.log_level()

    if log_level?(level, threshold) do
      case {device(), Keyword.get(options, :stderr, false)} do
        {device, false} ->
          do_log(device, level, message)

        {:stdio, true} ->
          do_log(:stderr, level, message)

        {:file, true} ->
          do_log(:file, level, message)
          do_log(:stderr, level, message)
      end
    end
  end

  @spec device() :: device()
  defp device do
    case Application.fetch_env!(:appsignal, :config)[:log] do
      "stdout" -> :stdio
      _ -> :file
    end
  end

  @spec log_level?(log_level(), log_level()) :: bool()
  defp log_level?(level, threshold) do
    Enum.find_index(@log_levels, &(&1 == level)) >=
      Enum.find_index(@log_levels, &(&1 == threshold))
  end

  @spec do_log(device(), log_level(), String.t()) :: :ok
  defp do_log(device, level, message) do
    time = NaiveDateTime.from_erl!(:calendar.local_time())
    pid = System.pid()

    puts(device, format(device, time, pid, level, message))
  end

  @spec puts(device(), String.t()) :: :ok
  defp puts(device, content)

  defp puts(:file, content) do
    @file_module.write(Appsignal.Config.log_file_path(), content <> "\n", [:append, :utf8])
    :ok
  end

  defp puts(device, content) do
    @io.puts(device, content)
    :ok
  end

  @spec format(device(), NaiveDateTime.t(), binary(), log_level(), String.t()) :: String.t()
  defp format(device, time, pid, level, message) do
    time = format_time(time)
    level = format_level(level)

    case device do
      :stdio -> "\n[#{time} (process) ##{pid}][appsignal][#{level}] #{message}"
      :stderr -> "\n[appsignal][#{level}] #{message}"
      :file -> "[#{time} (process) ##{pid}][#{level}] #{message}"
    end
  end

  @spec format_level(log_level()) :: String.t()
  defp format_level(level) do
    case level do
      :trace -> "TRACE"
      :debug -> "DEBUG"
      :info -> "INFO"
      :warn -> "WARNING"
      :error -> "ERROR"
    end
  end

  @spec format_time(NaiveDateTime.t()) :: String.t()
  defp format_time(time) do
    time
    |> NaiveDateTime.truncate(:second)
    |> NaiveDateTime.to_iso8601()
  end
end