package com.htopweb.system; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import oshi.SystemInfo; import oshi.hardware.CentralProcessor; import oshi.hardware.GlobalMemory; import oshi.hardware.HardwareAbstractionLayer; import oshi.software.os.OperatingSystem; import org.springframework.stereotype.Service; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @Service public class SystemMetricsService { private static final Path HOST_PROC = Path.of("/host/proc"); private static final Path HOST_ETC = Path.of("/host/etc"); private final OperatingSystem _operatingSystem; private final CentralProcessor _processor; private final GlobalMemory _memory; private final AtomicReference _previousHostCpuTicks = new AtomicReference<>(); private static final Logger log = LoggerFactory.getLogger(SystemMetricsService.class); public SystemMetricsService() { SystemInfo systemInfo = new SystemInfo(); HardwareAbstractionLayer hardware = systemInfo.getHardware(); _operatingSystem = systemInfo.getOperatingSystem(); _processor = hardware.getProcessor(); _memory = hardware.getMemory(); _previousHostCpuTicks.set(readHostCpuTicks()); } public SystemMetricsResponse getMetrics() { CpuTicks currentTicks = readHostCpuTicks(); double cpuUsage = 0.0; CpuTicks previous = _previousHostCpuTicks.get(); if (currentTicks != null && previous != null) { long totalDelta = currentTicks.total() - previous.total(); long idleDelta = currentTicks.idle() - previous.idle(); if (totalDelta > 0 && idleDelta >= 0 && idleDelta <= totalDelta) { cpuUsage = (double) (totalDelta - idleDelta) / totalDelta; } } if (currentTicks != null) { _previousHostCpuTicks.set(currentTicks); } Map hostMemory = readHostMemory(); long totalMemory = hostMemory.getOrDefault("MemTotal", _memory.getTotal()); long availableMemory = hostMemory.getOrDefault( "MemAvailable", _memory.getAvailable() ); availableMemory = Math.max(0L, Math.min(totalMemory, availableMemory)); long usedMemory = Math.max(0L, totalMemory - availableMemory); double memoryUsage = totalMemory > 0 ? (double) usedMemory / totalMemory * 100.0 : 0.0; Map osRelease = readHostOsRelease(); return new SystemMetricsResponse( getHostOsFamily(osRelease), getHostOsVersion(osRelease), readHostHostname(), _processor.getProcessorIdentifier().getName(), getLogicalProcessorCount(), round(cpuUsage * 100.0), totalMemory, availableMemory, usedMemory, round(memoryUsage) ); } private CpuTicks readHostCpuTicks() { Path statFile = HOST_PROC.resolve("stat"); try { if (!Files.isRegularFile(statFile) || !Files.isReadable(statFile)) { log.warn("Host CPU statistics are unavailable: {}", statFile); return null; } for (String line : Files.readAllLines(statFile)) { if (!line.startsWith("cpu ")) { continue; } String[] fields = line.trim().split("\\s+"); if (fields.length < 5) { log.warn("Invalid host CPU statistics in {}", statFile); return null; } long total = 0L; for (int i = 1; i < fields.length; i++) { total += Long.parseLong(fields[i]); } long idle = Long.parseLong(fields[4]); if (fields.length > 5) { idle += Long.parseLong(fields[5]); } return new CpuTicks(total, idle); } log.warn("Aggregate CPU line not found in {}", statFile); } catch (IOException | NumberFormatException e) { log.warn("Failed to read host CPU statistics: {}", e.getMessage()); } return null; } private Map readHostMemory() { Map values = new HashMap<>(); Path meminfoFile = HOST_PROC.resolve("meminfo"); try { if (!Files.isRegularFile(meminfoFile)) { return values; } for (String line : Files.readAllLines(meminfoFile)) { int separator = line.indexOf(':'); if (separator < 0) { continue; } String key = line.substring(0, separator).trim(); if (!key.equals("MemTotal") && !key.equals("MemAvailable")) { continue; } String[] parts = line.substring(separator + 1).trim().split("\\s+"); if (parts.length > 0) { // Linux meminfo values are reported in kB. values.put(key, Long.parseLong(parts[0]) * 1024L); } } } catch (IOException | NumberFormatException e) { log.debug("Failed to read host memory info: {}", e.getMessage()); } return values; } private int getLogicalProcessorCount() { Path statFile = HOST_PROC.resolve("stat"); try { if (Files.isRegularFile(statFile)) { int count = 0; for (String line : Files.readAllLines(statFile)) { if (line.matches("^cpu\\d+\\s+.*")) { count++; } } if (count > 0) { return count; } } } catch (IOException e) { log.debug("Failed to read host processor count: {}", e.getMessage()); } return _processor.getLogicalProcessorCount(); } private String readHostHostname() { try { Path file = HOST_ETC.resolve("hostname"); if (Files.isRegularFile(file)) { String hostname = Files.readString(file).trim(); if (!hostname.isBlank()) { return hostname; } } } catch (IOException e) { log.debug("Failed to read host hostname: {}", e.getMessage()); } return _operatingSystem.getNetworkParams().getHostName(); } private Map readHostOsRelease() { Map values = new HashMap<>(); try { Path file = HOST_ETC.resolve("os-release"); if (!Files.isRegularFile(file)) { return values; } for (String line : Files.readAllLines(file)) { int separator = line.indexOf('='); if (separator <= 0 || line.startsWith("#")) { continue; } String key = line.substring(0, separator).trim(); String value = line.substring(separator + 1).trim(); if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")))) { value = value.substring(1, value.length() - 1); } values.put(key, value); } } catch (IOException e) { log.debug("Failed to read host os-release: {}", e.getMessage()); } return values; } private String getHostOsFamily(Map osRelease) { String name = osRelease.get("NAME"); return name != null && !name.isBlank() ? name : _operatingSystem.getFamily(); } private String getHostOsVersion(Map osRelease) { String version = osRelease.get("VERSION"); if (version == null || version.isBlank()) { version = osRelease.get("VERSION_ID"); } return version != null && !version.isBlank() ? version : _operatingSystem.getVersionInfo().getVersion(); } private double round(double value) { return Math.round(value * 10.0) / 10.0; } private record CpuTicks(long total, long idle) { } }