./ct_report/coverage/service_mongoose_system_metrics.COVER.html

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 104 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 104 ok
59 end.
60
61 -spec start(mongoose_service:options()) -> {ok, pid()}.
62 start(Opts) ->
63 128 Spec = {?MODULE, {?MODULE, start_link, [Opts]}, temporary, brutal_kill, worker, [?MODULE]},
64 128 {ok, _} = ejabberd_sup:start_child(Spec).
65
66 -spec stop() -> ok.
67 stop() ->
68 128 ejabberd_sup:stop_child(?MODULE).
69
70 -spec config_spec() -> mongoose_config_spec:config_section().
71 config_spec() ->
72 104 #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 104 #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 128 gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
97
98 -spec init(mongoose_service:options()) -> {ok, system_metrics_state()}.
99 init(Opts) ->
100 128 case report_transparency(Opts) of
101 1 skip -> ignore;
102 continue ->
103 127 #{initial_report := InitialReport, periodic_report := PeriodicReport} = Opts,
104 127 TrackingIds = tracking_ids(Opts),
105 127 erlang:send_after(InitialReport, self(), spawn_reporter),
106 127 {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 43 ServicePid = self(),
116 43 case get_client_id() of
117 {ok, ClientId} ->
118 43 {Pid, Monitor} = spawn_monitor(
119 fun() ->
120 43 Reports = mongoose_system_metrics_collector:collect(PrevReport),
121 43 mongoose_system_metrics_sender:send(ClientId, Reports, TrackingIds),
122 43 mongoose_system_metrics_file:save(Reports),
123 43 ServicePid ! {prev_report, Reports}
124 end),
125 43 erlang:send_after(ReportAfter, self(), spawn_reporter),
126 43 {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 32 {noreply, State#system_metrics_state{reporter_monitor = none, reporter_pid = none}};
137 handle_info({prev_report, Report}, State) ->
138 32 {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 43 case mongoose_cluster_id:get_cached_cluster_id() of
150
:-(
{error, _} = Err -> Err;
151 43 {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 126 [predefined_tracking_id()].
159
160 -spec predefined_tracking_id() -> tracking_id().
161 predefined_tracking_id() ->
162 127 case os:getenv("CI") of
163 127 "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 126 File = mongoose_system_metrics_file:location(),
174 126 Text = iolist_to_binary(io_lib:format(msg_accept_terms_and_conditions(), [File])),
175 126 ?LOG_WARNING(#{what => report_transparency, text => Text, report_filename => File}),
176 126 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 126 <<"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">>.
Line Hits Source