feat(prometheus): auth metrics with text/plain

This commit is contained in:
JimMoen 2024-01-12 05:40:28 +08:00
parent 092159b071
commit e0feb580b6
No known key found for this signature in database
9 changed files with 467 additions and 8 deletions

View File

@ -1106,6 +1106,7 @@ tr_prometheus_collectors(Conf) ->
prometheus_summary, prometheus_summary,
%% emqx collectors %% emqx collectors
emqx_prometheus, emqx_prometheus,
{'/prometheus/auth', emqx_prometheus_auth},
emqx_prometheus_mria emqx_prometheus_mria
%% builtin vm collectors %% builtin vm collectors
| prometheus_collectors(Conf) | prometheus_collectors(Conf)

View File

@ -16,3 +16,15 @@
-define(APP, emqx_prometheus). -define(APP, emqx_prometheus).
-define(PROMETHEUS, [prometheus]). -define(PROMETHEUS, [prometheus]).
-define(PROMETHEUS_DEFAULT_REGISTRY, default).
-define(PROMETHEUS_AUTH_REGISTRY, '/prometheus/auth').
-define(PROMETHEUS_AUTH_COLLECTOR, emqx_prometheus_auth).
-define(PROMETHEUS_DATA_INTEGRATION_REGISTRY, '/prometheus/data_integration').
-define(PROMETHEUS_DATA_INTEGRATION_COLLECTOR, emqx_prometheus_data_integration).
-define(PROMETHEUS_ALL_REGISTRYS, [
?PROMETHEUS_DEFAULT_REGISTRY,
?PROMETHEUS_AUTH_REGISTRY,
?PROMETHEUS_DATA_INTEGRATION_REGISTRY
]).

View File

@ -3,7 +3,8 @@
{deps, [ {deps, [
{emqx, {path, "../emqx"}}, {emqx, {path, "../emqx"}},
{emqx_utils, {path, "../emqx_utils"}}, {emqx_utils, {path, "../emqx_utils"}},
{prometheus, {git, "https://github.com/emqx/prometheus.erl", {tag, "v4.10.0.1"}}} {emqx_auth, {path, "../emqx_auth"}},
{prometheus, {git, "https://github.com/emqx/prometheus.erl", {tag, "v4.10.0.2"}}}
]}. ]}.
{edoc_opts, [{preprocess, true}]}. {edoc_opts, [{preprocess, true}]}.

View File

@ -5,7 +5,7 @@
{vsn, "5.0.19"}, {vsn, "5.0.19"},
{modules, []}, {modules, []},
{registered, [emqx_prometheus_sup]}, {registered, [emqx_prometheus_sup]},
{applications, [kernel, stdlib, prometheus, emqx, emqx_management]}, {applications, [kernel, stdlib, prometheus, emqx, emqx_auth, emqx_management]},
{mod, {emqx_prometheus_app, []}}, {mod, {emqx_prometheus_app, []}},
{env, []}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},

View File

