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