Current section
Files
Jump to
Current section
Files
lib/dogstat.ex
defmodule DogStat do
@moduledoc """
This module provides helper functions to persist meaningful metrics to StatsD or DogstatsD servers.
Code is based on [Statix](https://github.com/lexmag/statix) library.
"""
use GenServer
alias DogStat.Packet
@type key :: iodata
@type options :: [sample_rate: float, tags: [String.t]]
@type on_send :: :ok | {:error, term}
@doc """
Starts a metric collector process.
`opts` accepts connection arguments:
* `enabled?` - enables or disables metrics reporting;
* `host` - StatsD server host;
* `port` - StatsD server port;
* `namespace` - will be used as prefix to collected metrics;
* `send_tags?` - allows to disable tags for StatsD servers that don't support them;
* `sink` - if set to list, all metrics will be stored in a process state, useful for testing;
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc false
def init(opts) do
{:ok, socket} = :gen_udp.open(0, [active: false])
state =
opts
|> get_config()
|> Map.put(:socket, socket)
{:ok, state}
end
@doc """
Changes DogStat configuration at run-time. Accepts `opts` is identical to a `start_link/1`.
"""
def configure(opts) do
GenServer.call(__MODULE__, {:configure, opts})
end
defp get_config(opts) do
enabled? = Keyword.get(opts, :enabled?, true)
host = opts |> Keyword.get(:host, "127.0.0.1") |> String.to_char_list()
port = Keyword.get(opts, :port, 8125)
sink = Keyword.get(opts, :sink, nil)
send_tags? = Keyword.get(opts, :send_tags?, true)
namespace = Keyword.get(opts, :namespace, nil)
namespace = if namespace, do: [namespace, ?.], else: ""
{:ok, address} = :inet.getaddr(host, :inet)
header = Packet.header(address, port)
%{
enabled?: enabled?,
send_tags?: send_tags?,
header: [header | namespace],
sink: sink
}
end
@doc """
Increments the StatsD counter identified by `key` by the given `value`.
`value` is supposed to be zero or positive and `c:decrement/3` should be
used for negative values.
## Examples
iex> increment("hits", 1, [])
:ok
"""
@spec increment(key, value :: number, options) :: on_send
def increment(key, val \\ 1, options \\ []) when is_number(val) do
transmit(:counter, key, val, options)
end
@doc """
Decrements the StatsD counter identified by `key` by the given `value`.
Works same as `c:increment/3` but subtracts `value` instead of adding it. For
this reason `value` should be zero or negative.
## Examples
iex> decrement("open_connections", 1, [])
:ok
"""
@spec decrement(key, value :: number, options) :: on_send
def decrement(key, val \\ 1, options \\ []) when is_number(val) do
transmit(:counter, key, [?-, to_string(val)], options)
end
@doc """
Writes to the StatsD gauge identified by `key`.
## Examples
iex> gauge("cpu_usage", 0.83, [])
:ok
"""
@spec gauge(key, value :: String.Chars.t, options) :: on_send
def gauge(key, val, options \\ []) do
transmit(:gauge, key, val, options)
end
@doc """
Writes `value` to the histogram identified by `key`.
Not all StatsD-compatible servers support histograms. An example of a such
server [statsite](https://github.com/statsite/statsite).
## Examples
iex> histogram("online_users", 123, [])
:ok
"""
@spec histogram(key, value :: String.Chars.t, options) :: on_send
def histogram(key, val, options \\ []) do
transmit(:histogram, key, val, options)
end
@doc """
Writes the given `value` to the StatsD timing identified by `key`.
`value` is expected in milliseconds.
## Examples
iex> timing("rendering", 12, [])
:ok
"""
@spec timing(key, value :: String.Chars.t, options) :: on_send
def timing(key, val, options \\ []) do
transmit(:timing, key, val, options)
end
@doc """
Writes the given `value` to the StatsD set identified by `key`.
## Examples
iex> set("unique_visitors", "user1", [])
:ok
"""
@spec set(key, value :: String.Chars.t, options) :: on_send
def set(key, val, options \\ []) do
transmit(:set, key, val, options)
end
@doc """
Measures the execution time of the given `function` and writes that to the
StatsD timing identified by `key`.
This function returns the value returned by `function`, making it suitable for
easily wrapping existing code.
## Examples
iex> measure("integer_to_string", [], fn -> Integer.to_string(123) end)
"123"
"""
@spec measure(key, options, function :: (() -> result)) :: result when result: var
def measure(key, options \\ [], fun) when is_function(fun, 0) do
{elapsed, result} = :timer.tc(fun)
timing(key, div(elapsed, 1000), options)
result
end
@doc false
def transmit(type, key, val, options) when (is_binary(key) or is_list(key)) and is_list(options) do
sample_rate = Keyword.get(options, :sample_rate)
if is_nil(sample_rate) or sample_rate >= :rand.uniform() do
GenServer.cast(__MODULE__, {:transmit, type, key, to_string(val), options})
end
:ok
end
@doc false
def handle_cast({:transmit, _type, _key, _value, _options}, %{enabled?: false} = state),
do: {:noreply, state}
# Transmits message to a sink
@doc false
def handle_cast({:transmit, type, key, value, options}, %{sink: sink} = state) when is_list(sink) do
%{header: header} = state
packet = %{type: type, key: key, value: value, options: options, header: header}
{:noreply, %{state | sink: [packet | sink]}}
end
# Transmits message to a StatsD server
@doc false
def handle_cast({:transmit, type, key, value, options}, state) do
%{header: header, socket: socket, send_tags?: send_tags?} = state
packet = Packet.build(header, type, key, value, send_tags?, options)
Port.command(socket, packet)
receive do
{:inet_reply, _port, status} -> status
end
{:noreply, state}
end
@doc false
def handle_call({:configure, opts}, _from, state) do
state = Map.merge(state, get_config(opts))
{:reply, {:ok, state}, state}
end
end