Current section
Files
Jump to
Current section
Files
lib/probe/definitions.ex
defmodule Instruments.Probe.Definitions do
@moduledoc false
use GenServer
alias Instruments.Probe
alias Instruments.Probe.Errors
@type definition_errors :: {:error, {:probe_names_taken, [String.t()]}}
@type definition_response :: {:ok, [String.t()]} | definition_errors
@probe_prefix Application.get_env(:instruments, :probe_prefix)
@table_name :probe_definitions
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init([]) do
table_name = @table_name
^table_name = :ets.new(table_name, [:named_table, :set, :protected, read_concurrency: true])
{:ok, nil}
end
@doc """
Defines a probe. If the definition fails, an exception is thrown.
@see define/3
"""
@spec define!(String.t(), Probe.probe_type(), Probe.probe_options()) :: [String.t()]
def define!(name, type, options) do
case define(name, type, options) do
{:ok, probe_names} ->
probe_names
{:error, {:probe_names_taken, taken_names}} ->
raise Errors.ProbeNameTakenError.exception(taken_names: taken_names)
end
end
@doc """
Defines a probe.
The probe type can be:
* `gauge`: A single emitted value
* `counter`: A value that's incremented or decremeted over time.
If the value is negative, a decrement command is issued,
otherwise an increment command is executed.
* `histogram`: A value combined into a series and then listed as percentages.
* `timing`: A millisecond timing value.
Returns `{:ok, [probe_name]}` or `{:error, reason}`.
"""
@spec define(String.t(), Probe.probe_type(), Probe.probe_options()) :: definition_response
def define(base_name, type, options) do
name = to_probe_name(@probe_prefix, base_name)
defn_fn = fn ->
cond do
Keyword.has_key?(options, :function) ->
Probe.Supervisor.start_probe(name, type, options, Probe.Function)
Keyword.has_key?(options, :mfa) ->
{{module, fun, args}, options} = Keyword.pop(options, :mfa)
probe_fn = fn -> :erlang.apply(module, fun, args) end
options = Keyword.put(options, :function, probe_fn)
Probe.Supervisor.start_probe(name, type, options, Probe.Function)
Keyword.has_key?(options, :module) ->
probe_module = Keyword.get(options, :module)
Probe.Supervisor.start_probe(name, type, options, probe_module)
end
end
definitions =
case Keyword.get(options, :keys) do
keys when is_list(keys) ->
Enum.map(keys, fn key -> "#{name}.#{key}" end)
nil ->
[name]
end
unique_names = unique_names(definitions, options)
GenServer.call(__MODULE__, {:define, unique_names, defn_fn})
end
def handle_call({:define, probe_names, transaction}, _from, _) do
response =
case used_probe_names(probe_names) do
[] ->
added_probes =
Enum.map(probe_names, fn probe_name ->
true = :ets.insert_new(@table_name, {probe_name, probe_name})
probe_name
end)
transaction.()
{:ok, added_probes}
used_probe_names ->
{:error, {:probe_names_taken, used_probe_names}}
end
{:reply, response, nil}
end
@spec unique_names([String.t()], Probe.probe_options()) :: [String.t()]
defp unique_names(probe_names, options) do
case Keyword.get(options, :tags) do
tags when is_list(tags) ->
tag_string = Enum.join(Enum.sort(tags), ",")
for probe_name <- probe_names do
"#{probe_name}.tags:#{tag_string}"
end
nil ->
probe_names
end
end
@spec used_probe_names([String.t()]) :: [String.t()]
defp used_probe_names(probe_names) do
probe_names
|> Enum.map(&:ets.match(@table_name, {&1, :"$1"}))
|> List.flatten()
end
def to_probe_name(nil, base_name), do: base_name
def to_probe_name(probe_prefix, base_name), do: "#{probe_prefix}.#{base_name}"
end