diff --git a/lib/custom_functions.ex b/lib/custom_functions.ex index 66aec4b..2df177d 100644 --- a/lib/custom_functions.ex +++ b/lib/custom_functions.ex @@ -86,6 +86,11 @@ defmodule Instruments.CustomFunctions do def measure(key, options \\ [], func) do Instruments.measure([unquote(prefix_with_dot), key], options, func) end + + @doc false + def send_service_check(key, status, options \\ []) do + Instruments.send_service_check([unquote(prefix_with_dot), key], status, options) + end end end end diff --git a/lib/instruments.ex b/lib/instruments.ex index 9f3e9b5..c472a33 100644 --- a/lib/instruments.ex +++ b/lib/instruments.ex @@ -231,6 +231,79 @@ defmodule Instruments do end end + @doc """ + Sends a service check to DataDog + + Reports the health status of a service. Status must be one of: + `:ok` (0), `:warning` (1), `:critical` (2), or `:unknown` (3). + + ## Options + + * `tags` - A list of String tags + * `message` - A description of the current status + * `hostname` - The hostname to associate with the check + * `timestamp` - A Unix timestamp for the check + + ## Examples + + Instruments.send_service_check("my.service", :ok) + Instruments.send_service_check("my.service", :critical, + tags: ["env:prod"], + message: "connection refused", + hostname: "web-01", + timestamp: 1234567890 + ) + + """ + defmacro send_service_check(name_ast, status, opts \\ []) do + name_iodata = MacroHelpers.to_iolist(name_ast, __CALLER__) + + quote do + status_code = + case unquote(status) do + :ok -> "0" + :warning -> "1" + :critical -> "2" + :unknown -> "3" + end + + header = ["_sc", "|", unquote(name_iodata), "|", status_code] + + opts = unquote(opts) + + message = + Enum.reduce([:timestamp, :hostname, :tags, :message], header, fn + :timestamp, acc -> + case Keyword.get(opts, :timestamp) do + nil -> acc + ts -> [acc, "|d:", Integer.to_string(ts)] + end + + :hostname, acc -> + case Keyword.get(opts, :hostname) do + nil -> acc + h -> [acc, "|h:", h] + end + + :tags, acc -> + case Keyword.get(opts, :tags) do + nil -> acc + tag_list -> [acc, "|#", Enum.intersperse(tag_list, ",")] + end + + :message, acc -> + case Keyword.get(opts, :message) do + nil -> acc + m -> [acc, "|m:", m] + end + end) + + unquote(@metrics_module) + |> Process.whereis() + |> :gen_udp.send(Instruments.statsd_host(), Instruments.statsd_port(), message) + end + end + @doc false def flush_all_probes(wait_for_flush \\ true, flush_timeout_ms \\ 10_000) do Probe.Supervisor diff --git a/lib/macro_helpers.ex b/lib/macro_helpers.ex index ff8d748..cbee31d 100644 --- a/lib/macro_helpers.ex +++ b/lib/macro_helpers.ex @@ -3,7 +3,7 @@ defmodule Instruments.MacroHelpers do alias Instruments.RateTracker - @safe_metric_types [:increment, :decrement, :gauge, :event, :set, :distribution] + @safe_metric_types [:increment, :decrement, :gauge, :event, :set, :distribution, :service_check] @metrics_module Application.get_env(:instruments, :reporter_module, Instruments.Statix) @@ -27,6 +27,15 @@ defmodule Instruments.MacroHelpers do end end + @spec to_iolist( + atom() + | bitstring() + | maybe_improper_list() + | number() + | {any(), any()} + | {atom() | {any(), list(), atom() | list()}, keyword(), atom() | list()}, + any() + ) :: bitstring() | maybe_improper_list() @doc """ Transforms metric keys into iolists. A metric key can be: diff --git a/test/custom_functions_test.exs b/test/custom_functions_test.exs index 04ee7ce..bb6a02d 100644 --- a/test/custom_functions_test.exs +++ b/test/custom_functions_test.exs @@ -91,6 +91,14 @@ defmodule Instruments.CustomFunctionsTest do assert_metric_reported(:timing, "custom.my.measure", 10..11, tags: ["timing:short"]) end + + test "to send_service_check calls" do + Custom.send_service_check("my.check", :ok) + assert_metric_reported(:service_check, "custom.my.check", :ok) + + Custom.send_service_check("my.check", :critical, tags: ["env:prod"]) + assert_metric_reported(:service_check, "custom.my.check", :critical, tags: ["env:prod"]) + end end test "setting a runtime prefix" do diff --git a/test/instruments_test.exs b/test/instruments_test.exs index a1af164..290dcf1 100644 --- a/test/instruments_test.exs +++ b/test/instruments_test.exs @@ -149,6 +149,36 @@ defmodule InstrumentsTest do assert_metric_reported(:event, "my_title", "my text", tags: ["host:any", "another:tag"]) end + test "sending service checks" do + Instruments.send_service_check("my.service", :ok) + assert_metric_reported(:service_check, "my.service", :ok) + + Instruments.send_service_check("my.service", :warning) + assert_metric_reported(:service_check, "my.service", :warning) + + Instruments.send_service_check("my.service", :critical) + assert_metric_reported(:service_check, "my.service", :critical) + + Instruments.send_service_check("my.service", :unknown) + assert_metric_reported(:service_check, "my.service", :unknown) + end + + test "sending service checks with all options" do + Instruments.send_service_check("my.service", :critical, + timestamp: 1_234_567_890, + hostname: "web-01", + tags: ["env:prod"], + message: "connection refused" + ) + + assert_metric_reported(:service_check, "my.service", :critical, + timestamp: 1_234_567_890, + hostname: "web-01", + tags: ["env:prod"], + message: "connection refused" + ) + end + test "sending events with a title that's a variable blows up" do quoted = quote do diff --git a/test/support/fake_statsd.ex b/test/support/fake_statsd.ex index 422e01b..5cfc73a 100644 --- a/test/support/fake_statsd.ex +++ b/test/support/fake_statsd.ex @@ -28,6 +28,19 @@ defmodule FakeStatsd do |> do_decode end + defp do_decode(["_sc", name, status | rest]) do + status_atom = + case status do + "0" -> :ok + "1" -> :warning + "2" -> :critical + "3" -> :unknown + end + + opts = decode_service_check_metadata(rest) + {:service_check, name, status_atom, opts} + end + defp do_decode([name_and_val, type | rest]) do opts = decode_tags_and_sampling(rest) {name, val} = decode_name_and_value(name_and_val) @@ -113,4 +126,27 @@ defmodule FakeStatsd do end end end + + defp decode_service_check_metadata(fields), + do: decode_service_check_metadata(fields, []) + + defp decode_service_check_metadata([], accum), do: Enum.reverse(accum) + + defp decode_service_check_metadata([<<"d:", ts::binary>> | rest], accum) do + {timestamp, ""} = Integer.parse(ts) + decode_service_check_metadata(rest, Keyword.put(accum, :timestamp, timestamp)) + end + + defp decode_service_check_metadata([<<"h:", hostname::binary>> | rest], accum) do + decode_service_check_metadata(rest, Keyword.put(accum, :hostname, hostname)) + end + + defp decode_service_check_metadata([<<"#", tags::binary>> | rest], accum) do + tag_list = String.split(tags, ",") + decode_service_check_metadata(rest, Keyword.put(accum, :tags, tag_list)) + end + + defp decode_service_check_metadata([<<"m:", message::binary>> | rest], accum) do + decode_service_check_metadata(rest, Keyword.put(accum, :message, message)) + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 0bbda98..c760d2b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,7 +1,7 @@ ExUnit.start() defmodule MetricsAssertions do - @safe_metric_types [:increment, :decrement, :gauge, :event, :set, :distribution] + @safe_metric_types [:increment, :decrement, :gauge, :event, :set, :distribution, :service_check] use ExUnit.Case def assert_metric_reported(metric_type, metric_name) do