Packages

macula

0.20.21
7.1.0 7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_gateway_system macula_gateway_health.erl
Raw

src/macula_gateway_system/macula_gateway_health.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% Macula Gateway Health Check Server
%%%
%%% Provides HTTP health endpoints for Kubernetes liveness and readiness probes.
%%% Runs on a separate port (8080) from the main QUIC gateway (9443).
%%%
%%% Endpoints:
%%% GET /health - Overall health status
%%% GET /ready - Readiness check (can accept traffic)
%%% GET /live - Liveness check (process is alive)
%%% GET /metrics - Basic metrics (optional)
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_gateway_health).
-behaviour(gen_server).
-include("macula_config.hrl").
-include_lib("kernel/include/logger.hrl").
%% API
-export([
start_link/1,
stop/0,
is_healthy/0,
set_ready/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2
]).
-record(state, {
port :: inet:port_number(),
listen_socket :: gen_tcp:socket() | undefined,
ready :: boolean(),
started_at :: integer()
}).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Start the health check server.
-spec start_link(proplists:proplist()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
%% @doc Stop the health check server.
-spec stop() -> ok.
stop() ->
gen_server:stop(?MODULE).
%% @doc Check if the gateway is healthy.
-spec is_healthy() -> boolean().
is_healthy() ->
case whereis(?MODULE) of
undefined -> false;
_Pid -> gen_server:call(?MODULE, is_healthy, 1000)
end.
%% @doc Set the readiness state.
-spec set_ready(boolean()) -> ok.
set_ready(Ready) ->
gen_server:cast(?MODULE, {set_ready, Ready}).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init(Opts) ->
Port = proplists:get_value(health_port, Opts, ?DEFAULT_HEALTH_PORT),
?LOG_INFO("Starting health check server on port ~p", [Port]),
case gen_tcp:listen(Port, [
{active, false},
{reuseaddr, true},
binary
]) of
{ok, ListenSocket} ->
%% Start accepting connections
spawn_link(fun() -> accept_loop(ListenSocket) end),
?LOG_INFO("Health check server listening on port ~p", [Port]),
State = #state{
port = Port,
listen_socket = ListenSocket,
ready = false, % Not ready until gateway starts
started_at = erlang:system_time(second)
},
{ok, State};
{error, Reason} ->
?LOG_ERROR("Failed to start health server: ~p", [Reason]),
{stop, {health_server_failed, Reason}}
end.
handle_call(is_healthy, _From, State) ->
{reply, true, State};
handle_call(get_state, _From, State) ->
{reply, State, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast({set_ready, Ready}, State) ->
{noreply, State#state{ready = Ready}};
handle_cast(_Request, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{listen_socket = Socket}) ->
case Socket of
undefined -> ok;
_ -> gen_tcp:close(Socket)
end,
ok.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @private
accept_loop(ListenSocket) ->
case gen_tcp:accept(ListenSocket, 5000) of
{ok, Socket} ->
spawn(fun() -> handle_request(Socket) end),
accept_loop(ListenSocket);
{error, timeout} ->
accept_loop(ListenSocket);
{error, closed} ->
ok;
{error, Reason} ->
?LOG_WARNING("Accept error: ~p", [Reason]),
timer:sleep(1000),
accept_loop(ListenSocket)
end.
%% @private
handle_request(Socket) ->
%% Read the HTTP request line (e.g., "GET /health HTTP/1.1\r\n")
case gen_tcp:recv(Socket, 0, 5000) of
{ok, Data} ->
%% Parse the request line to extract the path
Path = parse_http_request(Data),
%% Handle the request
Response = case Path of
<<"/health">> -> health_response();
<<"/ready">> -> ready_response();
<<"/live">> -> live_response();
<<"/metrics">> -> metrics_response();
_ -> not_found_response()
end,
gen_tcp:send(Socket, Response),
gen_tcp:close(Socket);
{error, _Reason} ->
gen_tcp:close(Socket)
end.
%% @private
parse_http_request(Data) ->
%% Extract path from HTTP request line like "GET /path HTTP/1.1\r\n..."
extract_path(binary:split(Data, <<" ">>, [global])).
%% @doc Extract path from HTTP request parts.
extract_path([<<"GET">>, Path | _Rest]) ->
%% Remove query string if present
strip_query_string(binary:split(Path, <<"?">>));
extract_path(_) ->
<<"/">>.
%% @doc Strip query string from path.
strip_query_string([PathOnly | _]) -> PathOnly;
strip_query_string(_) -> <<"/">>.
%% @private
health_response() ->
case whereis(?MODULE) of
undefined ->
http_response(503, "Service Unavailable", "text/plain", <<"Service unavailable">>);
_Pid ->
State = gen_server:call(?MODULE, get_state, 1000),
Uptime = erlang:system_time(second) - State#state.started_at,
Body = iolist_to_binary(io_lib:format(
"{\"status\":\"healthy\",\"ready\":~s,\"uptime\":~p}",
[atom_to_list(State#state.ready), Uptime]
)),
http_response(200, "OK", "application/json", Body)
end.
%% @private
ready_response() ->
case whereis(?MODULE) of
undefined ->
http_response(503, "Service Unavailable", "text/plain", <<"Service unavailable">>);
_Pid ->
State = gen_server:call(?MODULE, get_state, 1000),
ready_response_for_state(State#state.ready)
end.
%% @private
ready_response_for_state(true) ->
http_response(200, "OK", "text/plain", <<"Ready">>);
ready_response_for_state(false) ->
http_response(503, "Service Unavailable", "text/plain", <<"Not ready">>).
%% @private
live_response() ->
%% Always return 200 if we can handle the request
http_response(200, "OK", "text/plain", <<"Live">>).
%% @private
metrics_response() ->
case whereis(?MODULE) of
undefined ->
http_response(503, "Service Unavailable", "text/plain", <<"Service unavailable">>);
_Pid ->
build_metrics_response()
end.
%% @private
build_metrics_response() ->
State = gen_server:call(?MODULE, get_state, 1000),
Uptime = erlang:system_time(second) - State#state.started_at,
DiagInfo = get_diagnostics_metrics(),
GatewayStats = get_gateway_stats(),
ReadyInt = ready_to_int(State#state.ready),
Body = iolist_to_binary([
"# HELP macula_gateway_uptime_seconds Gateway uptime in seconds\n",
"# TYPE macula_gateway_uptime_seconds gauge\n",
io_lib:format("macula_gateway_uptime_seconds ~p~n", [Uptime]),
"# HELP macula_gateway_ready Gateway ready status (1=ready, 0=not ready)\n",
"# TYPE macula_gateway_ready gauge\n",
io_lib:format("macula_gateway_ready ~p~n", [ReadyInt]),
"# HELP macula_gateway_process_count Number of Erlang processes\n",
"# TYPE macula_gateway_process_count gauge\n",
io_lib:format("macula_gateway_process_count ~p~n", [maps:get(process_count, DiagInfo, 0)]),
"# HELP macula_gateway_memory_bytes Total memory used by the VM in bytes\n",
"# TYPE macula_gateway_memory_bytes gauge\n",
io_lib:format("macula_gateway_memory_bytes ~p~n", [maps:get(memory_bytes, DiagInfo, 0)]),
"# HELP macula_gateway_process_memory_bytes Memory used by Erlang processes in bytes\n",
"# TYPE macula_gateway_process_memory_bytes gauge\n",
io_lib:format("macula_gateway_process_memory_bytes ~p~n", [maps:get(process_memory_bytes, DiagInfo, 0)]),
"# HELP macula_gateway_diagnostics_available Diagnostics service availability (1=available, 0=unavailable)\n",
"# TYPE macula_gateway_diagnostics_available gauge\n",
io_lib:format("macula_gateway_diagnostics_available ~p~n", [maps:get(diagnostics_available, DiagInfo, 0)]),
"# HELP macula_gateway_clients_total Number of connected clients\n",
"# TYPE macula_gateway_clients_total gauge\n",
io_lib:format("macula_gateway_clients_total ~p~n", [maps:get(clients, GatewayStats, 0)]),
"# HELP macula_gateway_subscriptions_total Number of active subscriptions\n",
"# TYPE macula_gateway_subscriptions_total gauge\n",
io_lib:format("macula_gateway_subscriptions_total ~p~n", [maps:get(subscriptions, GatewayStats, 0)]),
"# HELP macula_gateway_registrations_total Number of registered procedures\n",
"# TYPE macula_gateway_registrations_total gauge\n",
io_lib:format("macula_gateway_registrations_total ~p~n", [maps:get(registrations, GatewayStats, 0)])
]),
http_response(200, "OK", "text/plain; version=0.0.4", Body).
%% @private
ready_to_int(true) -> 1;
ready_to_int(false) -> 0.
%% @private
%% @doc Get diagnostics metrics from the diagnostics service
get_diagnostics_metrics() ->
case whereis(macula_gateway_diagnostics) of
undefined ->
#{diagnostics_available => 0};
_Pid ->
MemoryInfo = erlang:memory(),
#{
diagnostics_available => 1,
process_count => erlang:system_info(process_count),
memory_bytes => proplists:get_value(total, MemoryInfo, 0),
process_memory_bytes => proplists:get_value(processes, MemoryInfo, 0)
}
end.
%% @private
%% @doc Get gateway statistics
get_gateway_stats() ->
case whereis(macula_gateway) of
undefined ->
#{clients => 0, subscriptions => 0, registrations => 0};
Pid ->
get_gateway_stats_from_pid(Pid)
end.
%% @private
get_gateway_stats_from_pid(Pid) ->
case macula_gateway:get_stats(Pid) of
Stats when is_map(Stats) -> Stats;
_ -> #{clients => 0, subscriptions => 0, registrations => 0}
end.
%% @private
not_found_response() ->
http_response(404, "Not Found", "text/plain", <<"Not found">>).
%% @private
http_response(Status, StatusText, ContentType, Body) ->
ContentLength = byte_size(Body),
[
<<"HTTP/1.1 ">>, integer_to_binary(Status), <<" ">>, list_to_binary(StatusText), <<"\r\n">>,
<<"Content-Type: ">>, list_to_binary(ContentType), <<"\r\n">>,
<<"Content-Length: ">>, integer_to_binary(ContentLength), <<"\r\n">>,
<<"Connection: close\r\n">>,
<<"\r\n">>,
Body
].