diff --git a/src/discord.rs b/src/discord.rs index 2e29618..c8326ec 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -70,37 +70,78 @@ fn category_channel( #[async_trait] impl EventHandler for Handler { + async fn channel_create(&self, _ctx: Context, channel: GuildChannel) { + info!( + guild_id = channel.guild_id.get(), + channel_id = channel.id.get(), + "Channel create" + ); + + self.metrics_handler + .channel + .get_or_create(&ChannelLabels::new(&channel)) + .set(1); + } + + async fn channel_delete( + &self, + _ctx: Context, + channel: GuildChannel, + _messages: Option>, + ) { + info!( + guild_id = channel.guild_id.get(), + channel_id = channel.id.get(), + "Channel delete" + ); + + self.metrics_handler + .channel + .remove(&ChannelLabels::new(&channel)); + } + + async fn channel_update(&self, _ctx: Context, old: Option, new: GuildChannel) { + info!( + guild_id = new.guild_id.get(), + channel_id = new.id.get(), + "Channel update" + ); + + // Decrement old if available + if let Some(old) = old { + self.metrics_handler + .channel + .remove(&ChannelLabels::new(&old)); + } + + // Increment new + self.metrics_handler + .channel + .get_or_create(&ChannelLabels::new(&new)) + .set(1); + } + async fn guild_create(&self, ctx: Context, guild: Guild, _is_new: Option) { info!(guild_id = guild.id.get(), "Guild create"); // Handle `guild` metric self.metrics_handler .guild - .get_or_create(&GuildsLabels { - guild_id: guild.id.into(), - }) + .get_or_create(&GuildsLabels::new(&guild)) .set(1); // Handle `channel` metric for channel in guild.channels.values() { self.metrics_handler .channel - .get_or_create(&ChannelLabels { - guild_id: guild.id.get(), - channel_id: channel.id.get(), - channel_name: channel.name.clone(), - channel_nsfw: channel.nsfw.into(), - channel_type: channel.kind.name().to_string(), - }) + .get_or_create(&ChannelLabels::new(channel)) .set(1); } // Handle `boost` metric self.metrics_handler .boost - .get_or_create(&BoostLabels { - guild_id: guild.id.into(), - }) + .get_or_create(&BoostLabels::new(guild.id)) .set( guild .premium_subscription_count @@ -112,9 +153,7 @@ impl EventHandler for Handler { // Handle `member` metric self.metrics_handler .member - .get_or_create(&MemberLabels { - guild_id: guild.id.into(), - }) + .get_or_create(&MemberLabels::new(guild.id)) .set( guild .member_count @@ -128,16 +167,12 @@ impl EventHandler for Handler { let Ok(members) = guild.members(&ctx.http, None, members_after).await else { warn!(guild_id = guild.id.get(), "Failed to count guild bots"); // Remove metric to indicate no bots were counted (successfully) - self.metrics_handler.bot.remove(&BotLabels { - guild_id: guild.id.into(), - }); + self.metrics_handler.bot.remove(&BotLabels::new(guild.id)); break; }; self.metrics_handler .bot - .get_or_create(&BotLabels { - guild_id: guild.id.into(), - }) + .get_or_create(&BotLabels::new(guild.id)) .inc_by( members .iter() @@ -158,21 +193,14 @@ impl EventHandler for Handler { // Handle `member_status` metric self.metrics_handler .member_status - .get_or_create(&MemberStatusLabels { - guild_id: guild.id.into(), - status: presence.status.name().to_string(), - }) + .get_or_create(&MemberStatusLabels::new(guild.id, presence.status)) .inc(); // Handle `activity` metric for activity in &presence.activities { self.metrics_handler .activity - .get_or_create(&ActivityLabels { - guild_id: guild.id.into(), - activity_application_id: activity.application_id.map(Into::into), - activity_name: activity.name.clone(), - }) + .get_or_create(&ActivityLabels::new(guild.id, activity)) .inc(); } @@ -186,127 +214,89 @@ impl EventHandler for Handler { } // Handle `member_voice` metric - for (_, voice) in guild.voice_states { + for voice in guild.voice_states.values() { if let Some(channel_id) = &voice.channel_id { - let (category, channel) = category_channel(&ctx, guild.id, *channel_id); + let (category_id, channel_id) = category_channel(&ctx, guild.id, *channel_id); self.metrics_handler .member_voice - .get_or_create(&MemberVoiceLabels { - guild_id: guild.id.into(), - category_id: category.as_ref().map(|ch| ch.get()), - channel_id: channel.into(), - self_stream: voice.self_stream.unwrap_or(false).into(), - self_video: voice.self_video.into(), - self_deaf: voice.self_deaf.into(), - self_mute: voice.self_mute.into(), - }) + .get_or_create(&MemberVoiceLabels::new( + guild.id, + category_id, + channel_id, + voice, + )) .inc(); } } } - async fn guild_delete( - &self, - _ctx: Context, - incomplete: UnavailableGuild, - _full: Option, - ) { + async fn guild_delete(&self, ctx: Context, incomplete: UnavailableGuild, full: Option) { info!(guild_id = incomplete.id.get(), "Guild delete"); + let Some(guild) = full else { + warn!( + guild_id = incomplete.id.get(), + "failed to get guild from cache, this might cause inconsistencies in the metrics" + ); + return; + }; + // Handle `guild` metric self.metrics_handler .guild - .get_or_create(&GuildsLabels { - guild_id: incomplete.id.into(), - }) - .set(0); + .remove(&GuildsLabels::new(&guild)); - // Handle `bot` metric - self.metrics_handler - .bot - .get_or_create(&BotLabels { - guild_id: incomplete.id.into(), - }) - .set(0); - - // TODO handle other metrics as well! Consider syncing with guild_create "is_new" - } + // Handle `channel` metric + for channel in guild.channels.values() { + self.metrics_handler + .channel + .remove(&ChannelLabels::new(channel)); + } - async fn channel_create(&self, _ctx: Context, channel: GuildChannel) { - info!( - guild_id = channel.guild_id.get(), - channel_id = channel.id.get(), - "Channel create" - ); + // Handle `boost` metric + self.metrics_handler + .boost + .remove(&BoostLabels::new(guild.id)); + // Handle `member` metric self.metrics_handler - .channel - .get_or_create(&ChannelLabels { - guild_id: channel.guild_id.get(), - channel_id: channel.id.get(), - channel_name: channel.name.clone(), - channel_nsfw: channel.nsfw.into(), - channel_type: channel.kind.name().to_string(), - }) - .set(1); - } + .member + .remove(&MemberLabels::new(guild.id)); - async fn channel_update(&self, _ctx: Context, old: Option, new: GuildChannel) { - info!( - guild_id = new.guild_id.get(), - channel_id = new.id.get(), - "Channel update" - ); + // Handle `bot` metric + self.metrics_handler.bot.remove(&BotLabels::new(guild.id)); - // Decrement old if available - if let Some(old) = old { + for (user_id, presence) in &guild.presences { + // Handle `member_status` metric self.metrics_handler - .channel - .get_or_create(&ChannelLabels { - guild_id: old.guild_id.get(), - channel_id: old.id.get(), - channel_name: old.name.clone(), - channel_nsfw: old.nsfw.into(), - channel_type: old.kind.name().to_string(), - }) - .set(0); - } + .member_status + .remove(&MemberStatusLabels::new(guild.id, presence.status)); - // Increment new - self.metrics_handler - .channel - .get_or_create(&ChannelLabels { - guild_id: new.guild_id.get(), - channel_id: new.id.get(), - channel_name: new.name.clone(), - channel_nsfw: new.nsfw.into(), - channel_type: new.kind.name().to_string(), - }) - .set(1); - } + // Handle `activity` metric + for activity in &presence.activities { + self.metrics_handler + .activity + .remove(&ActivityLabels::new(guild.id, activity)); + } - async fn channel_delete( - &self, - _ctx: Context, - channel: GuildChannel, - _messages: Option>, - ) { - info!( - guild_id = channel.guild_id.get(), - channel_id = channel.id.get(), - "Channel delete" - ); + // remove user presences from handler cache + self.users.write().await.remove(&(guild.id, *user_id)); + } - self.metrics_handler - .channel - .get_or_create(&ChannelLabels { - guild_id: channel.guild_id.get(), - channel_id: channel.id.get(), - channel_name: channel.name.clone(), - channel_nsfw: channel.nsfw.into(), - channel_type: channel.kind.name().to_string(), - }) - .set(0); + // Handle `member_voice` metric + for voice in guild.voice_states.values() { + if let Some(channel_id) = &voice.channel_id { + let (category_id, channel_id) = category_channel(&ctx, guild.id, *channel_id); + self.metrics_handler + .member_voice + .remove(&MemberVoiceLabels::new( + guild.id, + category_id, + channel_id, + voice, + )); + } + } } async fn guild_member_addition(&self, _ctx: Context, new_member: Member) { @@ -319,18 +309,14 @@ impl EventHandler for Handler { // Handle `member` metric self.metrics_handler .member - .get_or_create(&MemberLabels { - guild_id: new_member.guild_id.into(), - }) + .get_or_create(&MemberLabels::new(new_member.guild_id)) .inc(); // Handle `bot` metric if new_member.user.bot { self.metrics_handler .bot - .get_or_create(&BotLabels { - guild_id: new_member.guild_id.into(), - }) + .get_or_create(&BotLabels::new(new_member.guild_id)) .inc(); } } @@ -351,18 +337,14 @@ impl EventHandler for Handler { // Handle `member` metric self.metrics_handler .member - .get_or_create(&MemberLabels { - guild_id: guild_id.into(), - }) + .get_or_create(&MemberLabels::new(guild_id)) .dec(); // Handle `bot` metric if user.bot { self.metrics_handler .bot - .get_or_create(&BotLabels { - guild_id: guild_id.into(), - }) + .get_or_create(&BotLabels::new(guild_id)) .dec(); } } @@ -370,16 +352,22 @@ impl EventHandler for Handler { async fn guild_update( &self, _ctx: Context, - _old_data_if_available: Option, + old_data_if_available: Option, new_data: PartialGuild, ) { info!(guild_id = new_data.id.get(), "Guild Update"); + // Handle `guild` metric + if let Some(guild) = old_data_if_available { + self.metrics_handler + .guild + .remove(&GuildsLabels::new(&guild)); + } + + // Handle `boost` metric self.metrics_handler .boost - .get_or_create(&BoostLabels { - guild_id: new_data.id.into(), - }) + .get_or_create(&BoostLabels::new(new_data.id)) .set( new_data .premium_subscription_count @@ -401,16 +389,12 @@ impl EventHandler for Handler { return; } - let (category, channel) = category_channel(&ctx, guild_id, msg.channel_id); + let (category_id, channel_id) = category_channel(&ctx, guild_id, msg.channel_id); // Handle `message_sent` metric self.metrics_handler .message_sent - .get_or_create(&MessageSentLabels { - guild_id: guild_id.into(), - category_id: category.as_ref().map(|ch| ch.get()), - channel_id: channel.into(), - }) + .get_or_create(&MessageSentLabels::new(guild_id, category_id, channel_id)) .inc(); // Handle `emote_used` metric @@ -422,14 +406,14 @@ impl EventHandler for Handler { self.metrics_handler .emote_used - .get_or_create(&EmoteUsedLabels { - guild_id: guild_id.into(), - category_id: category.as_ref().map(|ch| ch.get()), - channel_id: channel.into(), - reaction: false.into(), - emoji_id: emoji.id.into(), - emoji_name: Some(emoji.name), - }) + .get_or_create(&EmoteUsedLabels::new( + guild_id, + category_id, + channel_id, + false, + emoji.id, + Some(emoji.name), + )) .inc(); } } @@ -453,19 +437,19 @@ impl EventHandler for Handler { return; }; - let (category, channel) = category_channel(&ctx, guild_id, add_reaction.channel_id); + let (category_id, channel_id) = category_channel(&ctx, guild_id, add_reaction.channel_id); // Handle `emote_used` metric self.metrics_handler .emote_used - .get_or_create(&EmoteUsedLabels { - guild_id: guild_id.into(), - category_id: category.as_ref().map(|ch| ch.get()), - channel_id: channel.into(), - reaction: true.into(), - emoji_id: id.into(), - emoji_name: name, - }) + .get_or_create(&EmoteUsedLabels::new( + guild_id, + category_id, + channel_id, + true, + id, + name, + )) .inc(); } @@ -485,21 +469,17 @@ impl EventHandler for Handler { // Handle `member_status` metric (decrement) self.metrics_handler .member_status - .get_or_create(&MemberStatusLabels { - guild_id: guild_id.into(), - status: cached_user.presence.status.name().to_string(), - }) + .get_or_create(&MemberStatusLabels::new( + guild_id, + cached_user.presence.status, + )) .dec(); // Handle `activity` metric (decrement) for activity in &cached_user.presence.activities { self.metrics_handler .activity - .get_or_create(&ActivityLabels { - guild_id: guild_id.into(), - activity_application_id: activity.application_id.map(Into::into), - activity_name: activity.name.clone(), - }) + .get_or_create(&ActivityLabels::new(guild_id, activity)) .dec(); } } @@ -507,21 +487,14 @@ impl EventHandler for Handler { // Handle `member_status` metric self.metrics_handler .member_status - .get_or_create(&MemberStatusLabels { - guild_id: guild_id.into(), - status: new_data.status.name().to_string(), - }) + .get_or_create(&MemberStatusLabels::new(guild_id, new_data.status)) .inc(); // Handle `activity` metric for activity in &new_data.activities { self.metrics_handler .activity - .get_or_create(&ActivityLabels { - guild_id: guild_id.into(), - activity_application_id: activity.application_id.map(Into::into), - activity_name: activity.name.clone(), - }) + .get_or_create(&ActivityLabels::new(guild_id, activity)) .inc(); } @@ -560,20 +533,17 @@ impl EventHandler for Handler { break 'dec; }; - let (category, channel) = category_channel(&ctx, guild_id, *channel_id); + let (category_id, channel_id) = category_channel(&ctx, guild_id, *channel_id); // Handle `member_voice` metric (decrement) self.metrics_handler .member_voice - .get_or_create(&MemberVoiceLabels { - guild_id: guild_id.into(), - category_id: category.as_ref().map(|ch| ch.get()), - channel_id: channel.into(), - self_stream: old.self_stream.unwrap_or(false).into(), - self_video: old.self_video.into(), - self_deaf: old.self_deaf.into(), - self_mute: old.self_mute.into(), - }) + .get_or_create(&MemberVoiceLabels::new( + guild_id, + category_id, + channel_id, + &old, + )) .dec(); } @@ -590,20 +560,17 @@ impl EventHandler for Handler { break 'inc; }; - let (category, channel) = category_channel(&ctx, guild_id, *channel_id); + let (category_id, channel_id) = category_channel(&ctx, guild_id, *channel_id); // Handle `member_voice` metric self.metrics_handler .member_voice - .get_or_create(&MemberVoiceLabels { - guild_id: guild_id.into(), - category_id: category.as_ref().map(|ch| ch.get()), - channel_id: channel.into(), - self_stream: new.self_stream.unwrap_or(false).into(), - self_video: new.self_video.into(), - self_deaf: new.self_deaf.into(), - self_mute: new.self_mute.into(), - }) + .get_or_create(&MemberVoiceLabels::new( + guild_id, + category_id, + channel_id, + &new, + )) .inc(); } } diff --git a/src/metrics.rs b/src/metrics.rs index 19d536a..87c5443 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -12,6 +12,10 @@ use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::metrics::gauge::Gauge; use prometheus_client::registry::Registry; +use serenity::all::{ + Activity, ApplicationId, ChannelId, EmojiId, Guild, GuildChannel, GuildId, OnlineStatus, + VoiceState, +}; use std::net::SocketAddr; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -54,6 +58,17 @@ impl EncodeLabelValue for Boolean { #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct GuildsLabels { pub guild_id: u64, + pub guild_name: String, +} + +impl GuildsLabels { + /// Creates a new instance of [`GuildsLabels`]. + pub fn new(guild: &Guild) -> Self { + Self { + guild_id: guild.id.get(), + guild_name: guild.name.clone(), + } + } } /// [`ChannelLabels`] are the [labels](EncodeLabelSet) for the `channel` metric. @@ -66,18 +81,49 @@ pub struct ChannelLabels { pub channel_type: String, } +impl ChannelLabels { + /// Creates a new instance of [`ChannelLabels`]. + pub fn new(channel: &GuildChannel) -> Self { + Self { + guild_id: channel.guild_id.get(), + channel_id: channel.id.get(), + channel_name: channel.name.clone(), + channel_nsfw: Boolean(channel.nsfw), + channel_type: channel.kind.name().to_string(), + } + } +} + /// [`BoostLabels`] are the [labels](EncodeLabelSet) for the `boost` metric. #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct BoostLabels { pub guild_id: u64, } +impl BoostLabels { + /// Creates a new instance of [`BoostLabels`]. + pub fn new(guild_id: GuildId) -> Self { + Self { + guild_id: guild_id.get(), + } + } +} + /// [`MemberLabels`] are the [labels](EncodeLabelSet) for the `member` metric. #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct MemberLabels { pub guild_id: u64, } +impl MemberLabels { + /// Creates a new instance of [`MemberLabels`]. + pub fn new(guild_id: GuildId) -> Self { + Self { + guild_id: guild_id.get(), + } + } +} + /// [`BotLabels`] are the [labels](EncodeLabelSet) for the `bot` metric. /// /// This metric is not included in the member metric (using a label) as the user bot status has to @@ -88,6 +134,15 @@ pub struct BotLabels { pub guild_id: u64, } +impl BotLabels { + /// Creates a new instance of [`BotLabels`]. + pub fn new(guild_id: GuildId) -> Self { + Self { + guild_id: guild_id.get(), + } + } +} + /// [`MemberStatusLabels`] are the [labels](EncodeLabelSet) for the `member_status` metric. #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct MemberStatusLabels { @@ -95,6 +150,16 @@ pub struct MemberStatusLabels { pub status: String, } +impl MemberStatusLabels { + /// Creates a new instance of [`MemberStatusLabels`]. + pub fn new(guild_id: GuildId, status: OnlineStatus) -> Self { + Self { + guild_id: guild_id.get(), + status: status.name().to_string(), + } + } +} + /// [`MemberVoiceLabels`] are the [labels](EncodeLabelSet) for the `member_voice` metric. #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct MemberVoiceLabels { @@ -107,6 +172,26 @@ pub struct MemberVoiceLabels { pub self_mute: Boolean, } +impl MemberVoiceLabels { + /// Creates a new instance of [`MemberVoiceLabels`]. + pub fn new( + guild_id: GuildId, + category_id: Option, + channel_id: ChannelId, + voice: &VoiceState, + ) -> Self { + Self { + guild_id: guild_id.get(), + category_id: category_id.map(ChannelId::get), + channel_id: channel_id.get(), + self_stream: voice.self_stream.unwrap_or(false).into(), + self_video: voice.self_video.into(), + self_deaf: voice.self_deaf.into(), + self_mute: voice.self_mute.into(), + } + } +} + /// [`MessageSentLabels`] are the [labels](EncodeLabelSet) for the `message_sent` metric. #[allow(clippy::struct_field_names)] #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] @@ -116,6 +201,17 @@ pub struct MessageSentLabels { pub channel_id: u64, } +impl MessageSentLabels { + /// Creates a new instance of [`MessageSentLabels`]. + pub fn new(guild_id: GuildId, category_id: Option, channel_id: ChannelId) -> Self { + Self { + guild_id: guild_id.get(), + category_id: category_id.map(ChannelId::get), + channel_id: channel_id.get(), + } + } +} + /// [`EmoteUsedLabels`] are the [labels](EncodeLabelSet) for the `emote_used` metric. #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct EmoteUsedLabels { @@ -127,6 +223,27 @@ pub struct EmoteUsedLabels { pub emoji_name: Option, } +impl EmoteUsedLabels { + /// Creates a new instance of [`EmoteUsedLabels`]. + pub fn new( + guild_id: GuildId, + category_id: Option, + channel_id: ChannelId, + reaction: bool, + emoji_id: EmojiId, + emoji_name: Option, + ) -> Self { + Self { + guild_id: guild_id.get(), + category_id: category_id.map(ChannelId::get), + channel_id: channel_id.get(), + reaction: Boolean(reaction), + emoji_id: emoji_id.get(), + emoji_name, + } + } +} + /// [`ActivityLabels`] are the [labels](EncodeLabelSet) for the `activity` metric. #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] pub struct ActivityLabels { @@ -135,6 +252,17 @@ pub struct ActivityLabels { pub activity_name: String, } +impl ActivityLabels { + /// Creates a new instance of [`ActivityLabels`]. + pub fn new(guild_id: GuildId, activity: &Activity) -> Self { + Self { + guild_id: guild_id.get(), + activity_application_id: activity.application_id.map(ApplicationId::get), + activity_name: activity.name.clone(), + } + } +} + /// Handler is the [servable](serve) bundle of metrics for the exporter. pub struct Handler { registry: Registry,