eal/log_storage.ex

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

use Croma

defmodule AntikytheraEal.LogStorage do
  alias Antikythera.GearName
  alias AntikytheraCore.Path, as: CorePath

  defmodule Behaviour do
    @moduledoc """
    Interface to work with storage of gear log files.

    See `AntikytheraEal` for common information about pluggable interfaces defined in antikythera.

    Gear's log messages are written to a file in local filesystem (by `AntikytheraCore.GearLog.Writer`).
    Log files are then rotated after certain times and then uploaded to log storage.

    Typically rotated log files are uploaded by a script that runs outside of antikythera,
    in order to correctly upload logs even after ErlangVM crashes.
    `upload_rotated_logs/1` can also be used to upload logs to the storage, but this callback is
    basically intended for on-demand (i.e. gear administrator initiated) uploads.
    """

    @doc """
    Lists already-uploaded log files, whose last log messages are generated on the specified date, of a gear.

    `date_str` is of the form of `"20180101"`.

    Returns list of pairs, where each pair consists of

    - key which identifies the log file
    - size of the log file
    """
    @callback list(gear_name :: GearName.t(), date_str :: String.t()) :: [
                {String.t(), non_neg_integer}
              ]

    @doc """
    Generates download URLs of multiple log files specified by list of keys.

    Keys can be obtained by `list/2`.
    """
    @callback download_urls(keys :: [String.t()]) :: [String.t()]

    @doc """
    Upload all log files which have already been rotated but not yet uploaded.
    """
    @callback upload_rotated_logs(GearName.t()) :: :ok
  end

  defmodule FileSystem do
    @behaviour Behaviour

    @impl true
    defun list(gear_name :: v[GearName.t()], date_str :: v[String.t()]) :: [
            {String.t(), non_neg_integer}
          ] do
      dir = CorePath.gear_log_file_path(gear_name) |> Path.dirname()
      # match timestamp such as "20150825120000", exclude .uploaded.gz
      Path.wildcard(Path.join(dir, "#{gear_name}.log.#{date_str}??????.gz"))
      |> Enum.map(fn path -> {path, File.stat!(path).size} end)
    end

    @impl true
    defun download_urls(paths :: v[[String.t()]]) :: [String.t()] do
      Enum.map(paths, fn path -> "file://#{path}" end)
    end

    @impl true
    defun upload_rotated_logs(gear_name :: v[GearName.t()]) :: :ok do
      # to test that Writer process prevents multiple processes from running simultaneously
      :timer.sleep(100)
      gear_log_dir = CorePath.gear_log_file_path(gear_name) |> Path.dirname()
      # match timestamp such as "20150825120000", exclude .uploaded.gz
      Path.wildcard(Path.join(gear_log_dir, "#{gear_name}.log.??????????????.gz"))
      |> Enum.each(fn path ->
        File.rename(path, String.replace_suffix(path, ".gz", ".uploaded.gz"))
      end)
    end
  end

  use AntikytheraEal.ImplChooser
end