@@ -4,6 +4,8 @@ use env_logger::Env;
44use hostname:: get as get_hostname_raw;
55use log:: { error, info} ;
66use regex:: Regex ;
7+ use solana_client:: rpc_client:: RpcClient ;
8+ use solana_metrics:: { datapoint_info, set_host_id} ;
79use std:: collections:: HashSet ;
810use std:: path:: Path ;
911use std:: process:: Command ;
@@ -34,6 +36,14 @@ struct Args {
3436 /// Directory to scan for snapshot files
3537 #[ arg( short, long) ]
3638 snapshot_directory : String ,
39+
40+ /// Solana JSON RPC URL to fetch current epoch for metrics
41+ #[ arg(
42+ long,
43+ env = "RPC_URL" ,
44+ default_value = "https://api.mainnet-beta.solana.com"
45+ ) ]
46+ rpc_url : String ,
3747}
3848
3949#[ tokio:: main]
@@ -64,6 +74,8 @@ async fn main() -> Result<()> {
6474 // Get hostname
6575 let hostname = get_hostname ( ) ?;
6676
77+ set_host_id ( hostname. clone ( ) ) ;
78+
6779 // Determine bucket name
6880 let bucket_name = args
6981 . bucket
@@ -125,6 +137,18 @@ async fn main() -> Result<()> {
125137 }
126138 }
127139
140+ // Emit metric about whether snapshot for current epoch is present in GCS
141+ if let Err ( e) = emit_current_epoch_snapshot_metric (
142+ & args. rpc_url ,
143+ & bucket_name,
144+ & hostname,
145+ & args. cluster ,
146+ )
147+ . await
148+ {
149+ error ! ( "Error emitting snapshot metric: {}" , e) ;
150+ }
151+
128152 // Wait for the next polling interval
129153 sleep ( Duration :: from_secs ( args. interval ) ) . await ;
130154 }
@@ -188,7 +212,7 @@ async fn scan_and_upload_files(
188212}
189213
190214/// Scans directory for snapshots & uploads after deriving the associated epoch
191- #[ allow( clippy:: arithmetic_side_effects) ]
215+ #[ allow( clippy:: arithmetic_side_effects, clippy :: integer_division ) ]
192216async fn scan_and_upload_snapshot_files (
193217 dir_path : & Path ,
194218 bucket_name : & str ,
@@ -311,6 +335,51 @@ async fn upload_file(
311335 Ok ( ( ) )
312336}
313337
338+ async fn emit_current_epoch_snapshot_metric (
339+ rpc_url : & str ,
340+ bucket_name : & str ,
341+ hostname : & str ,
342+ cluster : & str ,
343+ ) -> Result < ( ) > {
344+ // Fetch current epoch via Solana RPC
345+ let client = RpcClient :: new ( rpc_url. to_string ( ) ) ;
346+ let epoch_info = client
347+ . get_epoch_info ( )
348+ . with_context ( || format ! ( "Failed to fetch epoch info from {}" , rpc_url) ) ?;
349+ let epoch = epoch_info. epoch ;
350+
351+ // Build GCS prefix path used by upload_file: {epoch}/{hostname}/snapshot-*.tar.zst
352+ // First, list objects under epoch/hostname and look for any snapshot-*.tar.zst
353+ let list_output = Command :: new ( "/opt/gcloud/google-cloud-sdk/bin/gcloud" )
354+ . args ( [
355+ "storage" ,
356+ "ls" ,
357+ & format ! ( "gs://{}/{}/{}/" , bucket_name, epoch, hostname) ,
358+ ] )
359+ . output ( )
360+ . with_context ( || "Failed to execute gcloud ls for snapshot metric" ) ?;
361+
362+ let uploaded = if list_output. status . success ( ) {
363+ let stdout = String :: from_utf8_lossy ( & list_output. stdout ) ;
364+ stdout
365+ . lines ( )
366+ . any ( |line| line. contains ( "snapshot-" ) && line. ends_with ( ".tar.zst" ) )
367+ } else {
368+ false
369+ } ;
370+
371+ datapoint_info ! (
372+ "tip_router_gcp_uploader.snapshot_present" ,
373+ ( "epoch" , epoch as i64 , i64 ) ,
374+ ( "present" , uploaded, bool ) ,
375+ "cluster" => cluster,
376+ "hostname" => hostname,
377+ "bucket" => bucket_name,
378+ ) ;
379+
380+ Ok ( ( ) )
381+ }
382+
314383fn get_hostname ( ) -> Result < String > {
315384 let hostname = get_hostname_raw ( )
316385 . context ( "Failed to get hostname" ) ?
0 commit comments