@@ -20,7 +20,7 @@ use aya::{
2020 programs:: { KProbe , TracePoint , UProbe } ,
2121 Ebpf ,
2222} ;
23- use std:: collections:: HashMap ;
23+ use std:: collections:: { HashMap , HashSet } ;
2424use std:: net:: { IpAddr , Ipv4Addr , Ipv6Addr } ;
2525use std:: time:: Duration ;
2626
@@ -205,9 +205,23 @@ async fn main() -> anyhow::Result<()> {
205205 livenessProbe on it will restart-loop the pod") ;
206206 }
207207
208+ let collector = CollectorMeta :: from_env (
209+ files,
210+ std:: env:: var_os ( "A3S_OBSERVER_SSL" ) . is_some ( ) ,
211+ attached,
212+ ) ;
213+
208214 let mut sigint = tokio:: signal:: unix:: signal ( tokio:: signal:: unix:: SignalKind :: interrupt ( ) ) ?;
209215 let mut sigterm = tokio:: signal:: unix:: signal ( tokio:: signal:: unix:: SignalKind :: terminate ( ) ) ?;
210216 let mut stats = Stats :: default ( ) ;
217+ emit_collector_heartbeat (
218+ exporter. as_ref ( ) ,
219+ & collector,
220+ 0 ,
221+ & stats,
222+ 0 ,
223+ exporter. output_drops ( ) ,
224+ ) ;
211225 let mut report = tokio:: time:: interval ( Duration :: from_secs ( 60 ) ) ;
212226 report. tick ( ) . await ; // consume the immediate first tick
213227 loop {
@@ -221,6 +235,7 @@ async fn main() -> anyhow::Result<()> {
221235 . map( |v| v. iter( ) . copied( ) . sum( ) )
222236 . unwrap_or( 0 ) ;
223237 let output_dropped = exporter. output_drops( ) ;
238+ emit_collector_heartbeat( exporter. as_ref( ) , & collector, 60 , & stats, dropped, output_dropped) ;
224239 tracing:: info!(
225240 exec = stats. exec,
226241 exit = stats. exit,
@@ -543,6 +558,110 @@ struct Stats {
543558 llm : u64 ,
544559 ssl : u64 ,
545560 sec : u64 ,
561+ agents : HashSet < String > ,
562+ }
563+
564+ struct CollectorMeta {
565+ collector_id : String ,
566+ node_name : Option < String > ,
567+ namespace : Option < String > ,
568+ pod_name : Option < String > ,
569+ version : String ,
570+ mode : String ,
571+ attached_probes : u32 ,
572+ enabled_features : Vec < String > ,
573+ }
574+
575+ impl CollectorMeta {
576+ fn from_env ( files : bool , ssl : bool , attached : usize ) -> Self {
577+ let node_name = env_any ( & [ "A3S_NODE_NAME" , "NODE_NAME" , "K8S_NODE_NAME" ] ) . or_else ( hostname) ;
578+ let namespace = env_any ( & [ "A3S_NAMESPACE" , "POD_NAMESPACE" , "K8S_NAMESPACE" ] ) ;
579+ let pod_name = env_any ( & [ "A3S_POD_NAME" , "POD_NAME" , "HOSTNAME" ] ) ;
580+ let collector_id = env_any ( & [ "A3S_OBSERVER_COLLECTOR_ID" , "COLLECTOR_ID" ] )
581+ . or_else ( || pod_name. clone ( ) )
582+ . or_else ( || node_name. clone ( ) )
583+ . unwrap_or_else ( || "a3s-observer" . to_string ( ) ) ;
584+ let mut enabled_features = vec ! [
585+ "exec" . to_string( ) ,
586+ "network" . to_string( ) ,
587+ "dns" . to_string( ) ,
588+ "security" . to_string( ) ,
589+ ] ;
590+ if files {
591+ enabled_features. push ( "files" . to_string ( ) ) ;
592+ }
593+ if ssl {
594+ enabled_features. push ( "ssl" . to_string ( ) ) ;
595+ }
596+ let mode = if files || ssl {
597+ "observe+extensions"
598+ } else {
599+ "observe"
600+ }
601+ . to_string ( ) ;
602+ Self {
603+ collector_id,
604+ node_name,
605+ namespace,
606+ pod_name,
607+ version : env ! ( "CARGO_PKG_VERSION" ) . to_string ( ) ,
608+ mode,
609+ attached_probes : attached as u32 ,
610+ enabled_features,
611+ }
612+ }
613+ }
614+
615+ fn env_any ( names : & [ & str ] ) -> Option < String > {
616+ names. iter ( ) . find_map ( |name| {
617+ std:: env:: var ( name)
618+ . ok ( )
619+ . map ( |v| v. trim ( ) . to_string ( ) )
620+ . filter ( |v| !v. is_empty ( ) )
621+ } )
622+ }
623+
624+ fn hostname ( ) -> Option < String > {
625+ std:: fs:: read_to_string ( "/etc/hostname" )
626+ . ok ( )
627+ . map ( |s| s. trim ( ) . to_string ( ) )
628+ . filter ( |s| !s. is_empty ( ) )
629+ }
630+
631+ fn emit_collector_heartbeat (
632+ exporter : & dyn Exporter ,
633+ meta : & CollectorMeta ,
634+ interval_secs : u64 ,
635+ stats : & Stats ,
636+ dropped : u64 ,
637+ output_dropped : u64 ,
638+ ) {
639+ exporter. export ( & EnrichedEvent {
640+ identity : Identity :: default ( ) ,
641+ provider : None ,
642+ event : AgentEvent :: CollectorHeartbeat {
643+ collector_id : meta. collector_id . clone ( ) ,
644+ node_name : meta. node_name . clone ( ) ,
645+ namespace : meta. namespace . clone ( ) ,
646+ pod_name : meta. pod_name . clone ( ) ,
647+ version : meta. version . clone ( ) ,
648+ mode : meta. mode . clone ( ) ,
649+ attached_probes : meta. attached_probes ,
650+ enabled_features : meta. enabled_features . clone ( ) ,
651+ interval_secs,
652+ observed_agents : stats. agents . len ( ) as u64 ,
653+ exec : stats. exec ,
654+ exit : stats. exit ,
655+ egress : stats. egress ,
656+ dns : stats. dns ,
657+ file : stats. file ,
658+ llm : stats. llm ,
659+ ssl : stats. ssl ,
660+ sec : stats. sec ,
661+ dropped,
662+ output_dropped,
663+ } ,
664+ } ) ;
546665}
547666
548667/// Export an event and count it by kind for the throughput report.
@@ -558,6 +677,12 @@ fn emit(exporter: &dyn Exporter, stats: &mut Stats, ev: EnrichedEvent) {
558677 AgentEvent :: SslContent { .. } => stats. ssl += 1 ,
559678 AgentEvent :: LlmApi { .. } => stats. llm += 1 ,
560679 AgentEvent :: SecurityAction { .. } => stats. sec += 1 ,
680+ AgentEvent :: CollectorHeartbeat { .. } => { }
681+ }
682+ if !matches ! ( ev. event, AgentEvent :: CollectorHeartbeat { .. } ) {
683+ if let Some ( agent) = & ev. identity . agent {
684+ stats. agents . insert ( agent. clone ( ) ) ;
685+ }
561686 }
562687 exporter. export ( & ev) ;
563688}
0 commit comments