Packages
Erlang/Elixir Cloud Framework Health Check CloudI Service
Current section
3 Versions
Jump to
Current section
3 Versions
Compare versions
5
files changed
+307
additions
-61
deletions
| @@ -14,9 +14,9 @@ | |
| 14 14 | <h3><a name="Health_Check_CloudI_Service">Health Check CloudI Service</a></h3><p> |
| 15 15 | Each interval, do a DNS lookup of a hostname (if a hostname was provided) |
| 16 16 | and check the health of each IP address.</p>. |
| 17 | - <p>Copyright © 2022 Michael Truog</p> |
| 17 | + <p>Copyright © 2022-2023 Michael Truog</p> |
| 18 18 | |
| 19 | - <p><b>Version:</b> 2.0.5 Oct 11 2022 22:10:05 |
| 19 | + <p><b>Version:</b> 2.0.7 Oct 26 2023 14:01:42 |
| 20 20 | ------------------------------------------------------------------------</p> |
| 21 21 | <p><b>Behaviours:</b> <a href="deps/cloudi_core/doc/cloudi_service.html"><tt>cloudi_service</tt></a>.</p> |
| 22 22 | <p><b>Authors:</b> Michael Truog (<a href="mailto:mjtruog at protonmail dot com"><tt>mjtruog at protonmail dot com</tt></a>).</p> |
| @@ -95,7 +95,7 @@ when the restore occurred (TimeRestored) to provide the event duration.</p> | |
| 95 95 | |
| 96 96 | |
| 97 97 | <h3 class="typedecl"><a name="type-tcp_failure">tcp_failure()</a></h3> |
| 98 | - <p><tt>tcp_failure() = fun((Name::<a href="#type-hostname">hostname()</a>, IP::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/inet.html#type-ip_address">inet:ip_address()</a>, Port::<a href="#type-tcp_port">tcp_port()</a>, Reason::atom()) -> ok)</tt></p> |
| 98 | + <p><tt>tcp_failure() = fun((Name::<a href="#type-hostname">hostname()</a>, IP::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/inet.html#type-ip_address">inet:ip_address()</a>, Port::<a href="#type-tcp_port">tcp_port()</a>, Reason::any()) -> ok)</tt></p> |
| 99 99 | |
| 100 100 | |
| 101 101 | <h3 class="typedecl"><a name="type-tcp_port">tcp_port()</a></h3> |
| @@ -107,7 +107,7 @@ when the restore occurred (TimeRestored) to provide the event duration.</p> | |
| 107 107 | |
| 108 108 | |
| 109 109 | <h3 class="typedecl"><a name="type-tcp_test">tcp_test()</a></h3> |
| 110 | - <p><tt>tcp_test() = fun((Socket::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/gen_tcp.html#type-socket">gen_tcp:socket()</a>, Timeout::1..4294967295) -> ok | {error, atom()})</tt></p> |
| 110 | + <p><tt>tcp_test() = fun((Socket::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/gen_tcp.html#type-socket">gen_tcp:socket()</a>, Timeout::1..4294967295) -> ok | {error, any()}) | fun((Name::<a href="#type-hostname">hostname()</a>, IP::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/inet.html#type-ip_address">inet:ip_address()</a>, Port::<a href="#type-tcp_port">tcp_port()</a>, Timeout::1..4294967295) -> ok | {error, any()})</tt></p> |
| 111 111 | |
| 112 112 | |
| 113 113 | <h2><a name="index">Function Index</a></h2> |
| @@ -115,6 +115,16 @@ when the restore occurred (TimeRestored) to provide the event duration.</p> | |
| 115 115 | <tr><td valign="top"><a href="#cloudi_service_handle_request-11">cloudi_service_handle_request/11</a></td><td></td></tr> |
| 116 116 | <tr><td valign="top"><a href="#cloudi_service_init-4">cloudi_service_init/4</a></td><td></td></tr> |
| 117 117 | <tr><td valign="top"><a href="#cloudi_service_terminate-3">cloudi_service_terminate/3</a></td><td></td></tr> |
| 118 | + <tr><td valign="top"><a href="#tcp_test_http-3">tcp_test_http/3</a></td><td> |
| 119 | + <h4><a name="HTTP_tcp_test_function.">HTTP tcp_test function.</a></h4> |
| 120 | + Add as |
| 121 | + {{cloudi_service_health_check, tcp_test_http, [Method, Path, StatusCode]}} |
| 122 | + with values provided for Method, Path and StatusCode.</td></tr> |
| 123 | + <tr><td valign="top"><a href="#tcp_test_https-3">tcp_test_https/3</a></td><td> |
| 124 | + <h4><a name="HTTPS_tcp_test_function.">HTTPS tcp_test function.</a></h4> |
| 125 | + Add as |
| 126 | + {{cloudi_service_health_check, tcp_test_https, [Method, Path, StatusCode]}} |
| 127 | + with values provided for Method, Path and StatusCode.</td></tr> |
| 118 128 | </table> |
| 119 129 | |
| 120 130 | <h2><a name="functions">Function Details</a></h2> |
| @@ -142,6 +152,26 @@ when the restore occurred (TimeRestored) to provide the event duration.</p> | |
| 142 152 | <p><tt>cloudi_service_terminate(Reason, Timeout, State) -> any()</tt></p> |
| 143 153 | <p> </p> |
| 144 154 | </div> |
| 155 | + |
| 156 | + <h3 class="function"><a name="tcp_test_http-3">tcp_test_http/3</a></h3> |
| 157 | + <div class="spec"> |
| 158 | + <p><tt>tcp_test_http(Method::head | get, Path::nonempty_string(), StatusCode::200..399) -> fun((Name::<a href="#type-hostname">hostname()</a>, IP::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/inet.html#type-ip_address">inet:ip_address()</a>, Port::<a href="#type-tcp_port">tcp_port()</a>, Timeout::1..4294967295) -> ok | {error, any()})</tt><br></p> |
| 159 | + <p> </p> |
| 160 | + </div><p> |
| 161 | + <h4><a name="HTTP_tcp_test_function.">HTTP tcp_test function.</a></h4> |
| 162 | + Add as |
| 163 | + {{cloudi_service_health_check, tcp_test_http, [Method, Path, StatusCode]}} |
| 164 | + with values provided for Method, Path and StatusCode.</p> |
| 165 | + |
| 166 | + <h3 class="function"><a name="tcp_test_https-3">tcp_test_https/3</a></h3> |
| 167 | + <div class="spec"> |
| 168 | + <p><tt>tcp_test_https(Method::head | get, Path::nonempty_string(), StatusCode::200..399) -> fun((Name::<a href="#type-hostname">hostname()</a>, IP::<a href="/home/george/organized/work/financial_helper_apps/mc2_all-patterns/intraday_system/cloudi_org/kernel/doc/inet.html#type-ip_address">inet:ip_address()</a>, Port::<a href="#type-tcp_port">tcp_port()</a>, Timeout::1..4294967295) -> ok | {error, any()})</tt><br></p> |
| 169 | + <p> </p> |
| 170 | + </div><p> |
| 171 | + <h4><a name="HTTPS_tcp_test_function.">HTTPS tcp_test function.</a></h4> |
| 172 | + Add as |
| 173 | + {{cloudi_service_health_check, tcp_test_https, [Method, Path, StatusCode]}} |
| 174 | + with values provided for Method, Path and StatusCode.</p> |
| 145 175 | <hr> |
| 146 176 | |
| 147 177 | <div class="navbar"><a name="#navbar_bottom"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div> |
| @@ -19,5 +19,5 @@ | |
| 19 19 | {<<"name">>,<<"cloudi_core">>}, |
| 20 20 | {<<"optional">>,false}, |
| 21 21 | {<<"repository">>,<<"hexpm">>}, |
| 22 | - {<<"requirement">>,<<">= 2.0.6">>}]]}. |
| 23 | - {<<"version">>,<<"2.0.6">>}. |
| 22 | + {<<"requirement">>,<<">= 2.0.7">>}]]}. |
| 23 | + {<<"version">>,<<"2.0.7">>}. |
| @@ -4,7 +4,7 @@ | |
| 4 4 | {deps, |
| 5 5 | [{cloudi_core, ".*", |
| 6 6 | {git, "https://github.com/CloudI/cloudi_core.git", |
| 7 | - {tag, "v2.0.6"}}}]}. |
| 7 | + {tag, "v2.0.7"}}}]}. |
| 8 8 | |
| 9 9 | {erl_opts, |
| 10 10 | [deterministic, |
| @@ -3,7 +3,7 @@ | |
| 3 3 | |
| 4 4 | {application, cloudi_service_health_check, |
| 5 5 | [{description, "Health Check CloudI Service"}, |
| 6 | - {vsn, "2.0.6"}, |
| 6 | + {vsn, "2.0.7"}, |
| 7 7 | {modules, [ |
| 8 8 | cloudi_service_health_check |
| 9 9 | ]}, |
| @@ -41,7 +41,7 @@ | |
| 41 41 | %%% |
| 42 42 | %%% MIT License |
| 43 43 | %%% |
| 44 | - %%% Copyright (c) 2022 Michael Truog <mjtruog at protonmail dot com> |
| 44 | + %%% Copyright (c) 2022-2023 Michael Truog <mjtruog at protonmail dot com> |
| 45 45 | %%% |
| 46 46 | %%% Permission is hereby granted, free of charge, to any person obtaining a |
| 47 47 | %%% copy of this software and associated documentation files (the "Software"), |
| @@ -62,8 +62,8 @@ | |
| 62 62 | %%% DEALINGS IN THE SOFTWARE. |
| 63 63 | %%% |
| 64 64 | %%% @author Michael Truog <mjtruog at protonmail dot com> |
| 65 | - %%% @copyright 2022 Michael Truog |
| 66 | - %%% @version 2.0.5 {@date} {@time} |
| 65 | + %%% @copyright 2022-2023 Michael Truog |
| 66 | + %%% @version 2.0.7 {@date} {@time} |
| 67 67 | %%%------------------------------------------------------------------------ |
| 68 68 | |
| 69 69 | -module(cloudi_service_health_check). |
| @@ -71,6 +71,10 @@ | |
| 71 71 | |
| 72 72 | -behaviour(cloudi_service). |
| 73 73 | |
| 74 | + %% external interface |
| 75 | + -export([tcp_test_http/3, |
| 76 | + tcp_test_https/3]). |
| 77 | + |
| 74 78 | %% cloudi_service callbacks |
| 75 79 | -export([cloudi_service_init/4, |
| 76 80 | cloudi_service_handle_request/11, |
| @@ -88,6 +92,11 @@ | |
| 88 92 | % May be set for each host too. |
| 89 93 | -define(DEFAULT_IPV6, true). |
| 90 94 | % May be set for each host too. |
| 95 | + -define(DEFAULT_DNS_NAMESERVERS, undefined). |
| 96 | + % If set to undefined, use system calls for DNS requests. |
| 97 | + % If set to a list of {inet:ip_address(), Port :: 1..65535} |
| 98 | + % then use the specific nameservers for DNS requests. |
| 99 | + % May be set for each host too. |
| 91 100 | -define(DEFAULT_ERROR_LEVEL, error). |
| 92 101 | -define(DEFAULT_DEBUG, false). % log output for debugging |
| 93 102 | -define(DEFAULT_DEBUG_LEVEL, debug). |
| @@ -110,6 +119,8 @@ | |
| 110 119 | % server's liveness, this function can be provided to test |
| 111 120 | % the socket. Otherwise, only the ability of the server to |
| 112 121 | % accept a TCP connection for the configured port is tested. |
| 122 | + % If the function needs to create the socket, the function arity |
| 123 | + % can be 4 instead of 2. |
| 113 124 | -define(DEFAULT_PING_FAILURE, undefined). |
| 114 125 | -define(DEFAULT_PING_RESTORED, undefined). |
| 115 126 | |
| @@ -149,7 +160,7 @@ | |
| 149 160 | fun((Name :: hostname(), |
| 150 161 | IP :: inet:ip_address(), |
| 151 162 | Port :: tcp_port(), |
| 152 | - Reason :: atom()) -> |
| 163 | + Reason :: any()) -> |
| 153 164 | ok). |
| 154 165 | -type tcp_restored() :: |
| 155 166 | fun((Name :: hostname(), |
| @@ -161,7 +172,12 @@ | |
| 161 172 | -type tcp_test() :: |
| 162 173 | fun((Socket :: gen_tcp:socket(), |
| 163 174 | Timeout :: 1..?TIMEOUT_MAX_ERLANG) -> |
| 164 | - ok | {error, atom()}). |
| 175 | + ok | {error, any()}) | |
| 176 | + fun((Name :: hostname(), |
| 177 | + IP :: inet:ip_address(), |
| 178 | + Port :: tcp_port(), |
| 179 | + Timeout :: 1..?TIMEOUT_MAX_ERLANG) -> |
| 180 | + ok | {error, any()}). |
| 165 181 | -type ping_failure() :: |
| 166 182 | fun((Name :: hostname(), |
| 167 183 | IP :: inet:ip_address(), |
| @@ -207,6 +223,8 @@ | |
| 207 223 | :: interval(), |
| 208 224 | start_time |
| 209 225 | :: cloudi_timestamp:native_monotonic(), |
| 226 | + dns_nameservers |
| 227 | + :: nonempty_list({inet:ip_address(), 1..65535}) | undefined, |
| 210 228 | dns_disabled = false |
| 211 229 | :: boolean(), % name is an IP address |
| 212 230 | dns_failed = false |
| @@ -267,6 +285,112 @@ | |
| 267 285 | %%% External interface functions |
| 268 286 | %%%------------------------------------------------------------------------ |
| 269 287 | |
| 288 | + %%%------------------------------------------------------------------------ |
| 289 | + %%% tcp_test HTTP/HTTPS functions |
| 290 | + %%%------------------------------------------------------------------------ |
| 291 | + |
| 292 | + %%------------------------------------------------------------------------- |
| 293 | + %% @doc |
| 294 | + %% ===HTTP tcp_test function.=== |
| 295 | + %% Add as |
| 296 | + %% {{cloudi_service_health_check, tcp_test_http, [Method, Path, StatusCode]}} |
| 297 | + %% with values provided for Method, Path and StatusCode. |
| 298 | + %% @end |
| 299 | + %%------------------------------------------------------------------------- |
| 300 | + |
| 301 | + -spec tcp_test_http(Method :: head | get, |
| 302 | + Path :: nonempty_string(), |
| 303 | + StatusCode :: 200..399) -> |
| 304 | + fun((Name :: hostname(), |
| 305 | + IP :: inet:ip_address(), |
| 306 | + Port :: tcp_port(), |
| 307 | + Timeout :: 1..?TIMEOUT_MAX_ERLANG) -> |
| 308 | + ok | {error, any()}). |
| 309 | + |
| 310 | + tcp_test_http(Method, [_ | _] = Path, StatusCode) |
| 311 | + when is_atom(Method), is_integer(StatusCode) -> |
| 312 | + Profile = default, |
| 313 | + fun(Name, IP, Port, Timeout) -> |
| 314 | + SocketAddress = case IP of |
| 315 | + {_, _, _, _} -> |
| 316 | + inet:ntoa(IP) ++ ":" ++ erlang:integer_to_list(Port); |
| 317 | + {_, _, _, _, _, _, _, _} -> |
| 318 | + "[" ++ inet:ntoa(IP) ++ "]:" ++ erlang:integer_to_list(Port) |
| 319 | + end, |
| 320 | + URL = "http://" ++ SocketAddress ++ Path, |
| 321 | + RequestHeaders = [{<<"host">>, erlang:list_to_binary(Name)}], |
| 322 | + case hackney:request(Method, URL, RequestHeaders, <<>>, |
| 323 | + [with_body, |
| 324 | + {connect_timeout, Timeout}, |
| 325 | + {recv_timeout, Timeout}, |
| 326 | + {pool, Profile}]) of |
| 327 | + {ok, ResponseStatusCode, ResponseHeaders, Response} -> |
| 328 | + if |
| 329 | + ResponseStatusCode == StatusCode -> |
| 330 | + ok; |
| 331 | + true -> |
| 332 | + {error, |
| 333 | + {http_response_mismatch, |
| 334 | + ResponseStatusCode, ResponseHeaders, Response}} |
| 335 | + end; |
| 336 | + {error, _} = Error -> |
| 337 | + Error |
| 338 | + end |
| 339 | + end. |
| 340 | + |
| 341 | + %%------------------------------------------------------------------------- |
| 342 | + %% @doc |
| 343 | + %% ===HTTPS tcp_test function.=== |
| 344 | + %% Add as |
| 345 | + %% {{cloudi_service_health_check, tcp_test_https, [Method, Path, StatusCode]}} |
| 346 | + %% with values provided for Method, Path and StatusCode. |
| 347 | + %% @end |
| 348 | + %%------------------------------------------------------------------------- |
| 349 | + |
| 350 | + -spec tcp_test_https(Method :: head | get, |
| 351 | + Path :: nonempty_string(), |
| 352 | + StatusCode :: 200..399) -> |
| 353 | + fun((Name :: hostname(), |
| 354 | + IP :: inet:ip_address(), |
| 355 | + Port :: tcp_port(), |
| 356 | + Timeout :: 1..?TIMEOUT_MAX_ERLANG) -> |
| 357 | + ok | {error, any()}). |
| 358 | + |
| 359 | + tcp_test_https(Method, [_ | _] = Path, StatusCode) |
| 360 | + when is_atom(Method), is_integer(StatusCode) -> |
| 361 | + Profile = default, |
| 362 | + fun(Name, IP, Port, Timeout) -> |
| 363 | + SocketAddress = case IP of |
| 364 | + {_, _, _, _} -> |
| 365 | + inet:ntoa(IP) ++ ":" ++ erlang:integer_to_list(Port); |
| 366 | + {_, _, _, _, _, _, _, _} -> |
| 367 | + "[" ++ inet:ntoa(IP) ++ "]:" ++ erlang:integer_to_list(Port) |
| 368 | + end, |
| 369 | + URL = "https://" ++ SocketAddress ++ Path, |
| 370 | + RequestHeaders = [{<<"host">>, erlang:list_to_binary(Name)}], |
| 371 | + SSLOptions = [{server_name_indication, Name}] ++ |
| 372 | + hackney_ssl:check_hostname_opts(Name) ++ |
| 373 | + hackney_ssl:cipher_opts(), |
| 374 | + case hackney:request(Method, URL, RequestHeaders, <<>>, |
| 375 | + [with_body, |
| 376 | + {ssl_options, SSLOptions}, |
| 377 | + {connect_timeout, Timeout}, |
| 378 | + {recv_timeout, Timeout}, |
| 379 | + {pool, Profile}]) of |
| 380 | + {ok, ResponseStatusCode, ResponseHeaders, Response} -> |
| 381 | + if |
| 382 | + ResponseStatusCode == StatusCode -> |
| 383 | + ok; |
| 384 | + true -> |
| 385 | + {error, |
| 386 | + {https_response_mismatch, |
| 387 | + ResponseStatusCode, ResponseHeaders, Response}} |
| 388 | + end; |
| 389 | + {error, _} = Error -> |
| 390 | + Error |
| 391 | + end |
| 392 | + end. |
| 393 | + |
| 270 394 | %%%------------------------------------------------------------------------ |
| 271 395 | %%% Callback functions from cloudi_service |
| 272 396 | %%%------------------------------------------------------------------------ |
| @@ -277,10 +401,12 @@ cloudi_service_init(Args, Prefix, _Timeout, Dispatcher) -> | |
| 277 401 | {interval, ?DEFAULT_INTERVAL}, |
| 278 402 | {ipv4, ?DEFAULT_IPV4}, |
| 279 403 | {ipv6, ?DEFAULT_IPV6}, |
| 404 | + {dns_nameservers, ?DEFAULT_DNS_NAMESERVERS}, |
| 280 405 | {error_level, ?DEFAULT_ERROR_LEVEL}, |
| 281 406 | {debug, ?DEFAULT_DEBUG}, |
| 282 407 | {debug_level, ?DEFAULT_DEBUG_LEVEL}], |
| 283 | - [Hosts, IntervalDefault, IPv4Allowed, IPv6Allowed, ErrorLogLevel, |
| 408 | + [Hosts, IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers, |
| 409 | + ErrorLogLevel, |
| 284 410 | Debug, DebugLevel] = cloudi_proplists:take_values(Defaults, Args), |
| 285 411 | true = is_list(Hosts) andalso (Hosts /= []), |
| 286 412 | true = is_integer(IntervalDefault) andalso |
| @@ -290,6 +416,7 @@ cloudi_service_init(Args, Prefix, _Timeout, Dispatcher) -> | |
| 290 416 | true = is_boolean(IPv6Allowed), |
| 291 417 | true = (IPv4Allowed /= false) orelse |
| 292 418 | (IPv6Allowed /= false), |
| 419 | + true = dns_nameservers(DNSNameservers), |
| 293 420 | Service = cloudi_service:self(Dispatcher), |
| 294 421 | ProcessIndex = cloudi_service:process_index(Dispatcher), |
| 295 422 | ProcessCount = cloudi_service:process_count(Dispatcher), |
| @@ -298,7 +425,8 @@ cloudi_service_init(Args, Prefix, _Timeout, Dispatcher) -> | |
| 298 425 | true = (ProcessCountMin =:= ProcessCountMax) andalso |
| 299 426 | (ProcessCountMin =:= ProcessCount), |
| 300 427 | HostsLoaded = hosts_load(Hosts, ProcessIndex, ProcessCount, Service, |
| 301 | - IntervalDefault, IPv4Allowed, IPv6Allowed), |
| 428 | + IntervalDefault, IPv4Allowed, IPv6Allowed, |
| 429 | + DNSNameservers), |
| 302 430 | true = ((ErrorLogLevel =:= trace) orelse |
| 303 431 | (ErrorLogLevel =:= debug) orelse |
| 304 432 | (ErrorLogLevel =:= info) orelse |
| @@ -318,6 +446,7 @@ cloudi_service_init(Args, Prefix, _Timeout, Dispatcher) -> | |
| 318 446 | Debug =:= true -> |
| 319 447 | DebugLevel |
| 320 448 | end, |
| 449 | + ok = cloudi_service:subscribe(Dispatcher, "hosts.erl"), |
| 321 450 | ok = cloudi_service:subscribe(Dispatcher, "hosts.erl/get"), |
| 322 451 | ok = cloudi_service:subscribe(Dispatcher, "hosts.json/get"), |
| 323 452 | ok = cloudi_service:subscribe(Dispatcher, |
| @@ -366,6 +495,11 @@ cloudi_service_handle_request(_RequestType, Name, _Pattern, | |
| 366 495 | {Last, |
| 367 496 | HostsInfo, |
| 368 497 | TextFormatValue} = case cloudi_service_name:suffix(Prefix, Name) of |
| 498 | + "hosts.erl" -> |
| 499 | + % request from an internal service |
| 500 | + {ProcessIndexLast == ProcessIndex, |
| 501 | + hosts_list(Hosts, DurationsDown), |
| 502 | + <<"none">>}; |
| 369 503 | "hosts.erl/get" -> |
| 370 504 | {ProcessIndexLast == ProcessIndex, |
| 371 505 | hosts_list(Hosts, DurationsDown), |
| @@ -383,6 +517,8 @@ cloudi_service_handle_request(_RequestType, Name, _Pattern, | |
| 383 517 | if |
| 384 518 | Last =:= true -> |
| 385 519 | Response = if |
| 520 | + TextFormatValue == <<"none">> -> |
| 521 | + HostsInfo; |
| 386 522 | TextFormatValue == <<"erl">> -> |
| 387 523 | convert_term_to_erlang(HostsInfo); |
| 388 524 | TextFormatValue == <<"json">> -> |
| @@ -414,19 +550,21 @@ cloudi_service_terminate(_Reason, _Timeout, _State) -> | |
| 414 550 | %%%------------------------------------------------------------------------ |
| 415 551 | |
| 416 552 | hosts_load(Hosts, ProcessIndex, ProcessCount, Service, |
| 417 | - IntervalDefault, IPv4Allowed, IPv6Allowed) -> |
| 553 | + IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers) -> |
| 418 554 | hosts_load(Hosts, trie:new(), |
| 419 555 | 0, ProcessIndex, ProcessCount, Service, |
| 420 | - IntervalDefault, IPv4Allowed, IPv6Allowed, |
| 556 | + IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers, |
| 421 557 | cloudi_environment:lookup()). |
| 422 558 | |
| 423 | - hosts_load([], HostsLoaded, _, _, _, _, _, _, _, _) -> |
| 559 | + hosts_load([], HostsLoaded, _, _, _, _, _, _, _, _, _) -> |
| 424 560 | HostsLoaded; |
| 425 561 | hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, |
| 426 562 | ProcessIndex, ProcessIndex, ProcessCount, Service, |
| 427 | - IntervalDefault, IPv4Allowed, IPv6Allowed, Environment) -> |
| 563 | + IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers, |
| 564 | + Environment) -> |
| 428 565 | true = is_list(HostnameRaw), |
| 429 | - Hostname = cloudi_environment:transform(HostnameRaw, Environment), |
| 566 | + Hostname = hostname_to_ascii(cloudi_environment:transform(HostnameRaw, |
| 567 | + Environment)), |
| 430 568 | false = trie:is_key(Hostname, HostsLoaded), |
| 431 569 | Defaults = [ |
| 432 570 | {health, ?DEFAULT_HEALTH}, |
| @@ -434,6 +572,7 @@ hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, | |
| 434 572 | {interval, IntervalDefault}, |
| 435 573 | {ipv4, IPv4Allowed}, |
| 436 574 | {ipv6, IPv6Allowed}, |
| 575 | + {dns_nameservers, DNSNameservers}, |
| 437 576 | {dns_failure, ?DEFAULT_DNS_FAILURE}, |
| 438 577 | {dns_restored, ?DEFAULT_DNS_RESTORED}, |
| 439 578 | {dns_ip_added, ?DEFAULT_DNS_IP_ADDED}, |
| @@ -445,6 +584,7 @@ hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, | |
| 445 584 | {ping_failure, ?DEFAULT_PING_FAILURE}, |
| 446 585 | {ping_restored, ?DEFAULT_PING_RESTORED}], |
| 447 586 | [Health, Port, Interval, IPv4AllowedHost, IPv6AllowedHost, |
| 587 | + DNSNameserversHost, |
| 448 588 | DNSFailure0, DNSRestored0, DNSIPAdded0, DNSIPRemovedHealthy0, |
| 449 589 | DNSIPRemovedFailed0, TCPFailure0, TCPRestored0, TCPTest0, PingFailure0, |
| 450 590 | PingRestored0] = cloudi_proplists:take_values(Defaults, Args), |
| @@ -456,6 +596,7 @@ hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, | |
| 456 596 | true = is_boolean(IPv6AllowedHost), |
| 457 597 | true = (IPv4AllowedHost /= false) orelse |
| 458 598 | (IPv6AllowedHost /= false), |
| 599 | + true = dns_nameservers(DNSNameserversHost), |
| 459 600 | DNSFailureN = cloudi_args_type: |
| 460 601 | function_optional(DNSFailure0, 2), |
| 461 602 | DNSRestoredN = cloudi_args_type: |
| @@ -471,7 +612,7 @@ hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, | |
| 471 612 | TCPRestoredN = cloudi_args_type: |
| 472 613 | function_optional(TCPRestored0, 5), |
| 473 614 | TCPTestN = cloudi_args_type: |
| 474 | - function_optional(TCPTest0, 2), |
| 615 | + function_optional_pick_any(TCPTest0, [2, 4]), |
| 475 616 | PingFailureN = cloudi_args_type: |
| 476 617 | function_optional(PingFailure0, 3), |
| 477 618 | PingRestoredN = cloudi_args_type: |
| @@ -494,6 +635,7 @@ hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, | |
| 494 635 | port = Port, |
| 495 636 | interval = Interval, |
| 496 637 | start_time = TimeStart, |
| 638 | + dns_nameservers = DNSNameserversHost, |
| 497 639 | dns_failure = DNSFailureN, |
| 498 640 | dns_restored = DNSRestoredN, |
| 499 641 | dns_ip_added = DNSIPAddedN, |
| @@ -522,17 +664,20 @@ hosts_load([{HostnameRaw, Args} | Hosts], HostsLoaded, | |
| 522 664 | hosts_load(Hosts, trie:store(Hostname, HostLoadedN, HostsLoaded), |
| 523 665 | (ProcessIndex + 1) rem ProcessCount, |
| 524 666 | ProcessIndex, ProcessCount, Service, |
| 525 | - IntervalDefault, IPv4Allowed, IPv6Allowed, Environment); |
| 667 | + IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers, |
| 668 | + Environment); |
| 526 669 | hosts_load([{HostnameRaw, _} | Hosts], HostsLoaded, |
| 527 670 | Index, ProcessIndex, ProcessCount, Service, |
| 528 | - IntervalDefault, IPv4Allowed, IPv6Allowed, Environment) -> |
| 671 | + IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers, |
| 672 | + Environment) -> |
| 529 673 | true = is_list(HostnameRaw), |
| 530 674 | Hostname = cloudi_environment:transform(HostnameRaw, Environment), |
| 531 675 | false = trie:is_key(Hostname, HostsLoaded), |
| 532 676 | hosts_load(Hosts, HostsLoaded, |
| 533 677 | (Index + 1) rem ProcessCount, |
| 534 678 | ProcessIndex, ProcessCount, Service, |
| 535 | - IntervalDefault, IPv4Allowed, IPv6Allowed, Environment). |
| 679 | + IntervalDefault, IPv4Allowed, IPv6Allowed, DNSNameservers, |
| 680 | + Environment). |
| 536 681 | |
| 537 682 | health_check(Hostname, |
| 538 683 | #state{service = Service, |
| @@ -596,7 +741,8 @@ health_check_dns(Host0, TimeoutDNS, ErrorLogLevel) -> | |
| 596 741 | health_check_dns_ipv4(#host{ipv4_allowed = false} = Host, _) -> |
| 597 742 | {ok, Host}; |
| 598 743 | health_check_dns_ipv4(#host{name = Hostname, |
| 599 | - ipv6_allowed = IPv6Allowed} = Host, |
| 744 | + ipv6_allowed = IPv6Allowed, |
| 745 | + dns_nameservers = undefined} = Host, |
| 600 746 | Timeout) -> |
| 601 747 | case inet_getaddrs(Hostname, inet, Timeout) of |
| 602 748 | {ok, IPv4} -> |
| @@ -605,12 +751,28 @@ health_check_dns_ipv4(#host{name = Hostname, | |
| 605 751 | {ok, Host#host{ipv4 = []}}; |
| 606 752 | {error, _} = Error -> |
| 607 753 | Error |
| 754 | + end; |
| 755 | + health_check_dns_ipv4(#host{name = Hostname, |
| 756 | + ipv6_allowed = IPv6Allowed, |
| 757 | + dns_nameservers = DNSNameservers} = Host, |
| 758 | + Timeout) -> |
| 759 | + case inet_res:lookup(Hostname, in, a, |
| 760 | + [{nameservers, DNSNameservers}, |
| 761 | + {alt_nameservers, []}, |
| 762 | + {timeout, Timeout}], infinity) of |
| 763 | + [] when IPv6Allowed =:= true -> |
| 764 | + {ok, Host#host{ipv4 = []}}; |
| 765 | + [] -> |
| 766 | + {error, nxdomain}; |
| 767 | + [_ | _] = IPv4 -> |
| 768 | + {ok, Host#host{ipv4 = lists:usort(IPv4)}} |
| 608 769 | end. |
| 609 770 | |
| 610 771 | health_check_dns_ipv6(#host{ipv6_allowed = false} = Host, _) -> |
| 611 772 | {ok, Host}; |
| 612 773 | health_check_dns_ipv6(#host{name = Hostname, |
| 613 | - ipv4_allowed = IPv4Allowed} = Host, |
| 774 | + ipv4_allowed = IPv4Allowed, |
| 775 | + dns_nameservers = undefined} = Host, |
| 614 776 | Timeout) -> |
| 615 777 | case inet_getaddrs(Hostname, inet6, Timeout) of |
| 616 778 | {ok, IPv6} -> |
| @@ -619,6 +781,21 @@ health_check_dns_ipv6(#host{name = Hostname, | |
| 619 781 | {ok, Host#host{ipv6 = []}}; |
| 620 782 | {error, _} = Error -> |
| 621 783 | Error |
| 784 | + end; |
| 785 | + health_check_dns_ipv6(#host{name = Hostname, |
| 786 | + ipv4_allowed = IPv4Allowed, |
| 787 | + dns_nameservers = DNSNameservers} = Host, |
| 788 | + Timeout) -> |
| 789 | + case inet_res:lookup(Hostname, in, aaaa, |
| 790 | + [{nameservers, DNSNameservers}, |
| 791 | + {alt_nameservers, []}, |
| 792 | + {timeout, Timeout}], infinity) of |
| 793 | + [] when IPv4Allowed =:= true -> |
| 794 | + {ok, Host#host{ipv6 = []}}; |
| 795 | + [] -> |
| 796 | + {error, nxdomain}; |
| 797 | + [_ | _] = IPv6 -> |
| 798 | + {ok, Host#host{ipv6 = lists:usort(IPv6)}} |
| 622 799 | end. |
| 623 800 | |
| 624 801 | health_check_update_ips(#host{dns_failed = true} = HostN, _, _, _, _) -> |
| @@ -677,32 +854,10 @@ health_check_status_tcp(#host{ipv4 = IPv4, | |
| 677 854 | |
| 678 855 | health_check_status_tcp_ips([], HostN, _, _, _, _) -> |
| 679 856 | HostN; |
| 680 | - health_check_status_tcp_ips([IP | IPs], |
| 681 | - #host{port = Port, |
| 682 | - tcp_test = TCPTest} = Host0, |
| 683 | - Family, Timeout, |
| 857 | + health_check_status_tcp_ips([IP | IPs], Host0, Family, Timeout, |
| 684 858 | ErrorLogLevel, DebugLogLevel) -> |
| 685 | - HostN = case gen_tcp:connect(IP, Port, [Family], Timeout) of |
| 686 | - {ok, Socket} -> |
| 687 | - Host1 = if |
| 688 | - TCPTest =:= undefined -> |
| 689 | - tcp_restored(Host0, IP, Port, |
| 690 | - ErrorLogLevel, DebugLogLevel); |
| 691 | - is_function(TCPTest) -> |
| 692 | - case TCPTest(Socket, Timeout) of |
| 693 | - ok -> |
| 694 | - tcp_restored(Host0, IP, Port, |
| 695 | - ErrorLogLevel, DebugLogLevel); |
| 696 | - {error, Reason} when is_atom(Reason) -> |
| 697 | - tcp_failure(Host0, IP, Port, |
| 698 | - Reason, Timeout, ErrorLogLevel) |
| 699 | - end |
| 700 | - end, |
| 701 | - ok = gen_tcp:close(Socket), |
| 702 | - Host1; |
| 703 | - {error, Reason} -> |
| 704 | - tcp_failure(Host0, IP, Port, Reason, Timeout, ErrorLogLevel) |
| 705 | - end, |
| 859 | + HostN = tcp_test(Host0, IP, Family, Timeout, |
| 860 | + ErrorLogLevel, DebugLogLevel), |
| 706 861 | health_check_status_tcp_ips(IPs, HostN, Family, Timeout, |
| 707 862 | ErrorLogLevel, DebugLogLevel). |
| 708 863 | |
| @@ -734,7 +889,7 @@ health_check_status_ping_ips([IP | IPs], | |
| 734 889 | end, |
| 735 890 | Reason = cloudi_string: |
| 736 891 | format("ping exited with ~s (stdout/stderr below)~n~ts", |
| 737 | - [StatusStr, erlang:iolist_to_binary(Output)]), |
| 892 | + [StatusStr, unicode:characters_to_binary(Output)]), |
| 738 893 | ping_failure(Host0, IP, Reason, ErrorLogLevel) |
| 739 894 | end, |
| 740 895 | health_check_status_ping_ips(IPs, HostN, |
| @@ -809,7 +964,7 @@ dns_restored(#host{name = Hostname, | |
| 809 964 | NanoSeconds = cloudi_timestamp:convert(TimeRestored - TimeFailure, |
| 810 965 | native, nanosecond), |
| 811 966 | ?LOG(ErrorLogLevel, |
| 812 | - "\"~s\" DNS restored after ~s", |
| 967 | + "\"~s\" DNS restored~n after ~s", |
| 813 968 | [Hostname, |
| 814 969 | cloudi_timestamp:nanoseconds_to_string(NanoSeconds)]), |
| 815 970 | if |
| @@ -858,7 +1013,7 @@ dns_ip_removed_failed(#host{name = Hostname, | |
| 858 1013 | NanoSeconds = cloudi_timestamp:convert(TimeRemoved - TimeFailure, |
| 859 1014 | native, nanosecond), |
| 860 1015 | ?LOG(ErrorLogLevel, |
| 861 | - "\"~s\" DNS IP ~s (failed) removed after ~s", |
| 1016 | + "\"~s\" DNS IP ~s (failed) removed~n after ~s", |
| 862 1017 | [Hostname, inet:ntoa(IP), |
| 863 1018 | cloudi_timestamp:nanoseconds_to_string(NanoSeconds)]), |
| 864 1019 | if |
| @@ -869,6 +1024,18 @@ dns_ip_removed_failed(#host{name = Hostname, | |
| 869 1024 | end, |
| 870 1025 | Host. |
| 871 1026 | |
| 1027 | + dns_nameserver([]) -> |
| 1028 | + true; |
| 1029 | + dns_nameserver([{IP, Port} | Nameservers]) |
| 1030 | + when Port >= 1 andalso Port =< 65535 -> |
| 1031 | + true = inet:is_ip_address(IP), |
| 1032 | + dns_nameserver(Nameservers). |
| 1033 | + |
| 1034 | + dns_nameservers(undefined) -> |
| 1035 | + true; |
| 1036 | + dns_nameservers([_ | _] = Nameservers) -> |
| 1037 | + dns_nameserver(Nameservers). |
| 1038 | + |
| 872 1039 | inet_getaddrs(Hostname, Family, Timeout) -> |
| 873 1040 | % Erlang/OTP inet:getaddrs/3 is undocumented and fails to timeout |
| 874 1041 | % (in Erlang/OTP 25.0) |
| @@ -888,6 +1055,46 @@ inet_getaddrs(Hostname, Family, Timeout) -> | |
| 888 1055 | {error, nxdomain} |
| 889 1056 | end. |
| 890 1057 | |
| 1058 | + tcp_test(#host{name = Hostname, |
| 1059 | + port = Port, |
| 1060 | + tcp_test = TCPTest} = HostN, |
| 1061 | + IP, _, Timeout, |
| 1062 | + ErrorLogLevel, DebugLogLevel) |
| 1063 | + when is_function(TCPTest, 4) -> |
| 1064 | + case TCPTest(Hostname, IP, Port, Timeout) of |
| 1065 | + ok -> |
| 1066 | + tcp_restored(HostN, IP, Port, |
| 1067 | + ErrorLogLevel, DebugLogLevel); |
| 1068 | + {error, Reason} -> |
| 1069 | + tcp_failure(HostN, IP, Port, |
| 1070 | + Reason, Timeout, ErrorLogLevel) |
| 1071 | + end; |
| 1072 | + tcp_test(#host{port = Port, |
| 1073 | + tcp_test = TCPTest} = Host0, |
| 1074 | + IP, Family, Timeout, |
| 1075 | + ErrorLogLevel, DebugLogLevel) -> |
| 1076 | + case gen_tcp:connect(IP, Port, [Family], Timeout) of |
| 1077 | + {ok, Socket} -> |
| 1078 | + HostN = if |
| 1079 | + TCPTest =:= undefined -> |
| 1080 | + tcp_restored(Host0, IP, Port, |
| 1081 | + ErrorLogLevel, DebugLogLevel); |
| 1082 | + is_function(TCPTest, 2) -> |
| 1083 | + case TCPTest(Socket, Timeout) of |
| 1084 | + ok -> |
| 1085 | + tcp_restored(Host0, IP, Port, |
| 1086 | + ErrorLogLevel, DebugLogLevel); |
| 1087 | + {error, Reason} -> |
| 1088 | + tcp_failure(Host0, IP, Port, |
| 1089 | + Reason, Timeout, ErrorLogLevel) |
| 1090 | + end |
| 1091 | + end, |
| 1092 | + ok = gen_tcp:close(Socket), |
| 1093 | + HostN; |
| 1094 | + {error, Reason} -> |
| 1095 | + tcp_failure(Host0, IP, Port, Reason, Timeout, ErrorLogLevel) |
| 1096 | + end. |
| 1097 | + |
| 891 1098 | tcp_failure(#host{name = Hostname, |
| 892 1099 | health_failed_count = HealthFailedCount, |
| 893 1100 | health_failed_time = HealthFailedTime, |
| @@ -902,13 +1109,13 @@ tcp_failure(#host{name = Hostname, | |
| 902 1109 | TimeFailure = cloudi_timestamp:native_monotonic(), |
| 903 1110 | ReasonInfo = if |
| 904 1111 | Reason =:= timeout -> |
| 905 | - cloudi_string:format(" (after ~w milliseconds)", |
| 1112 | + cloudi_string:format("~n (after ~w milliseconds)", |
| 906 1113 | [Timeout]); |
| 907 1114 | true -> |
| 908 1115 | "" |
| 909 1116 | end, |
| 910 1117 | ?LOG(ErrorLogLevel, |
| 911 | - "\"~s\" TCP failure: ~s port ~w failed: ~s~s", |
| 1118 | + "\"~s\" TCP failure: ~s port ~w failed:~n ~tp~s", |
| 912 1119 | [Hostname, inet:ntoa(IP), Port, Reason, ReasonInfo]), |
| 913 1120 | if |
| 914 1121 | TCPFailure =:= undefined -> |
| @@ -949,7 +1156,7 @@ tcp_restored(#host{name = Hostname, | |
| 949 1156 | NanoSeconds = cloudi_timestamp:convert(TimeRestored - TimeFailure, |
| 950 1157 | native, nanosecond), |
| 951 1158 | ?LOG(ErrorLogLevel, |
| 952 | - "\"~s\" TCP restored: ~s port ~w after ~s", |
| 1159 | + "\"~s\" TCP restored: ~s port ~w~n after ~s", |
| 953 1160 | [Hostname, inet:ntoa(IP), Port, |
| 954 1161 | cloudi_timestamp:nanoseconds_to_string(NanoSeconds)]), |
| 955 1162 | if |
| @@ -975,7 +1182,7 @@ ping_failure(#host{name = Hostname, | |
| 975 1182 | % executed for each ip address failure |
| 976 1183 | TimeFailure = cloudi_timestamp:native_monotonic(), |
| 977 1184 | ?LOG(ErrorLogLevel, |
| 978 | - "\"~s\" ping failure: ~s failed: ~s", |
| 1185 | + "\"~s\" ping failure: ~s failed:~n~s", |
| 979 1186 | [Hostname, inet:ntoa(IP), Reason]), |
| 980 1187 | if |
| 981 1188 | PingFailure =:= undefined -> |
| @@ -1016,7 +1223,7 @@ ping_restored(#host{name = Hostname, | |
| 1016 1223 | NanoSeconds = cloudi_timestamp:convert(TimeRestored - TimeFailure, |
| 1017 1224 | native, nanosecond), |
| 1018 1225 | ?LOG(ErrorLogLevel, |
| 1019 | - "\"~s\" ping restored: ~s after ~s", |
| 1226 | + "\"~s\" ping restored: ~s~n after ~s", |
| 1020 1227 | [Hostname, inet:ntoa(IP), |
| 1021 1228 | cloudi_timestamp:nanoseconds_to_string(NanoSeconds)]), |
| 1022 1229 | if |
| @@ -1284,6 +1491,15 @@ hosts_list_health_failed_time_ips(HealthFailedTimeIP, TimeOffset) -> | |
| 1284 1491 | microseconds_epoch_to_string(MicroSeconds)}] | L] |
| 1285 1492 | end, [], HealthFailedTimeIP). |
| 1286 1493 | |
| 1494 | + hostname_to_ascii(Name) -> |
| 1495 | + case lists:all(fun idna_ucs:is_ascii/1, Name) of |
| 1496 | + true -> |
| 1497 | + Name; |
| 1498 | + false -> |
| 1499 | + idna:utf8_to_ascii( |
| 1500 | + erlang:binary_to_list(unicode:characters_to_binary(Name))) |
| 1501 | + end. |
| 1502 | + |
| 1287 1503 | convert_term_to_erlang(Result) -> |
| 1288 1504 | convert_term_to_erlang_string(Result). |
| 1289 1505 | |
| @@ -1309,7 +1525,7 @@ convert_term_to_json_option([[{Key, _} | _] | _] = Value) | |
| 1309 1525 | || Options <- Value]; |
| 1310 1526 | convert_term_to_json_option([H | _] = Value) |
| 1311 1527 | when is_integer(H), H > 0 -> |
| 1312 | - erlang:list_to_binary(Value); |
| 1528 | + unicode:characters_to_binary(Value); |
| 1313 1529 | convert_term_to_json_option([[H | _] | _] = Value) |
| 1314 1530 | when is_integer(H), H > 0 -> |
| 1315 1531 | convert_term_to_json_strings(Value); |
| @@ -1335,7 +1551,7 @@ convert_term_to_json_options([{Key, Value} | Options]) -> | |
| 1335 1551 | convert_term_to_json_strings([]) -> |
| 1336 1552 | []; |
| 1337 1553 | convert_term_to_json_strings([S | L]) -> |
| 1338 | - [erlang:list_to_binary(S) | |
| 1554 | + [unicode:characters_to_binary(S) | |
| 1339 1555 | convert_term_to_json_strings(L)]. |
| 1340 1556 | |
| 1341 1557 | json_encode(Term) -> |