defmodule Integrations.Http do @moduledoc """ HTTP endpoint monitor. Checks that a URL responds within an acceptable time and with an expected HTTP status code. Uses `Mint` for requests to collect connection phases (DNS, TCP, SSL handshakes, TTFB, and download times) and peer certificates. Collection only — see `Integrations.Http.Display` (same package) for the dashboard panel. `Display.BundledDefault` auto-hooks it whenever this monitor starts, same as a single-module package would; a release without `raven_web` simply never compiles the display half and runs this monitor headless. ## Params * `:url` — The URL to check. Required. * `:method` — The HTTP method (e.g., :get, :post). Defaults to :get. * `:timeout_ms` — Request timeout in milliseconds. Defaults to `10_000`. * `:tls_verify` — Whether to verify TLS certificate chain. Defaults to true. * `:headers` — Custom headers to send. * `:body` — Request body to send. * `:follow_redirects`— Whether to follow 3xx redirects. Defaults to false. * `:assertions` — List of assertion maps to evaluate. ## Example config %CodeNameRaven.Monitor.Config{ id: "example-web", name: "Example Website", module: Integrations.Http, params: %{url: "https://example.com"}, interval_ms: 60_000, enabled: true, tags: ["web", "external"] } """ use CodeNameRaven.Monitor @default_timeout_ms 10_000 @high_latency_ms 3_000 @impl true def params_template, do: %{url: "https://"} @impl true def params_schema do [ url: [type: :string, required: true, doc: "URL to check"], method: [type: {:in, [:get, :post, :put, :patch, :delete, :head]}, default: :get], timeout_ms: [type: :non_neg_integer, default: 10_000], tls_verify: [type: :boolean, default: true], headers: [type: {:list, :any}, default: [], doc: "List of %{name: _, value: _} maps"], assertions: [type: {:list, :any}, default: [], doc: "List of assertion maps"], body: [type: :string, required: false, doc: "Request body"], follow_redirects: [type: :any, default: false] ] end @impl true def target_uri(%{url: url}) when is_binary(url), do: {:ok, url} def target_uri(_params), do: :none @impl true def collect(%{url: _} = params, state) do method = Map.get(params, :method, :get) url = params.url timeout_ms = Map.get(params, :timeout_ms, @default_timeout_ms) tls_verify = Map.get(params, :tls_verify, true) headers = Map.get(params, :headers, []) body = Map.get(params, :body, nil) follow_redirects = Map.get(params, :follow_redirects, false) opts = [ timeout_ms: timeout_ms, tls_verify: tls_verify, headers: headers, body: body, follow_redirects: follow_redirects ] case CodeNameRaven.Protocol.Http.request(method, url, opts) do {:ok, raw_result} -> cert_expiry_days = if raw_result.cert_expiry_at do diff_seconds = DateTime.diff(raw_result.cert_expiry_at, DateTime.utc_now()) max(0, div(diff_seconds, 86400)) else nil end mapped_raw = %{ status_code: raw_result.status, latency_ms: raw_result.timing.total_ms, body: raw_result.body, headers: raw_result.headers, cert_expiry_days: cert_expiry_days, timing: %{ dns_ms: raw_result.timing.dns_ms, tcp_ms: raw_result.timing.tcp_ms, ssl_ms: raw_result.timing.ssl_ms, ttfb_ms: raw_result.timing.ttfb_ms, download_ms: raw_result.timing.download_ms }, redirects: raw_result.redirects } # Parse and evaluate assertions inline alias CodeNameRaven.Monitor.Assertion assertions = Assertion.parse(params[:assertions] || []) {assertion_status, failures} = Assertion.evaluate_all(assertions, mapped_raw) result = Map.merge(mapped_raw, %{ assertions: assertions, assertion_status: assertion_status, assertion_failures: failures }) {:ok, result, state} {:error, reason} -> {:error, format_error(reason), state} end end def collect(params, state), do: {:error, "missing required param :url — got: #{inspect(params)}", state} @impl true def healthy?(%{assertions: [_ | _], assertion_status: status}), do: status def healthy?(%{status_code: code, latency_ms: latency} = result) do # Fallback if no assertions configured expected = Map.get(result, :expected_status, 200) cond do not status_matches?(code, expected) -> :down latency >= @high_latency_ms -> :degraded true -> :up end end @impl true def metrics(result) do %{ latency_ms: result[:latency_ms], status_code: result[:status_code], cert_expiry_days: result[:cert_expiry_days], dns_ms: result[:timing][:dns_ms], tcp_ms: result[:timing][:tcp_ms], ssl_ms: result[:timing][:ssl_ms], ttfb_ms: result[:timing][:ttfb_ms], download_ms: result[:timing][:download_ms], redirects: result[:redirects] } |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Map.new() end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp status_matches?(code, expected) when is_integer(expected), do: code == expected defp status_matches?(code, %Range{} = range), do: code in range defp status_matches?(code, expected) when is_list(expected), do: code in expected defp format_error(%{reason: reason}), do: inspect(reason) defp format_error(reason) when is_atom(reason), do: to_string(reason) defp format_error(reason), do: inspect(reason) end