Major improvements: multi-stage Docker, security hardening, host metrics, tests
- Multi-stage Docker build: Dockerfile compiles frontend (Node.js) and backend (Gradle) inside Docker; no local Java/Node.js required - Security hardening: replaced full host root mount with specific directory mounts (/proc, /sys, /etc, /var, /home, /opt, /tmp, /srv, /usr) - Host metrics consistency: LinuxMemoryService and CpuDetailsService now read from /host/proc instead of container OSHI - Concurrency: replaced synchronized with AtomicReference in 3 services - Global error handling: added @RestControllerAdvice for JSON error responses - Unit tests: added 32 tests for /proc parsers, CPU usage, process uptime - Docker security: non-root user, read-only FS, no-new-privileges, healthcheck - Debug logging: added log.debug() in silent catch blocks - Documentation: updated README, INSTALLATION, DEVELOPMENT, CONFIGURATION, ARCHITECTURE, CHANGELOG Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package com.htopweb.system;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
|
||||
log.error("Unexpected error: {}", e.getMessage(), e);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of(
|
||||
"error", "Internal server error",
|
||||
"message", e.getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e) {
|
||||
log.error("Runtime error: {}", e.getMessage(), e);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of(
|
||||
"error", "Internal server error",
|
||||
"message", e.getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,19 @@ 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_ROOT = Path.of("/host/root");
|
||||
private static final Path HOST_PROC = HOST_ROOT.resolve("proc");
|
||||
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 CpuTicks _previousHostCpuTicks;
|
||||
private final AtomicReference<CpuTicks> _previousHostCpuTicks = new AtomicReference<>();
|
||||
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SystemMetricsService.class);
|
||||
@@ -40,16 +41,17 @@ public class SystemMetricsService {
|
||||
_processor = hardware.getProcessor();
|
||||
_memory = hardware.getMemory();
|
||||
|
||||
_previousHostCpuTicks = readHostCpuTicks();
|
||||
_previousHostCpuTicks.set(readHostCpuTicks());
|
||||
}
|
||||
|
||||
public synchronized SystemMetricsResponse getMetrics() {
|
||||
public SystemMetricsResponse getMetrics() {
|
||||
CpuTicks currentTicks = readHostCpuTicks();
|
||||
double cpuUsage = 0.0;
|
||||
|
||||
if (currentTicks != null && _previousHostCpuTicks != null) {
|
||||
long totalDelta = currentTicks.total() - _previousHostCpuTicks.total();
|
||||
long idleDelta = currentTicks.idle() - _previousHostCpuTicks.idle();
|
||||
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;
|
||||
@@ -57,7 +59,7 @@ public class SystemMetricsService {
|
||||
}
|
||||
|
||||
if (currentTicks != null) {
|
||||
_previousHostCpuTicks = currentTicks;
|
||||
_previousHostCpuTicks.set(currentTicks);
|
||||
}
|
||||
|
||||
Map<String, Long> hostMemory = readHostMemory();
|
||||
@@ -165,8 +167,8 @@ public class SystemMetricsService {
|
||||
values.put(key, Long.parseLong(parts[0]) * 1024L);
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// Use OSHI values when host meminfo is unavailable.
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.debug("Failed to read host memory info: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -189,8 +191,8 @@ public class SystemMetricsService {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// Fall back to OSHI.
|
||||
} catch (IOException e) {
|
||||
log.debug("Failed to read host processor count: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return _processor.getLogicalProcessorCount();
|
||||
@@ -198,7 +200,7 @@ public class SystemMetricsService {
|
||||
|
||||
private String readHostHostname() {
|
||||
try {
|
||||
Path file = HOST_ROOT.resolve("etc/hostname");
|
||||
Path file = HOST_ETC.resolve("hostname");
|
||||
|
||||
if (Files.isRegularFile(file)) {
|
||||
String hostname = Files.readString(file).trim();
|
||||
@@ -207,8 +209,8 @@ public class SystemMetricsService {
|
||||
return hostname;
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// Fall back to OSHI.
|
||||
} catch (IOException e) {
|
||||
log.debug("Failed to read host hostname: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return _operatingSystem.getNetworkParams().getHostName();
|
||||
@@ -218,7 +220,7 @@ public class SystemMetricsService {
|
||||
Map<String, String> values = new HashMap<>();
|
||||
|
||||
try {
|
||||
Path file = HOST_ROOT.resolve("etc/os-release");
|
||||
Path file = HOST_ETC.resolve("os-release");
|
||||
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return values;
|
||||
@@ -242,8 +244,8 @@ public class SystemMetricsService {
|
||||
|
||||
values.put(key, value);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// Fall back to OSHI.
|
||||
} catch (IOException e) {
|
||||
log.debug("Failed to read host os-release: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return values;
|
||||
|
||||
@@ -16,6 +16,8 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Service
|
||||
public class SystemProcessesService {
|
||||
@@ -23,7 +25,7 @@ public class SystemProcessesService {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SystemProcessesService.class);
|
||||
|
||||
private static final Path HOST_PROC = Path.of("/host/root/proc");
|
||||
private static final Path HOST_PROC = Path.of("/host/proc");
|
||||
private static final Path PROC = Path.of("/proc");
|
||||
|
||||
private final OperatingSystem _operatingSystem;
|
||||
@@ -31,10 +33,10 @@ public class SystemProcessesService {
|
||||
private final long _clockTicksPerSecond;
|
||||
private final long _pageSizeBytes;
|
||||
|
||||
private final Map<Integer, ProcessSnapshot> _previousProcesses =
|
||||
new HashMap<>();
|
||||
private final AtomicReference<Map<Integer, ProcessSnapshot>> _previousProcesses =
|
||||
new AtomicReference<>(new HashMap<>());
|
||||
|
||||
private long _previousSampleNanos = System.nanoTime();
|
||||
private final AtomicLong _previousSampleNanos = new AtomicLong(System.nanoTime());
|
||||
|
||||
public SystemProcessesService() {
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
@@ -54,7 +56,7 @@ public class SystemProcessesService {
|
||||
);
|
||||
}
|
||||
|
||||
public synchronized List<ProcessInfoResponse> getProcesses() {
|
||||
public List<ProcessInfoResponse> getProcesses() {
|
||||
Path procRoot = Files.isDirectory(HOST_PROC) ? HOST_PROC : PROC;
|
||||
|
||||
if (!Files.isReadable(procRoot)) {
|
||||
@@ -63,14 +65,15 @@ public class SystemProcessesService {
|
||||
}
|
||||
|
||||
long nowNanos = System.nanoTime();
|
||||
long prevNanos = _previousSampleNanos.getAndSet(nowNanos);
|
||||
double elapsedSeconds =
|
||||
(nowNanos - _previousSampleNanos) / 1_000_000_000.0;
|
||||
_previousSampleNanos = nowNanos;
|
||||
(nowNanos - prevNanos) / 1_000_000_000.0;
|
||||
|
||||
long bootTimeSeconds = readBootTime(procRoot);
|
||||
Map<Integer, ProcessSnapshot> currentProcesses = new HashMap<>();
|
||||
List<ProcessInfoResponse> result = new ArrayList<>();
|
||||
|
||||
Map<Integer, ProcessSnapshot> previousMap = _previousProcesses.get();
|
||||
try (var entries = Files.list(procRoot)) {
|
||||
entries.filter(path ->
|
||||
path.getFileName().toString().matches("\\d+"))
|
||||
@@ -90,8 +93,7 @@ public class SystemProcessesService {
|
||||
|
||||
currentProcesses.put(pid, current);
|
||||
|
||||
ProcessSnapshot previous =
|
||||
_previousProcesses.get(pid);
|
||||
ProcessSnapshot previous = previousMap.get(pid);
|
||||
|
||||
double cpuUsage = 0.0;
|
||||
|
||||
@@ -126,8 +128,8 @@ public class SystemProcessesService {
|
||||
current.threadCount()
|
||||
));
|
||||
} catch (IOException | NumberFormatException
|
||||
| SecurityException ignored) {
|
||||
// A process may exit or become inaccessible.
|
||||
| SecurityException e) {
|
||||
log.debug("Process {} became inaccessible: {}", path.getFileName(), e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
@@ -136,8 +138,7 @@ public class SystemProcessesService {
|
||||
return getProcessesFromOshi();
|
||||
}
|
||||
|
||||
_previousProcesses.clear();
|
||||
_previousProcesses.putAll(currentProcesses);
|
||||
_previousProcesses.set(currentProcesses);
|
||||
|
||||
result.sort(Comparator.comparingDouble(
|
||||
ProcessInfoResponse::cpuUsagePercent
|
||||
@@ -210,8 +211,8 @@ public class SystemProcessesService {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// The process may exit while status is being read.
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.debug("Failed to read thread count: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -224,8 +225,8 @@ public class SystemProcessesService {
|
||||
return Long.parseLong(line.substring(6).trim());
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// Boot time unavailable.
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.debug("Failed to read boot time: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return 0L;
|
||||
|
||||
@@ -1,61 +1,121 @@
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.CentralProcessor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Service
|
||||
public class CpuDetailsService {
|
||||
|
||||
private final CentralProcessor _processor;
|
||||
private long[][] _previousCoreTicks;
|
||||
private long[] _previousSystemTicks;
|
||||
private static final Path HOST_PROC = Path.of("/host/proc");
|
||||
private static final Logger log = LoggerFactory.getLogger(CpuDetailsService.class);
|
||||
|
||||
private static final String CPU_LINE_PREFIX = "cpu";
|
||||
|
||||
private final AtomicReference<CpuTicks[]> _previousCoreTicks = new AtomicReference<>();
|
||||
|
||||
public CpuDetailsService() {
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
_processor = systemInfo.getHardware().getProcessor();
|
||||
|
||||
_previousCoreTicks = _processor.getProcessorCpuLoadTicks();
|
||||
_previousSystemTicks = _processor.getSystemCpuLoadTicks();
|
||||
_previousCoreTicks.set(readHostCpuTicks());
|
||||
}
|
||||
|
||||
public synchronized CpuDetailsResponse getDetails() {
|
||||
long[][] currentCoreTicks = _processor.getProcessorCpuLoadTicks();
|
||||
long[] currentSystemTicks = _processor.getSystemCpuLoadTicks();
|
||||
public CpuDetailsResponse getDetails() {
|
||||
CpuTicks[] currentTicks = readHostCpuTicks();
|
||||
CpuTicks[] previousTicks = _previousCoreTicks.getAndSet(currentTicks);
|
||||
|
||||
double[] coreLoads = _processor.getProcessorCpuLoadBetweenTicks(
|
||||
_previousCoreTicks,
|
||||
currentCoreTicks
|
||||
);
|
||||
List<CpuDetailsResponse.CpuCoreResponse> cores = new ArrayList<>();
|
||||
|
||||
double systemLoad = _processor.getSystemCpuLoadBetweenTicks(
|
||||
_previousSystemTicks,
|
||||
currentSystemTicks
|
||||
);
|
||||
|
||||
_previousCoreTicks = currentCoreTicks;
|
||||
_previousSystemTicks = currentSystemTicks;
|
||||
|
||||
List<CpuDetailsResponse.CpuCoreResponse> cores =
|
||||
new ArrayList<>(coreLoads.length);
|
||||
|
||||
for (int i = 0; i < coreLoads.length; i++) {
|
||||
cores.add(new CpuDetailsResponse.CpuCoreResponse(
|
||||
i,
|
||||
toPercent(coreLoads[i] * 100.0)
|
||||
));
|
||||
if (previousTicks == null || currentTicks == null) {
|
||||
// First call — return zeros.
|
||||
if (currentTicks != null) {
|
||||
for (int i = 0; i < currentTicks.length; i++) {
|
||||
cores.add(new CpuDetailsResponse.CpuCoreResponse(i, 0.0));
|
||||
}
|
||||
}
|
||||
return new CpuDetailsResponse(0.0, cores.size(), List.copyOf(cores));
|
||||
}
|
||||
|
||||
double totalUsage = 0.0;
|
||||
int coreCount = Math.min(previousTicks.length, currentTicks.length);
|
||||
|
||||
for (int i = 0; i < coreCount; i++) {
|
||||
CpuTicks curr = currentTicks[i];
|
||||
CpuTicks prev = previousTicks[i];
|
||||
|
||||
long totalDelta = curr.total() - prev.total();
|
||||
long idleDelta = curr.idle() - prev.idle();
|
||||
|
||||
double usage = 0.0;
|
||||
if (totalDelta > 0 && idleDelta >= 0 && idleDelta <= totalDelta) {
|
||||
usage = (double) (totalDelta - idleDelta) / totalDelta * 100.0;
|
||||
}
|
||||
|
||||
totalUsage += usage;
|
||||
cores.add(new CpuDetailsResponse.CpuCoreResponse(i, toPercent(usage)));
|
||||
}
|
||||
|
||||
double avgTotal = coreCount > 0 ? totalUsage / coreCount : 0.0;
|
||||
|
||||
return new CpuDetailsResponse(
|
||||
toPercent(systemLoad * 100.0),
|
||||
_processor.getLogicalProcessorCount(),
|
||||
toPercent(avgTotal),
|
||||
coreCount,
|
||||
List.copyOf(cores)
|
||||
);
|
||||
}
|
||||
|
||||
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 new CpuTicks[0];
|
||||
}
|
||||
|
||||
List<CpuTicks> ticksList = new ArrayList<>();
|
||||
|
||||
for (String line : Files.readAllLines(statFile)) {
|
||||
// Match cpu0, cpu1, etc. (not the aggregate "cpu " line).
|
||||
if (!line.startsWith(CPU_LINE_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] fields = line.trim().split("\\s+");
|
||||
// Skip aggregate "cpu" line (no digit after "cpu").
|
||||
if (fields.length < 2 || !Character.isDigit(fields[0].charAt(3))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fields.length < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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]); // iowait
|
||||
}
|
||||
|
||||
ticksList.add(new CpuTicks(total, idle));
|
||||
}
|
||||
|
||||
return ticksList.toArray(new CpuTicks[0]);
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.warn("Failed to read host CPU statistics: {}", e.getMessage());
|
||||
return new CpuTicks[0];
|
||||
}
|
||||
}
|
||||
|
||||
private double toPercent(double value) {
|
||||
if (!Double.isFinite(value)) {
|
||||
return 0.0;
|
||||
@@ -65,4 +125,7 @@ public class CpuDetailsService {
|
||||
Math.max(0.0, Math.min(100.0, value)) * 10.0
|
||||
) / 10.0;
|
||||
}
|
||||
|
||||
private record CpuTicks(long total, long idle) {
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,10 @@ public class LinuxDiskService {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(LinuxDiskService.class);
|
||||
|
||||
private static final Path HOST_ROOT = Path.of("/host/root");
|
||||
private static final Path HOST_PROC = Path.of("/host/proc");
|
||||
private static final Path HOST_ROOT = Path.of("/host");
|
||||
private static final Path HOST_MOUNTS =
|
||||
HOST_ROOT.resolve("proc/1/mounts");
|
||||
HOST_PROC.resolve("1/mounts");
|
||||
|
||||
private static final Set<String> EXCLUDED_TYPES = Set.of(
|
||||
"proc", "sysfs", "devtmpfs", "devpts", "tmpfs",
|
||||
@@ -79,6 +80,7 @@ public class LinuxDiskService {
|
||||
).normalize();
|
||||
|
||||
if (!accessiblePath.startsWith(HOST_ROOT)
|
||||
|| accessiblePath.equals(HOST_ROOT)
|
||||
|| !Files.exists(accessiblePath)) {
|
||||
log.warn("Host mount path is unavailable: {}", accessiblePath);
|
||||
return null;
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.GlobalMemory;
|
||||
import oshi.hardware.VirtualMemory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.Map;
|
||||
|
||||
@Service
|
||||
public class LinuxMemoryService {
|
||||
|
||||
private final GlobalMemory _memory;
|
||||
private final VirtualMemory _virtualMemory;
|
||||
|
||||
private static final Path HOST_PROC = Path.of("/host/proc");
|
||||
private static final Logger log = LoggerFactory.getLogger(LinuxMemoryService.class);
|
||||
|
||||
public LinuxMemoryService() {
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
_memory = systemInfo.getHardware().getMemory();
|
||||
_virtualMemory = _memory.getVirtualMemory();
|
||||
|
||||
}
|
||||
|
||||
public LinuxMemoryResponse getDetails() {
|
||||
long totalRam = Math.max(0L, _memory.getTotal());
|
||||
long availableRam = Math.max(0L, Math.min(totalRam, _memory.getAvailable()));
|
||||
Map<String, Long> meminfo = readHostMeminfo();
|
||||
Map<String, Long> swaps = readHostSwaps();
|
||||
|
||||
long totalRam = meminfo.getOrDefault("MemTotal", 0L);
|
||||
long availableRam = meminfo.getOrDefault("MemAvailable", 0L);
|
||||
long usedRam = Math.max(0L, totalRam - availableRam);
|
||||
|
||||
long totalSwap = Math.max(0L, _virtualMemory.getSwapTotal());
|
||||
long usedSwap = Math.max(
|
||||
0L,
|
||||
Math.min(totalSwap, _virtualMemory.getSwapUsed())
|
||||
);
|
||||
long totalSwap = swaps.getOrDefault("total", 0L);
|
||||
long usedSwap = swaps.getOrDefault("used", 0L);
|
||||
long freeSwap = Math.max(0L, totalSwap - usedSwap);
|
||||
|
||||
return new LinuxMemoryResponse(
|
||||
@@ -43,6 +43,81 @@ public class LinuxMemoryService {
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, Long> readHostMeminfo() {
|
||||
Map<String, Long> values = new HashMap<>();
|
||||
Path meminfoFile = HOST_PROC.resolve("meminfo");
|
||||
|
||||
try {
|
||||
if (!Files.isRegularFile(meminfoFile)) {
|
||||
log.warn("Host meminfo is unavailable: {}", 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.warn("Failed to read host meminfo: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private Map<String, Long> readHostSwaps() {
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
result.put("total", 0L);
|
||||
result.put("used", 0L);
|
||||
|
||||
Path swapsFile = HOST_PROC.resolve("swaps");
|
||||
|
||||
try {
|
||||
if (!Files.isRegularFile(swapsFile)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
long totalSwap = 0L;
|
||||
long usedSwap = 0L;
|
||||
|
||||
for (String line : Files.readAllLines(swapsFile)) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("Filename")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = line.split("\\s+");
|
||||
// Format: Filename Type Size Used Priority
|
||||
if (parts.length >= 4) {
|
||||
// Size and Used columns are in kB.
|
||||
long size = Long.parseLong(parts[2]) * 1024L;
|
||||
long used = Long.parseLong(parts[3]) * 1024L;
|
||||
totalSwap += size;
|
||||
usedSwap += used;
|
||||
}
|
||||
}
|
||||
|
||||
result.put("total", totalSwap);
|
||||
result.put("used", usedSwap);
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.warn("Failed to read host swaps: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private double percentage(long used, long total) {
|
||||
if (total <= 0) {
|
||||
return 0.0;
|
||||
|
||||
@@ -21,13 +21,14 @@ public class LinuxNetworkService {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(LinuxNetworkService.class);
|
||||
|
||||
private static final Path HOST_ROOT = Path.of("/host/root");
|
||||
private static final Path HOST_PROC = Path.of("/host/proc");
|
||||
private static final Path HOST_SYS = Path.of("/host/sys");
|
||||
|
||||
private static final Path HOST_NET_DEV =
|
||||
HOST_ROOT.resolve("proc/1/net/dev");
|
||||
HOST_PROC.resolve("1/net/dev");
|
||||
|
||||
private static final Path HOST_NET_CLASS =
|
||||
HOST_ROOT.resolve("sys/class/net");
|
||||
HOST_SYS.resolve("class/net");
|
||||
|
||||
private static final Set<String> EXCLUDED_INTERFACES =
|
||||
Set.of("lo", "docker0");
|
||||
|
||||
Reference in New Issue
Block a user