1 |
|
-module(service_mongoose_system_metrics). |
2 |
|
-author('jan.ciesla@erlang-solutions.com'). |
3 |
|
|
4 |
|
-behaviour(mongoose_service). |
5 |
|
-behaviour(gen_server). |
6 |
|
|
7 |
|
-include("mongoose_config_spec.hrl"). |
8 |
|
|
9 |
|
-define(DEFAULT_INITIAL_REPORT, timer:minutes(5)). |
10 |
|
-define(DEFAULT_REPORT_AFTER, timer:hours(3)). |
11 |
|
-ifdef(PROD_NODE). |
12 |
|
-define(TRACKING_ID, "UA-151671255-3"). |
13 |
|
-else. |
14 |
|
-define(TRACKING_ID, "UA-151671255-2"). |
15 |
|
-endif. |
16 |
|
-define(TRACKING_ID_CI, "UA-151671255-1"). |
17 |
|
|
18 |
|
-include("mongoose.hrl"). |
19 |
|
|
20 |
|
-export([start/1, stop/0, config_spec/0]). |
21 |
|
-export([start_link/1, |
22 |
|
init/1, |
23 |
|
handle_call/3, |
24 |
|
handle_cast/2, |
25 |
|
handle_info/2, |
26 |
|
terminate/2]). |
27 |
|
|
28 |
|
%% config spec callbacks |
29 |
|
-export([process_report_option/1]). |
30 |
|
|
31 |
|
-export([verify_if_configured/0]). |
32 |
|
|
33 |
|
-ignore_xref([start_link/1]). |
34 |
|
|
35 |
|
-record(system_metrics_state, |
36 |
|
{report_after :: non_neg_integer(), |
37 |
|
reporter_monitor = none :: none | reference(), |
38 |
|
reporter_pid = none :: none | pid(), |
39 |
|
prev_report = [] :: [mongoose_system_metrics_collector:report_struct()], |
40 |
|
tracking_ids :: [tracking_id()]}). |
41 |
|
|
42 |
|
-type system_metrics_state() :: #system_metrics_state{}. |
43 |
|
-type client_id() :: string(). |
44 |
|
-type tracking_id() :: string(). |
45 |
|
|
46 |
|
-spec verify_if_configured() -> ok | ignore. |
47 |
|
verify_if_configured() -> |
48 |
83 |
Services = mongoose_config:get_opt(services, []), |
49 |
83 |
case proplists:is_defined(?MODULE, Services) of |
50 |
|
false -> |
51 |
|
%% Technically, notice level. |
52 |
|
%% Though make it louder, in case people set minimum level as warning. |
53 |
1 |
?LOG_WARNING(#{what => system_metrics_disabled, |
54 |
:-( |
text => msg_removed_from_config()}), |
55 |
1 |
ignore; |
56 |
|
true -> |
57 |
82 |
ok |
58 |
|
end. |
59 |
|
|
60 |
|
-spec start(proplists:proplist()) -> {ok, pid()}. |
61 |
|
start(Args) -> |
62 |
102 |
Spec = {?MODULE, {?MODULE, start_link, [Args]}, temporary, brutal_kill, worker, [?MODULE]}, |
63 |
102 |
{ok, _} = ejabberd_sup:start_child(Spec). |
64 |
|
|
65 |
|
-spec stop() -> ok. |
66 |
|
stop() -> |
67 |
103 |
ejabberd_sup:stop_child(?MODULE). |
68 |
|
|
69 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
70 |
|
config_spec() -> |
71 |
82 |
#section{ |
72 |
|
items = #{<<"initial_report">> => #option{type = integer, |
73 |
|
validate = non_negative}, |
74 |
|
<<"periodic_report">> => #option{type = integer, |
75 |
|
validate = non_negative}, |
76 |
|
<<"report">> => #option{type = boolean, |
77 |
|
process = fun ?MODULE:process_report_option/1, |
78 |
|
wrap = item}, |
79 |
|
<<"tracking_id">> => #option{type = string, |
80 |
|
validate = non_empty} |
81 |
|
} |
82 |
|
}. |
83 |
|
|
84 |
:-( |
process_report_option(true) -> report; |
85 |
:-( |
process_report_option(false) -> no_report. |
86 |
|
|
87 |
|
-spec start_link(proplists:proplist()) -> {ok, pid()}. |
88 |
|
start_link(Args) -> |
89 |
102 |
gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []). |
90 |
|
|
91 |
|
-spec init(proplists:proplist()) -> {ok, system_metrics_state()}. |
92 |
|
init(Args) -> |
93 |
102 |
case report_transparency(Args) of |
94 |
:-( |
skip -> ignore; |
95 |
|
continue -> |
96 |
102 |
{InitialReport, ReportAfter, TrackingIds} = metrics_module_config(Args), |
97 |
102 |
erlang:send_after(InitialReport, self(), spawn_reporter), |
98 |
102 |
{ok, #system_metrics_state{report_after = ReportAfter, |
99 |
|
tracking_ids = TrackingIds}} |
100 |
|
end. |
101 |
|
|
102 |
|
handle_info(spawn_reporter, #system_metrics_state{report_after = ReportAfter, |
103 |
|
reporter_monitor = none, |
104 |
|
reporter_pid = none, |
105 |
|
prev_report = PrevReport, |
106 |
|
tracking_ids = TrackingIds} = State) -> |
107 |
40 |
ServicePid = self(), |
108 |
40 |
case get_client_id() of |
109 |
|
{ok, ClientId} -> |
110 |
40 |
{Pid, Monitor} = spawn_monitor( |
111 |
|
fun() -> |
112 |
40 |
Reports = mongoose_system_metrics_collector:collect(PrevReport), |
113 |
40 |
mongoose_system_metrics_sender:send(ClientId, Reports, TrackingIds), |
114 |
40 |
mongoose_system_metrics_file:save(Reports), |
115 |
40 |
ServicePid ! {prev_report, Reports} |
116 |
|
end), |
117 |
40 |
erlang:send_after(ReportAfter, self(), spawn_reporter), |
118 |
40 |
{noreply, State#system_metrics_state{reporter_monitor = Monitor, |
119 |
|
reporter_pid = Pid}}; |
120 |
:-( |
{error, _} -> {stop, no_client_id, State} |
121 |
|
end; |
122 |
|
handle_info(spawn_reporter, #system_metrics_state{reporter_pid = Pid} = State) -> |
123 |
:-( |
exit(Pid, kill), |
124 |
:-( |
self() ! spawn_reporter, |
125 |
:-( |
{noreply, State#system_metrics_state{reporter_monitor = none, reporter_pid = none}}; |
126 |
|
handle_info({'DOWN', CollectorMonitor, _, _, _}, |
127 |
|
#system_metrics_state{reporter_monitor = CollectorMonitor} = State) -> |
128 |
28 |
{noreply, State#system_metrics_state{reporter_monitor = none, reporter_pid = none}}; |
129 |
|
handle_info({prev_report, Report}, State) -> |
130 |
28 |
{noreply, State#system_metrics_state{prev_report = Report}}; |
131 |
|
handle_info(_Message, State) -> |
132 |
:-( |
{noreply, State}. |
133 |
|
|
134 |
|
|
135 |
|
% %%----------------------------------------- |
136 |
|
% %% Helpers |
137 |
|
% %%----------------------------------------- |
138 |
|
|
139 |
|
-spec get_client_id() -> {ok, client_id()} | {error, any()}. |
140 |
|
get_client_id() -> |
141 |
40 |
case mongoose_cluster_id:get_cached_cluster_id() of |
142 |
:-( |
{error, _} = Err -> Err; |
143 |
40 |
{ok, ID} when is_binary(ID) -> {ok, binary_to_list(ID)} |
144 |
|
end. |
145 |
|
|
146 |
|
-spec metrics_module_config(list()) -> {non_neg_integer(), non_neg_integer(), [tracking_id()]}. |
147 |
|
metrics_module_config(Args) -> |
148 |
102 |
{InitialReport, ReportAfter, TrackingId} = get_config(Args, os:getenv("CI")), |
149 |
102 |
TrackingIds = case proplists:lookup(tracking_id, Args) of |
150 |
101 |
none -> [TrackingId]; |
151 |
1 |
{_, ExtraTrackingId} -> [TrackingId, ExtraTrackingId] |
152 |
|
end, |
153 |
102 |
{InitialReport, ReportAfter, TrackingIds}. |
154 |
|
|
155 |
|
get_config(Args, "true") -> |
156 |
102 |
I = proplists:get_value(initial_report, Args, timer:seconds(20)), |
157 |
102 |
R = proplists:get_value(periodic_report, Args, timer:minutes(5)), |
158 |
102 |
{I, R, ?TRACKING_ID_CI}; |
159 |
|
get_config(Args, _) -> |
160 |
:-( |
I = proplists:get_value(initial_report, Args, ?DEFAULT_INITIAL_REPORT), |
161 |
:-( |
R = proplists:get_value(periodic_report, Args, ?DEFAULT_REPORT_AFTER), |
162 |
:-( |
{I, R, ?TRACKING_ID}. |
163 |
|
|
164 |
|
-spec report_transparency(proplists:proplist()) -> skip | continue. |
165 |
|
report_transparency(Args) -> |
166 |
102 |
case {explicit_no_report(Args), explicit_gathering_agreement(Args)} of |
167 |
:-( |
{true, _} -> skip; |
168 |
:-( |
{_, true} -> continue; |
169 |
|
{_, _} -> |
170 |
102 |
File = mongoose_system_metrics_file:location(), |
171 |
102 |
Text = iolist_to_binary(io_lib:format(msg_accept_terms_and_conditions(), [File])), |
172 |
102 |
?LOG_WARNING(#{what => report_transparency, |
173 |
:-( |
text => Text, report_filename => File}), |
174 |
102 |
continue |
175 |
|
end. |
176 |
|
|
177 |
|
explicit_no_report(Args) -> |
178 |
102 |
proplists:get_value(no_report, Args, false). |
179 |
|
explicit_gathering_agreement(Args) -> |
180 |
102 |
proplists:get_value(report, Args, false). |
181 |
|
|
182 |
|
% %%----------------------------------------- |
183 |
|
% %% Unused |
184 |
|
% %%----------------------------------------- |
185 |
|
|
186 |
|
handle_cast(_Request, State) -> |
187 |
:-( |
{noreply, State}. |
188 |
|
handle_call(_Request, _From, State) -> |
189 |
:-( |
{reply, ok, State}. |
190 |
|
terminate(_Reason, _State) -> |
191 |
:-( |
ok. |
192 |
|
|
193 |
|
%%----------------------------------------- |
194 |
|
%% Internal |
195 |
|
%%----------------------------------------- |
196 |
|
msg_removed_from_config() -> |
197 |
1 |
<<"We're sorry to hear you don't want to share the system's metrics with us. " |
198 |
|
"These metrics would enable us to improve MongooseIM and know where to focus our efforts. " |
199 |
|
"To stop being notified, you can add this to the services section of your config file: \n" |
200 |
|
" [services.service_mongoose_system_metrics]\n" |
201 |
|
" report = false\n" |
202 |
|
"For more info on how to customise, read, enable, and disable the metrics visit: \n" |
203 |
|
"- MongooseIM docs - \n" |
204 |
|
" https://esl.github.io/MongooseDocs/latest/operation-and-maintenance/System-Metrics-Privacy-Policy/ \n" |
205 |
|
"- MongooseIM GitHub page - https://github.com/esl/MongooseIM">>. |
206 |
|
|
207 |
|
msg_accept_terms_and_conditions() -> |
208 |
102 |
<<"We are gathering the MongooseIM system's metrics to analyse the trends and needs of our users, " |
209 |
|
"improve MongooseIM, and know where to focus our efforts. " |
210 |
|
"For more info on how to customise, read, enable, and disable these metrics visit: \n" |
211 |
|
"- MongooseIM docs - \n" |
212 |
|
" https://esl.github.io/MongooseDocs/latest/operation-and-maintenance/System-Metrics-Privacy-Policy/ \n" |
213 |
|
"- MongooseIM GitHub page - https://github.com/esl/MongooseIM \n" |
214 |
|
"The last sent report is also written to a file ~s">>. |