From 0a047ffde0bfc82db384dd02c84c7d8ccf3c02e0 Mon Sep 17 00:00:00 2001 From: bgrozev Date: Thu, 15 Feb 2024 09:19:27 -0600 Subject: [PATCH] feat: Add a prometheus /metrics endpoint. (#517) * chore: Update jicoco. * feat: Port some metrics to prometheus. * chore: Bump jetty. * feat: Add a /metrics endpoint for prometheus. * feat: Port most stats to metrics. --- pom.xml | 10 +- .../metrics/JigasiMetricsContainer.java | 31 +++ .../rest/AbstractJettyBundleActivator.java | 9 +- .../org/jitsi/jigasi/rest/MetricsHandler.java | 63 ++++++ .../org/jitsi/jigasi/stats/Statistics.java | 208 +++++++++++------- 5 files changed, 237 insertions(+), 84 deletions(-) create mode 100644 src/main/java/org/jitsi/jigasi/metrics/JigasiMetricsContainer.java create mode 100644 src/main/java/org/jitsi/jigasi/rest/MetricsHandler.java diff --git a/pom.xml b/pom.xml index 2af026371..4c0f48812 100644 --- a/pom.xml +++ b/pom.xml @@ -22,8 +22,8 @@ 4.4.6 1.0.3 - 1.1-107-gfb316f8 - 11.0.10 + 1.1-133-g768ef2e + 11.0.14 @@ -435,6 +435,12 @@ 1.6 test + + org.jitsi + jicoco-metrics + ${jicoco.version} + compile + diff --git a/src/main/java/org/jitsi/jigasi/metrics/JigasiMetricsContainer.java b/src/main/java/org/jitsi/jigasi/metrics/JigasiMetricsContainer.java new file mode 100644 index 000000000..edbf996c7 --- /dev/null +++ b/src/main/java/org/jitsi/jigasi/metrics/JigasiMetricsContainer.java @@ -0,0 +1,31 @@ +/* + * Jigasi, the JItsi GAteway to SIP. + * + * Copyright @ 2024 - present 8x8, Inc + * + * 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. + */ +package org.jitsi.jigasi.metrics; + +import io.prometheus.client.*; +import org.jitsi.metrics.*; + +public class JigasiMetricsContainer extends MetricsContainer +{ + public final static JigasiMetricsContainer INSTANCE = new JigasiMetricsContainer(); + + private JigasiMetricsContainer() + { + super(CollectorRegistry.defaultRegistry, "jitsi_jigasi"); + } +} diff --git a/src/main/java/org/jitsi/jigasi/rest/AbstractJettyBundleActivator.java b/src/main/java/org/jitsi/jigasi/rest/AbstractJettyBundleActivator.java index 1b434e2b6..accef3e9e 100644 --- a/src/main/java/org/jitsi/jigasi/rest/AbstractJettyBundleActivator.java +++ b/src/main/java/org/jitsi/jigasi/rest/AbstractJettyBundleActivator.java @@ -377,9 +377,16 @@ protected Server initializeServer(BundleContext bundleContext) server.addConnector(connector); Handler handler = initializeHandler(bundleContext, server); + Handler metricsHandler = new MetricsHandler(); + HandlerCollection handlers = new HandlerCollection(); if (handler != null) - server.setHandler(handler); + { + handlers.addHandler(handler); + } + handlers.addHandler(metricsHandler); + + server.setHandler(handlers); return server; } diff --git a/src/main/java/org/jitsi/jigasi/rest/MetricsHandler.java b/src/main/java/org/jitsi/jigasi/rest/MetricsHandler.java new file mode 100644 index 000000000..77c140ccb --- /dev/null +++ b/src/main/java/org/jitsi/jigasi/rest/MetricsHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright @ 2024 - present, 8x8 Inc + * + * 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. + */ +package org.jitsi.jigasi.rest; + +import io.prometheus.client.exporter.common.*; +import jakarta.servlet.*; +import jakarta.servlet.http.*; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.*; +import org.jitsi.jigasi.metrics.*; +import org.jitsi.jigasi.stats.*; + +import java.io.*; + +public class MetricsHandler extends AbstractHandler +{ + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + if ("/metrics".equals(target)) + { + String accept = request.getHeader("Accept"); + + Statistics.updateMetrics(); + + String responseBody; + if (accept != null && accept.startsWith("application/openmetrics-text")) + { + responseBody = JigasiMetricsContainer.INSTANCE.getPrometheusMetrics( + TextFormat.CONTENT_TYPE_OPENMETRICS_100); + response.setContentType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + } + else if (accept != null && accept.startsWith("text/plain")) + { + responseBody = JigasiMetricsContainer.INSTANCE.getPrometheusMetrics(TextFormat.CONTENT_TYPE_004); + response.setContentType(TextFormat.CONTENT_TYPE_004); + } + else + { + responseBody = JigasiMetricsContainer.INSTANCE.getJsonString(); + response.setContentType(RESTUtil.JSON_CONTENT_TYPE_WITH_CHARSET); + } + + Writer writer = response.getWriter(); + writer.write(responseBody); + response.setStatus(200); + } + } +} diff --git a/src/main/java/org/jitsi/jigasi/stats/Statistics.java b/src/main/java/org/jitsi/jigasi/stats/Statistics.java index b3489d1ce..fbfa6d419 100644 --- a/src/main/java/org/jitsi/jigasi/stats/Statistics.java +++ b/src/main/java/org/jitsi/jigasi/stats/Statistics.java @@ -22,7 +22,6 @@ import java.text.*; import java.util.*; import java.util.concurrent.*; -import java.util.concurrent.atomic.*; import java.util.stream.*; import jakarta.servlet.http.*; @@ -31,7 +30,9 @@ import net.java.sip.communicator.util.osgi.ServiceUtils; import org.apache.commons.lang3.*; import org.eclipse.jetty.server.*; +import org.jitsi.jigasi.metrics.*; import org.jitsi.jigasi.version.*; +import org.jitsi.metrics.*; import org.jitsi.utils.logging.Logger; import org.osgi.framework.*; import org.json.simple.*; @@ -138,64 +139,98 @@ public class Statistics /** * Total number of times with dropped media since started. */ - private static final AtomicLong totalMediaDroppedCount = new AtomicLong(); + private static final CounterMetric totalMediaDroppedCount = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_COUNT_DROPPED_MEDIA, + "Total number of times with dropped media since started."); /** * Total number of participants since started. */ - private static int totalParticipantsCount = 0; + private static final CounterMetric totalParticipantsCount = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_PARTICIPANTS, + "Total number of participants since started."); /** * Total number of conferences since started. */ - private static int totalConferencesCount = 0; + private static CounterMetric totalConferencesCount = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CONFERENCES_COMPLETED, + "Total number of conferences since started."); /** * Total number of calls with dropped media since started. */ - private static AtomicLong totalCallsWithMediaDroppedCount - = new AtomicLong(); + private static CounterMetric totalCallsWithMediaDroppedCount = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_WITH_DROPPED_MEDIA, + "Total number of calls with dropped media since started."); /** * Total number of calls with xmpp connection failed since started. */ - private static AtomicLong totalCallsWithConnectionFailedCount - = new AtomicLong(); + private static CounterMetric totalCallsWithConnectionFailedCount = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_WITH_CONNECTION_FAILED, + "Total number of calls with xmpp connection failed since started." + ); /** - * Total number of calls with xmpp call terminated and sip call waiting - * for new xmpp call. + * Total number of calls with xmpp call terminated and sip call waiting for new xmpp call. */ - private static AtomicLong totalCallsWithSipCallWaiting - = new AtomicLong(); + private static CounterMetric totalCallsWithSipCallWaiting = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_WITH_SIP_CALL_WAITING, + "Total number of calls with xmpp call terminated and sip call waiting for new xmpp call."); /** * Total number of calls with xmpp call terminated and sip call waiting * for new xmpp call and new xmpp call and both calls were connected * and operational. */ - private static AtomicLong totalCallsWithSipCalReconnected - = new AtomicLong(); + private static CounterMetric totalCallsWithSipCallReconnected = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_WITH_SIP_CALL_RECONNECTED, + "Total number of calls with xmpp call terminated and sip call waiting."); /** * Total number of calls with xmpp call receiving transport replace for moving to a new bridge. */ - private static AtomicLong totalCallsWithJvbMigrate = new AtomicLong(); + private static CounterMetric totalCallsWithJvbMigrate = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_WITH_JVB_MIGRATE, + "Total number of calls with xmpp call receiving transport replace for moving to a new bridge."); /** * Total number of calls with xmpp call not receiving media from the bridge. */ - private static AtomicLong totalCallsJvbNoMedia = new AtomicLong(); + private static CounterMetric totalCallsJvbNoMedia = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_JVB_NO_MEDIA, + "Total number of calls with xmpp call not receiving media from the bridge."); /** * Total number of calls dropped due to no response to sip heartbeat. */ - private static AtomicLong totalCallsWithNoHeartBeatResponse = new AtomicLong(); + private static CounterMetric totalCallsWithNoHeartBeatResponse = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CALLS_NO_HEARTBEAT, + "Total number of calls dropped due to no response to sip heartbeat."); /** * Cumulative number of seconds of all conferences. */ - private static long cumulativeConferenceSeconds = 0; + private static CounterMetric cumulativeConferenceSeconds = JigasiMetricsContainer.INSTANCE.registerCounter( + TOTAL_CONFERENCE_SECONDS, + "Cumulative number of seconds of all conferences"); + + private static final LongGaugeMetric threadsMetric = JigasiMetricsContainer.INSTANCE.registerLongGauge( + "threads", + "Number of JVM threads."); + private static final BooleanMetric shutdownMetric = JigasiMetricsContainer.INSTANCE.registerBooleanMetric( + SHUTDOWN_IN_PROGRESS, + "Whether jigasi is in graceful shutdown mode."); + private static final LongGaugeMetric conferencesMetric = JigasiMetricsContainer.INSTANCE.registerLongGauge( + CONFERENCES, + "Number of conferences."); + private static final LongGaugeMetric participantsMetric = JigasiMetricsContainer.INSTANCE.registerLongGauge( + PARTICIPANTS, + "Number of participants."); + private static final DoubleGaugeMetric stressMetric = JigasiMetricsContainer.INSTANCE.registerDoubleGauge( + STRESS_LEVEL, + "Stress level."); /** * The DateFormat to be utilized by Statistics @@ -238,40 +273,69 @@ public static synchronized void sendJSON( HttpServletResponse response) throws IOException { + updateMetrics(); + Map stats = new HashMap<>(); stats.putAll(getSessionStats()); - stats.put(THREADS, - ManagementFactory.getThreadMXBean().getThreadCount()); + stats.put(THREADS, threadsMetric.get()); // TIMESTAMP stats.put(TIMESTAMP, currentTimeMillis()); // TOTAL stats - stats.put(TOTAL_CONFERENCES_COMPLETED, totalConferencesCount); - stats.put(TOTAL_PARTICIPANTS, totalParticipantsCount); - stats.put(TOTAL_CONFERENCE_SECONDS, cumulativeConferenceSeconds); - stats.put(TOTAL_CALLS_WITH_DROPPED_MEDIA, - totalCallsWithMediaDroppedCount.get()); + stats.put(TOTAL_CONFERENCES_COMPLETED, totalConferencesCount.get()); + stats.put(TOTAL_PARTICIPANTS, totalParticipantsCount.get()); + stats.put(TOTAL_CONFERENCE_SECONDS, cumulativeConferenceSeconds.get()); + stats.put(TOTAL_CALLS_WITH_DROPPED_MEDIA, totalCallsWithMediaDroppedCount.get()); stats.put(TOTAL_COUNT_DROPPED_MEDIA, totalMediaDroppedCount.get()); - stats.put(TOTAL_CALLS_WITH_CONNECTION_FAILED, - totalCallsWithConnectionFailedCount.get()); - stats.put(TOTAL_CALLS_WITH_SIP_CALL_WAITING, - totalCallsWithSipCallWaiting.get()); - stats.put(TOTAL_CALLS_WITH_SIP_CALL_RECONNECTED, - totalCallsWithSipCalReconnected.get()); + stats.put(TOTAL_CALLS_WITH_CONNECTION_FAILED, totalCallsWithConnectionFailedCount.get()); + stats.put(TOTAL_CALLS_WITH_SIP_CALL_WAITING, totalCallsWithSipCallWaiting.get()); + stats.put(TOTAL_CALLS_WITH_SIP_CALL_RECONNECTED, totalCallsWithSipCallReconnected.get()); stats.put(TOTAL_CALLS_WITH_JVB_MIGRATE, totalCallsWithJvbMigrate.get()); stats.put(TOTAL_CALLS_JVB_NO_MEDIA, totalCallsJvbNoMedia.get()); stats.put(TOTAL_CALLS_NO_HEARTBEAT, totalCallsWithNoHeartBeatResponse.get()); - stats.put(SHUTDOWN_IN_PROGRESS, - JigasiBundleActivator.isShutdownInProgress()); + stats.put(SHUTDOWN_IN_PROGRESS, shutdownMetric.get()); response.setStatus(HttpServletResponse.SC_OK); new JSONObject(stats).writeJSONString(response.getWriter()); } + public static void updateMetrics() + { + threadsMetric.set(ManagementFactory.getThreadMXBean().getThreadCount()); + shutdownMetric.set(JigasiBundleActivator.isShutdownInProgress()); + + // get sessions from all gateways + List sessions = new ArrayList<>(); + JigasiBundleActivator.getAvailableGateways().forEach(gw -> sessions.addAll(gw.getActiveSessions())); + + int participants = 0; + int conferences = 0; + + for (AbstractGatewaySession ses : sessions) + { + if (ses.getJvbChatRoom() == null) + { + continue; + } + + // do not count focus + int conferenceEndpoints = ses.getJvbChatRoom().getMembersCount() - 1; + participants += conferenceEndpoints; + conferences++; + } + + double stressLevel = conferences / CONFERENCES_THRESHOLD; + + conferencesMetric.set(conferences); + participantsMetric.set(participants); + stressMetric.set(stressLevel); + + } + /** * Counts conferences count, participants count and conference size * distributions. @@ -280,16 +344,19 @@ public static synchronized void sendJSON( */ private static Map getSessionStats() { + + Map stats = new HashMap<>(); + stats.put(CONFERENCES, conferencesMetric.get()); + stats.put(PARTICIPANTS, participantsMetric.get()); + stats.put(STRESS_LEVEL, stressMetric.get()); + + // Calculate the conference sizes separately because we don't have a metric for them. TODO: port to a metric. // get sessions from all gateways List sessions = new ArrayList<>(); JigasiBundleActivator.getAvailableGateways().forEach( gw -> sessions.addAll(gw.getActiveSessions())); int[] conferenceSizes = new int[CONFERENCE_SIZE_BUCKETS]; - Map stats = new HashMap<>(); - - int participants = 0; - int conferences = 0; for (AbstractGatewaySession ses : sessions) { @@ -301,7 +368,6 @@ private static Map getSessionStats() // do not count focus int conferenceEndpoints = ses.getJvbChatRoom().getMembersCount() - 1; - participants += conferenceEndpoints; int idx = conferenceEndpoints < conferenceSizes.length ? conferenceEndpoints @@ -310,21 +376,16 @@ private static Map getSessionStats() { conferenceSizes[idx]++; } - conferences++; } - stats.put(CONFERENCES, conferences); JSONArray conferenceSizesJson = new JSONArray(); for (int size : conferenceSizes) + { conferenceSizesJson.add(size); + } stats.put(CONFERENCE_SIZES, conferenceSizesJson); - stats.put(PARTICIPANTS, participants); - - double stressLevel = conferences / CONFERENCES_THRESHOLD; - stats.put(STRESS_LEVEL, stressLevel); - return stats; } @@ -343,7 +404,7 @@ private static String currentTimeMillis() */ public static void addTotalParticipantsCount(int value) { - totalParticipantsCount += value; + totalParticipantsCount.add(value); } /** @@ -353,7 +414,7 @@ public static void addTotalParticipantsCount(int value) */ public static void addTotalConferencesCount(int value) { - totalConferencesCount += value; + totalConferencesCount.add(value); } /** @@ -361,7 +422,7 @@ public static void addTotalConferencesCount(int value) */ public static void incrementTotalMediaDropped() { - totalMediaDroppedCount.incrementAndGet(); + totalMediaDroppedCount.inc(); } /** @@ -369,7 +430,7 @@ public static void incrementTotalMediaDropped() */ public static void incrementTotalCallsWithMediaDropped() { - totalCallsWithMediaDroppedCount.incrementAndGet(); + totalCallsWithMediaDroppedCount.inc(); } /** @@ -377,7 +438,7 @@ public static void incrementTotalCallsWithMediaDropped() */ public static void incrementTotalCallsWithConnectionFailed() { - totalCallsWithConnectionFailedCount.incrementAndGet(); + totalCallsWithConnectionFailedCount.inc(); } /** @@ -386,7 +447,7 @@ public static void incrementTotalCallsWithConnectionFailed() */ public static void incrementTotalCallsWithSipCallWaiting() { - totalCallsWithSipCallWaiting.incrementAndGet(); + totalCallsWithSipCallWaiting.inc(); } /** @@ -395,7 +456,7 @@ public static void incrementTotalCallsWithSipCallWaiting() */ public static void incrementTotalCallsWithSipCallReconnected() { - totalCallsWithSipCalReconnected.incrementAndGet(); + totalCallsWithSipCallReconnected.inc(); } /** @@ -404,7 +465,7 @@ public static void incrementTotalCallsWithSipCallReconnected() */ public static void incrementTotalCallsWithJvbMigrate() { - totalCallsWithJvbMigrate.incrementAndGet(); + totalCallsWithJvbMigrate.inc(); } /** @@ -412,7 +473,7 @@ public static void incrementTotalCallsWithJvbMigrate() */ public static void incrementTotalCallsJvbNoMedia() { - totalCallsJvbNoMedia.incrementAndGet(); + totalCallsJvbNoMedia.inc(); } /** @@ -420,7 +481,7 @@ public static void incrementTotalCallsJvbNoMedia() */ public static void incrementTotalCallsWithNoSipHeartbeat() { - totalCallsWithNoHeartBeatResponse.incrementAndGet(); + totalCallsWithNoHeartBeatResponse.inc(); } /** @@ -429,7 +490,7 @@ public static void incrementTotalCallsWithNoSipHeartbeat() */ public static void addCumulativeConferenceSeconds(long value) { - cumulativeConferenceSeconds += value; + cumulativeConferenceSeconds.add(value); } /** @@ -459,18 +520,11 @@ public static void updatePresenceStatusForXmppProviders() * * @param ppss the list of protocol providers to update. */ - public static void updatePresenceStatusForXmppProviders( - List ppss) + public static void updatePresenceStatusForXmppProviders(List ppss) { - final Map stats = getSessionStats(); - - ppss.forEach(pps -> - updatePresenceStatusForXmppProvider( - pps, - (int)stats.get(PARTICIPANTS), - (int)stats.get(CONFERENCES), - (double)stats.get(STRESS_LEVEL), - JigasiBundleActivator.isShutdownInProgress())); + updateMetrics(); + + ppss.forEach(Statistics::updatePresenceStatusForXmppProvider); } /** @@ -480,16 +534,8 @@ public static void updatePresenceStatusForXmppProviders( * Adds a {@link ColibriStatsExtension} to our presence in the brewery room. * * @param pps the protocol provider service - * @param participants the participant count. - * @param conferences the active session/conference count. - * @param stressLevel the current stress level - */ - private static void updatePresenceStatusForXmppProvider( - ProtocolProviderService pps, - int participants, - int conferences, - double stressLevel, - boolean shutdownInProgress) + */ + private static void updatePresenceStatusForXmppProvider(ProtocolProviderService pps) { if (ProtocolNames.JABBER.equals(pps.getProtocolName()) && pps.getAccountID() instanceof JabberAccountID @@ -520,16 +566,16 @@ private static void updatePresenceStatusForXmppProvider( ColibriStatsExtension stats = new ColibriStatsExtension(); stats.addStat(new ColibriStatsExtension.Stat( CONFERENCES, - conferences)); + conferencesMetric.get())); stats.addStat(new ColibriStatsExtension.Stat( PARTICIPANTS, - participants)); + participantsMetric.get())); stats.addStat(new ColibriStatsExtension.Stat( SHUTDOWN_IN_PROGRESS, - shutdownInProgress)); + shutdownMetric.get())); stats.addStat((new ColibriStatsExtension.Stat( STRESS_LEVEL, - stressLevel + stressMetric.get() ))); String region = JigasiBundleActivator.getConfigurationService()