defmodule DataDaemon do
@moduledoc ~S"""
DataDog StatsD reporter.
## Quick Setup
```elixir
# In your config/config.exs file
config :my_app, Sample.DataDog,
url: "statsd+udp://localhost:8125"
# In your application code
defmodule Sample.DataDog do
@moduledoc ~S"My DataDog reporter."
use DataDaemon,
otp_app: :my_app,
extensions: [:datadog]
end
defmodule Sample.App do
alias Sample.DataDog
def send_metrics do
Sample.DataDog.start_link()
tags = [zone: "us-east-1a"]
DataDog.gauge("request.queue_depth", 12, tags: tags)
DataDog.distribution("connections", 123, tags: tags)
DataDog.histogram("request.file_size", 1034, tags: tags)
DataDog.timing("request.duration", 34, tags: tags)
DataDog.increment("request.count_total", tags: tags)
DataDog.decrement("request.count_total", tags: tags)
DataDog.count("request.count_total", 2, tags: tags)
end
end
```
"""
@typedoc ~S"""
Metric key.
"""
@type key :: iodata
@typedoc ~S"""
Possible metric values.
"""
@type value :: integer | float | String.t()
@typedoc ~S"""
Supported metric types.
"""
@type type :: :counter | :gauge | :histogram | :set | :timing | String.t()
@typedoc ~S"""
Metric tag value.
"""
@type tag ::
:atom
| String.Chars.t()
| {:system, String.t()}
| {:config, atom, atom}
@typedoc ~S"""
Metric tags.
"""
@type tags :: [tag | {tag, tag}]
@extensions %{
datadog: DataDaemon.Extensions.DataDog,
erlang_vm: DataDaemon.Extensions.VM
}
import DataDaemon.Util, only: [config: 5, package: 4]
@doc @moduledoc
defmacro __using__(opts \\ []) do
{otp_app, otp_config} =
case Keyword.fetch(opts, :otp_app) do
{:ok, app} -> {app, Application.get_env(app, __CALLER__.module, [])}
_ -> {:data_daemon, []}
end
config = fn setting, default -> config(opts, otp_app, __CALLER__.module, setting, default) end
namespace = if ns = config.(:namespace, nil), do: String.replace(ns, ~r/^(.*?)\.*$/, "\\1.")
decorators = if config.(:decorators, true), do: __MODULE__.Decorators.enable()
plug = if config.(:plug, true) && Code.ensure_loaded?(Plug), do: __MODULE__.Plug.enable()
tags = config.(:tags, [])
extensions =
case config.(:extensions, []) do
nil -> []
extensions when is_list(extensions) -> Enum.map(extensions, &(@extensions[&1] || &1))
extension -> [@extensions[extension] || extension]
end
extension_imports =
Enum.reduce(
extensions,
nil,
"e location: :keep do
unquote(&2)
use unquote(&1), unquote(opts)
end
)
hound_config = Keyword.merge(opts[:hound] || [], otp_config[:hound] || [])
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
quote location: :keep do
@opts unquote(opts |> Keyword.merge(otp_config) |> Keyword.put(:hound, hound_config))
@spec config(setting :: atom, default :: term) :: term
defp config(setting, default) do
import DataDaemon.Util, only: [config: 5]
config(@opts, unquote(otp_app), __MODULE__, setting, default)
end
@doc false
@spec otp :: atom
def otp, do: unquote(otp_app)
@doc false
@spec child_spec(opts :: Keyword.t()) :: map
def child_spec(opts \\ []), do: DataDaemon.child_spec(__MODULE__, opts)
@doc """
Start the DataDaemon.
## Example
```elixir
iex> {:error, {:already_started, pid}} = #{inspect(__MODULE__)}.start_link
iex> is_pid(pid)
true
```
"""
@spec start_link(opts :: Keyword.t()) :: Supervisor.on_start()
def start_link(opts \\ []) do
options =
@opts
|> Keyword.merge(Application.get_env(otp(), __MODULE__, []))
|> Keyword.merge(opts)
driver =
case config(:mode, :send) do
:send -> DataDaemon
:log -> DataDaemon.LogDaemon
:test -> DataDaemon.TestDaemon
end
Code.compiler_options(ignore_module_conflict: true)
Code.compile_quoted(
quote do
defmodule unquote(__MODULE__.Driver) do
@moduledoc false
@doc false
def send_metric(key, value, type, opts) do
unquote(driver).metric(unquote(__MODULE__.Sender), key, value, type, opts)
end
end
end
)
Code.compiler_options(ignore_module_conflict: false)
children =
Enum.reduce(unquote(extensions), [], fn ext, acc ->
if child = ext.child_spec(__MODULE__, options) do
[child | acc]
else
acc
end
end)
with started = {:ok, _} <-
driver.start_link(__MODULE__, [{:children, children} | options]) do
Enum.each(unquote(extensions), & &1.init(__MODULE__, options))
started
end
end
### Extensions / Plugs ###
unquote(extension_imports)
unquote(decorators)
unquote(plug)
### Methods ###
@doc """
Increment a COUNT metric by an arbitrary value.
The given metric key will commonly be treated as a rate.
## Example
```elixir
iex> #{inspect(__MODULE__)}.count("request.count_total", 2)
:ok
iex> #{inspect(__MODULE__)}.count("request.count_total", 2, zone: "us-east-1a")
:ok
```
"""
@spec count(DataDaemon.key(), integer, Keyword.t()) :: :ok | {:error, atom}
def count(key, value, opts \\ []), do: metric(key, value, :counter, opts)
@doc """
Increment a COUNT metric.
The given metric key will commonly be treated as a rate.
It is possible to pass a custom increment value,
which makes it an alias for count.
## Example
```elixir
iex> #{inspect(__MODULE__)}.increment("request.count_total")
:ok
iex> #{inspect(__MODULE__)}.increment("request.count_total", zone: "us-east-1a")
:ok
```
"""
@spec increment(DataDaemon.key(), integer | Keyword.t(), Keyword.t()) ::
:ok | {:error, atom}
def increment(key, value \\ 1, opts \\ [])
def increment(key, value, opts) when is_number(value),
do: metric(key, value, :counter, opts)
def increment(key, opts, []) when is_list(opts), do: metric(key, 1, :counter, opts)
@doc """
Decrement a COUNT metric.
The given metric key will commonly be treated as a rate.
It is possible to pass a custom decrement value,
which makes it an alias for count with a negative value.
## Example
```elixir
iex> #{inspect(__MODULE__)}.decrement("request.count_total")
:ok
iex> #{inspect(__MODULE__)}.decrement("request.count_total", zone: "us-east-1a")
:ok
```
"""
@spec decrement(DataDaemon.key(), integer | Keyword.t(), Keyword.t()) ::
:ok | {:error, atom}
def decrement(key, value \\ -1, opts \\ [])
def decrement(key, value, opts) when is_number(value),
do: metric(key, -value, :counter, opts)
def decrement(key, opts, []) when is_list(opts), do: metric(key, -1, :counter, opts)
@doc """
Gauge measures the value of a metric key at a particular time.
## Example
```elixir
iex> #{inspect(__MODULE__)}.gauge("request.queue_depth", 12)
:ok
iex> #{inspect(__MODULE__)}.gauge("request.queue_depth", 12, zone: "us-east-1a")
:ok
```
"""
@spec gauge(DataDaemon.key(), integer, Keyword.t()) :: :ok | {:error, atom}
def gauge(key, value, opts \\ []), do: metric(key, value, :gauge, opts)
@doc """
Histogram tracks the statistical distribution of a set of values on each host.
The value for the given metric keys needs to be an integer, but can be negative.
## Example
```elixir
iex> #{inspect(__MODULE__)}.histogram("request.file_size", 1034)
:ok
iex> #{inspect(__MODULE__)}.histogram("request.file_size", 1034, zone: "us-east-1a")
:ok
```
"""
@spec histogram(DataDaemon.key(), integer, Keyword.t()) :: :ok | {:error, atom}
def histogram(key, value, opts \\ []), do: metric(key, value, :histogram, opts)
@doc """
Set counts the number of unique elements in a group.
The value for the given metric key can be any string.
## Example
```elixir
iex> #{inspect(__MODULE__)}.set("unique.users", "bob")
:ok
iex> #{inspect(__MODULE__)}.set("unique.users", "bob", zone: "us-east-1a")
:ok
```
"""
@spec set(DataDaemon.key(), String.t(), Keyword.t()) :: :ok | {:error, atom}
def set(key, value, opts \\ []), do: metric(key, value, :set, opts)
@doc """
Measure timing data.
The value for the given metric key needs to represent a positive amount
of time spend.
## Example
```elixir
iex> #{inspect(__MODULE__)}.timing("request.duration", 34)
:ok
iex> #{inspect(__MODULE__)}.timing("request.duration", 34, zone: "us-east-1a")
:ok
```
"""
@spec timing(DataDaemon.key(), pos_integer, Keyword.t()) :: :ok | {:error, atom}
def timing(key, value, opts \\ []), do: metric(key, value, :timing, opts)
@doc """
Record any custom metric.
The value for the given metric key can be any string or integer,
while the custom type can be represented by a string.
The standard options apply.
## Example
Custom [d]istrubtion type:
```elixir
iex> #{inspect(__MODULE__)}.metric("connections", 123, "d")
:ok
iex> #{inspect(__MODULE__)}.metric("connections", 123, "d", zone: "us-east-1a")
:ok
```
"""
@spec metric(DataDaemon.key(), DataDaemon.value(), DataDaemon.type(), Keyword.t()) ::
:ok | {:error, atom}
def metric(key, value, type, opts \\ []) do
__MODULE__.Driver.send_metric(
unquote(if namespace, do: quote(do: [unquote(namespace), key]), else: quote(do: key)),
value,
type,
unquote(
if tags == [],
do: quote(do: opts),
else: quote(do: Keyword.update(opts, :tags, unquote(tags), &(unquote(tags) ++ &1)))
)
)
end
defmodule Driver do
@moduledoc false
@doc false
def send_metric(_, _, _, _), do: Enum.random([{:error, :not_started}])
end
defmodule Sender do
@moduledoc false
@doc false
def send(_), do: Enum.random([{:error, :not_started}])
end
end
end
### Connection Logic ###
@doc false
@spec child_spec(module, opts :: Keyword.t()) :: map
def child_spec(module, opts \\ []) do
%{
id: module,
start: {module, :start_link, [opts]}
}
end
alias DataDaemon.Resolver
@doc false
@spec start_link(module, Keyword.t()) :: Supervisor.on_start()
def start_link(module, opts \\ []) do
children = [Resolver.child_spec(module, opts) | Keyword.get(opts, :children, [])]
opts = [strategy: :one_for_one, name: Module.concat(module, Supervisor)]
Supervisor.start_link(children, opts)
end
@doc false
@spec metric(module, DataDaemon.key(), DataDaemon.value(), DataDaemon.type(), Keyword.t()) ::
:ok | {:error, atom}
def metric(reporter, key, value, type, opts \\ []) do
reporter.send(package(key, value, type, opts))
end
end