From 7dddbf273aaf6a4bef27d5da896c6a50abfd05cd Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Tue, 26 May 2026 13:19:50 -0400 Subject: [PATCH 1/3] Observability: Clean up Oshi No longer need Oshi to send system metrics to Prometheus, since we can directly monitor system resource usage of kubernetes nodes. Keeping the commit sha, since it's still included in all of the metrics generated by Spring Actuator. --- pom.xml | 5 ---- .../utilities/SystemMetricsConfig.java | 29 ++++++------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/pom.xml b/pom.xml index 11b7fe7c3..f086ab06f 100644 --- a/pom.xml +++ b/pom.xml @@ -191,11 +191,6 @@ io.micrometer micrometer-registry-prometheus - - com.github.oshi - oshi-core - 6.6.6 - org.springframework.boot spring-boot-starter-jdbc diff --git a/src/main/java/org/patinanetwork/codebloom/utilities/SystemMetricsConfig.java b/src/main/java/org/patinanetwork/codebloom/utilities/SystemMetricsConfig.java index 16aa8b1df..8db6d5a21 100644 --- a/src/main/java/org/patinanetwork/codebloom/utilities/SystemMetricsConfig.java +++ b/src/main/java/org/patinanetwork/codebloom/utilities/SystemMetricsConfig.java @@ -7,18 +7,21 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import oshi.SystemInfo; -import oshi.hardware.GlobalMemory; +/** + * Configuration for system metrics exposed to Prometheus through Spring Actuator. + * Prometheus ingests the metrics from Actuator through a Kubernetes object defined in the k8s-manifest. + * + * @see Service Monitor + */ @Configuration @EnableConfigurationProperties(CommitShaProperties.class) public class SystemMetricsConfig { - @Bean - public SystemInfo systemInfo() { - return new SystemInfo(); - } + /** + * Add commit sha to metrics so that the deployed version of the code being run on the instance is known. + */ @Bean public MeterBinder applicationInfoMetrics(CommitShaProperties commitShaProperties) { return registry -> { @@ -26,18 +29,4 @@ public MeterBinder applicationInfoMetrics(CommitShaProperties commitShaPropertie registry.gauge("application.info", tags, 1, n -> 1.0); }; } - - @Bean - public MeterBinder systemMemoryMetrics(SystemInfo systemInfo) { - return registry -> { - GlobalMemory memory = systemInfo.getHardware().getMemory(); - registry.gauge("system.info.memory.total", memory, GlobalMemory::getTotal); - registry.gauge("system.info.memory.available", memory, GlobalMemory::getAvailable); - registry.gauge("system.info.memory.used", memory, m -> m.getTotal() - m.getAvailable()); - registry.gauge( - "system.info.memory.usage", - memory, - m -> (double) (m.getTotal() - m.getAvailable()) / m.getTotal() * 100); - }; - } } From 0c7a0d5ac41b5850c23e08451c8d26d80af9b529 Mon Sep 17 00:00:00 2001 From: Angela Yu <146024318+angelayu0530@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:31:45 -0400 Subject: [PATCH 2/3] =?UTF-8?q?Tan-18:=20Remind=20people=20who=20haven?= =?UTF-8?q?=E2=80=99t=20sent=20standup=20msg=20by=20EOD=20(#938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tan-18: Wrote Logic toSend Reminder Messages if someone forgets to write stand up * TAN-18: PR changes TAN-18: fixed to use union --- internal/standup-bot/src/discord/client.rs | 57 ++++++++++++++++++-- internal/standup-bot/src/discord/handlers.rs | 42 +++++++++++++++ internal/standup-bot/src/main.rs | 56 ++++++++++++++++++- internal/standup-bot/src/redis/client.rs | 53 ++++++++++++++++++ internal/standup-bot/src/utils/standup.rs | 12 +++++ 5 files changed, 215 insertions(+), 5 deletions(-) diff --git a/internal/standup-bot/src/discord/client.rs b/internal/standup-bot/src/discord/client.rs index e2c719d11..4f46b352e 100644 --- a/internal/standup-bot/src/discord/client.rs +++ b/internal/standup-bot/src/discord/client.rs @@ -7,6 +7,7 @@ use serenity::{ Client, Error, all::{ + Cache, ChannelId, Colour, CreateEmbed, @@ -14,6 +15,10 @@ use serenity::{ CreateMessage, CreateThread, GatewayIntents, + GuildId, + Member, + RoleId, + UserId, }, http::Http, }; @@ -23,16 +28,18 @@ use crate::{ credentials::DiscordCredentials, handlers::Handler, }, + redis, utils::latch::base::{ CountdownLatch, Latch, }, }; -const INTENTS: GatewayIntents = GatewayIntents::GUILD_MEMBERS; +const INTENTS: GatewayIntents = GatewayIntents::GUILD_MEMBERS.union(GatewayIntents::GUILD_MESSAGES); const ROLE_ID: u64 = 1391944565409316944; static HTTP: OnceLock> = OnceLock::new(); +static CACHE: OnceLock> = OnceLock::new(); pub async fn init_in_bg(discord_creds: &DiscordCredentials) -> Result<(), Error> { let latch = CountdownLatch::new(1); @@ -47,6 +54,11 @@ pub async fn init_in_bg(discord_creds: &DiscordCredentials) -> Result<(), Error> .set(http) .inspect_err(|_| println!("Attempted to save Discord HTTP client more than once")); + let cache = client.cache.clone(); + let _ = CACHE + .set(cache) + .inspect_err(|_| println!("Attempted to save Discord Cache more than once")); + tokio::spawn(async move { if let Err(e) = client.start().await { println!("Client error: {e:?}"); @@ -62,6 +74,10 @@ pub fn get_http() -> Arc { HTTP.get().expect("Discord not initialized").clone() } +pub fn get_cache() -> Arc { + CACHE.get().expect("Discord not initialized").clone() +} + pub async fn send_standup_message(discord_creds: &DiscordCredentials) -> Result<(), Error> { println!("Sending standup message!"); @@ -87,9 +103,42 @@ pub async fn send_standup_message(discord_creds: &DiscordCredentials) -> Result< let thread_builder = CreateThread::new("Daily Standup Thread"); - channel + let thread = channel .create_thread_from_message(get_http().as_ref(), msg.id, thread_builder) .await - .inspect_err(|e| println!("Error creating thread: {e:#?}")) - .map(|_| ()) + .inspect_err(|e| println!("Error creating thread: {e:#?}"))?; + + if let Err(e) = redis::client::set_standup_thread_id(thread.id.get()).await { + eprintln!("Failed to persist standup thread id: {e:#?}"); + } + + Ok(()) +} + +pub async fn get_standup_role_members(guild_id: u64) -> Result, Error> { + let guild = GuildId::new(guild_id); + let role = RoleId::new(ROLE_ID); + + let members = get_cache() + .guild(guild) + .map(|g| { + g.members + .values() + .filter(|m| m.roles.contains(&role)) + .cloned() + .collect() + }) + .unwrap_or_default(); + + Ok(members) +} + +pub async fn send_eod_reminder_dm(user_id: u64) -> Result<(), Error> { + let user = UserId::new(user_id).to_user(get_http().as_ref()).await?; + + let message = CreateMessage::new().content( + "Reminder: you haven't posted your update in today's standup thread yet. Please add one before end of day!", + ); + + user.dm(get_http().as_ref(), message).await.map(|_| ()) } diff --git a/internal/standup-bot/src/discord/handlers.rs b/internal/standup-bot/src/discord/handlers.rs index 8ccb7a6df..9a6ff5f85 100644 --- a/internal/standup-bot/src/discord/handlers.rs +++ b/internal/standup-bot/src/discord/handlers.rs @@ -1,5 +1,8 @@ +use chrono::Utc; +use chrono_tz::US; use serenity::{ all::{ + ChunkGuildFilter, Command, Context, CreateInteractionResponse, @@ -7,6 +10,7 @@ use serenity::{ EventHandler, GuildId, Interaction, + Message, Ready, }, async_trait, @@ -17,6 +21,7 @@ use crate::{ commands, credentials, }, + redis, utils::latch::base::{ CountdownLatch, Latch as _, @@ -51,6 +56,31 @@ impl EventHandler for Handler { } } + async fn message(&self, _ctx: Context, new_message: Message) { + if new_message.author.bot { + return; + } + + let thread_id = match redis::client::get_standup_thread_id().await { + Ok(Some(id)) => id, + Ok(None) => return, + Err(e) => { + eprintln!("Failed to get standup thread id: {e:#?}"); + return; + } + }; + + if new_message.channel_id.get() != thread_id { + return; + } + + let today = Utc::now().with_timezone(&US::Eastern).date_naive(); + if let Err(e) = redis::client::add_standup_reply(today, new_message.author.id.get()).await + { + eprintln!("Failed to record standup reply: {e:#?}"); + } + } + async fn ready(&self, ctx: Context, ready: Ready) { println!("{} is connected!", ready.user.name); @@ -61,6 +91,18 @@ impl EventHandler for Handler { self.latch.count_down(); } + + async fn cache_ready(&self, ctx: Context, _guilds: Vec) { + let creds = credentials::get_discord_credentials(); + + ctx.shard.chunk_guild( + GuildId::new(creds.guild_id), + None, + false, + ChunkGuildFilter::None, + None, + ); + } } pub async fn get_commands(guild_id: u64, ctx: Context) -> Result, serenity::Error> { diff --git a/internal/standup-bot/src/main.rs b/internal/standup-bot/src/main.rs index 62aafc137..49ff81824 100644 --- a/internal/standup-bot/src/main.rs +++ b/internal/standup-bot/src/main.rs @@ -1,10 +1,14 @@ use std::error::Error; use chrono::Utc; +use chrono_tz::US; use dotenvy::dotenv; use tokio::time::interval; -use crate::utils::standup::is_time_to_send_standup_message; +use crate::utils::standup::{ + is_time_to_send_eod_reminder, + is_time_to_send_standup_message, +}; mod discord; mod redis; @@ -52,5 +56,55 @@ async fn main() -> Result<(), Box> { } } }); + + let cloned_discord_creds = discord_creds.clone(); + tokio::spawn(async move { + let today = Utc::now().with_timezone(&US::Eastern).date_naive(); + + let already_sent = match redis::client::get_eod_reminder_sent(today).await { + Ok(v) => v, + Err(e) => { + eprintln!("Failed to get eod_reminder_sent from Redis: {e:#?}"); + return; + } + }; + + if !is_time_to_send_eod_reminder(already_sent) { + return; + } + + let members = + match discord::client::get_standup_role_members(cloned_discord_creds.guild_id) + .await + { + Ok(m) => m, + Err(e) => { + eprintln!("Failed to fetch standup role members: {e:#?}"); + return; + } + }; + + let replied = match redis::client::get_standup_replies(today).await { + Ok(r) => r, + Err(e) => { + eprintln!("Failed to get standup replies from Redis: {e:#?}"); + return; + } + }; + + for member in members { + let user_id = member.user.id.get(); + if replied.contains(&user_id) { + continue; + } + if let Err(e) = discord::client::send_eod_reminder_dm(user_id).await { + eprintln!("Failed to send EOD reminder DM to {user_id}: {e:#?}"); + } + } + + if let Err(e) = redis::client::set_eod_reminder_sent(today).await { + eprintln!("Failed to save eod_reminder_sent to Redis: {e:#?}"); + } + }); } } diff --git a/internal/standup-bot/src/redis/client.rs b/internal/standup-bot/src/redis/client.rs index 0b6c86201..b5e1732d0 100644 --- a/internal/standup-bot/src/redis/client.rs +++ b/internal/standup-bot/src/redis/client.rs @@ -9,8 +9,10 @@ use bb8_redis::{ }; use chrono::{ DateTime, + NaiveDate, Utc, }; +use std::collections::HashSet; use std::sync::OnceLock; use crate::redis::{ @@ -58,3 +60,54 @@ pub async fn get_last_standup() -> Result>, RedisClientErro .transpose() .map_err(RedisClientError::from) } + +pub async fn set_standup_thread_id(thread_id: u64) -> Result<(), RedisClientError> { + let mut conn = get_pool().await?; + + Ok(conn + .set("standup:current_thread_id", thread_id.to_string()) + .await?) +} + +pub async fn get_standup_thread_id() -> Result, RedisClientError> { + let mut conn = get_pool().await?; + + let value: Option = conn.get("standup:current_thread_id").await?; + + Ok(value.and_then(|v| v.parse::().ok())) +} + +pub async fn add_standup_reply(date: NaiveDate, user_id: u64) -> Result<(), RedisClientError> { + let mut conn = get_pool().await?; + + Ok(conn + .sadd(format!("standup:{date}:replied"), user_id.to_string()) + .await?) +} + +pub async fn get_standup_replies(date: NaiveDate) -> Result, RedisClientError> { + let mut conn = get_pool().await?; + + let members: Vec = conn.smembers(format!("standup:{date}:replied")).await?; + + Ok(members + .into_iter() + .filter_map(|v| v.parse::().ok()) + .collect()) +} + +pub async fn set_eod_reminder_sent(date: NaiveDate) -> Result<(), RedisClientError> { + let mut conn = get_pool().await?; + + Ok(conn + .set(format!("standup:{date}:eod_reminder_sent"), "1") + .await?) +} + +pub async fn get_eod_reminder_sent(date: NaiveDate) -> Result { + let mut conn = get_pool().await?; + + let value: Option = conn.get(format!("standup:{date}:eod_reminder_sent")).await?; + + Ok(value.is_some()) +} diff --git a/internal/standup-bot/src/utils/standup.rs b/internal/standup-bot/src/utils/standup.rs index 4d18b1e6c..d52492cf4 100644 --- a/internal/standup-bot/src/utils/standup.rs +++ b/internal/standup-bot/src/utils/standup.rs @@ -30,3 +30,15 @@ pub fn is_time_to_send_standup_message(last_standup_time: Option>) None => true, } } + +pub fn is_time_to_send_eod_reminder(already_sent_today: bool) -> bool { + let now = Utc::now().with_timezone(&US::Eastern); + let is_reminder_day = now.weekday() == Weekday::Sat; + let is_reminder_time = now.hour() >= 17; + + if !(is_reminder_day && is_reminder_time) { + return false; + } + + !already_sent_today +} From 7f2209d1f609aeb1e59b5af3b16fcba40dd23a6f Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Sun, 24 May 2026 15:19:34 -0400 Subject: [PATCH 3/3] Docker: Point to patinanetwork account Moving off of Tahmid's docker account. Retag images for that. TEST=none --- .env.ci | Bin 854 -> 854 bytes .github/scripts/build-image/index.ts | 8 ++++---- .../scripts/build-image/internal/standup-bot.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.env.ci b/.env.ci index 8eea77872ef7b20be9640f38f18c161621376426..4c3609e7c7a7817aab90dbeb30505f80bda35b23 100644 GIT binary patch literal 854 zcmV-c1F8G~M@dveQdv+`0Ay;6S7~9%5nS<8n?Ojtn~_P9kFSfGJ2VJE7%l!5G|{lC zV*HWRU>!(AfSawRjLy6604F6RFBfGc`?lm~K{5o-z?O3zn3(p2;O;2->H z-2aB6g2Gb7Wx09QCmxZ3m`cbW^}pyb9u`|6%c;*?IZ^H)U`A$k>(JK(Col=ZTeVvu zx@x7HZMb=I14+{G2DKb|Y})BwEp1go(ax11QMvoH`l9}643FZ|Dk#_&$J(6`&Y6m2 z1^G2LI6y~ks)IWnAchpwZcN2}m7N3bO8q6l3whp7(P1j0si|n#wU8{35eLbHVt1;L7%iCmWP6n zW9f?cxYV1lx+`WSMj9DAWGnUi?BC3r95e+@<|0EI*%$pP1+_BePvx~nA}-d?nHs#N z>MofYib&*jnRX@u_^awUj0(yZC>DqBTWkQxP|v8A|M`{4*P z9a11)%n+yYh#@&`7as~kLez^WUhV(1bW(iRVb@8{zIf=x+nfnGiePDsFTQ*4?f<95 zSY<9$L5)@ZkcT2i#X!3}c##hzPFiXs%YSlLXm8~eY2spCV!;hZ7DCXJZ0Iu6I$1w! g$py4UjYa{RDVM1A+K{K(k9dL_%G1W$JSsE@nSzF=Bme*a literal 854 zcmV-c1F8G~M@dveQdv+`06Eu+Vi#ekpOc%L>ZzFei3q^K3ZDbA1NXcMn)ziyopJ;v zJCg9{5>Aj&jD3FklVEh)2u{14p-u=QBhxjyAjaj$Q`n; zbZEaZFy-MCjbh2p5}T{RYs;Vz_!%f-C6_9Ea(tbtS$=289<5{?r6?jJqgxGP64IPl ztb3k)(i{ij1Ei=sD0&djY*(@cDhu?Epr8W;5SnMm&+DQ4Zn6=Hz=8}1WSW0?zGo0P z1WxGg^XY&@ZN6Yszle8CBV=iQoiT|r!u{p#Tp9;pKBpwkywB{$z}V-2L=%azIF1w` z>I6Z;=!%MBEuD^q6Zql$is2J8L~z(!`GR^zfJIDoEabX1l=ZwWl;`pRZ>}At<)kVF zrmcoYtw}kR^#EaJt(6bM95QSZZ83w*-7-Au!EM}9d~<;8bPDKkcqUX7bZiyY`x z!(2AI&%-UU@`o&q2UOAVhtM3YAt{sM!SZgj`ibp9{k2_*HssaL;ABz*Thad=e|H8_ zFy+vwHw>utN^C4RM}y6zD*My}ME2uu%0ZHMM~ygo=?}=V(0ObfI!7eCuX<{V!vls6 z`R#fBVO+Lrf1!SEm*H6o6~B627mXL`z4g8*tI?q4M2XC`Fs#h~gwDc*U;|rc{ZrjG zde8O!d1%5mvW|<7Iaho@IgK~oF`WyQR^d1tc~NOd>|>YhtfZ)_2)ndg%=zO_J{U6I zcYG7VsN0p&JtC}EhHsX=YX*X7+Vv-vrJz^y8kg*9BV~HG!;e0ow2+w;R~Zg;d_9H$ z;MCYM4%Y)HAP3`oiuBq^h#uzd-O@gb^GaK{pec;4KcOM7P5D|lovGbZ$xib@f)9^? zNj@7@R*jp@dDHKg&t%ZzN-z{qI|=MUEKGw+!ylJa#zHMy8W_#A7d1JPNGo0@)ycdsxj_ZHHNtny&JPCF_=KZ1AZ14vO