Huge changes

This commit is contained in:
andreas.dinauer 2025-10-26 18:22:56 +01:00
parent dc6354f033
commit 61c62738b5
44 changed files with 1575 additions and 13 deletions

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ nb-configuration.xml
/.quarkus/cli/plugins/
# TLS Certificates
.certs/
/src/main/resources/dev/

View File

@ -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"

9
pg-docker-compose.yaml Normal file
View File

@ -0,0 +1,9 @@
services:
db:
image: postgres
restart: always
shm_size: 128mb
environment:
POSTGRES_PASSWORD: postgres
ports:
- "6666:5432"

16
pom.xml
View File

@ -65,6 +65,22 @@
<artifactId>kubernetes-httpclient-vertx</artifactId>
<version>7.3.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
<!-- Hibernate ORM specific dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<!-- JDBC driver dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- Test -->
<dependency>

View File

@ -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("%", ""));

View File

@ -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<String> 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);
}
}

View File

@ -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<User> 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();
}
}

View File

@ -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<IndexCollection> jobs)
{
}

View File

@ -0,0 +1,9 @@
package dev.dinauer.monitoring;
import io.fabric8.kubernetes.api.model.Pod;
import java.util.List;
public record MonitoredResource<E, T>(E resource, List<T> jobs)
{
}

View File

@ -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<MonitoredResource<Pod, IndexCollection>> get(@PathParam("monitoring-id") String monitoringId) throws IOException
{
List<MonitoredResource<Pod, IndexCollection>> result = new ArrayList<>();
MonitoringConfig config = monitoringRepo.findById(monitoringId);
List<Pod> 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;
}
}

View File

@ -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<MonitoringConfig> 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);
}
}
}
}

View File

@ -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<MonitoringConfig> get() throws JsonProcessingException
{
return monitoringRepo.listAll();
}
}

View File

@ -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<Pod> 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");
}
}

View File

@ -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<MonitoredResource<Node, IndexCollection>> get(@QueryParam("from") ZonedDateTime from, @QueryParam("to") ZonedDateTime to) throws IOException
{
List<MonitoredResource<Node, IndexCollection>> result = new ArrayList<>();
List<Node> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
package dev.dinauer.monitoring.entity;
public enum MonitoringTargetType
{
DEPLOYMENT,
STATEFUL_SET,
LABELS
}

View File

@ -0,0 +1,6 @@
package dev.dinauer.monitoring.entity;
public enum MonitoringType
{
VOLUME, CPU, MEMORY, HEALTHCHECK
}

View File

@ -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<String, String> getLabels()
{
if (labels != null)
{
try
{
return OBJECT_MAPPER.readValue(labels, new TypeReference<Map<String, String>>() {});
}
catch (JsonProcessingException e)
{
throw new RuntimeException("Cannot read labels for target config.");
}
}
return null;
}
public TargetConfig setLabels(Map<String, String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<MonitoringConfig, String>
{
public List<MonitoringConfig> findByMonitoringType(MonitoringType type)
{
return list("type = :type", Parameters.with("type", type));
}
}

View File

@ -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<String, IndexMetric> 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<String, IndexMetric> getMetrics()
{
try
{
return OBJECT_MAPPER.readValue(metrics, new TypeReference<Map<String, IndexMetric>>() {});
}
catch (JsonProcessingException e)
{
throw new RuntimeException(e);
}
}
}

View File

@ -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;
}
}

View File

@ -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<IndexCollection, String>
{
public Optional<IndexCollection> 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<IndexCollection> 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));
}
}

View File

@ -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<String, Long> 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<String, Long> entry : values.entrySet())
{
metrics.add(entry.getKey().toUpperCase(), entry.getValue());
}
indexMetricsRepo.persist(metrics);
}
}
}

View File

@ -0,0 +1,6 @@
package dev.dinauer.monitoring.indexing;
public enum TimeUnit
{
RAW, HOUR, DAY
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<Log, String>
{
}

View File

@ -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<String> 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.");
}
}

View File

@ -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<Pod> 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))));
}
}
}

View File

@ -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<NodeStats> 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<String, Long> 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);
}
}

View File

@ -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)
{
}

View File

@ -0,0 +1,5 @@
package dev.dinauer.monitoring.volume;
public record UsageMetrics(long total, long used, int percentage)
{
}

View File

@ -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<Pod> 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<String> 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.");
}
}

View File

@ -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)
{
}

View File

@ -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<VolumeUsage> findByMonitoringIdAndPodId(String monitoringId, String podId) throws IOException
{
Path file = getMonitoringPath(monitoringId).resolve(podId);
if (Files.exists(file))
{
List<VolumeUsage> 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<VolumeUsage> findAll(String monitoringId) throws IOException
{
List<VolumeUsage> result = new ArrayList<>();
try (Stream<Path> 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;
}
}

View File

@ -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");
}
}

View File

@ -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<Pod> 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<Pod> findByLabels(String namespace, Map<String, String> labels)
{
return clientProvider.getClient().pods().inNamespace(namespace).withLabels(labels).list().getItems();
}
public List<Pod> findAll()
{
return clientProvider.getClient().pods().inAnyNamespace().list().getItems();

View File

@ -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'");
}
}

View File

@ -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
%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

View File

@ -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');

View File

@ -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);
}
}