cmake: avoid de-duplication of user's CXXFLAGS
[quassel.git] / src / core / metricsserver.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2022 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "metricsserver.h"
22
23 #include <utility>
24
25 #include <QByteArray>
26 #include <QDebug>
27 #include <QHostAddress>
28 #include <QStringList>
29 #include <QTcpSocket>
30
31 #include "core.h"
32 #include "corenetwork.h"
33
34 MetricsServer::MetricsServer(QObject* parent)
35     : QObject(parent)
36 {
37     connect(&_server, &QTcpServer::newConnection, this, &MetricsServer::incomingConnection);
38     connect(&_v6server, &QTcpServer::newConnection, this, &MetricsServer::incomingConnection);
39 }
40
41 bool MetricsServer::startListening()
42 {
43     bool success = false;
44
45     uint16_t port = Quassel::optionValue("metrics-port").toUShort();
46
47     const QString listen = Quassel::optionValue("metrics-listen");
48     const QStringList listen_list = listen.split(",", QString::SkipEmptyParts);
49     for (const QString& listen_term : listen_list) { // TODO: handle multiple interfaces for same TCP version gracefully
50         QHostAddress addr;
51         if (!addr.setAddress(listen_term)) {
52             qCritical() << qPrintable(
53                 tr("Invalid listen address %1")
54                     .arg(listen_term)
55             );
56         }
57         else {
58             switch (addr.protocol()) {
59             case QAbstractSocket::IPv6Protocol:
60                 if (_v6server.listen(addr, port)) {
61                     qInfo() << qPrintable(
62                         tr("Listening for metrics requests on IPv6 %1 port %2")
63                             .arg(addr.toString())
64                             .arg(_v6server.serverPort())
65                     );
66                     success = true;
67                 }
68                 else
69                     qWarning() << qPrintable(
70                         tr("Could not open IPv6 interface %1:%2: %3")
71                             .arg(addr.toString())
72                             .arg(port)
73                             .arg(_v6server.errorString()));
74                 break;
75             case QAbstractSocket::IPv4Protocol:
76                 if (_server.listen(addr, port)) {
77                     qInfo() << qPrintable(
78                         tr("Listening for metrics requests on IPv4 %1 port %2")
79                             .arg(addr.toString())
80                             .arg(_server.serverPort())
81                     );
82                     success = true;
83                 }
84                 else {
85                     // if v6 succeeded on Any, the port will be already in use - don't display the error then
86                     if (!success || _server.serverError() != QAbstractSocket::AddressInUseError)
87                         qWarning() << qPrintable(
88                             tr("Could not open IPv4 interface %1:%2: %3")
89                                 .arg(addr.toString())
90                                 .arg(port)
91                                 .arg(_server.errorString()));
92                 }
93                 break;
94             default:
95                 qCritical() << qPrintable(
96                     tr("Invalid listen address %1, unknown network protocol")
97                         .arg(listen_term)
98                 );
99                 break;
100             }
101         }
102     }
103
104     if (!success) {
105         qWarning() << qPrintable(tr("Metrics could not open any network interfaces to listen on! No metrics functionality will be available"));
106     }
107
108     return success;
109 }
110
111 void MetricsServer::stopListening(const QString& msg)
112 {
113     bool wasListening = false;
114
115     if (_server.isListening()) {
116         wasListening = true;
117         _server.close();
118     }
119     if (_v6server.isListening()) {
120         wasListening = true;
121         _v6server.close();
122     }
123
124     if (wasListening) {
125         if (msg.isEmpty())
126             qInfo() << "No longer listening for metrics requests.";
127         else
128             qInfo() << qPrintable(msg);
129     }
130 }
131
132 void MetricsServer::incomingConnection()
133 {
134     auto server = qobject_cast<QTcpServer*>(sender());
135     Q_ASSERT(server);
136     while (server->hasPendingConnections()) {
137         QTcpSocket* socket = server->nextPendingConnection();
138         connect(socket, &QIODevice::readyRead, this, &MetricsServer::respond);
139         connect(socket, &QAbstractSocket::disconnected, socket, &QObject::deleteLater);
140     }
141 }
142
143 QString parseHttpString(const QByteArray& request, int& index)
144 {
145     QString content;
146     int end = request.indexOf(' ', index);
147     if (end == -1) {
148         end = request.length();
149     }
150
151     if (end > -1) {
152         content = QString::fromUtf8(request.mid(index, end - index));
153         index = end + 1;
154     }
155     else {
156         index = request.length();
157     }
158     return content;
159 }
160
161 void MetricsServer::respond()
162 {
163     auto socket = qobject_cast<QTcpSocket*>(sender());
164     Q_ASSERT(socket);
165
166     if (!socket->canReadLine()) {
167         return;
168     }
169
170     int index = 0;
171     QString verb;
172     QString requestPath;
173     QString version;
174     QByteArray request;
175     for (int i = 0; i < 5 && verb == ""; i++) {
176         request = socket->readLine(4096);
177         if (request.endsWith("\r\n")) {
178             request.chop(2);
179         }
180         else if (request.endsWith("\n")) {
181             request.chop(1);
182         }
183
184         verb = parseHttpString(request, index);
185         requestPath = parseHttpString(request, index);
186         version = parseHttpString(request, index);
187     }
188
189     if (requestPath == "/metrics") {
190         if (version == "HTTP/1.1") {
191             socket->write(
192                 "HTTP/1.1 200 OK\r\n"
193                 "Content-Type: text/plain; version=0.0.4\r\n"
194                 "Connection: close\r\n"
195                 "\r\n"
196             );
197         }
198         int64_t timestamp = QDateTime::currentMSecsSinceEpoch();
199         for (const auto& key : _sessions.keys()) {
200             const QString& name = _sessions[key];
201             socket->write("# HELP quassel_network_bytes_received Number of currently open connections from quassel clients\n");
202             socket->write("# TYPE quassel_client_sessions gauge\n");
203             socket->write(
204                 QString("quassel_client_sessions{user=\"%1\"} %2 %3\n")
205                     .arg(name)
206                     .arg(_clientSessions.value(key, 0))
207                     .arg(timestamp)
208                     .toUtf8()
209             );
210             socket->write("# HELP quassel_network_bytes_received Number of currently open connections to IRC networks\n");
211             socket->write("# TYPE quassel_network_sessions gauge\n");
212             socket->write(
213                 QString("quassel_network_sessions{user=\"%1\"} %2 %3\n")
214                     .arg(name)
215                     .arg(_networkSessions.value(key, 0))
216                     .arg(timestamp)
217                     .toUtf8()
218             );
219             socket->write("# HELP quassel_network_bytes_received Amount of bytes sent to IRC\n");
220             socket->write("# TYPE quassel_network_bytes_sent counter\n");
221             socket->write(
222                 QString("quassel_network_bytes_sent{user=\"%1\"} %2 %3\n")
223                     .arg(name)
224                     .arg(_networkDataTransmit.value(key, 0))
225                     .arg(timestamp)
226                     .toUtf8()
227             );
228             socket->write("# HELP quassel_network_bytes_received Amount of bytes received from IRC\n");
229             socket->write("# TYPE quassel_network_bytes_received counter\n");
230             socket->write(
231                 QString("quassel_network_bytes_received{user=\"%1\"} %2 %3\n")
232                     .arg(name)
233                     .arg(_networkDataReceive.value(key, 0))
234                     .arg(timestamp)
235                     .toUtf8()
236             );
237             socket->write("# HELP quassel_message_queue The number of messages currently queued for that user\n");
238             socket->write("# TYPE quassel_message_queue gauge\n");
239             socket->write(
240                 QString("quassel_message_queue{user=\"%1\"} %2 %3\n")
241                     .arg(name)
242                     .arg(_messageQueue.value(key, 0))
243                     .arg(timestamp)
244                     .toUtf8()
245             );
246             socket->write("# HELP quassel_login_attempts The number of times the user has attempted to log in\n");
247             socket->write("# TYPE quassel_login_attempts counter\n");
248             socket->write(
249                 QString("quassel_login_attempts{user=\"%1\",successful=\"false\"} %2 %3\n")
250                     .arg(name)
251                     .arg(_loginAttempts.value(key, 0) - _successfulLogins.value(key, 0))
252                     .arg(timestamp)
253                     .toUtf8()
254             );
255             socket->write(
256                 QString("quassel_login_attempts{user=\"%1\",successful=\"true\"} %2 %3\n")
257                     .arg(name)
258                     .arg(_successfulLogins.value(key, 0))
259                     .arg(timestamp)
260                     .toUtf8()
261             );
262         }
263         if (!_certificateExpires.isNull()) {
264             socket->write("# HELP quassel_ssl_expire_time_seconds Expiration of the current TLS certificate in unixtime\n");
265             socket->write("# TYPE quassel_ssl_expire_time_seconds gauge\n");
266             socket->write(
267                 QString("quassel_ssl_expire_time_seconds %1 %2\n")
268                     .arg(_certificateExpires.toMSecsSinceEpoch() / 1000)
269                     .arg(timestamp)
270                     .toUtf8()
271             );
272         }
273         socket->close();
274     }
275     else if (requestPath == "/healthz") {
276         if (version == "HTTP/1.1") {
277             socket->write(
278                 "HTTP/1.1 200 OK\r\n"
279                 "Content-Type: text/plain\r\n"
280                 "Connection: close\r\n"
281                 "\r\n"
282             );
283         }
284         socket->write(
285             "OK\n"
286         );
287         socket->close();
288     }
289     else {
290         if (version == "HTTP/1.1") {
291             socket->write(
292                 "HTTP/1.1 404 Not Found\r\n"
293                 "Content-Type: text/html\r\n"
294                 "Connection: close\r\n"
295                 "\r\n"
296             );
297         }
298         socket->write(
299             QString(
300                 "<html>\n"
301                 "<head><title>404 Not Found</title></head>\n"
302                 "<body>\n"
303                 "<center><h1>404 Not Found</h1></center>\n"
304                 "<hr><center>quassel %1 </center>\n"
305                 "</body>\n"
306                 "</html>\n")
307                 .arg(Quassel::buildInfo().baseVersion)
308                 .toUtf8()
309         );
310         socket->close();
311     }
312 }
313
314 void MetricsServer::addLoginAttempt(UserId user, bool successful) {
315     _loginAttempts.insert(user, _loginAttempts.value(user, 0) + 1);
316     if (successful) {
317         _successfulLogins.insert(user, _successfulLogins.value(user, 0) + 1);
318     }
319 }
320
321 void MetricsServer::addLoginAttempt(const QString& user, bool successful) {
322     UserId userId = _sessions.key(user);
323     if (userId.isValid()) {
324         addLoginAttempt(userId, successful);
325     }
326 }
327
328 void MetricsServer::addSession(UserId user, const QString& name)
329 {
330     _sessions.insert(user, name);
331 }
332
333 void MetricsServer::removeSession(UserId user)
334 {
335     _sessions.remove(user);
336 }
337
338 void MetricsServer::addClient(UserId user)
339 {
340     _clientSessions.insert(user, _clientSessions.value(user, 0) + 1);
341 }
342
343 void MetricsServer::removeClient(UserId user)
344 {
345     int32_t count = _clientSessions.value(user, 0) - 1;
346     if (count <= 0) {
347         _clientSessions.remove(user);
348     }
349     else {
350         _clientSessions.insert(user, count);
351     }
352 }
353
354 void MetricsServer::addNetwork(UserId user)
355 {
356     _networkSessions.insert(user, _networkSessions.value(user, 0) + 1);
357 }
358
359 void MetricsServer::removeNetwork(UserId user)
360 {
361     int32_t count = _networkSessions.value(user, 0) - 1;
362     if (count <= 0) {
363         _networkSessions.remove(user);
364     }
365     else {
366         _networkSessions.insert(user, count);
367     }
368 }
369
370 void MetricsServer::transmitDataNetwork(UserId user, uint64_t size)
371 {
372     _networkDataTransmit.insert(user, _networkDataTransmit.value(user, 0) + size);
373 }
374
375 void MetricsServer::receiveDataNetwork(UserId user, uint64_t size)
376 {
377     _networkDataReceive.insert(user, _networkDataReceive.value(user, 0) + size);
378 }
379
380 void MetricsServer::messageQueue(UserId user, uint64_t size)
381 {
382     _messageQueue.insert(user, size);
383 }
384
385 void MetricsServer::setCertificateExpires(QDateTime expires)
386 {
387     _certificateExpires = std::move(expires);
388 }