Skip to main content

src/pharos@threshold_eval.erl

-module(pharos@threshold_eval).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/pharos/threshold_eval.gleam").
-export([evaluate_probe/2, evaluate/2, metric_name/1, sample_value/2, threshold_limit/1]).
-export_type([verdict/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Evaluate a `Threshold` against a decoded `Measurement`.\n"
    "\n"
    " Returns a three-way verdict so callers can branch without reaching\n"
    " for nested `case` over the cartesian product of measurements and\n"
    " thresholds. Helper functions match every measurement variant\n"
    " exhaustively, so adding a new measurement kind surfaces as a\n"
    " compiler warning here.\n"
).

-type verdict() :: not_applicable | healthy | breached.

-file("src/pharos/threshold_eval.gleam", 344).
-spec verdict(boolean()) -> verdict().
verdict(Is_breached) ->
    case Is_breached of
        true ->
            breached;

        false ->
            healthy
    end.

-file("src/pharos/threshold_eval.gleam", 33).
?DOC(
    " Evaluate a probe `threshold` against a decoded probe `sample`, returning\n"
    " the same `Verdict` as `evaluate/2`. A `NotApplicable` verdict means the\n"
    " threshold's field is absent from this sample, so the caller should neither\n"
    " breach nor recover.\n"
).
-spec evaluate_probe(
    gleam@dict:dict(binary(), float()),
    pharos@probe:probe_threshold()
) -> verdict().
evaluate_probe(Sample, Threshold) ->
    case gleam_stdlib:map_get(Sample, erlang:element(3, Threshold)) of
        {error, nil} ->
            not_applicable;

        {ok, Value} ->
            case erlang:element(4, Threshold) of
                {above, Limit} ->
                    verdict(Value > Limit);

                {below, Limit@1} ->
                    verdict(Value < Limit@1)
            end
    end.

-file("src/pharos/threshold_eval.gleam", 310).
-spec check_scheduler(
    pharos@measurement:measurement(),
    fun((pharos@measurement:scheduler_stats()) -> boolean())
) -> verdict().
check_scheduler(Measurement, Predicate) ->
    case Measurement of
        {beam_scheduler, Sched} ->
            verdict(Predicate(Sched));

        {beam_memory, _} ->
            not_applicable;

        {beam_run_queues, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 290).
-spec check_host_cpu(
    pharos@measurement:measurement(),
    fun((pharos@measurement:host_cpu_stats()) -> boolean())
) -> verdict().
check_host_cpu(Measurement, Predicate) ->
    case Measurement of
        {host_cpu, Cpu} ->
            verdict(Predicate(Cpu));

        {beam_memory, _} ->
            not_applicable;

        {beam_run_queues, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 333).
?DOC(
    " Apply `predicate` to a host probe's stats unless it is still reporting\n"
    " `Unimplemented` (its source unavailable), in which case it is\n"
    " `NotApplicable` so placeholder values never breach/recover.\n"
).
-spec check_host_probe(
    pharos@measurement:host_probe_status(),
    JVA,
    fun((JVA) -> boolean())
) -> verdict().
check_host_probe(Status, Stats, Predicate) ->
    case Status of
        unimplemented ->
            not_applicable;

        implemented ->
            verdict(Predicate(Stats))
    end.

-file("src/pharos/threshold_eval.gleam", 270).
?DOC(
    " Evaluate a host-disk threshold. Like host memory, a probe still reporting\n"
    " `Unimplemented` (os_mon unavailable) is `NotApplicable`.\n"
).
-spec check_host_disk(
    pharos@measurement:measurement(),
    fun((pharos@measurement:host_disk_stats()) -> boolean())
) -> verdict().
check_host_disk(Measurement, Predicate) ->
    case Measurement of
        {host_disk, Disk} ->
            check_host_probe(erlang:element(2, Disk), Disk, Predicate);

        {beam_memory, _} ->
            not_applicable;

        {beam_run_queues, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 247).
?DOC(
    " Evaluate a host-memory threshold. A host probe still reporting\n"
    " `Unimplemented` (e.g. the non-Linux `/proc/meminfo` fallback) is treated as\n"
    " `NotApplicable` so its placeholder zeros never produce breach/recover noise.\n"
).
-spec check_host_memory(
    pharos@measurement:measurement(),
    fun((pharos@measurement:host_memory_stats()) -> boolean())
) -> verdict().
check_host_memory(Measurement, Predicate) ->
    case Measurement of
        {host_memory, Host} ->
            check_host_probe(erlang:element(2, Host), Host, Predicate);

        {beam_memory, _} ->
            not_applicable;

        {beam_run_queues, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 224).
-spec check_persistent_term(
    pharos@measurement:measurement(),
    fun((pharos@measurement:persistent_term_stats()) -> boolean())
) -> verdict().
check_persistent_term(Measurement, Predicate) ->
    case Measurement of
        {beam_persistent_term, Term} ->
            verdict(Predicate(Term));

        {beam_memory, _} ->
            not_applicable;

        {beam_run_queues, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 204).
-spec check_system_counts(
    pharos@measurement:measurement(),
    fun((pharos@measurement:system_count_stats()) -> boolean())
) -> verdict().
check_system_counts(Measurement, Predicate) ->
    case Measurement of
        {beam_system_counts, Counts} ->
            verdict(Predicate(Counts));

        {beam_memory, _} ->
            not_applicable;

        {beam_run_queues, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 184).
-spec check_run_queue(
    pharos@measurement:measurement(),
    fun((pharos@measurement:run_queue_stats()) -> boolean())
) -> verdict().
check_run_queue(Measurement, Predicate) ->
    case Measurement of
        {beam_run_queues, Queue} ->
            verdict(Predicate(Queue));

        {beam_memory, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 164).
-spec check_memory(
    pharos@measurement:measurement(),
    fun((pharos@measurement:beam_memory_stats()) -> boolean())
) -> verdict().
check_memory(Measurement, Predicate) ->
    case Measurement of
        {beam_memory, Memory} ->
            verdict(Predicate(Memory));

        {beam_run_queues, _} ->
            not_applicable;

        {beam_system_counts, _} ->
            not_applicable;

        {beam_persistent_term, _} ->
            not_applicable;

        {process_info, _} ->
            not_applicable;

        {cluster_nodes, _} ->
            not_applicable;

        {host_memory, _} ->
            not_applicable;

        {host_disk, _} ->
            not_applicable;

        {host_cpu, _} ->
            not_applicable;

        {host_network, _} ->
            not_applicable;

        {beam_scheduler, _} ->
            not_applicable;

        {beam_reductions, _} ->
            not_applicable
    end.

-file("src/pharos/threshold_eval.gleam", 48).
?DOC(" Evaluate `threshold` against `measurement` and return a `Verdict`.\n").
-spec evaluate(pharos@measurement:measurement(), pharos@config:threshold()) -> verdict().
evaluate(Measurement, Threshold) ->
    case Threshold of
        {total_memory, Limit} ->
            check_memory(
                Measurement,
                fun(Memory) -> erlang:element(3, Memory) > Limit end
            );

        {process_memory, Limit@1} ->
            check_memory(
                Measurement,
                fun(Memory@1) -> erlang:element(4, Memory@1) > Limit@1 end
            );

        {system_memory, Limit@2} ->
            check_memory(
                Measurement,
                fun(Memory@2) -> erlang:element(6, Memory@2) > Limit@2 end
            );

        {binary_memory, Limit@3} ->
            check_memory(
                Measurement,
                fun(Memory@3) -> erlang:element(9, Memory@3) > Limit@3 end
            );

        {ets_memory, Limit@4} ->
            check_memory(
                Measurement,
                fun(Memory@4) -> erlang:element(11, Memory@4) > Limit@4 end
            );

        {total_run_queue, Limit@5} ->
            check_run_queue(
                Measurement,
                fun(Queue) -> erlang:element(2, Queue) > Limit@5 end
            );

        {cpu_run_queue, Limit@6} ->
            check_run_queue(
                Measurement,
                fun(Queue@1) -> erlang:element(3, Queue@1) > Limit@6 end
            );

        {process_count, Limit@7} ->
            check_system_counts(
                Measurement,
                fun(Counts) -> erlang:element(2, Counts) > Limit@7 end
            );

        {atom_count, Limit@8} ->
            check_system_counts(
                Measurement,
                fun(Counts@1) -> erlang:element(3, Counts@1) > Limit@8 end
            );

        {port_count, Limit@9} ->
            check_system_counts(
                Measurement,
                fun(Counts@2) -> erlang:element(4, Counts@2) > Limit@9 end
            );

        {persistent_term_count, Limit@10} ->
            check_persistent_term(
                Measurement,
                fun(Term) -> erlang:element(2, Term) > Limit@10 end
            );

        {persistent_term_memory, Limit@11} ->
            check_persistent_term(
                Measurement,
                fun(Term@1) -> erlang:element(4, Term@1) > Limit@11 end
            );

        {host_memory_used, Limit@12} ->
            check_host_memory(
                Measurement,
                fun(Host) -> erlang:element(5, Host) > Limit@12 end
            );

        {host_disk_used, Limit@13} ->
            check_host_disk(
                Measurement,
                fun(Disk) -> erlang:element(5, Disk) > Limit@13 end
            );

        {host_cpu_util, Limit@14} ->
            check_host_cpu(
                Measurement,
                fun(Cpu) -> erlang:element(2, Cpu) > Limit@14 end
            );

        {scheduler_utilization, Limit@15} ->
            check_scheduler(
                Measurement,
                fun(Sched) -> erlang:element(2, Sched) > Limit@15 end
            );

        {windowed, Over, _, _} ->
            evaluate(Measurement, Over)
    end.

-file("src/pharos/threshold_eval.gleam", 119).
?DOC(
    " The flattened metric name a threshold compares against (see\n"
    " `metric.from_measurement`).\n"
).
-spec metric_name(pharos@config:threshold()) -> binary().
metric_name(Threshold) ->
    case Threshold of
        {total_memory, _} ->
            <<"vm.memory.total"/utf8>>;

        {process_memory, _} ->
            <<"vm.memory.processes"/utf8>>;

        {system_memory, _} ->
            <<"vm.memory.system"/utf8>>;

        {binary_memory, _} ->
            <<"vm.memory.binary"/utf8>>;

        {ets_memory, _} ->
            <<"vm.memory.ets"/utf8>>;

        {total_run_queue, _} ->
            <<"vm.run_queue.total"/utf8>>;

        {cpu_run_queue, _} ->
            <<"vm.run_queue.cpu"/utf8>>;

        {process_count, _} ->
            <<"vm.system.process_count"/utf8>>;

        {atom_count, _} ->
            <<"vm.system.atom_count"/utf8>>;

        {port_count, _} ->
            <<"vm.system.port_count"/utf8>>;

        {persistent_term_count, _} ->
            <<"vm.persistent_term.count"/utf8>>;

        {persistent_term_memory, _} ->
            <<"vm.persistent_term.memory"/utf8>>;

        {host_memory_used, _} ->
            <<"host.memory.used"/utf8>>;

        {host_disk_used, _} ->
            <<"host.disk.used"/utf8>>;

        {host_cpu_util, _} ->
            <<"host.cpu.util"/utf8>>;

        {scheduler_utilization, _} ->
            <<"vm.scheduler.utilization"/utf8>>;

        {windowed, Over, _, _} ->
            metric_name(Over)
    end.

-file("src/pharos/threshold_eval.gleam", 107).
?DOC(
    " Extract the current value of the metric `threshold` compares, by name, from\n"
    " the measurement's flattened metrics. Returns `Error(Nil)` when the\n"
    " measurement doesn't carry that metric (wrong kind, or an `Unimplemented`\n"
    " host probe, which flattens to no metrics), so such ticks contribute no\n"
    " sample to a window.\n"
).
-spec sample_value(pharos@measurement:measurement(), pharos@config:threshold()) -> {ok,
        float()} |
    {error, nil}.
sample_value(Measurement, Threshold) ->
    Name = metric_name(Threshold),
    _pipe = pharos@metric:from_measurement(Measurement),
    _pipe@1 = gleam@list:find(
        _pipe,
        fun(M) -> erlang:element(2, M) =:= Name end
    ),
    gleam@result:map(_pipe@1, fun(M@1) -> erlang:element(3, M@1) end).

-file("src/pharos/threshold_eval.gleam", 142).
?DOC(" The threshold's comparison limit as a `Float` (integer limits are widened).\n").
-spec threshold_limit(pharos@config:threshold()) -> float().
threshold_limit(Threshold) ->
    case Threshold of
        {total_memory, Above} ->
            Above;

        {process_memory, Above@1} ->
            Above@1;

        {system_memory, Above@2} ->
            Above@2;

        {binary_memory, Above@3} ->
            Above@3;

        {ets_memory, Above@4} ->
            Above@4;

        {persistent_term_memory, Above@5} ->
            Above@5;

        {host_memory_used, Above@6} ->
            Above@6;

        {host_disk_used, Above@7} ->
            Above@7;

        {host_cpu_util, Above@8} ->
            Above@8;

        {scheduler_utilization, Above@9} ->
            Above@9;

        {total_run_queue, Above@10} ->
            erlang:float(Above@10);

        {cpu_run_queue, Above@11} ->
            erlang:float(Above@11);

        {process_count, Above@12} ->
            erlang:float(Above@12);

        {atom_count, Above@13} ->
            erlang:float(Above@13);

        {port_count, Above@14} ->
            erlang:float(Above@14);

        {persistent_term_count, Above@15} ->
            erlang:float(Above@15);

        {windowed, Over, _, _} ->
            threshold_limit(Over)
    end.