Repository: beam-telemetry/telemetry_metrics_statsd (elixir)

Issue:
Is IPv6 supported?
It seems like the `opts` given to `:gen_udp.open/2` maybe needs to have `:inet6` given to enable support for IPv6.   If so, and if I'm reading the [UDP module](https://github.com/beam-telemetry/telemetry_metrics_statsd/blob/6f0df1e21fbb68df05f8040f9d85c0e1c0a05c47/lib/telemetry_metrics_statsd/udp.ex) right, that's not currently possible.

Does that all seem right to you?  I could potentially make a PR for this, but I wanted to sanity check first 😄 

Pull request description:
Allow for inet6 option
Fixes #78

I'm using this to be able to send metrics over IPv6 on fly.io through WireGuard

Patch:
diff --git a/lib/telemetry_metrics_statsd.ex b/lib/telemetry_metrics_statsd.ex
index 0b56485..5ed7327 100644
--- a/lib/telemetry_metrics_statsd.ex
+++ b/lib/telemetry_metrics_statsd.ex
@@ -532,20 +532,34 @@ defmodule TelemetryMetricsStatsd do
     %{state | udp_config: %{udp_config | host: new_address}}
   end
 
-  defp configure_host_resolution(%{host: host, port: port}) when is_tuple(host) do
-    %{host: host, port: port}
+  defp configure_host_resolution(%{
+         host: host,
+         port: port,
+         inet_address_family: inet_address_family
+       })
+       when is_tuple(host) do
+    %{host: host, port: port, inet_address_family: inet_address_family}
   end
 
-  defp configure_host_resolution(%{host: host, port: port, host_resolution_interval: interval})
+  defp configure_host_resolution(%{
+         host: host,
+         port: port,
+         inet_address_family: inet_address_family,
+         host_resolution_interval: interval
+       })
        when is_integer(interval) do
-    {:ok, hostent(h_addr_list: [ip | _ips])} = :inet.gethostbyname(host)
+    {:ok, hostent(h_addr_list: [ip | _ips])} = :inet.gethostbyname(host, inet_address_family)
     Process.send_after(self(), :resolve_host, interval)
-    %{host: ip, port: port}
+    %{host: ip, port: port, inet_address_family: inet_address_family}
   end
 
-  defp configure_host_resolution(%{host: host, port: port}) do
-    {:ok, hostent(h_addr_list: [ip | _ips])} = :inet.gethostbyname(host)
-    %{host: ip, port: port}
+  defp configure_host_resolution(%{
+         host: host,
+         port: port,
+         inet_address_family: inet_address_family
+       }) do
+    {:ok, hostent(h_addr_list: [ip | _ips])} = :inet.gethostbyname(host, inet_address_family)
+    %{host: ip, port: port, inet_address_family: inet_address_family}
   end
 
   defp update_pool(pool_id, new_host, new_port) do
diff --git a/lib/telemetry_metrics_statsd/options.ex b/lib/telemetry_metrics_statsd/options.ex
index c547b70..5039132 100644
--- a/lib/telemetry_metrics_statsd/options.ex
+++ b/lib/telemetry_metrics_statsd/options.ex
@@ -21,6 +21,11 @@ defmodule TelemetryMetricsStatsd.Options do
       default: 8125,
       doc: "Port of the StatsD server."
     ],
+    inet_address_family: [
+      type: {:in, [:inet, :inet6, :local]},
+      default: :inet,
+      doc: "The inet address family, as specified by the Erlang `:inet.address_family type()`."
+    ],
     socket_path: [
       type: {:custom, __MODULE__, :socket_path, []},
       doc: "Path to the Unix Domain Socket used for publishing instead of the hostname and port."
diff --git a/lib/telemetry_metrics_statsd/udp.ex b/lib/telemetry_metrics_statsd/udp.ex
index bd7ac7b..31198f8 100644
--- a/lib/telemetry_metrics_statsd/udp.ex
+++ b/lib/telemetry_metrics_statsd/udp.ex
@@ -11,22 +11,21 @@ defmodule TelemetryMetricsStatsd.UDP do
 
   @type config :: %{
           :host => :inet.hostname() | :inet.ip_address() | :inet.local_address(),
-          optional(:port) => :inet.port_number()
+          optional(:port) => :inet.port_number(),
+          optional(:inet_address_family) => boolean()
         }
 
   @spec open(config()) ::
           {:ok, t()} | {:error, reason :: term()}
   def open(config) do
-    opts = [active: false]
+    opts = [{:active, false}]
 
     opts =
-      case config.host do
-        {:local, _} ->
-          [:local | opts]
-
-        _ ->
-          opts
-      end
+      Enum.reduce(config, opts, fn
+        {:host, {:local, _}}, opts -> [:local | opts]
+        {:inet_address_family, value}, opts -> [value | opts]
+        {_key, _value}, opts -> opts
+      end)
 
     case :gen_udp.open(0, opts) do
       {:ok, socket} ->


Test patch:
diff --git a/test/telemetry_metrics_statsd_test.exs b/test/telemetry_metrics_statsd_test.exs
index fa68cce..40bd74c 100644
--- a/test/telemetry_metrics_statsd_test.exs
+++ b/test/telemetry_metrics_statsd_test.exs
@@ -609,6 +609,17 @@ defmodule TelemetryMetricsStatsdTest do
     assert_reported(socket, "http.requests:1|c")
   end
 
+  test "IPv6 end-to-end test" do
+    {socket, port} = given_udp_port_opened(:inet6)
+    counter = given_counter("http.requests", event_name: "http.request")
+
+    start_reporter(host: "::1", inet_address_family: :inet6, metrics: [counter], port: port)
+
+    :telemetry.execute([:http, :request], %{latency: 211})
+
+    assert_reported(socket, "http.requests:1|c")
+  end
+
   describe "hostname resolution" do
     test "is performed on start by default" do
       counter = given_counter("http.request.count")
@@ -625,6 +636,22 @@ defmodule TelemetryMetricsStatsdTest do
       assert udp.host == {127, 0, 0, 1}
     end
 
+    test "Supports IPv6" do
+      counter = given_counter("http.request.count")
+
+      reporter =
+        start_reporter(
+          host: "::1",
+          inet_address_family: :inet6,
+          metrics: [counter]
+        )
+
+      pool_id = TelemetryMetricsStatsd.get_pool_id(reporter)
+
+      {:ok, udp} = TelemetryMetricsStatsd.get_udp(pool_id)
+      assert udp.host == {0, 0, 0, 0, 0, 0, 0, 1}
+    end
+
     test "is not periodically repeated by default" do
       counter = given_counter("http.request.count")
 
@@ -678,8 +705,8 @@ defmodule TelemetryMetricsStatsdTest do
     end
   end
 
-  defp given_udp_port_opened() do
-    {:ok, socket} = :gen_udp.open(0, [:binary, active: false])
+  defp given_udp_port_opened(inet_address_family \\ :inet) do
+    {:ok, socket} = :gen_udp.open(0, [:binary, inet_address_family, active: false])
     {:ok, port} = :inet.port(socket)
     {socket, port}
   end