Packages

HTTP endpoint monitor, with its dashboard panel bundled in the same package as a separate module (Integrations.Http.Display, #299) — one install, both halves; a release without raven_web simply runs the monitor headless.

Current section

Files

Jump to
raven_integration_http lib integrations http.ex
Raw

lib/integrations/http.ex

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