Skip to main content

lib/application/features/sampling/sampling_config.ex

defmodule EcsElixirCore.Application.Features.Sampling.SamplingConfig do
  @moduledoc """
  Manages sampling feature configuration.

  Reads sampling rules from application config, parses them into a
  `SamplingRuleSet` and caches the result in `:persistent_term` to avoid
  re-parsing on every request.

  This module belongs to the application layer because the application
  layer is responsible for managing configuration for both the middleware
  and its features.
  """

  alias EcsElixirCore.Domain.Model.Features.Sampling.Model.SamplingRuleSet
  alias EcsElixirCore.Domain.Shared.InternalLogging

  @cache_key {__MODULE__, :ruleset_cache}

  @doc "Returns the current sampling ruleset, loading and caching it on first access."
  @spec fetch_ruleset() :: {:ok, SamplingRuleSet.t()} | {:error, term()}
  def fetch_ruleset do
    source_app = Application.get_env(:ecs_elixir_core, :sampling_source_app, :ecs_elixir_core)
    source_key = Application.get_env(:ecs_elixir_core, :sampling_source_key, :ecs_sampling)

    config = Application.get_env(source_app, source_key, [])
    rules20x_json = get_config_value(config, :rules20XJson)
    rules40x_json = get_config_value(config, :rules40XJson)

    fingerprint = {source_app, source_key, rules20x_json, rules40x_json}

    case :persistent_term.get(@cache_key, :empty) do
      {^fingerprint, result} ->
        result

      _ ->
        result = parse_ruleset(rules20x_json, rules40x_json)
        log_rules_loaded(result, source_app, source_key)
        :persistent_term.put(@cache_key, {fingerprint, result})
        result
    end
  end

  @doc "Clears the cached ruleset, forcing a reload on the next call."
  @spec clear_cache() :: :ok
  def clear_cache do
    :persistent_term.erase(@cache_key)
    :ok
  end

  defp parse_ruleset(rules20x_json, rules40x_json) do
    with {:ok, rules20x} <- SamplingRuleSet.parse_rules(rules20x_json, :rules20x),
         {:ok, rules40x} <- SamplingRuleSet.parse_rules(rules40x_json, :rules40x),
         {:ok, ruleset} <- SamplingRuleSet.build_ruleset(rules20x, rules40x) do
      {:ok, ruleset}
    else
      {:error, _reason} = error -> error
    end
  end

  defp get_config_value(config, key) when is_map(config), do: Map.get(config, key)
  defp get_config_value(config, key) when is_list(config), do: Keyword.get(config, key)
  defp get_config_value(_config, _key), do: nil

  defp log_rules_loaded({:ok, %SamplingRuleSet{} = ruleset}, source_app, source_key) do
    rules20x_count = map_size(ruleset.rules20x)
    rules40x_count = map_size(ruleset.rules40x)
    total_rules = rules20x_count + rules40x_count

    InternalLogging.log_debug(
      "Sampling rules loaded from #{inspect(source_app)}:#{inspect(source_key)} - " <>
        "20X=#{rules20x_count}, 40X=#{rules40x_count}, total=#{total_rules}",
      __MODULE__
    )
  end

  defp log_rules_loaded({:error, _reason}, _source_app, _source_key), do: :ok
end