diff --git a/lib/music_library_web/telemetry.ex b/lib/music_library_web/telemetry.ex index 28132cf7..0becd670 100644 --- a/lib/music_library_web/telemetry.ex +++ b/lib/music_library_web/telemetry.ex @@ -69,6 +69,16 @@ defmodule MusicLibraryWeb.Telemetry do ] ), + # Rate Limiter + summary("req.rate_limiter.throttle.sleep_ms", + unit: :millisecond, + description: "Time spent waiting for rate limit cooldown", + tags: [:name], + reporter_options: [ + nav: "External APIs" + ] + ), + # Assets summary("music_library.assets.cache_size", unit: {:byte, :kilobyte}, diff --git a/lib/req/rate_limiter.ex b/lib/req/rate_limiter.ex index 06a56948..ff336e93 100644 --- a/lib/req/rate_limiter.ex +++ b/lib/req/rate_limiter.ex @@ -56,6 +56,12 @@ defmodule Req.RateLimiter do remaining = cooldown - elapsed if remaining > 0 do + :telemetry.execute( + [:req, :rate_limiter, :throttle], + %{sleep_ms: remaining}, + %{name: name} + ) + Process.sleep(remaining) end diff --git a/test/req/rate_limiter_test.exs b/test/req/rate_limiter_test.exs index 72be2745..a3c4826e 100644 --- a/test/req/rate_limiter_test.exs +++ b/test/req/rate_limiter_test.exs @@ -93,6 +93,56 @@ defmodule Req.RateLimiterTest do assert elapsed < 200 end + test "emits telemetry event when throttling" do + name = :"test_telemetry_#{System.unique_integer([:positive])}" + cooldown = 100 + + adapter = fn request -> + {request, Req.Response.new(status: 200, body: "ok")} + end + + request = + Req.new(url: "https://example.com", adapter: adapter) + |> RateLimiter.attach(name: name, cooldown: cooldown) + + ref = + :telemetry_test.attach_event_handlers(self(), [ + [:req, :rate_limiter, :throttle] + ]) + + {:ok, _} = Req.get(request) + {:ok, _} = Req.get(request) + + assert_received {[:req, :rate_limiter, :throttle], ^ref, %{sleep_ms: sleep_ms}, + %{name: ^name}} + + assert sleep_ms > 0 + end + + test "does not emit telemetry event when no throttling needed" do + name = :"test_no_telemetry_#{System.unique_integer([:positive])}" + cooldown = 50 + + adapter = fn request -> + {request, Req.Response.new(status: 200, body: "ok")} + end + + request = + Req.new(url: "https://example.com", adapter: adapter) + |> RateLimiter.attach(name: name, cooldown: cooldown) + + ref = + :telemetry_test.attach_event_handlers(self(), [ + [:req, :rate_limiter, :throttle] + ]) + + {:ok, _} = Req.get(request) + Process.sleep(cooldown + 10) + {:ok, _} = Req.get(request) + + refute_received {[:req, :rate_limiter, :throttle], ^ref, _, _} + end + test "different API names are tracked independently" do name_a = :"test_api_a_#{System.unique_integer([:positive])}" name_b = :"test_api_b_#{System.unique_integer([:positive])}"