Skip to content

Commit a42b424

Browse files
authored
Merge pull request #1299 from kernelkit/statd-add-service-stat
Add service runtime statistics and table formatting framework
2 parents a5bc493 + faebf70 commit a42b424

File tree

8 files changed

+479
-67
lines changed

8 files changed

+479
-67
lines changed

src/confd/yang/confd.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ MODULES=(
3838
3939
4040
41-
"infix-system@2025-10-18.yang"
41+
"infix-system@2025-12-02.yang"
4242
4343
4444

src/confd/yang/confd/infix-system.yang

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ module infix-system {
2828
contact "[email protected]";
2929
description "Infix augments and deviations to ietf-system.";
3030

31+
revision 2025-12-02 {
32+
description "Extend services with runtime statistics:
33+
- Add statistics container with memory-usage, uptime, restart-count";
34+
reference "internal";
35+
}
3136
revision 2025-10-18 {
3237
description "New system-state status:
3338
- Add system resource usage: memory, loadavg, filesystem usage
@@ -508,6 +513,28 @@ module infix-system {
508513
description
509514
"Detailed current status of the process.";
510515
}
516+
517+
container statistics {
518+
description "Service resource usage and runtime statistics";
519+
config false;
520+
521+
leaf memory-usage {
522+
type uint64;
523+
units "bytes";
524+
description "Current memory usage in bytes";
525+
}
526+
527+
leaf uptime {
528+
type uint64;
529+
units "seconds";
530+
description "Time service has been running";
531+
}
532+
533+
leaf restart-count {
534+
type uint32;
535+
description "Number of service restarts";
536+
}
537+
}
511538
}
512539
}
513540

src/statd/python/cli_pretty/cli_pretty.py

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,6 @@ class PadNtpSource:
190190
poll = 14
191191

192192

193-
class PadService:
194-
name = 16
195-
status = 8
196-
pid = 8
197-
description = 40
198-
199-
200193
class PadWifiScan:
201194
ssid = 40
202195
encryption = 30
@@ -218,6 +211,34 @@ class PadDiskUsage:
218211
avail = 12
219212
percent = 6
220213

214+
215+
def format_memory_bytes(bytes_val):
216+
"""Convert bytes to human-readable format"""
217+
if bytes_val == 0:
218+
return " "
219+
elif bytes_val < 1024:
220+
return f"{bytes_val}B"
221+
elif bytes_val < 1024 * 1024:
222+
return f"{bytes_val // 1024}K"
223+
elif bytes_val < 1024 * 1024 * 1024:
224+
return f"{bytes_val // (1024 * 1024):.1f}M"
225+
else:
226+
return f"{bytes_val // (1024 * 1024 * 1024):.1f}G"
227+
228+
229+
def format_uptime_seconds(seconds):
230+
"""Convert seconds to compact time format"""
231+
if seconds == 0:
232+
return " "
233+
elif seconds < 60:
234+
return f"{seconds}s"
235+
elif seconds < 3600:
236+
return f"{seconds // 60}m"
237+
elif seconds < 86400:
238+
return f"{seconds // 3600}h"
239+
else:
240+
return f"{seconds // 86400}d"
241+
221242
@classmethod
222243
def table_width(cls):
223244
"""Total width of disk usage table"""
@@ -260,6 +281,89 @@ def table_width(cls):
260281
return cls.zone_locked + cls.zone_name + cls.zone_type + cls.zone_data \
261282
+ cls.zone_services
262283

284+
class Column:
285+
"""Column definition for SimpleTable"""
286+
def __init__(self, name, align='left', formatter=None):
287+
self.name = name
288+
self.align = align
289+
self.formatter = formatter
290+
291+
class SimpleTable:
292+
"""Simple table formatter that handles ANSI colors correctly and calculates dynamic column widths"""
293+
294+
def __init__(self, columns):
295+
self.columns = columns
296+
self.rows = []
297+
298+
@staticmethod
299+
def visible_width(text):
300+
"""Return visible character count, excluding ANSI escape sequences"""
301+
ansi_pattern = r'\x1b\[[0-9;]*m'
302+
clean_text = re.sub(ansi_pattern, '', str(text))
303+
return len(clean_text)
304+
305+
def row(self, *values):
306+
"""Store row data for later formatting"""
307+
if len(values) != len(self.columns):
308+
raise ValueError(f"Expected {len(self.columns)} values, got {len(values)}")
309+
self.rows.append(values)
310+
311+
def print(self, styled=True):
312+
"""Calculate widths and print complete table"""
313+
column_widths = self._calculate_column_widths()
314+
print(self._format_header(column_widths, styled))
315+
for row_data in self.rows:
316+
print(self._format_row(row_data, column_widths))
317+
318+
def _calculate_column_widths(self):
319+
"""Calculate maximum width needed for each column"""
320+
widths = [self.visible_width(col.name) for col in self.columns]
321+
322+
for row_data in self.rows:
323+
for i, (value, column) in enumerate(zip(row_data, self.columns)):
324+
formatted_value = column.formatter(value) if column.formatter else value
325+
value_width = self.visible_width(str(formatted_value))
326+
widths[i] = max(widths[i], value_width)
327+
328+
return widths
329+
330+
def _format_header(self, column_widths, styled=True):
331+
"""Generate formatted header row"""
332+
header_parts = []
333+
for i, column in enumerate(self.columns):
334+
width = column_widths[i]
335+
if column.align == 'right':
336+
header_parts.append(f"{column.name:>{width}} ")
337+
else:
338+
header_parts.append(f"{column.name:{width}} ")
339+
340+
header_str = ''.join(header_parts).rstrip()
341+
return Decore.invert(header_str) if styled else header_str
342+
343+
def _format_row(self, row_data, column_widths):
344+
"""Format a single data row"""
345+
row_parts = []
346+
for i, (value, column) in enumerate(zip(row_data, self.columns)):
347+
formatted_value = self._format_column_value(value, column, column_widths[i])
348+
row_parts.append(formatted_value)
349+
350+
return ''.join(row_parts).rstrip()
351+
352+
def _format_column_value(self, value, column, width):
353+
"""Format a single column value with proper alignment"""
354+
if column.formatter:
355+
value = column.formatter(value)
356+
357+
value_str = str(value)
358+
visible_len = self.visible_width(value_str)
359+
360+
if column.align == 'right':
361+
padding = width - visible_len
362+
return ' ' * max(0, padding) + value_str + ' '
363+
else:
364+
padding = width - visible_len
365+
return value_str + ' ' * max(0, padding) + ' '
366+
263367

264368
class Decore():
265369
@staticmethod
@@ -1693,17 +1797,25 @@ def show_services(json):
16931797
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
16941798
services = services_data.get("service", [])
16951799

1696-
hdr = (f"{'NAME':<{PadService.name}}"
1697-
f"{'STATUS':<{PadService.status}}"
1698-
f"{'PID':>{PadService.pid -1}}"
1699-
f" {'DESCRIPTION'}")
1700-
print(Decore.invert(hdr))
1800+
# This is the first usage of simple table. I assume this will be
1801+
# copied so I left a lot of comments. If you copy it feel free
1802+
# to be less verbose..
1803+
service_table = SimpleTable([
1804+
Column('NAME'),
1805+
Column('STATUS'),
1806+
Column('PID', 'right'),
1807+
Column('MEM', 'right'),
1808+
Column('UP', 'right'),
1809+
Column('RST', 'right'),
1810+
Column('DESCRIPTION')
1811+
])
17011812

17021813
for svc in services:
17031814
name = svc.get('name', '')
17041815
status = svc.get('status', '')
17051816
pid = svc.get('pid', 0)
17061817
description = svc.get('description', '')
1818+
stats = svc.get('statistics', {})
17071819

17081820
if status in ('running', 'active', 'done'):
17091821
status_str = Decore.green(status)
@@ -1712,13 +1824,19 @@ def show_services(json):
17121824
else:
17131825
status_str = Decore.yellow(status)
17141826

1715-
pid_str = str(pid) if pid > 0 else '-'
1827+
pid_str = str(pid) if pid > 0 else ' '
17161828

1717-
row = f"{name:<{PadService.name}}"
1718-
row += f"{status_str:<{PadService.status + 9}}"
1719-
row += f"{pid_str:>{PadService.pid}}"
1720-
row += f" {description}"
1721-
print(row)
1829+
memory_bytes = int(stats.get('memory-usage', 0))
1830+
uptime_secs = int(stats.get('uptime', 0))
1831+
restart_count = stats.get('restart-count', 0)
1832+
1833+
memory_str = format_memory_bytes(memory_bytes)
1834+
uptime_str = format_uptime_seconds(uptime_secs)
1835+
1836+
service_table.row(name, status_str, pid_str, memory_str,
1837+
uptime_str, restart_count, description)
1838+
1839+
service_table.print()
17221840

17231841

17241842
def show_hardware(json):

src/statd/python/yanger/ietf_system.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,12 @@ def add_services(out):
189189
"pid": d["pid"],
190190
"name": d["identity"],
191191
"status": d["status"],
192-
"description": d["description"]
192+
"description": d["description"],
193+
"statistics": {
194+
"memory-usage": str(d.get("memory", 0)),
195+
"uptime": str(d.get("uptime", 0)),
196+
"restart-count": int(d.get("restarts", 0))
197+
}
193198
}
194199
services.append(entry)
195200

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
NAME STATUS PID DESCRIPTION
2-
udevd running 1185 Device event daemon (udev)
3-
dbus running 2248 D-Bus message bus daemon
4-
confd running 3039 Configuration daemon
5-
netopeer running 3548 NETCONF server
6-
dnsmasq running 2249 DHCP/DNS proxy
7-
tty:hvc0 running 3559 Getty on hvc0
8-
iitod running 2340 LED daemon
9-
klishd running 3560 CLI backend daemon
10-
mdns-alias running 3617 mDNS alias advertiser
11-
mstpd stopped - Spanning Tree daemon
12-
rauc running 3564 Software update service
13-
resolvconf done - Update DNS configuration
14-
statd running 3472 Status daemon
15-
staticd running 3653 Static routing daemon
16-
syslogd running 2241 System log daemon
17-
watchdogd running 2242 System watchdog daemon
18-
zebra running 3587 Zebra routing daemon
19-
mdns running 3616 Avahi mDNS-SD daemon
20-
chronyd running 3618 Chrony NTP v3/v4 daemon
21-
lldpd running 3633 LLDP daemon (IEEE 802.1ab)
22-
nginx running 3635 Web server
23-
rousette running 3636 RESTCONF server
24-
sshd running 3641 OpenSSH daemon
1+
NAME STATUS PID MEM UP RST DESCRIPTION
2+
udevd running 1185 23m 0 Device event daemon (udev)
3+
dbus running 2248 23m 0 D-Bus message bus daemon
4+
confd running 3039 23m 0 Configuration daemon
5+
netopeer running 3548 23m 0 NETCONF server
6+
dnsmasq running 2249 23m 0 DHCP/DNS proxy
7+
tty:hvc0 running 3559 23m 0 Getty on hvc0
8+
iitod running 2340 23m 0 LED daemon
9+
klishd running 3560 23m 0 CLI backend daemon
10+
mdns-alias running 3617 23m 0 mDNS alias advertiser
11+
mstpd stopped 0 Spanning Tree daemon
12+
rauc running 3564 23m 0 Software update service
13+
resolvconf done 2 Update DNS configuration
14+
statd running 3472 23m 0 Status daemon
15+
staticd running 3653 23m 0 Static routing daemon
16+
syslogd running 2241 23m 0 System log daemon
17+
watchdogd running 2242 23m 0 System watchdog daemon
18+
zebra running 3587 23m 0 Zebra routing daemon
19+
mdns running 3616 23m 0 Avahi mDNS-SD daemon
20+
chronyd running 3618 23m 0 Chrony NTP v3/v4 daemon
21+
lldpd running 3633 23m 0 LLDP daemon (IEEE 802.1ab)
22+
nginx running 3635 23m 0 Web server
23+
rousette running 3636 23m 0 RESTCONF server
24+
sshd running 3641 23m 0 OpenSSH daemon

0 commit comments

Comments
 (0)