defmodule Integrations.Tcp do @moduledoc """ TCP port connectivity monitor. Establishes a TCP connection to a host:port, records latency, then immediately closes the socket. Confirms that the port is open and accepting connections without performing any application-layer handshake. Uses raw `:gen_tcp` — no external dependencies required. Works for any TCP-speaking service: databases, message brokers, custom protocols, game servers, etc. Collection only — see `Integrations.Tcp.Display` (same package) for the dashboard panel. `Display.BundledDefault` auto-hooks it whenever this monitor starts. ## Params * `:host` — Hostname or IP address. Required. * `:port` — TCP port number. Required. * `:timeout_ms` — Connection timeout in milliseconds. Defaults to `5000`. * `:latency_degraded_ms` — Latency threshold for `:degraded` status. Defaults to `1000`. ## Health signal * `:up` — Connection established within timeout; latency below threshold. * `:degraded` — Connection established but latency exceeded threshold. * `:down` — Connection refused, host unreachable, timeout, or DNS failure. """ use CodeNameRaven.Monitor @default_timeout_ms 5_000 @default_lat_degraded_ms 1_000 @impl true def params_template do %{host: "", port: "", timeout_ms: "5000", latency_degraded_ms: "1000"} end @impl true def params_schema do [ host: [type: :string, required: true, doc: "Hostname or IP address"], port: [type: :non_neg_integer, required: true, doc: "TCP port number"], timeout_ms: [type: :non_neg_integer, default: 5_000, doc: "Connection timeout in milliseconds"], latency_degraded_ms: [type: :non_neg_integer, default: 1_000, doc: "Latency threshold for degraded status"] ] end @impl true def target_uri(params) do host = get_param(params, :host) port = get_param(params, :port) if host && port, do: {:ok, "tcp://#{host}:#{port}"}, else: :none end @impl true def identity_params(params) do %{ host: get_param(params, :host), port: parse_int(get_param(params, :port), nil) } end # --------------------------------------------------------------------------- # Collect # --------------------------------------------------------------------------- @impl true def collect(params, state) do host = get_param(params, :host) port = parse_int(get_param(params, :port), nil) cond do is_nil(host) -> {:error, "missing required param :host", state} is_nil(port) -> {:error, "missing required param :port", state} true -> timeout_ms = parse_int(get_param(params, :timeout_ms), @default_timeout_ms) tcp_opts = [:binary, active: false] t0 = System.monotonic_time(:millisecond) case :gen_tcp.connect(to_charlist(host), port, tcp_opts, timeout_ms) do {:ok, socket} -> latency_ms = System.monotonic_time(:millisecond) - t0 :gen_tcp.close(socket) {:ok, %{latency_ms: latency_ms, host: host, port: port}, state} {:error, :econnrefused} -> {:error, "connection refused on port #{port}", state} {:error, :timeout} -> {:error, "connection timed out after #{timeout_ms}ms", state} {:error, :nxdomain} -> {:error, "hostname not found: #{host}", state} {:error, :ehostunreach} -> {:error, "host unreachable: #{host}", state} {:error, reason} -> {:error, inspect(reason), state} end end end # --------------------------------------------------------------------------- # Healthy? # --------------------------------------------------------------------------- @impl true def healthy?(result) do lat_deg = result[:latency_degraded_ms] || @default_lat_degraded_ms if result.latency_ms >= lat_deg, do: :degraded, else: :up end # --------------------------------------------------------------------------- # Metrics # --------------------------------------------------------------------------- @impl true def metrics(result) do %{latency_ms: result.latency_ms} end # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- defp get_param(params, key) when is_atom(key) do v = params[key] || params[to_string(key)] if is_binary(v) and String.trim(v) == "", do: nil, else: v end defp parse_int(nil, default), do: default defp parse_int(v, _) when is_integer(v), do: v defp parse_int(v, default) when is_binary(v) do case Integer.parse(v) do {n, _} -> n :error -> default end end defp parse_int(_, default), do: default end