core/gear_log/log_rotation.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule AntikytheraCore.GearLog.LogRotation do
  @moduledoc """
  A Module to write and rotate a log file using `AntikytheraCore.GearLog.FileHandle`.
  """

  alias AntikytheraCore.GearLog.{FileHandle, Message}

  defmodule State do
    use Croma.Struct,
      recursive_new?: true,
      fields: [
        # FileHandle.t
        file_handle: Croma.Tuple,
        empty?: Croma.Boolean,
        timer: Croma.Reference,
        interval: Croma.NonNegInteger
      ]
  end

  defun init(interval :: v[non_neg_integer], file_path :: Path.t(), opts :: Keyword.t() \\ []) ::
          State.t() do
    handle = FileHandle.open(file_path, opts)
    timer = arrange_next_rotation(nil, interval)
    %State{file_handle: handle, empty?: true, timer: timer, interval: interval}
  end

  defun write_log(
          %State{file_handle: handle, timer: timer, interval: interval} = state,
          log :: Message.t()
        ) :: State.t() do
    case FileHandle.write(handle, log) do
      {:kept_open, new_handle} ->
        %State{state | file_handle: new_handle, empty?: false}

      {:rotated, new_handle} ->
        # Log file is just rotated as its size has exceeded the upper limit.
        # Note that the current message is written to the newly-opened log file and thus it's not empty.
        new_timer = arrange_next_rotation(timer, interval)
        %State{state | file_handle: new_handle, empty?: false, timer: new_timer}
    end
  end

  defun set_write_to_terminal(%State{file_handle: handle} = state, new_val :: boolean) ::
          State.t() do
    %State{state | file_handle: FileHandle.set_write_to_terminal(handle, new_val)}
  end

  defun restore_write_to_terminal(%State{file_handle: handle} = state) :: State.t() do
    %State{state | file_handle: FileHandle.restore_write_to_terminal(handle)}
  end

  defun rotate(
          %State{file_handle: handle, empty?: empty?, timer: timer, interval: interval} = state
        ) :: State.t() do
    new_timer = arrange_next_rotation(timer, interval)
    next_state = %State{state | timer: new_timer}

    if empty? do
      next_state
    else
      %State{next_state | file_handle: FileHandle.rotate(handle), empty?: true}
    end
  end

  defun terminate(%State{file_handle: handle}) :: :ok do
    FileHandle.close(handle)
  end

  defp arrange_next_rotation(timer, interval) do
    if timer do
      Process.cancel_timer(timer)
    end

    Process.send_after(self(), :rotate, interval)
  end
end