dev vouch dev on at. thats about it
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add http metrics

Luna 94f64c55 0e57b705

+144 -1
+2 -1
appview/config/test.exs
··· 3 3 config :atvouch, 4 4 port: 4057, 5 5 atvouch_did: "did:plc:test", 6 - atvouch_endpoint: "https://localhost:123" 6 + atvouch_endpoint: "https://localhost:123", 7 + metrics_token: "test-metrics-token" 7 8 8 9 config :atvouch, Atvouch.Repo, pool: Ecto.Adapters.SQL.Sandbox 9 10 config :atvouch, start_replicas: false
+2
appview/lib/atvouch/application.ex
··· 3 3 4 4 @impl true 5 5 def start(_type, _args) do 6 + Atvouch.Telemetry.setup() 7 + 6 8 port = Application.get_env(:atvouch, :port) 7 9 8 10 children =
+50
appview/lib/atvouch/telemetry.ex
··· 1 + defmodule Atvouch.Telemetry do 2 + @moduledoc false 3 + 4 + require Logger 5 + use Prometheus.Metric 6 + 7 + @handler_id "atvouch-bandit-metrics" 8 + 9 + def setup do 10 + Histogram.declare( 11 + name: :http_request_duration_seconds, 12 + help: "HTTP request duration in seconds", 13 + labels: [:method, :route, :status], 14 + buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 3, 4, 5, 6, 7, 8, 10, 15, 20], 15 + duration_unit: false 16 + ) 17 + 18 + Counter.declare( 19 + name: :http_requests_total, 20 + help: "Total number of HTTP requests", 21 + labels: [:method, :route, :status] 22 + ) 23 + 24 + :telemetry.attach( 25 + @handler_id, 26 + [:bandit, :request, :stop], 27 + &__MODULE__.handle_request_stop/4, 28 + nil 29 + ) 30 + end 31 + 32 + @doc false 33 + def handle_request_stop(_event, measurements, metadata, _config) do 34 + %{duration: duration} = measurements 35 + %{conn: conn} = metadata 36 + 37 + with %{plug_route: {route, _fun}} <- conn.private, 38 + false <- match?("/*" <> _, route) do 39 + # Strip the /*glob segment from forwarded routes (e.g. "/xrpc/*glob/dev.atvouch.alive") 40 + route = String.replace(route, "/*glob", "") 41 + 42 + duration_seconds = System.convert_time_unit(duration, :native, :microsecond) / 1_000_000 43 + labels = [conn.method, route, conn.status] 44 + Histogram.observe([name: :http_request_duration_seconds, labels: labels], duration_seconds) 45 + Counter.inc(name: :http_requests_total, labels: labels) 46 + else 47 + _ -> :ok 48 + end 49 + end 50 + end
+90
appview/test/atvouch/metrics_test.exs
··· 1 + defmodule Atvouch.MetricsTest do 2 + use ExUnit.Case, async: false 3 + 4 + @metrics_token "test-metrics-token" 5 + 6 + setup do 7 + # Start a real Bandit server on a random port 8 + {:ok, server} = Bandit.start_link(plug: Atvouch.Router, port: 0, ip: {127, 0, 0, 1}) 9 + {:ok, {_ip, port}} = ThousandIsland.listener_info(server) 10 + %{base_url: "http://127.0.0.1:#{port}", server: server} 11 + end 12 + 13 + test "GET /metrics requires authorization", %{base_url: base_url} do 14 + {:ok, resp} = http_get(base_url <> "/metrics") 15 + assert resp.status == 401 16 + end 17 + 18 + test "GET /metrics rejects invalid token", %{base_url: base_url} do 19 + {:ok, resp} = http_get(base_url <> "/metrics", [{"authorization", "Bearer wrong-token"}]) 20 + assert resp.status == 403 21 + end 22 + 23 + test "bandit telemetry flows into prometheus metrics", %{base_url: base_url} do 24 + # Make a real request through Bandit so telemetry fires naturally 25 + {:ok, resp} = http_get(base_url <> "/health") 26 + assert resp.status == 200 27 + 28 + # Now check that /metrics captured it 29 + {:ok, resp} = 30 + http_get(base_url <> "/metrics", [{"authorization", "Bearer #{@metrics_token}"}]) 31 + 32 + assert resp.status == 200 33 + 34 + assert resp.body =~ "http_request_duration_seconds" 35 + assert resp.body =~ "http_requests_total" 36 + assert resp.body =~ ~r/http_requests_total\{method="GET",route="\/health",status="200"\}/ 37 + end 38 + 39 + test "unmatched routes are not recorded in metrics", %{base_url: base_url} do 40 + {:ok, resp} = http_get(base_url <> "/nonexistent") 41 + assert resp.status == 404 42 + 43 + {:ok, resp} = 44 + http_get(base_url <> "/metrics", [{"authorization", "Bearer #{@metrics_token}"}]) 45 + 46 + refute resp.body =~ "/*_path" 47 + end 48 + 49 + defp http_get(url, headers \\ []) do 50 + uri = URI.parse(url) 51 + 52 + {:ok, conn} = Mint.HTTP.connect(:http, uri.host, uri.port) 53 + 54 + path = 55 + case uri.query do 56 + nil -> uri.path 57 + q -> "#{uri.path}?#{q}" 58 + end 59 + 60 + {:ok, conn, req_ref} = Mint.HTTP.request(conn, "GET", path, headers, nil) 61 + 62 + receive_response(conn, req_ref, %{status: nil, headers: [], body: ""}) 63 + end 64 + 65 + defp receive_response(conn, req_ref, acc) do 66 + receive do 67 + message -> 68 + {:ok, conn, responses} = Mint.HTTP.stream(conn, message) 69 + 70 + acc = 71 + Enum.reduce(responses, acc, fn 72 + {:status, ^req_ref, status}, acc -> %{acc | status: status} 73 + {:headers, ^req_ref, headers}, acc -> %{acc | headers: acc.headers ++ headers} 74 + {:data, ^req_ref, data}, acc -> %{acc | body: acc.body <> data} 75 + {:done, ^req_ref}, acc -> acc 76 + end) 77 + 78 + if Enum.any?(responses, &match?({:done, ^req_ref}, &1)) do 79 + Mint.HTTP.close(conn) 80 + {:ok, acc} 81 + else 82 + receive_response(conn, req_ref, acc) 83 + end 84 + after 85 + 5000 -> 86 + Mint.HTTP.close(conn) 87 + {:error, :timeout} 88 + end 89 + end 90 + end