From 092159b07106868c674564229fb926a51632ee30 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 12 Jan 2024 03:44:57 +0800 Subject: [PATCH] feat(prometheus): cert expiry epoch in endpoint `/prometheus/stats` --- apps/emqx_prometheus/src/emqx_prometheus.erl | 135 +++++++++++++++++- .../src/emqx_prometheus_api.erl | 2 +- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 327586996..3ac32a47c 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -24,6 +24,7 @@ -include("emqx_prometheus.hrl"). +-include_lib("public_key/include/public_key.hrl"). -include_lib("prometheus/include/prometheus_model.hrl"). -include_lib("emqx/include/logger.hrl"). @@ -32,6 +33,7 @@ [ create_mf/5, gauge_metric/1, + gauge_metrics/1, counter_metric/1 ] ). @@ -175,7 +177,10 @@ collect_mf(_Registry, Callback) -> VMData = emqx_vm_data(), LicenseData = emqx_license_data(), ClusterData = emqx_cluster_data(), + CertsData = emqx_certs_data(), + %% TODO: license expiry epoch and cert expiry epoch should be cached _ = [add_collect_family(Name, LicenseData, Callback, gauge) || Name <- emqx_license()], + _ = [add_collect_family(Name, CertsData, Callback, gauge) || Name <- emqx_certs()], _ = [add_collect_family(Name, Stats, Callback, gauge) || Name <- emqx_stats:names()], _ = [add_collect_family(Name, VMData, Callback, gauge) || Name <- emqx_vm()], _ = [add_collect_family(Name, ClusterData, Callback, gauge) || Name <- emqx_cluster()], @@ -195,8 +200,13 @@ collect(<<"json">>) -> Stats = emqx_stats:getstats(), VMData = emqx_vm_data(), LicenseData = emqx_license_data(), + %% TODO: FIXME! + %% emqx_metrics_olp()), + %% emqx_metrics_acl()), + %% emqx_metrics_authn()), #{ license => maps:from_list([collect_stats(Name, LicenseData) || Name <- emqx_license()]), + certs => collect_certs_json(emqx_certs_data()), stats => maps:from_list([collect_stats(Name, Stats) || Name <- emqx_stats:names()]), metrics => maps:from_list([collect_stats(Name, VMData) || Name <- emqx_vm()]), packets => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]), @@ -223,10 +233,7 @@ collect_metrics(Name, Metrics) -> emqx_collect(Name, Metrics). add_collect_family(Name, Data, Callback, Type) -> - Callback(create_schema(Name, <<"">>, Data, Type)). - -create_schema(Name, Help, Data, Type) -> - create_mf(Name, Help, Type, ?MODULE, Data). + Callback(create_mf(Name, _Help = <<"">>, Type, ?MODULE, Data)). %%-------------------------------------------------------------------- %% Collector @@ -529,7 +536,11 @@ emqx_collect(emqx_cluster_nodes_stopped, ClusterData) -> %%-------------------------------------------------------------------- %% License emqx_collect(emqx_license_expiry_at, LicenseData) -> - gauge_metric(?C(expiry_at, LicenseData)). + gauge_metric(?C(expiry_at, LicenseData)); +%%-------------------------------------------------------------------- +%% Certs +emqx_collect(emqx_cert_expiry_at, CertsData) -> + gauge_metrics(CertsData). %%-------------------------------------------------------------------- %% Indicators @@ -704,6 +715,120 @@ emqx_license_data() -> {expiry_at, emqx_license_checker:expiry_epoch()} ]. +emqx_certs() -> + [ + emqx_cert_expiry_at + ]. + +-define(LISTENER_TYPES, [ssl, wss, quic]). + +-spec emqx_certs_data() -> + [_Point :: {[Label], Epoch}] +when + Label :: TypeLabel | NameLabel | CertTypeLabel, + TypeLabel :: {listener_type, ssl | wss | quic}, + NameLabel :: {listener_name, atom()}, + CertTypeLabel :: {cert_type, cacertfile | certfile}, + Epoch :: non_neg_integer(). +emqx_certs_data() -> + case emqx_config:get([listeners], undefined) of + undefined -> + []; + AllListeners when is_map(AllListeners) -> + lists:foldl( + fun(ListenerType, PointsAcc) -> + PointsAcc ++ + points_of_listeners(ListenerType, AllListeners) + end, + _PointsInitAcc = [], + ?LISTENER_TYPES + ) + end. + +points_of_listeners(Type, AllListeners) -> + do_points_of_listeners(Type, maps:get(Type, AllListeners, undefined)). + +-define(CERT_TYPES, [cacertfile, certfile]). + +-spec do_points_of_listeners(Type, TypeOfListeners) -> + [_Point :: {[{LabelKey, LabelValue}], Epoch}] +when + Type :: ssl | wss | quic, + TypeOfListeners :: #{ListenerName :: atom() => ListenerConf :: map()} | undefined, + LabelKey :: atom(), + LabelValue :: atom(), + Epoch :: non_neg_integer(). +do_points_of_listeners(_, undefined) -> + []; +do_points_of_listeners(ListenerType, TypeOfListeners) -> + lists:foldl( + fun(Name, PointsAcc) -> + lists:foldl( + fun(CertType, AccIn) -> + case + emqx_utils_maps:deep_get( + [Name, ssl_options, CertType], TypeOfListeners, undefined + ) + of + undefined -> AccIn; + Path -> [gen_point(ListenerType, Name, CertType, Path) | AccIn] + end + end, + [], + ?CERT_TYPES + ) ++ PointsAcc + end, + [], + maps:keys(TypeOfListeners) + ). + +gen_point(Type, Name, CertType, Path) -> + { + %% Labels: [{_Labelkey, _LabelValue}] + [ + {listener_type, Type}, + {listener_name, Name}, + {cert_type, CertType} + ], + %% Value + cert_expiry_at_from_path(Path) + }. + +collect_certs_json(CertsData) -> + lists:foldl( + fun({Labels, Data}, AccIn) -> + [(maps:from_list(Labels))#{emqx_cert_expiry_at => Data} | AccIn] + end, + _InitAcc = [], + CertsData + ). + +%% TODO: cert manager for more generic utils functions +cert_expiry_at_from_path(Path0) -> + Path = emqx_schema:naive_env_interpolation(Path0), + {ok, PemBin} = file:read_file(Path), + [CertEntry | _] = public_key:pem_decode(PemBin), + Cert = public_key:pem_entry_decode(CertEntry), + {'utcTime', NotAfterUtc} = + Cert#'Certificate'.'tbsCertificate'#'TBSCertificate'.validity#'Validity'.'notAfter', + utc_time_to_epoch(NotAfterUtc). + +utc_time_to_epoch(UtcTime) -> + date_to_expiry_epoch(utc_time_to_datetime(UtcTime)). + +utc_time_to_datetime(Str) -> + {ok, [Year, Month, Day, Hour, Minute, Second], _} = io_lib:fread( + "~2d~2d~2d~2d~2d~2dZ", Str + ), + %% Alwoys Assuming YY is in 2000 + {{2000 + Year, Month, Day}, {Hour, Minute, Second}}. + +%% 62167219200 =:= calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}). +-define(EPOCH_START, 62167219200). +-spec date_to_expiry_epoch(calendar:datetime()) -> Seconds :: non_neg_integer(). +date_to_expiry_epoch(DateTime) -> + calendar:datetime_to_gregorian_seconds(DateTime) - ?EPOCH_START. + %% deprecated_since 5.0.10, remove this when 5.1.x do_start() -> emqx_prometheus_sup:start_child(?APP). diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 44e0fac16..5bfa3e3a5 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -181,7 +181,7 @@ recommend_setting_example() -> prometheus_data_schema() -> #{ description => - <<"Get Prometheus Data. Note that support for JSON output is deprecated and will be removed in v5.2.">>, + <<"Get Prometheus Data.">>, content => [ {'text/plain', #{schema => #{type => string}}},