From 61c62738b50cacb404a295f74292da11a420af84 Mon Sep 17 00:00:00 2001 From: "andreas.dinauer" Date: Sun, 26 Oct 2025 18:22:56 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Huge=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Dockerfile | 2 + pg-docker-compose.yaml | 9 ++ pom.xml | 16 +++ src/main/java/dev/dinauer/NodeResource.java | 5 +- src/main/java/dev/dinauer/ProcessRunner.java | 44 ++++++ .../java/dev/dinauer/login/LoginResource.java | 20 ++- .../dev/dinauer/monitoring/MonitoredNode.java | 10 ++ .../dinauer/monitoring/MonitoredResource.java | 9 ++ .../monitoring/MonitoringJobResource.java | 42 ++++++ .../monitoring/MonitoringJobRunner.java | 102 +++++++++++++ .../monitoring/MonitoringResource.java | 25 ++++ .../dinauer/monitoring/MonitoringService.java | 35 +++++ .../monitoring/NodeMonitoringJobResource.java | 59 ++++++++ .../dinauer/monitoring/entity/CpuConfig.java | 53 +++++++ .../monitoring/entity/HealthcheckConfig.java | 39 +++++ .../monitoring/entity/MonitoringConfig.java | 119 +++++++++++++++ .../entity/MonitoringTargetType.java | 8 ++ .../monitoring/entity/MonitoringType.java | 6 + .../monitoring/entity/TargetConfig.java | 135 ++++++++++++++++++ .../monitoring/entity/VolumeConfig.java | 67 +++++++++ .../entity/repo/MonitoringRepo.java | 18 +++ .../monitoring/indexing/IndexCollection.java | 115 +++++++++++++++ .../monitoring/indexing/IndexMetric.java | 46 ++++++ .../monitoring/indexing/IndexMetricsRepo.java | 22 +++ .../monitoring/indexing/IndexingService.java | 33 +++++ .../dinauer/monitoring/indexing/TimeUnit.java | 6 + .../indexing/TimestampGenerator.java | 28 ++++ .../java/dev/dinauer/monitoring/log/Log.java | 60 ++++++++ .../dev/dinauer/monitoring/log/LogRepo.java | 9 ++ .../monitoring/memory/ByteExtractor.java | 45 ++++++ .../memory/MemoryMonitoringJobRunner.java | 40 ++++++ .../nodes/NodeMonitoringService.java | 78 ++++++++++ .../dinauer/monitoring/nodes/NodeStats.java | 7 + .../monitoring/volume/UsageMetrics.java | 5 + .../volume/VolumeMonitoringJobRunner.java | 74 ++++++++++ .../monitoring/volume/VolumeUsage.java | 7 + .../monitoring/volume/VolumeUsageRepo.java | 80 +++++++++++ .../monitoring/volume/utils/Duration.java | 33 +++++ .../java/dev/dinauer/service/PodService.java | 21 +++ .../dev/dinauer/utils/StartupService.java | 5 + src/main/resources/application.properties | 17 ++- src/main/resources/import.sql | 11 ++ .../monitoring/memory/ByteExtractorTest.java | 22 +++ 44 files changed, 1575 insertions(+), 13 deletions(-) create mode 100644 pg-docker-compose.yaml create mode 100644 src/main/java/dev/dinauer/ProcessRunner.java create mode 100644 src/main/java/dev/dinauer/monitoring/MonitoredNode.java create mode 100644 src/main/java/dev/dinauer/monitoring/MonitoredResource.java create mode 100644 src/main/java/dev/dinauer/monitoring/MonitoringJobResource.java create mode 100644 src/main/java/dev/dinauer/monitoring/MonitoringJobRunner.java create mode 100644 src/main/java/dev/dinauer/monitoring/MonitoringResource.java create mode 100644 src/main/java/dev/dinauer/monitoring/MonitoringService.java create mode 100644 src/main/java/dev/dinauer/monitoring/NodeMonitoringJobResource.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/CpuConfig.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/HealthcheckConfig.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/MonitoringConfig.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/MonitoringTargetType.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/MonitoringType.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/TargetConfig.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/VolumeConfig.java create mode 100644 src/main/java/dev/dinauer/monitoring/entity/repo/MonitoringRepo.java create mode 100644 src/main/java/dev/dinauer/monitoring/indexing/IndexCollection.java create mode 100644 src/main/java/dev/dinauer/monitoring/indexing/IndexMetric.java create mode 100644 src/main/java/dev/dinauer/monitoring/indexing/IndexMetricsRepo.java create mode 100644 src/main/java/dev/dinauer/monitoring/indexing/IndexingService.java create mode 100644 src/main/java/dev/dinauer/monitoring/indexing/TimeUnit.java create mode 100644 src/main/java/dev/dinauer/monitoring/indexing/TimestampGenerator.java create mode 100644 src/main/java/dev/dinauer/monitoring/log/Log.java create mode 100644 src/main/java/dev/dinauer/monitoring/log/LogRepo.java create mode 100644 src/main/java/dev/dinauer/monitoring/memory/ByteExtractor.java create mode 100644 src/main/java/dev/dinauer/monitoring/memory/MemoryMonitoringJobRunner.java create mode 100644 src/main/java/dev/dinauer/monitoring/nodes/NodeMonitoringService.java create mode 100644 src/main/java/dev/dinauer/monitoring/nodes/NodeStats.java create mode 100644 src/main/java/dev/dinauer/monitoring/volume/UsageMetrics.java create mode 100644 src/main/java/dev/dinauer/monitoring/volume/VolumeMonitoringJobRunner.java create mode 100644 src/main/java/dev/dinauer/monitoring/volume/VolumeUsage.java create mode 100644 src/main/java/dev/dinauer/monitoring/volume/VolumeUsageRepo.java create mode 100644 src/main/java/dev/dinauer/monitoring/volume/utils/Duration.java create mode 100644 src/main/resources/import.sql create mode 100644 src/test/java/dev/dinauer/monitoring/memory/ByteExtractorTest.java diff --git a/.gitignore b/.gitignore index 91a800a..7d285c7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ nb-configuration.xml /.quarkus/cli/plugins/ # TLS Certificates .certs/ +/src/main/resources/dev/ diff --git a/Dockerfile b/Dockerfile index e64b609..114cd8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -105,6 +105,8 @@ COPY target/quarkus-app/quarkus/ /deployments/quarkus/ EXPOSE 8080 +RUN sudo chmod -R 777 /var/lib/kubooboo + USER quarkus ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0" diff --git a/pg-docker-compose.yaml b/pg-docker-compose.yaml new file mode 100644 index 0000000..83997da --- /dev/null +++ b/pg-docker-compose.yaml @@ -0,0 +1,9 @@ +services: + db: + image: postgres + restart: always + shm_size: 128mb + environment: + POSTGRES_PASSWORD: postgres + ports: + - "6666:5432" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0624fc5..4013765 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,22 @@ kubernetes-httpclient-vertx 7.3.1 + + io.quarkus + quarkus-scheduler + + + + + io.quarkus + quarkus-hibernate-orm-panache + + + + + io.quarkus + quarkus-jdbc-postgresql + diff --git a/src/main/java/dev/dinauer/NodeResource.java b/src/main/java/dev/dinauer/NodeResource.java index d711722..2fb0175 100644 --- a/src/main/java/dev/dinauer/NodeResource.java +++ b/src/main/java/dev/dinauer/NodeResource.java @@ -1,5 +1,6 @@ package dev.dinauer; +import dev.dinauer.monitoring.nodes.NodeStats; import dev.dinauer.utils.ClientProvider; import io.fabric8.kubernetes.api.model.Node; import io.fabric8.kubernetes.client.KubernetesClient; @@ -83,10 +84,6 @@ public class NodeResource throw new RuntimeException("Failed to retrieve top nodes."); } - public record NodeStats(Node node, Integer absoluteCpuUsage, Integer relativeCpuUsage, Integer totalCpu, Integer absoluteMemory, Integer relativeMemory, Integer totalMemory) - { - } - private Integer extractInteger(String input) { return Integer.valueOf(input.replace("m", "").replace("%", "")); diff --git a/src/main/java/dev/dinauer/ProcessRunner.java b/src/main/java/dev/dinauer/ProcessRunner.java new file mode 100644 index 0000000..232c2d8 --- /dev/null +++ b/src/main/java/dev/dinauer/ProcessRunner.java @@ -0,0 +1,44 @@ +package dev.dinauer; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@ApplicationScoped +public class ProcessRunner +{ + @Inject + Logger LOG; + + public String run(String command) throws IOException, InterruptedException + { + LOG.infof("Running command: %s", command); + ProcessBuilder pb = new ProcessBuilder(command.split("\\s+")); + pb.redirectErrorStream(true); + + Process p = pb.start(); + + List text = new ArrayList<>(); + try(BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) + { + String line; + while((line = br.readLine()) != null) + { + text.add(line); + } + } + int exitCode = p.waitFor(); + if(exitCode == 0) + { + return String.join("\n", text); + } + throw new RuntimeException("Error executing command: " + command); + } +} diff --git a/src/main/java/dev/dinauer/login/LoginResource.java b/src/main/java/dev/dinauer/login/LoginResource.java index e87ade9..932c182 100644 --- a/src/main/java/dev/dinauer/login/LoginResource.java +++ b/src/main/java/dev/dinauer/login/LoginResource.java @@ -6,14 +6,19 @@ import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import io.quarkus.elytron.security.common.BcryptUtil; +import org.jboss.logging.Logger; import java.io.IOException; import java.time.ZonedDateTime; +import java.util.Optional; @Path("/login") @ApplicationScoped public class LoginResource { + @Inject + Logger LOG; + @Inject UserRepo userRepo; @@ -22,11 +27,18 @@ public class LoginResource @Produces(MediaType.TEXT_PLAIN) public String login(Login login) throws IOException { - User user = userRepo.findByUsername(login.username()); - if(BcryptUtil.matches(login.password(), user.password())) + Optional userOptional = userRepo.findOptionalByUsername(login.username()); + if(userOptional.isPresent()) { - return Jwt.upn(user.username()).expiresAt(ZonedDateTime.now().plusDays(15).toInstant()).groups(user.roles()).sign(); + User user = userOptional.get(); + if(BcryptUtil.matches(login.password(), user.password())) + { + return Jwt.upn(user.username()).expiresAt(ZonedDateTime.now().plusDays(15).toInstant()).groups(user.roles()).sign(); + } + LOG.info("Cannot access user. Forbidden"); + throw new ForbiddenException(); } - throw new ForbiddenException(); + LOG.info("User not found"); + throw new NotFoundException(); } } diff --git a/src/main/java/dev/dinauer/monitoring/MonitoredNode.java b/src/main/java/dev/dinauer/monitoring/MonitoredNode.java new file mode 100644 index 0000000..80cffee --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/MonitoredNode.java @@ -0,0 +1,10 @@ +package dev.dinauer.monitoring; + +import dev.dinauer.monitoring.indexing.IndexCollection; +import io.fabric8.kubernetes.api.model.Node; + +import java.util.List; + +public record MonitoredNode(Node node, List jobs) +{ +} diff --git a/src/main/java/dev/dinauer/monitoring/MonitoredResource.java b/src/main/java/dev/dinauer/monitoring/MonitoredResource.java new file mode 100644 index 0000000..4462f7a --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/MonitoredResource.java @@ -0,0 +1,9 @@ +package dev.dinauer.monitoring; + +import io.fabric8.kubernetes.api.model.Pod; + +import java.util.List; + +public record MonitoredResource(E resource, List jobs) +{ +} diff --git a/src/main/java/dev/dinauer/monitoring/MonitoringJobResource.java b/src/main/java/dev/dinauer/monitoring/MonitoringJobResource.java new file mode 100644 index 0000000..b3c6afb --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/MonitoringJobResource.java @@ -0,0 +1,42 @@ +package dev.dinauer.monitoring; + +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.repo.MonitoringRepo; +import dev.dinauer.monitoring.indexing.IndexCollection; +import dev.dinauer.monitoring.indexing.IndexMetricsRepo; +import dev.dinauer.monitoring.indexing.TimeUnit; +import io.fabric8.kubernetes.api.model.Pod; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Path("/monitorings/{monitoring-id}/jobs") +public class MonitoringJobResource +{ + @Inject + MonitoringRepo monitoringRepo; + + @Inject + MonitoringService monitoringService; + + @Inject + IndexMetricsRepo indexMetricsRepo; + + @GET + public List> get(@PathParam("monitoring-id") String monitoringId) throws IOException + { + List> result = new ArrayList<>(); + MonitoringConfig config = monitoringRepo.findById(monitoringId); + List pods = monitoringService.findRunningPodsByMonitoringConfig(config); + for (Pod pod : pods) + { + result.add(new MonitoredResource<>(pod, indexMetricsRepo.findByResourceAndMetricAndTimeUnit(String.format("POD-%s", pod.getMetadata().getUid()), config.getType().toString(), TimeUnit.RAW))); + } + return result; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/MonitoringJobRunner.java b/src/main/java/dev/dinauer/monitoring/MonitoringJobRunner.java new file mode 100644 index 0000000..9d153a6 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/MonitoringJobRunner.java @@ -0,0 +1,102 @@ +package dev.dinauer.monitoring; + +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.MonitoringType; +import dev.dinauer.monitoring.entity.repo.MonitoringRepo; +import dev.dinauer.monitoring.log.Log; +import dev.dinauer.monitoring.log.LogRepo; +import dev.dinauer.monitoring.memory.MemoryMonitoringJobRunner; +import dev.dinauer.monitoring.nodes.NodeMonitoringService; +import dev.dinauer.monitoring.volume.VolumeMonitoringJobRunner; +import dev.dinauer.monitoring.volume.utils.Duration; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Startup +@ApplicationScoped +public class MonitoringJobRunner +{ + @Inject + Logger LOG; + + @Inject + VolumeMonitoringJobRunner volumeMonitoringJobRunner; + + @Inject + MemoryMonitoringJobRunner memoryMonitoringJobRunner; + + @Inject + NodeMonitoringService nodeMonitoringService; + + @Inject + MonitoringRepo monitoringRepo; + + @Inject + LogRepo logRepo; + + @PostConstruct + public void run() + { + List configs = monitoringRepo.listAll(); + for (MonitoringConfig config : configs) + { + schedule(config); + } + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + Runnable task = () -> { + try + { + nodeMonitoringService.run(); + } + catch (Exception e) + { + logRepo.persist(Log.init("Failed to node monitoring job")); + LOG.error("Failed to node monitoring job"); + } + }; + scheduler.scheduleAtFixedRate(task, 0, Duration.parse("5m"), TimeUnit.SECONDS); + } + + private void schedule(MonitoringConfig config) + { + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + Runnable task = () -> { + try + { + run(config); + } + catch (Exception e) + { + logRepo.persist(Log.init(String.format("Monitoring %s failed.", config.getConfigName()))); + LOG.errorf("Monitoring %s failed.", config.getConfigName()); + } + }; + scheduler.scheduleAtFixedRate(task, 0, Duration.parse(config.getInterval()), TimeUnit.SECONDS); + } + + private void run(MonitoringConfig config) throws IOException, InterruptedException + { + LOG.infof("Running %s %s monitoring.", config.getConfigName(), config.getType().toString().toLowerCase()); + switch (config.getType()) + { + case VOLUME -> + { + volumeMonitoringJobRunner.run(config); + } + case MEMORY -> + { + memoryMonitoringJobRunner.run(config); + } + } + } +} diff --git a/src/main/java/dev/dinauer/monitoring/MonitoringResource.java b/src/main/java/dev/dinauer/monitoring/MonitoringResource.java new file mode 100644 index 0000000..8820a34 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/MonitoringResource.java @@ -0,0 +1,25 @@ +package dev.dinauer.monitoring; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.MonitoringType; +import dev.dinauer.monitoring.entity.repo.MonitoringRepo; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import java.util.List; + +@Path("/monitorings") +public class MonitoringResource +{ + @Inject + MonitoringRepo monitoringRepo; + + @GET + public List get() throws JsonProcessingException + { + return monitoringRepo.listAll(); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/MonitoringService.java b/src/main/java/dev/dinauer/monitoring/MonitoringService.java new file mode 100644 index 0000000..08ed1d3 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/MonitoringService.java @@ -0,0 +1,35 @@ +package dev.dinauer.monitoring; + +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.TargetConfig; +import dev.dinauer.service.PodService; +import io.fabric8.kubernetes.api.model.Pod; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.resteasy.reactive.common.NotImplementedYet; + +import java.util.List; + +@ApplicationScoped +public class MonitoringService +{ + @Inject + PodService podService; + + public List findRunningPodsByMonitoringConfig(MonitoringConfig config) + { + TargetConfig targetConfig = config.getTargetConfig(); + switch (targetConfig.getType()) + { + case LABELS -> + { + return podService.findByLabels(targetConfig.getNamespace(), targetConfig.getLabels()).stream().filter(pod -> pod.getStatus().getPhase().equals("Running")).toList(); + } + case DEPLOYMENT, STATEFUL_SET -> + { + throw new NotImplementedYet(); + } + } + throw new RuntimeException("Invalid monitoring config type"); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/NodeMonitoringJobResource.java b/src/main/java/dev/dinauer/monitoring/NodeMonitoringJobResource.java new file mode 100644 index 0000000..240a9ae --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/NodeMonitoringJobResource.java @@ -0,0 +1,59 @@ +package dev.dinauer.monitoring; + +import dev.dinauer.monitoring.indexing.IndexCollection; +import dev.dinauer.monitoring.indexing.IndexMetric; +import dev.dinauer.monitoring.indexing.IndexMetricsRepo; +import dev.dinauer.monitoring.indexing.TimeUnit; +import dev.dinauer.utils.ClientProvider; +import io.fabric8.kubernetes.api.model.Node; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +@Path("/monitorings/nodes/jobs") +public class NodeMonitoringJobResource +{ + @Inject + IndexMetricsRepo indexMetricsRepo; + + @Inject + ClientProvider clientProvider; + + @GET + public List> get(@QueryParam("from") ZonedDateTime from, @QueryParam("to") ZonedDateTime to) throws IOException + { + List> result = new ArrayList<>(); + List nodes = clientProvider.getClient().nodes().list().getItems(); + for (Node node : nodes) + { + String resource = String.format("NODE-%s", node.getMetadata().getUid()); + result.add(new MonitoredResource<>(node, indexMetricsRepo.findByResourceAndMetricAndTimeUnit(resource, "NODE_METRICS", determineTimeUnit(from, to)))); + } + return result; + } + + private TimeUnit determineTimeUnit(ZonedDateTime from, ZonedDateTime to) + { + long day = 60 * 60 * 24; + long twoDays = day * 2; + long fifteenDays = day * 15; + + long dif = to.toEpochSecond() - from.toEpochSecond(); + if (dif < twoDays) + { + return TimeUnit.RAW; + } + if (dif < fifteenDays) + { + return TimeUnit.HOUR; + } + return TimeUnit.DAY; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/CpuConfig.java b/src/main/java/dev/dinauer/monitoring/entity/CpuConfig.java new file mode 100644 index 0000000..462789b --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/CpuConfig.java @@ -0,0 +1,53 @@ +package dev.dinauer.monitoring.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; + +@Entity +@Table(name = "cpu_config") +public class CpuConfig +{ + @Id + private String id; + + @Column(name = "container_name") + private String containerName; + + @OneToOne + @JoinColumn(name = "config_id") + @JsonIgnore + private MonitoringConfig config; + + public String getId() + { + return id; + } + + public CpuConfig setId(String id) + { + this.id = id; + return this; + } + + public String getContainerName() + { + return containerName; + } + + public CpuConfig setContainerName(String containerName) + { + this.containerName = containerName; + return this; + } + + public MonitoringConfig getConfig() + { + return config; + } + + public CpuConfig setConfig(MonitoringConfig config) + { + this.config = config; + return this; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/HealthcheckConfig.java b/src/main/java/dev/dinauer/monitoring/entity/HealthcheckConfig.java new file mode 100644 index 0000000..08fa452 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/HealthcheckConfig.java @@ -0,0 +1,39 @@ +package dev.dinauer.monitoring.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; + +@Entity +@Table(name = "healthcheck_config") +public class HealthcheckConfig +{ + @Id + private String id; + + @OneToOne + @JoinColumn(name = "config_id") + @JsonIgnore + private MonitoringConfig config; + + public String getId() + { + return id; + } + + public HealthcheckConfig setId(String id) + { + this.id = id; + return this; + } + + public MonitoringConfig getConfig() + { + return config; + } + + public HealthcheckConfig setConfig(MonitoringConfig config) + { + this.config = config; + return this; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/MonitoringConfig.java b/src/main/java/dev/dinauer/monitoring/entity/MonitoringConfig.java new file mode 100644 index 0000000..e20e204 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/MonitoringConfig.java @@ -0,0 +1,119 @@ +package dev.dinauer.monitoring.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "monitoring_config") +public class MonitoringConfig +{ + @Id + private String id; + + @Column(name = "config_name") + private String configName; + + @Enumerated(EnumType.STRING) + private MonitoringType type; + + private String interval; + + @OneToOne(mappedBy = "config") + private TargetConfig targetConfig; + + @OneToOne(mappedBy = "config") + private CpuConfig cpuConfig; + + @OneToOne(mappedBy = "config") + private HealthcheckConfig healthcheckConfig; + + @OneToOne(mappedBy = "config") + private VolumeConfig volumeConfig; + + public String getId() + { + return id; + } + + public MonitoringConfig setId(String id) + { + this.id = id; + return this; + } + + public String getConfigName() + { + return configName; + } + + public MonitoringConfig setConfigName(String configName) + { + this.configName = configName; + return this; + } + + public MonitoringType getType() + { + return type; + } + + public MonitoringConfig setType(MonitoringType type) + { + this.type = type; + return this; + } + + public String getInterval() + { + return interval; + } + + public MonitoringConfig setInterval(String interval) + { + this.interval = interval; + return this; + } + + public TargetConfig getTargetConfig() + { + return targetConfig; + } + + public MonitoringConfig setTargetConfig(TargetConfig targetConfig) + { + this.targetConfig = targetConfig; + return this; + } + + public CpuConfig getCpuConfig() + { + return cpuConfig; + } + + public MonitoringConfig setCpuConfig(CpuConfig cpuConfig) + { + this.cpuConfig = cpuConfig; + return this; + } + + public HealthcheckConfig getHealthcheckConfig() + { + return healthcheckConfig; + } + + public MonitoringConfig setHealthcheckConfig(HealthcheckConfig healthcheckConfig) + { + this.healthcheckConfig = healthcheckConfig; + return this; + } + + public VolumeConfig getVolumeConfig() + { + return volumeConfig; + } + + public MonitoringConfig setVolumeConfig(VolumeConfig volumeConfig) + { + this.volumeConfig = volumeConfig; + return this; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/MonitoringTargetType.java b/src/main/java/dev/dinauer/monitoring/entity/MonitoringTargetType.java new file mode 100644 index 0000000..6b89430 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/MonitoringTargetType.java @@ -0,0 +1,8 @@ +package dev.dinauer.monitoring.entity; + +public enum MonitoringTargetType +{ + DEPLOYMENT, + STATEFUL_SET, + LABELS +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/MonitoringType.java b/src/main/java/dev/dinauer/monitoring/entity/MonitoringType.java new file mode 100644 index 0000000..e320146 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/MonitoringType.java @@ -0,0 +1,6 @@ +package dev.dinauer.monitoring.entity; + +public enum MonitoringType +{ + VOLUME, CPU, MEMORY, HEALTHCHECK +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/TargetConfig.java b/src/main/java/dev/dinauer/monitoring/entity/TargetConfig.java new file mode 100644 index 0000000..273b55b --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/TargetConfig.java @@ -0,0 +1,135 @@ +package dev.dinauer.monitoring.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.*; + +import java.util.Map; + +@Entity +@Table(name = "target_config") +public class TargetConfig +{ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Id + private String id; + + @Enumerated(EnumType.STRING) + private MonitoringTargetType type; + + private String namespace; + + @Column(name = "deployment_name") + private String deploymentName; + + @Column(name = "stateful_set_name") + private String statefulSetName; + + private String labels; + + @OneToOne + @JoinColumn(name = "config_id") + @JsonIgnore + private MonitoringConfig config; + + public String getId() + { + return id; + } + + public TargetConfig setId(String id) + { + this.id = id; + return this; + } + + public MonitoringTargetType getType() + { + return type; + } + + public TargetConfig setType(MonitoringTargetType type) + { + this.type = type; + return this; + } + + public String getNamespace() + { + return namespace; + } + + public TargetConfig setNamespace(String namespace) + { + this.namespace = namespace; + return this; + } + + public String getDeploymentName() + { + return deploymentName; + } + + public TargetConfig setDeploymentName(String deploymentName) + { + this.deploymentName = deploymentName; + return this; + } + + public String getStatefulSetName() + { + return statefulSetName; + } + + public TargetConfig setStatefulSetName(String statefulSetName) + { + this.statefulSetName = statefulSetName; + return this; + } + + public Map getLabels() + { + if (labels != null) + { + try + { + return OBJECT_MAPPER.readValue(labels, new TypeReference>() {}); + } + catch (JsonProcessingException e) + { + throw new RuntimeException("Cannot read labels for target config."); + } + } + return null; + } + + public TargetConfig setLabels(Map labels) + { + if (labels != null) + { + try + { + this.labels = OBJECT_MAPPER.writeValueAsString(labels); + } + catch (JsonProcessingException e) + { + throw new RuntimeException("Cannot set labels for target config."); + } + } + return this; + } + + public MonitoringConfig getConfig() + { + return config; + } + + public TargetConfig setConfig(MonitoringConfig config) + { + this.config = config; + return this; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/entity/VolumeConfig.java b/src/main/java/dev/dinauer/monitoring/entity/VolumeConfig.java new file mode 100644 index 0000000..5e1a40b --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/VolumeConfig.java @@ -0,0 +1,67 @@ +package dev.dinauer.monitoring.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; + +@Entity +@Table(name = "volume_config") +public class VolumeConfig +{ + @Id + private String id; + + @Column(name = "mount_path") + private String mountPath; + + @Column(name = "container_name") + private String containerName; + + @OneToOne + @JoinColumn(name = "config_id") + @JsonIgnore + private MonitoringConfig config; + + public String getId() + { + return id; + } + + public VolumeConfig setId(String id) + { + this.id = id; + return this; + } + + public MonitoringConfig getConfig() + { + return config; + } + + public VolumeConfig setConfig(MonitoringConfig config) + { + this.config = config; + return this; + } + + public String getMountPath() + { + return mountPath; + } + + public VolumeConfig setMountPath(String mountPath) + { + this.mountPath = mountPath; + return this; + } + + public String getContainerName() + { + return containerName; + } + + public VolumeConfig setContainerName(String containerName) + { + this.containerName = containerName; + return this; + } +} \ No newline at end of file diff --git a/src/main/java/dev/dinauer/monitoring/entity/repo/MonitoringRepo.java b/src/main/java/dev/dinauer/monitoring/entity/repo/MonitoringRepo.java new file mode 100644 index 0000000..85374ff --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/entity/repo/MonitoringRepo.java @@ -0,0 +1,18 @@ +package dev.dinauer.monitoring.entity.repo; + +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.MonitoringType; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class MonitoringRepo implements PanacheRepositoryBase +{ + public List findByMonitoringType(MonitoringType type) + { + return list("type = :type", Parameters.with("type", type)); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/indexing/IndexCollection.java b/src/main/java/dev/dinauer/monitoring/indexing/IndexCollection.java new file mode 100644 index 0000000..34dc075 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/indexing/IndexCollection.java @@ -0,0 +1,115 @@ +package dev.dinauer.monitoring.indexing; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.*; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.UUID; + +@Entity +@Table(name = "metrics_index") +public class IndexCollection +{ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Id + private String id; + + private String resource; + + private String metric; + + private String timestamp; + + @Enumerated(EnumType.STRING) + private TimeUnit unit; + + private String metrics; + + public IndexCollection() + { + } + + public IndexCollection(String resource, String metric, String timestamp, TimeUnit unit) + { + this.id = UUID.randomUUID().toString(); + this.resource = resource; + this.timestamp = timestamp; + this.unit = unit; + this.metric = metric; + this.metrics = "{}"; + } + + public String getId() + { + return id; + } + + public void add(String key, long value) + { + Map metrics = getMetrics(); + IndexMetric metric = metrics.get(key); + if (metric != null) + { + metric.add(value); + } + else + { + IndexMetric newMetric = new IndexMetric(); + newMetric.add(value); + metrics.put(key, newMetric); + } + try + { + this.metrics = OBJECT_MAPPER.writeValueAsString(metrics); + } + catch (JsonProcessingException e) + { + throw new RuntimeException(e); + } + } + + private double calculateAverage(long sum, int count) + { + if (count == 0) + { + return 0; + } + return sum * 1.0 / count; + } + + public String getTimestamp() + { + return timestamp; + } + + public String getMetric() + { + return metric; + } + + public TimeUnit getUnit() + { + return unit; + } + + public String getResource() + { + return resource; + } + + public Map getMetrics() + { + try + { + return OBJECT_MAPPER.readValue(metrics, new TypeReference>() {}); + } + catch (JsonProcessingException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/dev/dinauer/monitoring/indexing/IndexMetric.java b/src/main/java/dev/dinauer/monitoring/indexing/IndexMetric.java new file mode 100644 index 0000000..7ac6c53 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/indexing/IndexMetric.java @@ -0,0 +1,46 @@ +package dev.dinauer.monitoring.indexing; + +public class IndexMetric +{ + private int count; + private long sum; + private double average; + + public IndexMetric() + { + this.count = 0; + this.sum = 0; + this.average = 0.0F; + } + + public void add(long value) + { + count = count + 1; + sum = sum + value; + average = calculateAverage(sum, count); + } + + private double calculateAverage(long sum, int count) + { + if (count == 0) + { + return 0; + } + return sum * 1.0 / count; + } + + public int getCount() + { + return count; + } + + public long getSum() + { + return sum; + } + + public double getAverage() + { + return average; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/indexing/IndexMetricsRepo.java b/src/main/java/dev/dinauer/monitoring/indexing/IndexMetricsRepo.java new file mode 100644 index 0000000..23d3d13 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/indexing/IndexMetricsRepo.java @@ -0,0 +1,22 @@ +package dev.dinauer.monitoring.indexing; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class IndexMetricsRepo implements PanacheRepositoryBase +{ + public Optional findByProperties(String resource, String metric, String timestamp, TimeUnit unit) + { + return find("resource = :resource AND metric = :metric AND timestamp = :timestamp AND unit = :unit", Parameters.with("resource", resource).and("metric", metric).and("timestamp", timestamp).and("unit", unit)).firstResultOptional(); + } + + public List findByResourceAndMetricAndTimeUnit(String resource, String metric, TimeUnit unit) + { + return list("resource = :resource AND metric = :metric AND unit = :unit ORDER BY timestamp ASC", Parameters.with("resource", resource).and("metric", metric).and("unit", unit)); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/indexing/IndexingService.java b/src/main/java/dev/dinauer/monitoring/indexing/IndexingService.java new file mode 100644 index 0000000..4b46dce --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/indexing/IndexingService.java @@ -0,0 +1,33 @@ +package dev.dinauer.monitoring.indexing; + +import io.quarkus.runtime.Startup; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Map; + +@Startup +@ApplicationScoped +public class IndexingService +{ + @Inject + IndexMetricsRepo indexMetricsRepo; + + @Transactional + public void index(String resource, String metric, Map values) + { + for (TimeUnit unit : TimeUnit.values()) + { + String timestamp = TimestampGenerator.generateTimestamp(ZonedDateTime.now(Clock.systemUTC()), unit); + IndexCollection metrics = indexMetricsRepo.findByProperties(resource, metric, timestamp, unit).orElse(new IndexCollection(resource, metric, timestamp, unit)); + for (Map.Entry entry : values.entrySet()) + { + metrics.add(entry.getKey().toUpperCase(), entry.getValue()); + } + indexMetricsRepo.persist(metrics); + } + } +} diff --git a/src/main/java/dev/dinauer/monitoring/indexing/TimeUnit.java b/src/main/java/dev/dinauer/monitoring/indexing/TimeUnit.java new file mode 100644 index 0000000..0e81a6b --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/indexing/TimeUnit.java @@ -0,0 +1,6 @@ +package dev.dinauer.monitoring.indexing; + +public enum TimeUnit +{ + RAW, HOUR, DAY +} diff --git a/src/main/java/dev/dinauer/monitoring/indexing/TimestampGenerator.java b/src/main/java/dev/dinauer/monitoring/indexing/TimestampGenerator.java new file mode 100644 index 0000000..74f50d7 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/indexing/TimestampGenerator.java @@ -0,0 +1,28 @@ +package dev.dinauer.monitoring.indexing; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +public class TimestampGenerator +{ + public static String generateTimestamp(ZonedDateTime timestamp, TimeUnit unit) + { + switch (unit) + { + case TimeUnit.RAW -> + { + return timestamp.format(DateTimeFormatter.ISO_DATE_TIME).substring(0, 19); + } + case TimeUnit.HOUR -> + { + return timestamp.format(DateTimeFormatter.ISO_DATE_TIME).substring(0, 13); + } + case TimeUnit.DAY -> + { + return timestamp.format(DateTimeFormatter.ISO_DATE_TIME).substring(0, 10); + } + } + throw new RuntimeException("Cannot index by chono unit " + unit); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/log/Log.java b/src/main/java/dev/dinauer/monitoring/log/Log.java new file mode 100644 index 0000000..97be5eb --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/log/Log.java @@ -0,0 +1,60 @@ +package dev.dinauer.monitoring.log; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.ZonedDateTime; +import java.util.UUID; + +@Entity +@Table(name = "log") +public class Log +{ + @Id + private String id; + private String message; + private ZonedDateTime timestamp; + + public static Log init(String message) + { + Log log = new Log(); + log.setId(UUID.randomUUID().toString()); + log.setMessage(message); + log.setTimestamp(ZonedDateTime.now()); + return log; + } + + public String getId() + { + return id; + } + + public Log setId(String id) + { + this.id = id; + return this; + } + + public String getMessage() + { + return message; + } + + public Log setMessage(String message) + { + this.message = message; + return this; + } + + public ZonedDateTime getTimestamp() + { + return timestamp; + } + + public Log setTimestamp(ZonedDateTime timestamp) + { + this.timestamp = timestamp; + return this; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/log/LogRepo.java b/src/main/java/dev/dinauer/monitoring/log/LogRepo.java new file mode 100644 index 0000000..928d29f --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/log/LogRepo.java @@ -0,0 +1,9 @@ +package dev.dinauer.monitoring.log; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class LogRepo implements PanacheRepositoryBase +{ +} diff --git a/src/main/java/dev/dinauer/monitoring/memory/ByteExtractor.java b/src/main/java/dev/dinauer/monitoring/memory/ByteExtractor.java new file mode 100644 index 0000000..ce9f4a2 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/memory/ByteExtractor.java @@ -0,0 +1,45 @@ +package dev.dinauer.monitoring.memory; + +import com.cronutils.utils.StringUtils; + +import java.util.Arrays; +import java.util.List; + +public class ByteExtractor +{ + public static long extractBytes(String input) + { + String[] sections = input.split("\n"); + if (sections.length == 2) + { + String metricsLine = sections[1]; + List metrics = Arrays.stream(metricsLine.split("\\s+")).filter(text -> !text.isBlank()).toList(); + if (metrics.size() == 3) + { + return convertToBytes(metrics.get(2)); + } + } + return 0; + } + + private static long convertToBytes(String input) + { + if (input.endsWith("Ki")) + { + return Long.parseLong(input.replace("Ki", "")) * 1024; + } + if (input.endsWith("Mi")) + { + return Long.parseLong(input.replace("Mi", "")) * 1024 * 1024; + } + if (input.endsWith("Gi")) + { + return Long.parseLong(input.replace("Gi", "")) * 1024 * 1024 * 1024; + } + if (StringUtils.isNumeric(input)) + { + return Long.parseLong(input); + } + throw new RuntimeException("Invalid byte representation."); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/memory/MemoryMonitoringJobRunner.java b/src/main/java/dev/dinauer/monitoring/memory/MemoryMonitoringJobRunner.java new file mode 100644 index 0000000..e95d660 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/memory/MemoryMonitoringJobRunner.java @@ -0,0 +1,40 @@ +package dev.dinauer.monitoring.memory; + +import dev.dinauer.ProcessRunner; +import dev.dinauer.monitoring.MonitoringService; +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.MonitoringType; +import dev.dinauer.monitoring.indexing.IndexingService; +import io.fabric8.kubernetes.api.model.Pod; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@ApplicationScoped +public class MemoryMonitoringJobRunner +{ + @Inject + ProcessRunner processRunner; + + @Inject + MonitoringService monitoringService; + + @Inject + IndexingService indexingService; + + public void run(MonitoringConfig config) throws IOException, InterruptedException + { + List pods = monitoringService.findRunningPodsByMonitoringConfig(config); + + for (Pod pod : pods) + { + String podId = pod.getMetadata().getUid(); + String podName = pod.getMetadata().getName(); + String result = processRunner.run(String.format("kubectl top pod %s -n %s", podName, config.getTargetConfig().getNamespace())); + indexingService.index(String.format("POD-%s", podId), MonitoringType.MEMORY.toString(), Map.ofEntries(Map.entry("CPU", ByteExtractor.extractBytes(result)))); + } + } +} diff --git a/src/main/java/dev/dinauer/monitoring/nodes/NodeMonitoringService.java b/src/main/java/dev/dinauer/monitoring/nodes/NodeMonitoringService.java new file mode 100644 index 0000000..939eec9 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/nodes/NodeMonitoringService.java @@ -0,0 +1,78 @@ +package dev.dinauer.monitoring.nodes; + +import dev.dinauer.ProcessRunner; +import dev.dinauer.monitoring.indexing.IndexingService; +import dev.dinauer.utils.ClientProvider; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@ApplicationScoped +public class NodeMonitoringService +{ + @Inject + ClientProvider clientProvider; + + @Inject + ProcessRunner processRunner; + + @Inject + IndexingService indexingService; + + public void run() throws IOException, InterruptedException + { + List result = new ArrayList<>(); + + String[] stats = getTopNodes().split("\n"); + + for(String nodeName : stats) + { + String[] parts = nodeName.split("\\s+"); + if(parts.length == 5) + { + String name = parts[0]; + String node = clientProvider.getClient().nodes().withName(name).get().getMetadata().getUid(); + int absoluteCpu = extractInteger(parts[1]); + int relativeCpu = extractInteger(parts[2]); + int absoluteMemory = extractMemory(parts[3]); + int relativeMemory = extractInteger(parts[4]); + Map metrics = Map.ofEntries( + Map.entry("RELATIVE_CPU", (long) relativeCpu), + Map.entry("RELATIVE_MEMORY", (long) relativeMemory), + Map.entry("ABSOLUTE_MEMORY", (long) absoluteMemory)); + indexingService.index(String.format("NODE-%s", node), "NODE_METRICS", metrics); + } + } + } + + private String getTopNodes() throws IOException, InterruptedException + { + String command = String.format("kubectl --kubeconfig=%s top nodes --no-headers", clientProvider.pathToKubeconfig()); + return processRunner.run(command); + } + + private Integer extractInteger(String input) + { + return Integer.valueOf(input.replace("m", "").replace("%", "")); + } + + private Integer extractMemory(String input) + { + if(input.contains("Ki")) + { + return Integer.parseInt(input.replace("Ki", "")); + } + if(input.contains("Mi")) + { + return Integer.parseInt(input.replace("Mi", "")) * 1024; + } + if(input.contains("Gi")) + { + return Integer.parseInt(input.replace("Gi", "")) * 1024 * 1024; + } + return Integer.parseInt(input); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/nodes/NodeStats.java b/src/main/java/dev/dinauer/monitoring/nodes/NodeStats.java new file mode 100644 index 0000000..43e6413 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/nodes/NodeStats.java @@ -0,0 +1,7 @@ +package dev.dinauer.monitoring.nodes; + +import io.fabric8.kubernetes.api.model.Node; + +public record NodeStats(Node node, Integer absoluteCpuUsage, Integer relativeCpuUsage, Integer totalCpu, Integer absoluteMemory, Integer relativeMemory, Integer totalMemory) +{ +} \ No newline at end of file diff --git a/src/main/java/dev/dinauer/monitoring/volume/UsageMetrics.java b/src/main/java/dev/dinauer/monitoring/volume/UsageMetrics.java new file mode 100644 index 0000000..e082148 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/volume/UsageMetrics.java @@ -0,0 +1,5 @@ +package dev.dinauer.monitoring.volume; + +public record UsageMetrics(long total, long used, int percentage) +{ +} diff --git a/src/main/java/dev/dinauer/monitoring/volume/VolumeMonitoringJobRunner.java b/src/main/java/dev/dinauer/monitoring/volume/VolumeMonitoringJobRunner.java new file mode 100644 index 0000000..3b53cff --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/volume/VolumeMonitoringJobRunner.java @@ -0,0 +1,74 @@ +package dev.dinauer.monitoring.volume; + +import dev.dinauer.ProcessRunner; +import dev.dinauer.monitoring.MonitoringService; +import dev.dinauer.monitoring.entity.MonitoringConfig; +import dev.dinauer.monitoring.entity.MonitoringType; +import dev.dinauer.monitoring.indexing.IndexingService; +import io.fabric8.kubernetes.api.model.Pod; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@ApplicationScoped +public class VolumeMonitoringJobRunner +{ + @Inject + Logger LOG; + + @Inject + ProcessRunner processRunner; + + @Inject + MonitoringService monitoringService; + + @Inject + VolumeUsageRepo volumeUsageRepo; + + @Inject + IndexingService indexingService; + + public void run(MonitoringConfig monitoring) throws IOException, InterruptedException + { + List pods = monitoringService.findRunningPodsByMonitoringConfig(monitoring); + + for (Pod pod : pods) + { + String podId = pod.getMetadata().getUid(); + String podName = pod.getMetadata().getName(); + if (pod.getStatus().getPhase().equals("Running")) + { + String result = processRunner.run(String.format("kubectl exec -i %s -c %s -n %s -- df -B1 --output=size,used,avail,pcent -h %s -B1", podName, monitoring.getVolumeConfig().getContainerName(), monitoring.getTargetConfig().getNamespace(), monitoring.getVolumeConfig().getMountPath())); + UsageMetrics usage = extractUsage(result); + volumeUsageRepo.persist(new VolumeUsage(UUID.randomUUID().toString(), monitoring.getId(), podId, podName, monitoring.getTargetConfig().getNamespace(), ZonedDateTime.now().truncatedTo(ChronoUnit.MILLIS), usage)); + indexingService.index(String.format("POD-%s", podId), MonitoringType.VOLUME.toString(), Map.ofEntries(Map.entry("VOLUME", (long) usage.percentage()))); + } + } + } + + private UsageMetrics extractUsage(String result) + { + String[] sections = result.split("\n"); + if (sections.length == 2) + { + String metrics = sections[1]; + List metricsSections = Arrays.stream(metrics.split("\\s+")).filter(metric -> !metric.isBlank()).toList(); + if (metricsSections.size() == 4) + { + long total = Long.parseLong(metricsSections.get(0)); + long used = Long.parseLong(metricsSections.get(1)); + int percentage = Integer.parseInt(metricsSections.get(3).replace("%", "")); + return new UsageMetrics(total, used, percentage); + } + } + throw new RuntimeException("Cannot extract usage."); + } +} diff --git a/src/main/java/dev/dinauer/monitoring/volume/VolumeUsage.java b/src/main/java/dev/dinauer/monitoring/volume/VolumeUsage.java new file mode 100644 index 0000000..47afdbf --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/volume/VolumeUsage.java @@ -0,0 +1,7 @@ +package dev.dinauer.monitoring.volume; + +import java.time.ZonedDateTime; + +public record VolumeUsage(String id, String monitoringId, String podId, String podName, String namespace, ZonedDateTime timestamp, UsageMetrics metrics) +{ +} diff --git a/src/main/java/dev/dinauer/monitoring/volume/VolumeUsageRepo.java b/src/main/java/dev/dinauer/monitoring/volume/VolumeUsageRepo.java new file mode 100644 index 0000000..5777ba6 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/volume/VolumeUsageRepo.java @@ -0,0 +1,80 @@ +package dev.dinauer.monitoring.volume; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import dev.dinauer.WorkdirProvider; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import org.jboss.resteasy.reactive.common.NotImplementedYet; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@ApplicationScoped +public class VolumeUsageRepo +{ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Inject + WorkdirProvider workdirProvider; + + private Path directory; + + @PostConstruct + void init() throws IOException + { + Path directory = workdirProvider.getWorkdirPath(Path.of("monitorings", "volumes", "jobs")); + Files.createDirectories(directory); + this.directory = directory; + } + + public void persist(VolumeUsage usage) throws IOException + { + Path file = getMonitoringPath(usage.monitoringId()).resolve(usage.podId()); + Files.write(file, (OBJECT_MAPPER.writeValueAsString(usage) + "\n").getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + + public List findByMonitoringIdAndPodId(String monitoringId, String podId) throws IOException + { + Path file = getMonitoringPath(monitoringId).resolve(podId); + if (Files.exists(file)) + { + List result = new ArrayList<>(); + for (String line : Files.readAllLines(file, StandardCharsets.UTF_8)) + { + result.add(OBJECT_MAPPER.readValue(line, VolumeUsage.class)); + } + return result; + } + return new ArrayList<>(); + } + + public List findAll(String monitoringId) throws IOException + { + List result = new ArrayList<>(); + try (Stream files = Files.list(getMonitoringPath(monitoringId))) + { + for (Path file : files.toList()) + { + result.add(OBJECT_MAPPER.readValue(Files.readString(file), VolumeUsage.class)); + } + } + return result; + } + + private Path getMonitoringPath(String monitoringId) throws IOException + { + Path monitoringDirectory = workdirProvider.getWorkdirPath(directory.resolve(monitoringId)); + Files.createDirectories(monitoringDirectory); + return monitoringDirectory; + } +} diff --git a/src/main/java/dev/dinauer/monitoring/volume/utils/Duration.java b/src/main/java/dev/dinauer/monitoring/volume/utils/Duration.java new file mode 100644 index 0000000..166abe9 --- /dev/null +++ b/src/main/java/dev/dinauer/monitoring/volume/utils/Duration.java @@ -0,0 +1,33 @@ +package dev.dinauer.monitoring.volume.utils; + +public class Duration +{ + public static long parse(String input) + { + if (input.length() > 1) + { + char unit = input.charAt(input.length() - 1); + long value = Long.parseLong(input.substring(0, input.length() - 1)); + switch (unit) + { + case 's' -> + { + return value; + } + case 'm' -> + { + return value * 60; + } + case 'h' -> + { + return value * 60 * 60; + } + default -> + { + throw new IllegalArgumentException(String.format("Invalid unit %s", unit)); + } + } + } + throw new RuntimeException("Invalid input"); + } +} diff --git a/src/main/java/dev/dinauer/service/PodService.java b/src/main/java/dev/dinauer/service/PodService.java index a4f3332..b568c30 100644 --- a/src/main/java/dev/dinauer/service/PodService.java +++ b/src/main/java/dev/dinauer/service/PodService.java @@ -2,13 +2,16 @@ package dev.dinauer.service; import dev.dinauer.utils.ClientProvider; import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.dsl.AppsAPIGroupDSL; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.util.List; +import java.util.Map; import java.util.Optional; @ApplicationScoped @@ -22,6 +25,24 @@ public class PodService return clientProvider.getClient().pods().inNamespace(namespace).list().getItems(); } + public List findByStatefulSet(String name, String namespace) + { + try(AppsAPIGroupDSL apps = clientProvider.getClient().apps()) + { + StatefulSet set = apps.statefulSets().inNamespace(namespace).withName(name).get(); + if (set != null) + { + return findByLabels(namespace, set.getSpec().getSelector().getMatchLabels()); + } + return null; + } + } + + public List findByLabels(String namespace, Map labels) + { + return clientProvider.getClient().pods().inNamespace(namespace).withLabels(labels).list().getItems(); + } + public List findAll() { return clientProvider.getClient().pods().inAnyNamespace().list().getItems(); diff --git a/src/main/java/dev/dinauer/utils/StartupService.java b/src/main/java/dev/dinauer/utils/StartupService.java index 2382447..8ba6fcd 100644 --- a/src/main/java/dev/dinauer/utils/StartupService.java +++ b/src/main/java/dev/dinauer/utils/StartupService.java @@ -7,6 +7,7 @@ import io.quarkus.runtime.Startup; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.jboss.logging.Logger; import java.io.IOException; import java.util.Set; @@ -18,6 +19,9 @@ public class StartupService private static final String INITIAL_USERNAME = "admin"; private static final String INITIAL_PASSWORD = "admin"; + @Inject + Logger LOG; + @Inject UserRepo userRepo; @@ -27,6 +31,7 @@ public class StartupService if(userRepo.findOptionalByUsername(INITIAL_USERNAME).isEmpty()) { userRepo.persist(buildInitialUser()); + LOG.infof("Initialized user 'admin'"); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index aa0b468..8989bbc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,12 +5,19 @@ quarkus.http.root-path=/api dev.dinauer.kobooboo.kubeconfigs.dir=/var/lib/kubooboo/config dev.dinauer.kubooboo.work.dir=/var/lib/kubooboo/work -%dev.dev.dinauer.kobooboo.kubeconfigs.dir=C:\\Users\\andre\\.kube\\config -%dev.dev.dinauer.kubooboo.work.dir=C:\\Users\\andre\\Documents\\dev\\kubeman\\backend\\src\\main\\resources\\dev +%dev.dev.dinauer.kobooboo.kubeconfigs.dir=/home/andreas/.kube/config +%dev.dev.dinauer.kubooboo.work.dir=/home/andreas/Documents/dev/kubooboo/backend/src/main/resources/dev # Keys -%prod.smallrye.jwt.sign.key.location=/etc/kubooboo/keys/privateKey.pem -%prod.mp.jwt.verify.publickey.location=/etc/kubooboo/keys/publicKey.pem +%prod.smallrye.jwt.sign.key.location=${PRIVATE_KEY_LOCATION} +%prod.mp.jwt.verify.publickey.location=${PUBLIC_KEY_LOCATION} # Keys Dev %dev.smallrye.jwt.sign.key.location=privateKey.pem -%dev.mp.jwt.verify.publickey.location=publicKey.pem \ No newline at end of file +%dev.mp.jwt.verify.publickey.location=publicKey.pem + +# Postgres +%dev.quarkus.datasource.db-kind = postgresql +%dev.quarkus.datasource.username = postgres +%dev.quarkus.datasource.password = postgres +%dev.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:6666/postgres +%dev,test.quarkus.hibernate-orm.schema-management.strategy = none \ No newline at end of file diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql new file mode 100644 index 0000000..2454683 --- /dev/null +++ b/src/main/resources/import.sql @@ -0,0 +1,11 @@ +INSERT INTO monitoring_config (id, config_name, type, interval) +VALUES ('5da234f4-3a34-4b1c-b72a-7330ca3b1dcf', 'Postgres Cluster', 'VOLUME', '30s'); +INSERT INTO target_config (id, type, namespace, labels, deployment_name, stateful_set_name, config_id) +VALUES ('4bd2f449-ed11-4f3f-830a-e4dc39cb21f5', 'LABELS', 'tavolio-prod', '{"cnpg.io/cluster":"postgres-cluster"}', null, null, '5da234f4-3a34-4b1c-b72a-7330ca3b1dcf'); +INSERT INTO volume_config (id, container_name, mount_path, config_id) +VALUES ('4090a60c-4517-4d76-b460-a0454014f30d', 'postgres', '/var/lib/postgresql/data', '5da234f4-3a34-4b1c-b72a-7330ca3b1dcf'); + +INSERT INTO monitoring_config (id, config_name, type, interval) +VALUES ('2da234f4-3a34-4b1c-b72a-7330ca3b1dcf', 'Postgres Cluster', 'MEMORY', '40s'); +INSERT INTO target_config (id, type, namespace, labels, deployment_name, stateful_set_name, config_id) +VALUES ('2bd2f449-ed11-4f3f-830a-e4dc39cb21f5', 'LABELS', 'tavolio-prod', '{"cnpg.io/cluster":"postgres-cluster"}', null, null, '2da234f4-3a34-4b1c-b72a-7330ca3b1dcf'); \ No newline at end of file diff --git a/src/test/java/dev/dinauer/monitoring/memory/ByteExtractorTest.java b/src/test/java/dev/dinauer/monitoring/memory/ByteExtractorTest.java new file mode 100644 index 0000000..cfe3957 --- /dev/null +++ b/src/test/java/dev/dinauer/monitoring/memory/ByteExtractorTest.java @@ -0,0 +1,22 @@ +package dev.dinauer.monitoring.memory; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ByteExtractorTest +{ + @Test + void test() + { + String input = """ + NAME CPU(cores) MEMORY(bytes) + postgres-cluster-1 13m 50Mi + """; + + long bytes = ByteExtractor.extractBytes(input); + + assertEquals(52428800, bytes); + } +}