@ -121,7 +121,7 @@ handle_info(_Msg, State) ->
{noreply, State}. {noreply, State}.
push_to_push_gateway(Url, Headers) when is_list(Headers) -> push_to_push_gateway(Url, Headers) when is_list(Headers) ->
Data = prometheus_text_format:format(), Data = prometheus_text_format:format(?PROMETHEUS_DEFAULT_REGISTRY),
case httpc:request(post, {Url, Headers, "text/plain", Data}, ?HTTP_OPTIONS, []) of case httpc:request(post, {Url, Headers, "text/plain", Data}, ?HTTP_OPTIONS, []) of
{ok, {{"HTTP/1.1", 200, _}, _RespHeaders, _RespBody}} -> {ok, {{"HTTP/1.1", 200, _}, _RespHeaders, _RespBody}} ->
ok; ok;
@ -168,10 +168,10 @@ join_url(Url, JobName0) ->
}), }),
lists:concat([Url, "/metrics/job/", unicode:characters_to_list(JobName1)]). lists:concat([Url, "/metrics/job/", unicode:characters_to_list(JobName1)]).
deregister_cleanup(_Registry) -> deregister_cleanup(?PROMETHEUS_DEFAULT_REGISTRY) ->
ok. ok.
collect_mf(_Registry, Callback) -> collect_mf(?PROMETHEUS_DEFAULT_REGISTRY, Callback) ->
Metrics = emqx_metrics:all(), Metrics = emqx_metrics:all(),
Stats = emqx_stats:getstats(), Stats = emqx_stats:getstats(),
VMData = emqx_vm_data(), VMData = emqx_vm_data(),
@ -192,6 +192,8 @@ collect_mf(_Registry, Callback) ->
_ = [add_collect_family(Name, Metrics, Callback, counter) || Name <- emqx_metrics_olp()], _ = [add_collect_family(Name, Metrics, Callback, counter) || Name <- emqx_metrics_olp()],
_ = [add_collect_family(Name, Metrics, Callback, counter) || Name <- emqx_metrics_acl()], _ = [add_collect_family(Name, Metrics, Callback, counter) || Name <- emqx_metrics_acl()],
_ = [add_collect_family(Name, Metrics, Callback, counter) || Name <- emqx_metrics_authn()], _ = [add_collect_family(Name, Metrics, Callback, counter) || Name <- emqx_metrics_authn()],
ok;
collect_mf(_Registry, _Callback) ->
ok. ok.
%% @private %% @private
@ -216,7 +218,7 @@ collect(<<"json">>) ->
session => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_session()]) session => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_session()])
}; };
collect(<<"prometheus">>) -> collect(<<"prometheus">>) ->
prometheus_text_format:format(). prometheus_text_format:format(?PROMETHEUS_DEFAULT_REGISTRY).
%% @private %% @private
collect_stats(Name, Stats) -> collect_stats(Name, Stats) ->
@ -809,6 +811,7 @@ cert_expiry_at_from_path(Path0) ->
{ok, PemBin} = file:read_file(Path), {ok, PemBin} = file:read_file(Path),
[CertEntry | _] = public_key:pem_decode(PemBin), [CertEntry | _] = public_key:pem_decode(PemBin),
Cert = public_key:pem_entry_decode(CertEntry), Cert = public_key:pem_entry_decode(CertEntry),
%% TODO: Not fully tested for all certs type
{'utcTime', NotAfterUtc} = {'utcTime', NotAfterUtc} =
Cert#'Certificate'.'tbsCertificate'#'TBSCertificate'.validity#'Validity'.'notAfter', Cert#'Certificate'.'tbsCertificate'#'TBSCertificate'.validity#'Validity'.'notAfter',
utc_time_to_epoch(NotAfterUtc). utc_time_to_epoch(NotAfterUtc).

View File

