@@ -84,6 +84,8 @@ class Systemctl2Mqtt:
8484 Activate the stats
8585 b_events
8686 Activate the events
87+ b_smaps
88+ Activate the smaps memory stats (more detailed memory info, but more cpu usage)
8789 systemctl_events
8890 Queue with systemctl events
8991 systemctl_stats
@@ -127,6 +129,7 @@ class Systemctl2Mqtt:
127129
128130 b_stats : bool = False
129131 b_events : bool = False
132+ b_smaps : bool = False
130133
131134 systemctl_events : Queue [dict [str , str ]] = Queue (maxsize = MAX_QUEUE_SIZE )
132135 systemctl_stats : Queue [list [str ]] = Queue (maxsize = MAX_QUEUE_SIZE )
@@ -185,6 +188,8 @@ def __init__(self, cfg: Systemctl2MqttConfig, do_not_exit: bool = False):
185188 self .b_events = True
186189 if self .cfg ["enable_stats" ]:
187190 self .b_stats = True
191+ if self .cfg ["enable_smaps" ]:
192+ self .b_smaps = True
188193
189194 main_logger .setLevel (self .cfg ["log_level" ].upper ())
190195 events_logger .setLevel (self .cfg ["log_level" ].upper ())
@@ -197,13 +202,19 @@ def __init__(self, cfg: Systemctl2MqttConfig, do_not_exit: bool = False):
197202 "Could not get systemctl version"
198203 ) from ex
199204
205+ if self .b_smaps and not self .b_stats :
206+ raise Systemctl2MqttConfigException (
207+ "Cannot enable smaps without stats, please enable stats as well."
208+ )
209+
200210 if not self .do_not_exit :
201211 main_logger .info ("Register signal handlers for SIGINT and SIGTERM" )
202212 signal .signal (signal .SIGTERM , self ._signal_handler )
203213 signal .signal (signal .SIGINT , self ._signal_handler )
204214
205215 main_logger .info ("Events enabled: %d" , self .b_events )
206216 main_logger .info ("Stats enabled: %d" , self .b_stats )
217+ main_logger .info ("Smaps enabled: %d" , self .b_smaps )
207218
208219 try :
209220 # Setup MQTT
@@ -720,6 +731,7 @@ def _register_service(self, service_entry: ServiceEvent) -> None:
720731 "unit_of_measurement" : None ,
721732 "device" : self ._device_definition (service_entry ),
722733 "device_class" : "running" ,
734+ "entity_category" : None ,
723735 "json_attributes_topic" : events_topic ,
724736 "qos" : self .cfg ["mqtt_qos" ],
725737 }
@@ -736,7 +748,7 @@ def _register_service(self, service_entry: ServiceEvent) -> None:
736748 )
737749
738750 # Stats
739- for label , field , device_class , unit , icon in STATS_REGISTRATION_ENTRIES :
751+ for label , field , device_class , unit , icon , category in STATS_REGISTRATION_ENTRIES :
740752 registration_topic = self .discovery_sensor_topic .format (
741753 INVALID_HA_TOPIC_CHARS .sub ("_" , f"{ service } _{ field } _stats" )
742754 )
@@ -756,6 +768,7 @@ def _register_service(self, service_entry: ServiceEvent) -> None:
756768 "payload_off" : None ,
757769 "json_attributes_topic" : stats_topic ,
758770 "device_class" : device_class ,
771+ "entity_category" : category ,
759772 "device" : self ._device_definition (service_entry ),
760773 "qos" : self .cfg ["mqtt_qos" ],
761774 }
@@ -800,7 +813,7 @@ def _unregister_service(self, service: str) -> None:
800813 )
801814
802815 # Stats
803- for _ , field , _ , _ , _ in STATS_REGISTRATION_ENTRIES :
816+ for _ , field , _ , _ , _ , _ in STATS_REGISTRATION_ENTRIES :
804817 self ._mqtt_send (
805818 self .discovery_sensor_topic .format (
806819 INVALID_HA_TOPIC_CHARS .sub ("_" , f"{ service } _{ field } _stats" )
@@ -975,6 +988,38 @@ def _handle_events_queue(self) -> None:
975988 retain = True ,
976989 )
977990
991+ def get_smaps (self , pid : int ) -> dict [str , int ]:
992+ """Parse /proc/<pid>/smaps_rollup into a dictionary.
993+
994+ Keys are field names (e.g. 'Pss', 'Pss_Anon', 'Pss_File', ...).
995+ Values are integers in kilobytes.
996+
997+ Parameters
998+ ----------
999+ pid
1000+ The PID of the process to get the smaps for
1001+
1002+ Raises
1003+ ------
1004+ ValueError
1005+ If the process is not found or smaps is not supported.
1006+
1007+ """
1008+
1009+ result = {}
1010+ path = f"/proc/{ pid } /smaps_rollup"
1011+ try :
1012+ with open (path ) as f :
1013+ for line in f :
1014+ parts = line .split ()
1015+ if len (parts ) >= 2 and parts [1 ].isdigit ():
1016+ key = parts [0 ].rstrip (":" )
1017+ val = int (parts [1 ]) # always in kB
1018+ result [key ] = val
1019+ except FileNotFoundError :
1020+ raise ValueError (f"Process { pid } not found or smaps not supported" ) from None
1021+ return result
1022+
9781023 def _handle_stats_queue (self ) -> None :
9791024 """Check if any stat is present in the queue and process it.
9801025
@@ -1045,11 +1090,36 @@ def _handle_stats_queue(self) -> None:
10451090 # self.known_stat_services[service][pid]["last"] - container_date
10461091 # ).total_seconds()
10471092
1093+ smaps = self .get_smaps (pid ) if self .b_smaps else {}
1094+
10481095 pid_stats = PIDStats (
10491096 {
10501097 "pid" : pid ,
10511098 "cpu" : float (stat [8 ]),
10521099 "memory" : parse_top_size (stat [5 ]) / 1024 , # KB --> MB
1100+ "memory_real" : (smaps .get ("Anonymous" , 0 ) + smaps .get ("SwapPss" , 0 )) / 1024 ,
1101+ "memory_real_pss" : (smaps .get ("Pss_Anon" , 0 ) + smaps .get ("Pss_Shm" , 0 ) + smaps .get ("SwapPss" , 0 )) / 1024 ,
1102+ "memory_pss_anon" : smaps .get ("Pss_Anon" , 0 ) / 1024 ,
1103+ "memory_pss" : smaps .get ("Pss" , 0 ) / 1024 ,
1104+ "memory_pss_file" : smaps .get ("Pss_File" , 0 ) / 1024 ,
1105+ "memory_pss_dirty" : smaps .get ("Pss_Dirty" , 0 ) / 1024 ,
1106+ "memory_pss_shmem" : smaps .get ("Pss_Shm" , 0 ) / 1024 ,
1107+ "memory_rss" : smaps .get ("Rss" , 0 ) / 1024 ,
1108+ "memory_shared_clean" : smaps .get ("Shared_Clean" , 0 ) / 1024 ,
1109+ "memory_shared_dirty" : smaps .get ("Shared_Dirty" , 0 ) / 1024 ,
1110+ "memory_private_clean" : smaps .get ("Private_Clean" , 0 ) / 1024 ,
1111+ "memory_private_dirty" : smaps .get ("Private_Dirty" , 0 ) / 1024 ,
1112+ "memory_referenced" : smaps .get ("Referenced" , 0 ) / 1024 ,
1113+ "memory_anonymous" : smaps .get ("Anonymous" , 0 ) / 1024 ,
1114+ "memory_lazyfree" : smaps .get ("LazyFree" , 0 ) / 1024 ,
1115+ "memory_anon_hugepages" : smaps .get ("AnonHugePages" , 0 ) / 1024 ,
1116+ "memory_shmem_pmd_mapped" : smaps .get ("ShmemPmdMapped" , 0 ) / 1024 ,
1117+ "memory_file_pmd_mapped" : smaps .get ("FilePmdMapped" , 0 ) / 1024 ,
1118+ "memory_shared_hugetlb" : smaps .get ("Shared_Hugetlb" , 0 ) / 1024 ,
1119+ "memory_private_hugetlb" : smaps .get ("Private_Hugetlb" , 0 ) / 1024 ,
1120+ "memory_swap" : smaps .get ("Swap" , 0 ) / 1024 ,
1121+ "memory_swappss" : smaps .get ("SwapPss" , 0 ) / 1024 ,
1122+ "memory_locked" : smaps .get ("Locked" , 0 ) / 1024 ,
10531123 }
10541124 )
10551125 stats_logger .debug ("Printing pid stats: %s" , pid_stats )
@@ -1060,6 +1130,29 @@ def _handle_stats_queue(self) -> None:
10601130 "host" : self .cfg ["systemctl2mqtt_hostname" ],
10611131 "cpu" : 0 ,
10621132 "memory" : 0 ,
1133+ "memory_real" : 0 ,
1134+ "memory_real_pss" : 0 ,
1135+ "memory_pss" : 0 ,
1136+ "memory_pss_anon" : 0 ,
1137+ "memory_pss_file" : 0 ,
1138+ "memory_pss_dirty" : 0 ,
1139+ "memory_pss_shmem" : 0 ,
1140+ "memory_rss" : 0 ,
1141+ "memory_shared_clean" : 0 ,
1142+ "memory_shared_dirty" : 0 ,
1143+ "memory_private_clean" : 0 ,
1144+ "memory_private_dirty" : 0 ,
1145+ "memory_referenced" : 0 ,
1146+ "memory_anonymous" : 0 ,
1147+ "memory_lazyfree" : 0 ,
1148+ "memory_anon_hugepages" : 0 ,
1149+ "memory_shmem_pmd_mapped" : 0 ,
1150+ "memory_file_pmd_mapped" : 0 ,
1151+ "memory_shared_hugetlb" : 0 ,
1152+ "memory_private_hugetlb" : 0 ,
1153+ "memory_swap" : 0 ,
1154+ "memory_swappss" : 0 ,
1155+ "memory_locked" : 0 ,
10631156 "pid_stats" : self .last_stat_services [service ][
10641157 "pid_stats"
10651158 ]
@@ -1071,6 +1164,30 @@ def _handle_stats_queue(self) -> None:
10711164
10721165 for pid_stat in service_stats ["pid_stats" ].values ():
10731166 service_stats ["memory" ] += pid_stat ["memory" ]
1167+ service_stats ["memory" ] += pid_stat ["memory" ]
1168+ service_stats ["memory_real" ] += pid_stat ["memory_real" ]
1169+ service_stats ["memory_real_pss" ] += pid_stat ["memory_real_pss" ]
1170+ service_stats ["memory_pss" ] += pid_stat ["memory_pss" ]
1171+ service_stats ["memory_pss_anon" ] += pid_stat ["memory_pss_anon" ]
1172+ service_stats ["memory_pss_file" ] += pid_stat ["memory_pss_file" ]
1173+ service_stats ["memory_pss_dirty" ] += pid_stat ["memory_pss_dirty" ]
1174+ service_stats ["memory_pss_shmem" ] += pid_stat ["memory_pss_shmem" ]
1175+ service_stats ["memory_rss" ] += pid_stat ["memory_rss" ]
1176+ service_stats ["memory_shared_clean" ] += pid_stat ["memory_shared_clean" ]
1177+ service_stats ["memory_shared_dirty" ] += pid_stat ["memory_shared_dirty" ]
1178+ service_stats ["memory_private_clean" ] += pid_stat ["memory_private_clean" ]
1179+ service_stats ["memory_private_dirty" ] += pid_stat ["memory_private_dirty" ]
1180+ service_stats ["memory_referenced" ] += pid_stat ["memory_referenced" ]
1181+ service_stats ["memory_anonymous" ] += pid_stat ["memory_anonymous" ]
1182+ service_stats ["memory_lazyfree" ] += pid_stat ["memory_lazyfree" ]
1183+ service_stats ["memory_anon_hugepages" ] += pid_stat ["memory_anon_hugepages" ]
1184+ service_stats ["memory_shmem_pmd_mapped" ] += pid_stat ["memory_shmem_pmd_mapped" ]
1185+ service_stats ["memory_file_pmd_mapped" ] += pid_stat ["memory_file_pmd_mapped" ]
1186+ service_stats ["memory_shared_hugetlb" ] += pid_stat ["memory_shared_hugetlb" ]
1187+ service_stats ["memory_private_hugetlb" ] += pid_stat ["memory_private_hugetlb" ]
1188+ service_stats ["memory_swap" ] += pid_stat ["memory_swap" ]
1189+ service_stats ["memory_swappss" ] += pid_stat ["memory_swappss" ]
1190+ service_stats ["memory_locked" ] += pid_stat ["memory_locked" ]
10741191 service_stats ["cpu" ] += pid_stat ["cpu" ]
10751192
10761193 self .last_stat_services [service ] = service_stats
@@ -1236,6 +1353,11 @@ def main() -> None:
12361353 help = "Publish Stats" ,
12371354 action = "store_true" ,
12381355 )
1356+ parser .add_argument (
1357+ "--smaps" ,
1358+ help = "Publish extended memory stats (more detailed memory info, but more cpu usage, only if --stats is enabled)" ,
1359+ action = "store_true" ,
1360+ )
12391361 parser .add_argument (
12401362 "--interval" ,
12411363 help = f"The number of seconds to record state and make an average (default: { STATS_RECORD_SECONDS_DEFAULT } )" ,
@@ -1273,6 +1395,7 @@ def main() -> None:
12731395 "mqtt_qos" : args .qos ,
12741396 "enable_events" : args .events ,
12751397 "enable_stats" : args .stats ,
1398+ "enable_smaps" : args .smaps ,
12761399 "stats_record_seconds" : args .interval ,
12771400 }
12781401 )
0 commit comments