Packages

Erlang/Elixir Cloud Framework Health Check CloudI Service

Current section

3 Versions

Jump to

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()) -&gt; 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()) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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) -&gt; 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) ->