@ -28,7 +28,8 @@
-export([ -export([
setting/2, setting/2,
stats/2 stats/2,
auth/2
]). ]).
-define(TAGS, [<<"Monitor">>]). -define(TAGS, [<<"Monitor">>]).
@ -39,6 +40,7 @@ api_spec() ->
paths() -> paths() ->
[ [
"/prometheus", "/prometheus",
"/prometheus/auth",
"/prometheus/stats" "/prometheus/stats"
]. ].
@ -61,6 +63,18 @@ schema("/prometheus") ->
#{200 => prometheus_setting_response()} #{200 => prometheus_setting_response()}
} }
}; };
schema("/prometheus/auth") ->
#{
'operationId' => auth,
get =>
#{
description => ?DESC(get_prom_auth_data),
tags => ?TAGS,
security => security(),
responses =>
#{200 => prometheus_data_schema()}
}
};
schema("/prometheus/stats") -> schema("/prometheus/stats") ->
#{ #{
'operationId' => stats, 'operationId' => stats,
@ -114,6 +128,20 @@ stats(get, #{headers := Headers}) ->
{200, #{<<"content-type">> => <<"text/plain">>}, Data} {200, #{<<"content-type">> => <<"text/plain">>}, Data}
end. end.
auth(get, #{headers := Headers}) ->
Type =
case maps:get(<<"accept">>, Headers, <<"text/plain">>) of
<<"application/json">> -> <<"json">>;
_ -> <<"prometheus">>
end,
Data = emqx_prometheus_auth:collect(Type),
case Type of
<<"json">> ->
{200, Data};
<<"prometheus">> ->
{200, #{<<"content-type">> => <<"text/plain">>}, Data}
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal funcs %% Internal funcs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -0,0 +1,400 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_prometheus_auth).
-export([
deregister_cleanup/1,
collect_mf/2,
collect_metrics/2
]).
-export([collect/1]).
-include("emqx_prometheus.hrl").
-include_lib("emqx_auth/include/emqx_authn_chains.hrl").
-include_lib("prometheus/include/prometheus.hrl").
-import(
prometheus_model_helpers,
[
create_mf/5,
gauge_metric/1,
gauge_metrics/1
]
).
-type authn_metric_key() ::
emqx_authn_enable
| emqx_authn_status
| emqx_authn_nomatch
| emqx_authn_total
| emqx_authn_success
| emqx_authn_failed
| emqx_authn_rate
| emqx_authn_rate_last5m
| emqx_authn_rate_max.
-type authz_metric_key() ::
emqx_authz_enable
| emqx_authz_status
| emqx_authz_nomatch
| emqx_authz_total
| emqx_authz_success
| emqx_authz_failed
| emqx_authz_rate
| emqx_authz_rate_last5m
| emqx_authz_rate_max.
%% Please don't remove this attribute, prometheus uses it to
%% automatically register collectors.
-behaviour(prometheus_collector).
%%--------------------------------------------------------------------
%% Macros
%%--------------------------------------------------------------------
-define(METRIC_NAME_PREFIX, "emqx_auth_").
-define(MG(K, MAP), maps:get(K, MAP)).
-define(MG0(K, MAP), maps:get(K, MAP, 0)).
%%--------------------------------------------------------------------
%% Collector API
%%--------------------------------------------------------------------
%% @private
deregister_cleanup(_) -> ok.
%% @private
-spec collect_mf(_Registry, Callback) -> ok when
_Registry :: prometheus_registry:registry(),
Callback :: prometheus_collector:collect_mf_callback().
%% erlfmt-ignore
collect_mf(?PROMETHEUS_AUTH_REGISTRY, Callback) ->
_ = [add_collect_family(Name, authn_data(), Callback, gauge) || Name <- authn()],
_ = [add_collect_family(Name, authn_users_count_data(), Callback, gauge) || Name <- authn_users_count()],
_ = [add_collect_family(Name, authz_data(), Callback, gauge) || Name <- authz()],
_ = [add_collect_family(Name, authz_rules_count_data(), Callback, gauge) || Name <- authz_rules_count()],
_ = [add_collect_family(Name, banned_count_data(), Callback, gauge) || Name <- banned()],
ok;
collect_mf(_, _) ->
ok.
%% @private
collect(<<"json">>) ->
%% TODO
#{};
collect(<<"prometheus">>) ->
prometheus_text_format:format(?PROMETHEUS_AUTH_REGISTRY).
add_collect_family(Name, Data, Callback, Type) ->
Callback(create_mf(Name, _Help = <<"">>, Type, ?MODULE, Data)).
collect_metrics(Name, Metrics) ->
collect_auth(Name, Metrics).
%%--------------------------------------------------------------------
%% Collector
%%--------------------------------------------------------------------
%%====================
%% Authn overview
collect_auth(K = emqx_authn_enable, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_status, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_nomatch, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_total, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_success, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_failed, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_rate, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_rate_last5m, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authn_rate_max, Data) ->
gauge_metrics(?MG(K, Data));
%%====================
%% Authn users count
%% Only provided for `password_based:built_in_database` and `scram:built_in_database`
collect_auth(K = emqx_authn_users_count, Data) ->
gauge_metrics(?MG(K, Data));
%%====================
%% Authz overview
collect_auth(K = emqx_authz_enable, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_status, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_nomatch, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_total, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_success, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_failed, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_rate, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_rate_last5m, Data) ->
gauge_metrics(?MG(K, Data));
collect_auth(K = emqx_authz_rate_max, Data) ->
gauge_metrics(?MG(K, Data));
%%====================
%% Authz rules count
%% Only provided for `file` and `built_in_database`
collect_auth(K = emqx_authz_rules_count, Data) ->
gauge_metrics(?MG(K, Data));
%%====================
%% Banned
collect_auth(emqx_banned_count, Data) ->
gauge_metric(Data).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
%%========================================
%% AuthN (Authentication)
%%========================================
%%====================
%% Authn overview
authn() ->
[
emqx_authn_enable,
emqx_authn_status,
emqx_authn_nomatch,
emqx_authn_total,
emqx_authn_success,
emqx_authn_failed,
emqx_authn_rate,
emqx_authn_rate_last5m,
emqx_authn_rate_max
].
-spec authn_data() -> #{Key => [Point]} when
Key :: authn_metric_key(),
Point :: {[Label], Metric},
Label :: IdLabel,
IdLabel :: {id, AuthnName :: binary()},
Metric :: number().
authn_data() ->
Authns = emqx_config:get([authentication]),
lists:foldl(
fun(Key, AccIn) ->
AccIn#{Key => authn_backend_to_points(Key, Authns)}
end,
#{},
authn()
).
-spec authn_backend_to_points(Key, list(Authn)) -> list(Point) when
Key :: authn_metric_key(),
Authn :: map(),
Point :: {[Label], Metric},
Label :: IdLabel,
IdLabel :: {id, AuthnName :: binary()},
Metric :: number().
authn_backend_to_points(Key, Authns) ->
do_authn_backend_to_points(Key, Authns, []).
do_authn_backend_to_points(_K, [], AccIn) ->
lists:reverse(AccIn);
do_authn_backend_to_points(K, [Authn | Rest], AccIn) ->
Id = authenticator_id(Authn),
Point = {[{id, Id}], do_metric(K, Authn, lookup_authn_metrics_local(Id))},
do_authn_backend_to_points(K, Rest, [Point | AccIn]).
lookup_authn_metrics_local(Id) ->
case emqx_authn_api:lookup_from_local_node(?GLOBAL, Id) of
{ok, {_Node, Status, #{counters := Counters, rate := Rate}, _ResourceMetrics}} ->
#{
emqx_authn_status => status_to_number(Status),
emqx_authn_nomatch => ?MG0(nomatch, Counters),
emqx_authn_total => ?MG0(total, Counters),
emqx_authn_success => ?MG0(success, Counters),
emqx_authn_failed => ?MG0(failed, Counters),
emqx_authn_rate => ?MG0(current, Rate),
emqx_authn_rate_last5m => ?MG0(last5m, Rate),
emqx_authn_rate_max => ?MG0(max, Rate)
};
{error, _Reason} ->
maps:from_keys(authn() -- [emqx_authn_enable], 0)
end.
%%====================
%% Authn users count
authn_users_count() ->
[emqx_authn_users_count].
-define(AUTHN_MNESIA, emqx_authn_mnesia).
-define(AUTHN_SCRAM_MNESIA, emqx_authn_scram_mnesia).
authn_users_count_data() ->
Samples = lists:foldl(
fun
(#{backend := built_in_database, mechanism := password_based} = Authn, AccIn) ->
[auth_data_sample_point(authn, Authn, ?AUTHN_MNESIA) | AccIn];
(#{backend := built_in_database, mechanism := scram} = Authn, AccIn) ->
[auth_data_sample_point(authn, Authn, ?AUTHN_SCRAM_MNESIA) | AccIn];
(_, AccIn) ->
AccIn
end,
[],
emqx_config:get([authentication])
),
#{emqx_authn_users_count => Samples}.
%%========================================
%% AuthZ (Authorization)
%%========================================
%%====================
%% Authz overview
authz() ->
[
emqx_authz_enable,
emqx_authz_status,
emqx_authz_nomatch,
emqx_authz_total,
emqx_authz_success,
emqx_authz_failed,
emqx_authz_rate,
emqx_authz_rate_last5m,
emqx_authz_rate_max
].
-spec authz_data() -> #{Key => [Point]} when
Key :: authz_metric_key(),
Point :: {[Label], Metric},
Label :: TypeLabel,
TypeLabel :: {type, AuthZType :: binary()},
Metric :: number().
authz_data() ->
Authzs = emqx_config:get([authorization, sources]),
lists:foldl(
fun(Key, AccIn) ->
AccIn#{Key => authz_backend_to_points(Key, Authzs)}
end,
#{},
authz()
).
-spec authz_backend_to_points(Key, list(Authz)) -> list(Point) when
Key :: authz_metric_key(),
Authz :: map(),
Point :: {[Label], Metric},
Label :: TypeLabel,
TypeLabel :: {type, AuthZType :: binary()},
Metric :: number().
authz_backend_to_points(Key, Authzs) ->
do_authz_backend_to_points(Key, Authzs, []).
do_authz_backend_to_points(_K, [], AccIn) ->
lists:reverse(AccIn);
do_authz_backend_to_points(K, [Authz | Rest], AccIn) ->
Type = maps:get(type, Authz),
Point = {[{type, Type}], do_metric(K, Authz, lookup_authz_metrics_local(Type))},
do_authz_backend_to_points(K, Rest, [Point | AccIn]).
lookup_authz_metrics_local(Type) ->
case emqx_authz_api_sources:lookup_from_local_node(Type) of
{ok, {_Node, Status, #{counters := Counters, rate := Rate}, _ResourceMetrics}} ->
#{
emqx_authz_status => status_to_number(Status),
emqx_authz_nomatch => ?MG0(nomatch, Counters),
emqx_authz_total => ?MG0(total, Counters),
emqx_authz_success => ?MG0(success, Counters),
emqx_authz_failed => ?MG0(failed, Counters),
emqx_authz_rate => ?MG0(current, Rate),
emqx_authz_rate_last5m => ?MG0(last5m, Rate),
emqx_authz_rate_max => ?MG0(max, Rate)
};
{error, _Reason} ->
maps:from_keys(authz() -- [emqx_authz_enable], 0)
end.
%%====================
%% Authz rules count
authz_rules_count() ->
[emqx_authz_rules_count].
-define(ACL_TABLE, emqx_acl).
authz_rules_count_data() ->
Samples = lists:foldl(
fun
(#{type := built_in_database} = Authz, AccIn) ->
[auth_data_sample_point(authz, Authz, ?ACL_TABLE) | AccIn];
(#{type := file}, AccIn) ->
#{annotations := #{rules := Rules}} = emqx_authz:lookup(file),
Size = erlang:length(Rules),
[{[{type, file}], Size} | AccIn];
(_, AccIn) ->
AccIn
end,
[],
emqx_config:get([authorization, sources])
),
#{emqx_authz_rules_count => Samples}.
%%========================================
%% Banned
%%========================================
%%====================
%% Banned count
banned() ->
[emqx_banned_count].
-define(BANNED_TABLE, emqx_banned).
banned_count_data() ->
mnesia_size(?BANNED_TABLE).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
authenticator_id(Authn) ->
emqx_authn_chains:authenticator_id(Authn).
auth_data_sample_point(authn, Authn, Tab) ->
Size = mnesia_size(Tab),
Id = authenticator_id(Authn),
{[{id, Id}], Size};
auth_data_sample_point(authz, #{type := Type} = _Authz, Tab) ->
Size = mnesia_size(Tab),
{[{type, Type}], Size}.
mnesia_size(Tab) ->
mnesia:table_info(Tab, size).
do_metric(emqx_authn_enable, #{enable := B}, _) ->
boolean_to_number(B);
do_metric(K, _, Metrics) ->
?MG0(K, Metrics).
boolean_to_number(true) -> 1;
boolean_to_number(false) -> 0.
status_to_number(connected) -> 1;
status_to_number(stopped) -> 0.

View File

@ -101,7 +101,7 @@ post_config_update(_ConfPath, _Req, _NewConf, _OldConf, _AppEnvs) ->
ok. ok.
update_prometheus(AppEnvs) -> update_prometheus(AppEnvs) ->
PrevCollectors = prometheus_registry:collectors(default), PrevCollectors = all_collectors(),
CurCollectors = proplists:get_value(collectors, proplists:get_value(prometheus, AppEnvs)), CurCollectors = proplists:get_value(collectors, proplists:get_value(prometheus, AppEnvs)),
lists:foreach( lists:foreach(
fun prometheus_registry:deregister_collector/1, fun prometheus_registry:deregister_collector/1,
@ -113,6 +113,15 @@ update_prometheus(AppEnvs) ->
), ),
application:set_env(AppEnvs). application:set_env(AppEnvs).
all_collectors() ->
lists:foldl(
fun(Registry, AccIn) ->
prometheus_registry:collectors(Registry) ++ AccIn
end,
_InitAcc = [],
?PROMETHEUS_ALL_REGISTRYS
).
update_push_gateway(Prometheus) -> update_push_gateway(Prometheus) ->
case is_push_gateway_server_enabled(Prometheus) of case is_push_gateway_server_enabled(Prometheus) of
true -> true ->

View File

@ -15,4 +15,9 @@ get_prom_data.desc:
get_prom_data.label: get_prom_data.label:
"""Prometheus Metrics""" """Prometheus Metrics"""
get_prom_auth_data.desc:
"""Get Prometheus Metrics for AuthN, AuthZ and Banned"""
get_prom_auth_data.label:
"""Prometheus Metrics for Auth"""
} }