Current section
Files
Jump to
Current section
Files
lib/integrations/tcp.ex
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