499 lines
16 KiB
Erlang
499 lines
16 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 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]).
|
|
|
|
%% for bpapi
|
|
-behaviour(emqx_prometheus_cluster).
|
|
-export([
|
|
fetch_from_local_node/1,
|
|
fetch_cluster_consistented_data/0,
|
|
aggre_or_zip_init_acc/0,
|
|
logic_sum_metrics/0
|
|
]).
|
|
|
|
%% %% @private
|
|
-export([
|
|
zip_json_auth_metrics/3
|
|
]).
|
|
|
|
-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,
|
|
counter_metrics/1
|
|
]
|
|
).
|
|
|
|
-type authn_metric_name() ::
|
|
emqx_authn_enable
|
|
| emqx_authn_status
|
|
| emqx_authn_nomatch
|
|
| emqx_authn_total
|
|
| emqx_authn_success
|
|
| emqx_authn_failed.
|
|
|
|
-type authz_metric_name() ::
|
|
emqx_authz_enable
|
|
| emqx_authz_status
|
|
| emqx_authz_nomatch
|
|
| emqx_authz_total
|
|
| emqx_authz_allow
|
|
| emqx_authz_deny.
|
|
|
|
%% 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)).
|
|
-define(PG0(K, PROPLISTS), proplists:get_value(K, PROPLISTS, 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) ->
|
|
RawData = emqx_prometheus_cluster:raw_data(?MODULE, ?GET_PROM_DATA_MODE()),
|
|
ok = add_collect_family(Callback, authn_metric_meta(), ?MG(authn_data, RawData)),
|
|
ok = add_collect_family(Callback, authn_users_count_metric_meta(), ?MG(authn_users_count_data, RawData)),
|
|
ok = add_collect_family(Callback, authz_metric_meta(), ?MG(authz_data, RawData)),
|
|
ok = add_collect_family(Callback, authz_rules_count_metric_meta(), ?MG(authz_rules_count_data, RawData)),
|
|
ok = add_collect_family(Callback, banned_count_metric_meta(), ?MG(banned_count_data, RawData)),
|
|
ok;
|
|
collect_mf(_, _) ->
|
|
ok.
|
|
|
|
%% @private
|
|
collect(<<"json">>) ->
|
|
RawData = emqx_prometheus_cluster:raw_data(?MODULE, ?GET_PROM_DATA_MODE()),
|
|
#{
|
|
emqx_authn => collect_json_data(?MG(authn_data, RawData)),
|
|
emqx_authz => collect_json_data(?MG(authz_data, RawData)),
|
|
emqx_banned => collect_banned_data()
|
|
};
|
|
collect(<<"prometheus">>) ->
|
|
prometheus_text_format:format(?PROMETHEUS_AUTH_REGISTRY).
|
|
|
|
add_collect_family(Callback, MetricWithType, Data) ->
|
|
_ = [add_collect_family(Name, Data, Callback, Type) || {Name, Type} <- MetricWithType],
|
|
ok.
|
|
|
|
add_collect_family(Name, Data, Callback, Type) ->
|
|
Callback(create_mf(Name, _Help = <<"">>, Type, ?MODULE, Data)).
|
|
|
|
collect_metrics(Name, Metrics) ->
|
|
collect_auth(Name, Metrics).
|
|
|
|
%% behaviour
|
|
fetch_from_local_node(Mode) ->
|
|
{node(self()), #{
|
|
authn_data => authn_data(Mode),
|
|
authz_data => authz_data(Mode)
|
|
}}.
|
|
|
|
fetch_cluster_consistented_data() ->
|
|
#{
|
|
authn_users_count_data => authn_users_count_data(),
|
|
authz_rules_count_data => authz_rules_count_data(),
|
|
banned_count_data => banned_count_data()
|
|
}.
|
|
|
|
aggre_or_zip_init_acc() ->
|
|
#{
|
|
authn_data => maps:from_keys(authn_metric(names), []),
|
|
authz_data => maps:from_keys(authz_metric(names), [])
|
|
}.
|
|
|
|
logic_sum_metrics() ->
|
|
[
|
|
emqx_authn_enable,
|
|
emqx_authn_status,
|
|
emqx_authz_enable,
|
|
emqx_authz_status
|
|
].
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% 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) ->
|
|
counter_metrics(?MG(K, Data));
|
|
collect_auth(K = emqx_authn_total, Data) ->
|
|
counter_metrics(?MG(K, Data));
|
|
collect_auth(K = emqx_authn_success, Data) ->
|
|
counter_metrics(?MG(K, Data));
|
|
collect_auth(K = emqx_authn_failed, Data) ->
|
|
counter_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) ->
|
|
counter_metrics(?MG(K, Data));
|
|
collect_auth(K = emqx_authz_total, Data) ->
|
|
counter_metrics(?MG(K, Data));
|
|
collect_auth(K = emqx_authz_allow, Data) ->
|
|
counter_metrics(?MG(K, Data));
|
|
collect_auth(K = emqx_authz_deny, Data) ->
|
|
counter_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_metric_meta() ->
|
|
[
|
|
{emqx_authn_enable, gauge},
|
|
{emqx_authn_status, gauge},
|
|
{emqx_authn_nomatch, counter},
|
|
{emqx_authn_total, counter},
|
|
{emqx_authn_success, counter},
|
|
{emqx_authn_failed, counter}
|
|
].
|
|
|
|
authn_metric(names) ->
|
|
emqx_prometheus_cluster:metric_names(authn_metric_meta()).
|
|
|
|
-spec authn_data(atom()) -> #{Key => [Point]} when
|
|
Key :: authn_metric_name(),
|
|
Point :: {[Label], Metric},
|
|
Label :: IdLabel,
|
|
IdLabel :: {id, AuthnName :: binary()},
|
|
Metric :: number().
|
|
authn_data(Mode) ->
|
|
Authns = emqx_config:get([authentication]),
|
|
lists:foldl(
|
|
fun(Key, AccIn) ->
|
|
AccIn#{Key => authn_backend_to_points(Mode, Key, Authns)}
|
|
end,
|
|
#{},
|
|
authn_metric(names)
|
|
).
|
|
|
|
-spec authn_backend_to_points(atom(), Key, list(Authn)) -> list(Point) when
|
|
Key :: authn_metric_name(),
|
|
Authn :: map(),
|
|
Point :: {[Label], Metric},
|
|
Label :: IdLabel,
|
|
IdLabel :: {id, AuthnName :: binary()},
|
|
Metric :: number().
|
|
authn_backend_to_points(Mode, Key, Authns) ->
|
|
do_authn_backend_to_points(Mode, Key, Authns, []).
|
|
|
|
do_authn_backend_to_points(_Mode, _K, [], AccIn) ->
|
|
lists:reverse(AccIn);
|
|
do_authn_backend_to_points(Mode, K, [Authn | Rest], AccIn) ->
|
|
Id = authenticator_id(Authn),
|
|
Point = {
|
|
with_node_label(Mode, [{id, Id}]),
|
|
do_metric(K, Authn, lookup_authn_metrics_local(Id))
|
|
},
|
|
do_authn_backend_to_points(Mode, 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}, _ResourceMetrics}} ->
|
|
#{
|
|
emqx_authn_status => emqx_prometheus_cluster: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)
|
|
};
|
|
{error, _Reason} ->
|
|
maps:from_keys(authn_metric(names) -- [emqx_authn_enable], 0)
|
|
end.
|
|
|
|
%%====================
|
|
%% Authn users count
|
|
|
|
authn_users_count_metric_meta() ->
|
|
[
|
|
{emqx_authn_users_count, gauge}
|
|
].
|
|
|
|
-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_metric_meta() ->
|
|
[
|
|
{emqx_authz_enable, gauge},
|
|
{emqx_authz_status, gauge},
|
|
{emqx_authz_nomatch, counter},
|
|
{emqx_authz_total, counter},
|
|
{emqx_authz_allow, counter},
|
|
{emqx_authz_deny, counter}
|
|
].
|
|
|
|
authz_metric(names) ->
|
|
emqx_prometheus_cluster:metric_names(authz_metric_meta()).
|
|
|
|
-spec authz_data(atom()) -> #{Key => [Point]} when
|
|
Key :: authz_metric_name(),
|
|
Point :: {[Label], Metric},
|
|
Label :: TypeLabel,
|
|
TypeLabel :: {type, AuthZType :: binary()},
|
|
Metric :: number().
|
|
authz_data(Mode) ->
|
|
Authzs = emqx_config:get([authorization, sources]),
|
|
lists:foldl(
|
|
fun(Key, AccIn) ->
|
|
AccIn#{Key => authz_backend_to_points(Mode, Key, Authzs)}
|
|
end,
|
|
#{},
|
|
authz_metric(names)
|
|
).
|
|
|
|
-spec authz_backend_to_points(atom(), Key, list(Authz)) -> list(Point) when
|
|
Key :: authz_metric_name(),
|
|
Authz :: map(),
|
|
Point :: {[Label], Metric},
|
|
Label :: TypeLabel,
|
|
TypeLabel :: {type, AuthZType :: binary()},
|
|
Metric :: number().
|
|
authz_backend_to_points(Mode, Key, Authzs) ->
|
|
do_authz_backend_to_points(Mode, Key, Authzs, []).
|
|
|
|
do_authz_backend_to_points(_Mode, _K, [], AccIn) ->
|
|
lists:reverse(AccIn);
|
|
do_authz_backend_to_points(Mode, K, [Authz | Rest], AccIn) ->
|
|
Type = maps:get(type, Authz),
|
|
Point = {
|
|
with_node_label(Mode, [{type, Type}]),
|
|
do_metric(K, Authz, lookup_authz_metrics_local(Type))
|
|
},
|
|
do_authz_backend_to_points(Mode, 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}, _ResourceMetrics}} ->
|
|
#{
|
|
emqx_authz_status => emqx_prometheus_cluster:status_to_number(Status),
|
|
emqx_authz_nomatch => ?MG0(nomatch, Counters),
|
|
emqx_authz_total => ?MG0(total, Counters),
|
|
emqx_authz_allow => ?MG0(allow, Counters),
|
|
emqx_authz_deny => ?MG0(deny, Counters)
|
|
};
|
|
{error, _Reason} ->
|
|
maps:from_keys(authz_metric(names) -- [emqx_authz_enable], 0)
|
|
end.
|
|
|
|
%%====================
|
|
%% Authz rules count
|
|
|
|
authz_rules_count_metric_meta() ->
|
|
[
|
|
{emqx_authz_rules_count, gauge}
|
|
].
|
|
|
|
-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_count_metric_meta() ->
|
|
[
|
|
{emqx_banned_count, gauge}
|
|
].
|
|
-define(BANNED_TABLE,
|
|
emqx_banned
|
|
).
|
|
banned_count_data() ->
|
|
mnesia_size(?BANNED_TABLE).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Collect functions
|
|
%%--------------------------------------------------------------------
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% merge / zip formatting funcs for type `application/json`
|
|
|
|
collect_json_data(Data) ->
|
|
emqx_prometheus_cluster:collect_json_data(Data, fun zip_json_auth_metrics/3).
|
|
|
|
collect_banned_data() ->
|
|
#{emqx_banned_count => banned_count_data()}.
|
|
|
|
%% for initialized empty AccIn
|
|
%% The following fields will be put into Result
|
|
%% For Authn:
|
|
%% `id`, `emqx_authn_users_count`
|
|
%% For Authz:
|
|
%% `type`, `emqx_authz_rules_count`n
|
|
zip_json_auth_metrics(Key, Points, [] = _AccIn) ->
|
|
lists:foldl(
|
|
fun({Lables, Metric}, AccIn2) ->
|
|
LablesKVMap = maps:from_list(Lables),
|
|
Point = (maps:merge(LablesKVMap, users_or_rule_count(LablesKVMap)))#{Key => Metric},
|
|
[Point | AccIn2]
|
|
end,
|
|
[],
|
|
Points
|
|
);
|
|
zip_json_auth_metrics(Key, Points, AllResultedAcc) ->
|
|
ThisKeyResult = lists:foldl(emqx_prometheus_cluster:point_to_map_fun(Key), [], Points),
|
|
lists:zipwith(fun maps:merge/2, AllResultedAcc, ThisKeyResult).
|
|
|
|
users_or_rule_count(#{id := Id}) ->
|
|
#{emqx_authn_users_count := Points} = authn_users_count_data(),
|
|
case lists:keyfind([{id, Id}], 1, Points) of
|
|
{_, Metric} ->
|
|
#{emqx_authn_users_count => Metric};
|
|
false ->
|
|
#{}
|
|
end;
|
|
users_or_rule_count(#{type := Type}) ->
|
|
#{emqx_authz_rules_count := Points} = authz_rules_count_data(),
|
|
case lists:keyfind([{type, Type}], 1, Points) of
|
|
{_, Metric} ->
|
|
#{emqx_authz_rules_count => Metric};
|
|
false ->
|
|
#{}
|
|
end;
|
|
users_or_rule_count(_) ->
|
|
#{}.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% Helper funcs
|
|
|
|
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}, _) ->
|
|
emqx_prometheus_cluster:boolean_to_number(B);
|
|
do_metric(emqx_authz_enable, #{enable := B}, _) ->
|
|
emqx_prometheus_cluster:boolean_to_number(B);
|
|
do_metric(K, _, Metrics) ->
|
|
?MG0(K, Metrics).
|
|
|
|
with_node_label(?PROM_DATA_MODE__NODE, Labels) ->
|
|
Labels;
|
|
with_node_label(?PROM_DATA_MODE__ALL_NODES_AGGREGATED, Labels) ->
|
|
Labels;
|
|
with_node_label(?PROM_DATA_MODE__ALL_NODES_UNAGGREGATED, Labels) ->
|
|
[{node, node(self())} | Labels].
|