diff --git a/lib/custom_functions.ex b/lib/custom_functions.ex index 7496ab9..2f126ed 100644 --- a/lib/custom_functions.ex +++ b/lib/custom_functions.ex @@ -81,6 +81,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 efabbe7..d559314 100644 --- a/lib/instruments.ex +++ b/lib/instruments.ex @@ -214,6 +214,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 804f68f..3ad1aa8 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] + @safe_metric_types [:increment, :decrement, :gauge, :event, :set, :service_check] @metrics_module Application.get_env(:instruments, :reporter_module, Instruments.Statix) diff --git a/test/custom_functions_test.exs b/test/custom_functions_test.exs index ffd4844..1822b5f 100644 --- a/test/custom_functions_test.exs +++ b/test/custom_functions_test.exs @@ -83,6 +83,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 d86e73e..f6cb222 100644 --- a/test/instruments_test.exs +++ b/test/instruments_test.exs @@ -137,6 +137,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 bb4220b..2ad85b4 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) @@ -109,4 +122,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 4d759c6..6d4acb2 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] + @safe_metric_types [:increment, :decrement, :gauge, :event, :set, :service_check] use ExUnit.Case def assert_metric_reported(metric_type, metric_name) do