Packages
macula
0.7.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
Current section
Files
src/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").
%% 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() ->
try
gen_server:call(?MODULE, is_healthy, 1000)
catch
_:_ -> false
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),
io:format("Starting health check server on port ~p~n", [Port]),
case gen_tcp:listen(Port, [
{active, false},
{reuseaddr, true},
binary
]) of
{ok, ListenSocket} ->
%% Start accepting connections
spawn_link(fun() -> accept_loop(ListenSocket) end),
io:format("Health check server listening on port ~p~n", [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} ->
io:format("Failed to start health server: ~p~n", [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} ->
io:format("Accept error: ~p~n", [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() ->
try
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)
catch
_:_ ->
http_response(503, "Service Unavailable", "text/plain", <<"Service unavailable">>)
end.
%% @private
ready_response() ->
try
State = gen_server:call(?MODULE, get_state, 1000),
case State#state.ready of
true ->
http_response(200, "OK", "text/plain", <<"Ready">>);
false ->
http_response(503, "Service Unavailable", "text/plain", <<"Not ready">>)
end
catch
_:_ ->
http_response(503, "Service Unavailable", "text/plain", <<"Service unavailable">>)
end.
%% @private
live_response() ->
%% Always return 200 if we can handle the request
http_response(200, "OK", "text/plain", <<"Live">>).
%% @private
metrics_response() ->
try
State = gen_server:call(?MODULE, get_state, 1000),
Uptime = erlang:system_time(second) - State#state.started_at,
%% Get diagnostics info
DiagInfo = get_diagnostics_metrics(),
%% Get gateway stats (if available)
GatewayStats = get_gateway_stats(),
%% Build Prometheus-formatted metrics
Body = iolist_to_binary([
%% Health server metrics
"# 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", [case State#state.ready of true -> 1; false -> 0 end]),
%% System metrics from diagnostics
"# 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)]),
%% Diagnostics service availability
"# 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)]),
%% Gateway connection metrics
"# 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)
catch
_:_ ->
http_response(503, "Service Unavailable", "text/plain", <<"Service unavailable">>)
end.
%% @private
%% @doc Get diagnostics metrics from the diagnostics service
get_diagnostics_metrics() ->
try
%% Try to get info from diagnostics service
case whereis(macula_gateway_diagnostics) of
undefined ->
#{diagnostics_available => 0};
_Pid ->
%% Get system info directly
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
catch
_:_ ->
#{diagnostics_available => 0}
end.
%% @private
%% @doc Get gateway statistics
get_gateway_stats() ->
try
case whereis(macula_gateway) of
undefined ->
#{clients => 0, subscriptions => 0, registrations => 0};
Pid ->
case catch macula_gateway:get_stats(Pid) of
Stats when is_map(Stats) -> Stats;
_ -> #{clients => 0, subscriptions => 0, registrations => 0}
end
end
catch
_:_ ->
#{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
].