diff --git a/README.md b/README.md index c653713..ee65f66 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ This Discord fork of Statix exists for [discord/instruments](https://github.com/ - [OTP26 compability via Port.command replacement](https://github.com/cabify/statix/pull/1) - Name change to discord_statix +- Unix Domain Socket support +- Support for Datadog events Statix is an Elixir client for StatsD-compatible servers. It is focused on speed without sacrificing simplicity, completeness, or correctness. diff --git a/lib/statix.ex b/lib/statix.ex index ea286ca..071eab9 100644 --- a/lib/statix.ex +++ b/lib/statix.ex @@ -252,6 +252,26 @@ defmodule Statix do """ @callback set(key, value :: String.Chars.t()) :: on_send + @doc """ + Sends a DataDog event. + + This uses the DogStatsD event format (`_e{title_len,text_len}:title|text`), + which is a DataDog-specific extension to the StatsD protocol. Standard StatsD + servers do not support events. + + ## Examples + + iex> MyApp.Statix.send_event("deploy", "v1.2.3", tags: ["env:prod"]) + :ok + + """ + @callback send_event(title :: iodata, text :: iodata, options) :: on_send + + @doc """ + Same as `send_event(title, text, [])`. + """ + @callback send_event(title :: iodata, text :: iodata) :: on_send + @doc """ Measures the execution time of the given `function` and writes that to the StatsD timing identified by `key`. @@ -322,23 +342,23 @@ defmodule Statix do unquote(current_statix) def increment(key, val \\ 1, options \\ []) when is_number(val) do - Statix.transmit(current_statix(), :counter, key, val, options) + Statix.transmit_metric(current_statix(), :counter, key, val, options) end def decrement(key, val \\ 1, options \\ []) when is_number(val) do - Statix.transmit(current_statix(), :counter, key, [?-, to_string(val)], options) + Statix.transmit_metric(current_statix(), :counter, key, [?-, to_string(val)], options) end def gauge(key, val, options \\ []) do - Statix.transmit(current_statix(), :gauge, key, val, options) + Statix.transmit_metric(current_statix(), :gauge, key, val, options) end def histogram(key, val, options \\ []) do - Statix.transmit(current_statix(), :histogram, key, val, options) + Statix.transmit_metric(current_statix(), :histogram, key, val, options) end def timing(key, val, options \\ []) do - Statix.transmit(current_statix(), :timing, key, val, options) + Statix.transmit_metric(current_statix(), :timing, key, val, options) end def measure(key, options \\ [], fun) when is_function(fun, 0) do @@ -350,7 +370,11 @@ defmodule Statix do end def set(key, val, options \\ []) do - Statix.transmit(current_statix(), :set, key, val, options) + Statix.transmit_metric(current_statix(), :set, key, val, options) + end + + def send_event(title, text, options \\ []) do + Statix.transmit_event(current_statix(), title, text, options) end defoverridable( @@ -360,7 +384,8 @@ defmodule Statix do histogram: 3, timing: 3, measure: 3, - set: 3 + set: 3, + send_event: 3 ) end end @@ -409,54 +434,55 @@ defmodule Statix do end @doc false - def transmit( - %{conn: %{transport: :uds, socket_path: path}, tags: tags}, - type, - key, - value, - options - ) + def transmit_metric(statix, type, key, value, options) when (is_binary(key) or is_list(key)) and is_list(options) do if should_send?(options) do - options = put_global_tags(options, tags) - - case Statix.ConnTracker.get(path) do - {:ok, conn} -> - case Conn.transmit(conn, type, key, to_string(value), options) do - :ok -> - :ok - - {:error, _reason} = error -> - Statix.ConnTracker.report_send_error(path) - error - end - - {:error, :not_found} -> - {:error, :socket_not_found} - end + do_transmit(statix, options, fn conn, opts -> + Conn.transmit_metric(conn, type, key, to_string(value), opts) + end) else :ok end end - def transmit( - %{conn: conn, pool: pool, tags: tags}, - type, - key, - value, - options - ) - when (is_binary(key) or is_list(key)) and is_list(options) do - if should_send?(options) do - options = put_global_tags(options, tags) + @doc false + def transmit_event(statix, title, text, options) + when is_list(options) do + do_transmit(statix, options, fn conn, opts -> + Conn.transmit_event(conn, title, text, opts) + end) + end - %{conn | sock: pick_name(pool)} - |> Conn.transmit(type, key, to_string(value), options) - else - :ok + defp do_transmit( + %{conn: %{transport: :uds, socket_path: path}, tags: tags}, + options, + send_fn + ) do + options = put_global_tags(options, tags) + + case Statix.ConnTracker.get(path) do + {:ok, conn} -> + case send_fn.(conn, options) do + :ok -> + :ok + + {:error, _reason} = error -> + Statix.ConnTracker.report_send_error(path) + error + end + + {:error, :not_found} -> + {:error, :socket_not_found} end end + defp do_transmit(%{conn: conn, pool: pool, tags: tags}, options, send_fn) do + options = put_global_tags(options, tags) + conn = %{conn | sock: pick_name(pool)} + + send_fn.(conn, options) + end + defp should_send?([]), do: true defp should_send?([{:sample_rate, rate} | _]), do: rate >= :rand.uniform() defp should_send?([_ | rest]), do: should_send?(rest) diff --git a/lib/statix/conn.ex b/lib/statix/conn.ex index b7310fe..b883802 100644 --- a/lib/statix/conn.ex +++ b/lib/statix/conn.ex @@ -70,11 +70,27 @@ defmodule Statix.Conn do end end - def transmit(%__MODULE__{sock: sock, prefix: prefix} = conn, type, key, val, options) + def transmit_event(%__MODULE__{sock: sock} = conn, title, text, options) + when is_list(options) do + result = + Packet.build_event(title, text, options) + |> transmit(conn) + + with {:error, error} <- result do + Logger.error(fn -> + if(is_atom(sock), do: "", else: "Statix ") <> + "#{inspect(sock)} event \"#{title}\" lost, error=#{inspect(error)}" + end) + end + + result + end + + def transmit_metric(%__MODULE__{sock: sock, prefix: prefix} = conn, type, key, val, options) when is_binary(val) and is_list(options) do result = prefix - |> Packet.build(type, key, val, options) + |> Packet.build_metric(type, key, val, options) |> transmit(conn) with {:error, error} <- result do diff --git a/lib/statix/packet.ex b/lib/statix/packet.ex index 53e657a..fe6daaf 100644 --- a/lib/statix/packet.ex +++ b/lib/statix/packet.ex @@ -1,12 +1,36 @@ defmodule Statix.Packet do @moduledoc false - def build(prefix, name, key, val, options) do + def build_metric(prefix, name, key, val, options) do [prefix, key, ?:, val, ?|, metric_type(name)] |> set_option(:sample_rate, options[:sample_rate]) |> set_option(:tags, options[:tags]) end + @doc """ + Builds a DataDog event packet. + + Uses the DogStatsD event format (`_e{title_len,text_len}:title|text`), + which is a DataDog-specific extension to the StatsD protocol. Standard StatsD + servers do not support events. + """ + def build_event(title, text, options) do + title_bin = IO.iodata_to_binary(title) + text_bin = IO.iodata_to_binary(text) + + [ + "_e{", + Integer.to_string(byte_size(title_bin)), + ",", + Integer.to_string(byte_size(text_bin)), + "}:", + title_bin, + "|", + text_bin + ] + |> set_option(:tags, options[:tags]) + end + metrics = %{ counter: "c", gauge: "g", diff --git a/test/statix/uds_test.exs b/test/statix/uds_test.exs index a3f93f6..848e9da 100644 --- a/test/statix/uds_test.exs +++ b/test/statix/uds_test.exs @@ -81,6 +81,14 @@ defmodule Statix.UDSTest do assert_receive {:test_server, _, "sample:3|ms|#foo:bar,baz"} end + test "send_event via UDS", _context do + TestStatix.send_event("my_title", "my text") + assert_receive {:test_server, _, "_e{8,7}:my_title|my text"} + + TestStatix.send_event("deploy", "v1.2.3", tags: ["env:prod"]) + assert_receive {:test_server, _, "_e{6,6}:deploy|v1.2.3|#env:prod"} + end + test "set via UDS", _context do TestStatix.set("sample", "user1") assert_receive {:test_server, _, "sample:user1|s"} diff --git a/test/statix_test.exs b/test/statix_test.exs index 077f0d9..f8ed7fe 100644 --- a/test/statix_test.exs +++ b/test/statix_test.exs @@ -137,6 +137,14 @@ defmodule StatixTest do refute_received _any end + test "send_event/2,3" do + __MODULE__.send_event("my_title", "my text") + assert_receive {:test_server, _, "_e{8,7}:my_title|my text"} + + send_event("deploy", "v1.2.3", tags: ["env:prod"]) + assert_receive {:test_server, _, "_e{6,6}:deploy|v1.2.3|#env:prod"} + end + test "set/2,3" do __MODULE__.set(["sample"], 2) assert_receive {:test_server, _, "sample:2|s"}