import 'dart:async'; import 'package:nyxx/nyxx.dart'; import 'package:prometheus_client/prometheus_client.dart'; import 'package:prometheus_client_shelf/shelf_handler.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_router/shelf_router.dart'; import 'package:prometheus_client_shelf/shelf_metrics.dart' as shelf_metrics; import 'package:prometheus_client/runtime_metrics.dart' as runtime_metrics; // final logger = Logger('Analytics'); /// A global instance of the [Analytics] plugin. final analytics = Analytics(); class Analytics extends NyxxPlugin { @override String get name => 'Analytics'; /// Whether to include runtime metrics in the analytics. final bool includeDefaultMetrics; /// The port to bind the analytics server to. final int port; /// The analytics route. final String route; /// The refresh interval for the analytics. final Duration refreshInterval; /// Custom gauges for the analytics. It's up to you to update these. final List gauges; /// Custom counters for the analytics. final List counters; /// Custom summaries for the analytics. final List summaries; /// Custom histograms for the analytics. final List histograms; Analytics({ this.includeDefaultMetrics = true, this.port = 4242, this.route = '/analytics', this.refreshInterval = const Duration(seconds: 5), this.gauges = const [], this.counters = const [], this.summaries = const [], this.histograms = const [], }); @override Future beforeConnect( ApiOptions apiOptions, ClientOptions clientOptions, ) async { final router = Router(); router.get('/', (req) => Response.found(route)); router.get(route, prometheusHandler()); final pipeline = const Pipeline(); if (includeDefaultMetrics) { runtime_metrics.register(); pipeline.addMiddleware(shelf_metrics.register()); } final server = await io.serve( pipeline.addHandler(router.call), '0.0.0.0', port, ); // this.s logger.info('Serving analytics at http://${server.address.host}:${server.port}$route'); } @override NyxxPluginState createState() => AnalyticsPluginState(this); } class AnalyticsPluginState extends NyxxPluginState { late final Timer timer; AnalyticsPluginState(super.plugin); @override Future afterConnect(NyxxRest client) async { await super.afterConnect(client); registerPeriodicMetrics(client); registerCustomMetrics(); } void registerCustomMetrics() { final m = [plugin.gauges, plugin.counters, plugin.summaries, plugin.histograms].expand((e) => e); if (m.isEmpty) return; for (dynamic metric in m) { metric.register(); } } void registerPeriodicMetrics(NyxxRest client) { final cachedUsers = Gauge(name: 'nyxx_analytics_cached_users', help: 'The number of cached users')..register(); final cachedGuilds = Gauge(name: 'nyxx_analytics_cached_guilds', help: 'The number of cached guilds')..register(); final cachedChannels = Gauge(name: 'nyxx_analytics_cached_channels', help: 'The number of cached channels') ..register(); final cachedMessages = Gauge(name: 'nyxx_analytics_cached_messages', help: 'The total number of cached messages') ..register(); final restLatency = Gauge( name: 'nyxx_analytics_rest_latency', help: 'The average REST latency', labelNames: ['client_id'], )..register(); final realRestLatency = Gauge( name: 'nyxx_analytics_real_rest_latency', help: 'The real average REST latency', labelNames: ['client_id'], )..register(); Gauge? gatewayLatency; Gauge? shardGatewayLatency; if (client is NyxxGateway) { gatewayLatency = Gauge( name: 'nyxx_analytics_gateway_latency', help: 'The average gateway latency', labelNames: ['client_id'], )..register(); shardGatewayLatency = Gauge( name: 'nyxx_analytics_shard_gateway_latency', help: 'The average gateway latency per shard', labelNames: ['client_id', 'shard_id'], )..register(); } Timer.periodic(plugin.refreshInterval, (timer) { cachedUsers.value = client.users.cache.length.toDouble(); cachedGuilds.value = client.guilds.cache.length.toDouble(); cachedChannels.value = client.channels.cache.length.toDouble(); cachedMessages.value = client.channels.cache.values .whereType() .fold(0, (count, channel) => count + channel.messages.cache.length) .toDouble(); restLatency.labels([client.user.id.toString()]).value = client.httpHandler.latency.inMilliseconds.toDouble(); realRestLatency.labels([client.user.id.toString()]).value = client.httpHandler.realLatency.inMilliseconds.toDouble(); if (client case final NyxxGateway client) { gatewayLatency!.labels([client.user.id.toString()]).value = client.gateway.latency.inMilliseconds.toDouble(); for (final shard in client.gateway.shards) { shardGatewayLatency!.labels([client.user.id.toString(), shard.id.toString()]).value = shard.latency.inMilliseconds.toDouble(); } } }); } }