Skip to content

Commit

Permalink
metrics: show system boots in metrics
Browse files Browse the repository at this point in the history
Show a boot as an metric event in the historical metrics overview. A
boot is likely to cause a high CPU/memory spikes so it is interesting
for a system administrator to be aware of them. We obtain the boot
information from systemd as `last` is deprecated and not all distros use
lastlog2 while `journalctl` is freely available.

Closes: #15983
  • Loading branch information
jelly committed Dec 17, 2024
1 parent bdf08e5 commit c8e37bf
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 6 deletions.
50 changes: 45 additions & 5 deletions pkg/metrics/metrics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,20 @@ class MetricsMinute extends React.Component {
});

let desc;
if (this.props.isExpanded && this.props.events) {
if (this.props.isExpanded && this.props.booted) {
const timestamp = this.props.startTime + (this.props.minute * 60000);
desc = (
<div className="metrics-events">
<time>{ timeformat.time(timestamp) }</time>
<span className="spikes_count" />
<span className="spikes_info">
<span className="type">
{_("Boot")}
</span>
</span>
</div>
);
} else if (this.props.isExpanded && this.props.events) {
const timestamp = this.props.startTime + (this.props.minute * 60000);

const logsPanel = (
Expand Down Expand Up @@ -1170,6 +1183,7 @@ class MetricsHour extends React.Component {
if (this.state.dataItems !== nextProps.data.length ||
this.state.isHourExpanded !== nextState.isHourExpanded ||
this.props.startTime !== nextProps.startTime ||
this.props.boots !== nextProps.boots ||
Object.keys(this.props.selectedVisibility).some(itm => this.props.selectedVisibility[itm] != nextProps.selectedVisibility[itm])) {
this.updateGraphs(nextProps.data, nextProps.startTime, nextProps.selectedVisibility, nextState.isHourExpanded);
return false;
Expand Down Expand Up @@ -1237,6 +1251,8 @@ class MetricsHour extends React.Component {
const dataOffset = minute * SAMPLES_PER_MIN;
const dataSlice = normData.slice(dataOffset, dataOffset + SAMPLES_PER_MIN);
const rawSlice = this.props.data.slice(dataOffset, dataOffset + SAMPLES_PER_MIN);
const is_boot = this.props.boots.includes(minute);

minuteGraphs.push(
<MetricsMinute
isExpanded={isHourExpanded}
Expand All @@ -1246,7 +1262,8 @@ class MetricsHour extends React.Component {
rawData={rawSlice}
events={minute_events[minute]}
startTime={this.props.startTime}
selectedVisibility={selectedVisibility} />
selectedVisibility={selectedVisibility}
booted={is_boot} />
);
}

Expand Down Expand Up @@ -1559,7 +1576,8 @@ class MetricsHistory extends React.Component {
packagekitExists: false,
isBeibootBridge: false,
isPythonPCPInstalled: null,
selectedVisibility: this.columns.reduce((a, v) => ({ ...a, [v[0]]: true }), {})
selectedVisibility: this.columns.reduce((a, v) => ({ ...a, [v[0]]: true }), {}),
boots: [], // journalctl --list-boots as [{started: Date, ended: Date}]
};

this.handleMoreData = this.handleMoreData.bind(this);
Expand Down Expand Up @@ -1622,7 +1640,23 @@ class MetricsHistory extends React.Component {

const isBeibootBridge = cmdline?.includes("ic# cockpit-bridge");

this.setState({ packagekitExists, isBeibootBridge });
let boots = [];
try {
// Only 14 days of metrics are shown
const output = await cockpit.spawn(["journalctl", "--list-boots", "--since", "-15d", "--output", "json"]);
const list_boots = JSON.parse(output);
boots = list_boots.map(boot => {
return {
started: new Date(boot?.first_entry / 1000),
ended: new Date(boot?.last_entry / 1000),
current_boot: boot?.index === 0,
};
});
} catch (exc) {
console.warn("journalctl --list-boots failed", exc);
}

this.setState({ packagekitExists, isBeibootBridge, boots });
}

handleMoreData() {
Expand Down Expand Up @@ -1927,14 +1961,20 @@ class MetricsHistory extends React.Component {
<Card isPlain>
<CardBody className="metrics-history">
{ this.state.hours.map((time, i) => {
const date_time = new Date(time);
const boot_minutes = this.state.boots.filter(reboot => reboot.started.getDay() === date_time.getDay() &&
reboot.started.getYear() === date_time.getYear() &&
reboot.started.getHours() === date_time.getHours())
.map(reboot => reboot.started.getMinutes());
const showHeader = i == 0 || timeformat.date(time) != timeformat.date(this.state.hours[i - 1]);

return (
<React.Fragment key={timeformat.dateTime(time)}>
{showHeader && <TextContent><Text component={TextVariants.h3} className="metrics-time"><time>{ timeformat.date(time) }</time></Text></TextContent>}
<MetricsHour key={time} startTime={parseInt(time)}
selectedVisibility={this.state.selectedVisibility}
data={this.data[time]} clipLeading={i == 0} />
data={this.data[time]} clipLeading={i == 0}
boots={boot_minutes} />
</React.Fragment>
);
})}
Expand Down
8 changes: 7 additions & 1 deletion test/verify/check-metrics
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,15 @@ class TestHistoryMetrics(testlib.MachineCase):
b.reload()
b.enter_page("/metrics")

# details for above load event
# Expand metrics when loaded
b.wait_in_text(".metrics-heading", "CPU")
b.click("#metrics-hour-1615197600000 button.metrics-events-expander")

# Show boot as event
with b.wait_timeout(60):
b.wait_in_text(".metrics-minute[data-minute='35']", "Boot")

# details for above load event
b.click(f"{load_minute_sel} .metrics-events button.spikes_info")
b.wait_in_text(".cockpit-log-panel", "load-hog")
action = "Stopping" if load_minute == 39 else "Started"
Expand Down

0 comments on commit c8e37bf

Please sign in to comment.