🚸 Improve Log Inspection

This commit is contained in:
Andreas Dinauer 2025-12-25 13:38:43 +01:00
parent e165df6076
commit a6b31b8ca5
29 changed files with 425 additions and 385 deletions

16
pom.xml
View File

@ -172,14 +172,6 @@
<lineEnding>LF</lineEnding>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>validate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>net.revelc.code</groupId>
@ -194,14 +186,6 @@
<include>**/*.java</include>
</includes>
</configuration>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -0,0 +1,48 @@
package dev.dinauer;
import dev.dinauer.service.DeploymentService;
import dev.dinauer.service.NodeService;
import dev.dinauer.service.PodService;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/resources/deployments")
@Authenticated
public class DeploymentResource
{
@Inject
DeploymentService deploymentService;
@Inject
PodService podService;
@PATCH
@Path("/{namespace}/{name}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces
public void rescaleDeployment(@PathParam("namespace") String namespace, @PathParam("name") String name, Integer replicaCount)
{
if (namespace != null && !namespace.isBlank() && name != null && !name.isBlank() && replicaCount != null && replicaCount > 0)
{
deploymentService.rescale(namespace, name, replicaCount);
}
else
{
throw new RuntimeException("Namespace or name cannot be null or empty. Replica count cannot be null and must be greater than 0.");
}
}
@GET
@Path("/{namespace}/{name}/pods")
public List<Pod> getPodsByDeployment(String namespace, String name)
{
return podService.findByDeployment(namespace, name);
}
}

View File

@ -1,36 +0,0 @@
package dev.dinauer;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.quarkus.security.Authenticated;
import io.smallrye.common.annotation.Blocking;
import dev.dinauer.service.IngressService;
@Path("/ingresses")
@Blocking
@Authenticated
public class IngressResource
{
@Inject
IngressService ingressService;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Ingress> getIngresses(@QueryParam("namespace") String namespace)
{
if (namespace != null)
{
ingressService.findByNamespace(namespace);
}
return ingressService.findAll();
}
}

View File

@ -0,0 +1,54 @@
package dev.dinauer;
import java.util.List;
import dev.dinauer.inspect.websocket.ResourceType;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import dev.dinauer.service.*;
@Path("/resources/nodes")
public class NodeResource
{
@Inject
DeploymentService deploymentService;
@Inject
NodeService nodeService;
@GET
public List<?> get()
{
return nodeService.findAll();
}
@GET
@Path("/{namespace}")
public Object getAllNodes()
{
return nodeService.findAll();
}
@PATCH
@Path("/{namespace}/{name}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces
@RolesAllowed("admin")
public void rescaleDeployment(@PathParam("namespace") String namespace, @PathParam("name") String name, Integer replicaCount)
{
if (ResourceType.DEPLOYMENT.equals("XY"))
{
if (namespace != null && !namespace.isBlank() && name != null && !name.isBlank() && replicaCount != null && replicaCount > 0)
{
deploymentService.rescale(namespace, name, replicaCount);
}
}
else
{
throw new RuntimeException("Must be a deployment or namespace or name cannot be null or empty. Replica count cannot be null and must be greater than 0.");
}
}
}

View File

@ -1,33 +1,33 @@
package dev.dinauer;
import java.util.ArrayList;
import java.util.List;
import dev.dinauer.inspect.env.EnvVar;
import dev.dinauer.inspect.env.EnvironmentVariableService;
import dev.dinauer.utils.ProcessRunner;
import io.fabric8.kubernetes.api.model.Pod;
import io.quarkus.security.Authenticated;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import io.fabric8.kubernetes.api.model.Pod;
import io.quarkus.runtime.Startup;
import io.smallrye.common.annotation.Blocking;
import dev.dinauer.service.PodService;
import dev.dinauer.utils.ClientProvider;
@Path("/pods")
@Startup
@ApplicationScoped
@Authenticated
public class PodResource
{
@Inject
PodService podService;
@Inject
ProcessRunner processRunner;
@Inject
ClientProvider clientProvider;
EnvironmentVariableService environmentVariableService;
@GET
@Produces(MediaType.APPLICATION_JSON)
@ -36,22 +36,7 @@ public class PodResource
@Path("/{namespace}/{name}/{containerName}/env")
public List<EnvVar> getEnv(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("containerName") String containerName)
{
return getVars(podService.findByNameAndNamespace(name, namespace), containerName);
}
private List<EnvVar> getVars(Pod pod, String containerName)
{
List<EnvVar> result = new ArrayList<>();
List<String> lines = processRunner.runToLines(String.format("kubectl exec -it %s -c %s -n %s -- env", pod.getMetadata().getName(), containerName, pod.getMetadata().getNamespace()));
for (String line : lines)
{
int indexOfFirstEquals = line.indexOf("=");
if (indexOfFirstEquals != -1)
{
result.add(new EnvVar(line.substring(0, indexOfFirstEquals), line.substring(indexOfFirstEquals + 1)));
}
}
return result;
return environmentVariableService.getVars(podService.findByNameAndNamespace(name, namespace), containerName);
}
@DELETE

View File

@ -1,137 +0,0 @@
package dev.dinauer;
import java.util.List;
import java.util.Optional;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.jboss.logging.Logger;
import dev.dinauer.service.*;
@Path("/resources/{resource}")
public class ResourceResource
{
@Inject
Logger LOG;
@Inject
StatefulSetService statefulSetService;
@Inject
DeploymentService deploymentService;
@Inject
PodService podService;
@Inject
CustomResourceDefinitionService customResourceDefinitionService;
@Inject
IngressService ingressService;
@Inject
ServiceService serviceService;
@Inject
NodeService nodeService;
@Inject
SecretService secretService;
@Inject
ConfigMapService configMapService;
@GET
public List<?> get(@PathParam("resource") ResourceType resourceType)
{
return getService(resourceType).findAll();
}
@GET
@Path("/{namespace}")
public List<?> getByNamespace(@PathParam("resource") ResourceType resourceType, @PathParam("namespace") String namespace)
{
return getService(resourceType).findByNamespace(namespace);
}
@GET
@Path("/{namespace}/{name}")
public Object getByNamespaceAndName(@PathParam("resource") ResourceType resourceType, @PathParam("namespace") String namespace, @PathParam("name") String name)
{
Optional<?> resourceOptional = getService(resourceType).findOptionalByNameAndNamespace(name, namespace);
if (resourceOptional.isPresent())
{
return resourceOptional.get();
}
throw new NotFoundException();
}
@DELETE
@Path("/{namespace}/{name}")
public void deleteByNamespaceAndName(@PathParam("resource") ResourceType resourceType, @PathParam("namespace") String namespace, @PathParam("name") String name)
{
getService(resourceType).delete(name, namespace);
}
@PATCH
@Path("/{namespace}/{name}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces
@RolesAllowed("admin")
public void rescaleDeployment(@PathParam("resource") ResourceType resourceType, @PathParam("namespace") String namespace, @PathParam("name") String name, Integer replicaCount)
{
if (ResourceType.DEPLOYMENT.equals(resourceType))
{
if (namespace != null && !namespace.isBlank() && name != null && !name.isBlank() && replicaCount != null && replicaCount > 0)
{
deploymentService.rescale(namespace, name, replicaCount);
}
}
else
{
throw new RuntimeException("Must be a deployment or namespace or name cannot be null or empty. Replica count cannot be null and must be greater than 0.");
}
}
private ResourceService<?> getService(ResourceType resourceType)
{
switch (resourceType)
{
case ResourceType.STATEFUL_SET -> {
return statefulSetService;
}
case ResourceType.DEPLOYMENT -> {
return deploymentService;
}
case ResourceType.SERVICE -> {
return serviceService;
}
case ResourceType.INGRESS -> {
return ingressService;
}
case ResourceType.POD -> {
return podService;
}
case ResourceType.CUSTOM_RESOURCE_DEFINITION -> {
return customResourceDefinitionService;
}
case ResourceType.NODE -> {
return nodeService;
}
case ResourceType.SECRET -> {
return secretService;
}
case ResourceType.CONFIG_MAP -> {
return configMapService;
}
default -> {
LOG.errorf("Invalid resource type %s.", resourceType);
throw new BadRequestException();
}
}
}
}

View File

@ -1,40 +0,0 @@
package dev.dinauer;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import io.fabric8.kubernetes.api.model.Service;
import io.quarkus.runtime.Startup;
import io.quarkus.security.Authenticated;
import io.smallrye.common.annotation.Blocking;
import dev.dinauer.service.ServiceService;
@Path("/services")
@Startup
@ApplicationScoped
@Blocking
@Authenticated
public class ServiceResource
{
@Inject
ServiceService serviceService;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Service> getServices(@QueryParam("namespace") String namespace)
{
if (namespace != null && !namespace.isBlank())
{
return serviceService.findByNamespace(namespace);
}
return serviceService.findAll();
}
}

View File

@ -21,11 +21,6 @@ public class WorkdirProvider
this.workdir = path;
}
public String getWorkdir(Path subpath)
{
return workdir.resolve(subpath).toString();
}
public Path getWorkdirPath(Path subpath)
{
return workdir.resolve(subpath);

View File

@ -20,11 +20,7 @@ public class TokenService
{
JsonWebToken token = getToken(queryString);
Optional<String> purpose = token.claim("purpose");
if (purpose.isPresent())
{
return purpose.get().equals("ws:connect");
}
return false;
return purpose.map(s -> s.equals("ws:connect")).orElse(false);
}
private JsonWebToken getToken(String query)

View File

@ -1,4 +1,4 @@
package dev.dinauer;
package dev.dinauer.inspect.env;
public record EnvVar(String key, String value)
{

View File

@ -0,0 +1,31 @@
package dev.dinauer.inspect.env;
import dev.dinauer.utils.ProcessRunner;
import io.fabric8.kubernetes.api.model.Pod;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class EnvironmentVariableService
{
@Inject
ProcessRunner processRunner;
public List<EnvVar> getVars(Pod pod, String containerName)
{
List<EnvVar> result = new ArrayList<>();
List<String> lines = processRunner.runToLines(String.format("kubectl exec -it %s -c %s -n %s -- env", pod.getMetadata().getName(), containerName, pod.getMetadata().getNamespace()));
for (String line : lines)
{
int indexOfFirstEquals = line.indexOf("=");
if (indexOfFirstEquals != -1)
{
result.add(new EnvVar(line.substring(0, indexOfFirstEquals), line.substring(indexOfFirstEquals + 1)));
}
}
return result;
}
}

View File

@ -0,0 +1,133 @@
package dev.dinauer.inspect.log;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import dev.dinauer.service.PodService;
import dev.dinauer.utils.ClientProvider;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.dsl.LogWatch;
import io.fabric8.kubernetes.client.dsl.PodResource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.jboss.logging.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ServerEndpoint("/logs/deployments/{namespace}/{name}")
@ApplicationScoped
public class DeploymentLogWebsocket
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
private final Map<Session, LogWatch> sessions = new HashMap<>();
@Inject
Logger LOG;
@Inject
ClientProvider clientProvider;
@Inject
ManagedExecutor executor;
@Inject
PodService podService;
@OnOpen
public void onOpen(Session session, @PathParam("namespace") String namespace, @PathParam("name") String name)
{
executor.submit(() -> {
List<Pod> pods = podService.findByDeployment(namespace, name);
List<KubernetesLog> existingLogs = new ArrayList<>();
for (Pod pod : pods)
{
PodResource resource = clientProvider.getClient().pods().inNamespace(pod.getMetadata().getNamespace()).withName(pod.getMetadata().getName());
existingLogs.addAll(podService.getLogs(resource));
}
send(session, existingLogs.stream().sorted(KubernetesLog::orderByTimestamp).toList());
for (Pod pod : pods)
{
CompletableFuture.runAsync(() -> {
LogWatch watch = clientProvider.getClient().pods().inNamespace(pod.getMetadata().getNamespace()).withName(pod.getMetadata().getName()).usingTimestamps().tailingLines(0).watchLog();
sessions.put(session, watch);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(watch.getOutput())))
{
String line;
while ((line = reader.readLine()) != null && !Thread.currentThread().isInterrupted())
{
send(session, toLog(List.of(line), pod.getMetadata().getName()));
}
LOG.info("Ended");
}
catch (Exception e)
{
LOG.errorf("Error reading output of log watch: %s", e.getMessage());
}
});
}
});
}
@OnClose
public void onClose(Session session) throws IOException
{
LogWatch watch = sessions.remove(session);
if (watch != null)
{
watch.close();
}
session.close();
}
private void send(Session session, List<KubernetesLog> logs)
{
try
{
session.getAsyncRemote().sendText(OBJECT_MAPPER.writeValueAsString(logs));
}
catch (Exception e)
{
LOG.errorf("Error sending logs to frontend via websocket: %s", e.getMessage());
}
}
private List<KubernetesLog> toLog(List<String> logs, String podName)
{
List<KubernetesLog> result = new ArrayList<>();
for (String log : logs)
{
int indexFirstSpace = log.indexOf(" ");
if (indexFirstSpace != -1)
{
String timestampRaw = log.substring(0, indexFirstSpace);
try
{
String message = log.substring(indexFirstSpace + 1);
result.add(new KubernetesLog(LocalDateTime.parse(timestampRaw, DateTimeFormatter.ISO_DATE_TIME), message, podName));
}
catch (Exception e)
{
LOG.errorf("Error parsing log: %s", e.getMessage());
}
}
}
return result;
}
}

View File

@ -1,7 +1,11 @@
package dev.dinauer.inspect.log;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public record KubernetesLog(LocalDateTime timestamp, String message)
public record KubernetesLog(LocalDateTime timestamp, String message, String podName)
{
public static int orderByTimestamp(KubernetesLog o1, KubernetesLog o2) {
return (int) (o1.timestamp.toEpochSecond(ZoneOffset.UTC) - o2.timestamp.toEpochSecond(ZoneOffset.UTC));
}
}

View File

@ -7,6 +7,11 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import dev.dinauer.inspect.utils.InputStreamWatcher;
import dev.dinauer.service.PodService;
import io.fabric8.kubernetes.client.dsl.PodResource;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.OnClose;
@ -15,7 +20,6 @@ import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.jboss.logging.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -26,9 +30,9 @@ import io.fabric8.kubernetes.client.dsl.LogWatch;
import dev.dinauer.utils.ClientProvider;
@ServerEndpoint("/logs/{namespace}/{name}")
@ServerEndpoint("/logs/pods/{namespace}/{name}")
@ApplicationScoped
public class LogWebsocket
public class PodLogWebsocket
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
private final Map<Session, LogWatch> sessions = new HashMap<>();
@ -40,32 +44,18 @@ public class LogWebsocket
ClientProvider clientProvider;
@Inject
ManagedExecutor executor;
PodService podService;
@OnOpen
public void onOpen(Session session, @PathParam("namespace") String namespace, @PathParam("name") String name)
{
executor.submit(() -> {
List<String> existingLogs = Arrays.stream(clientProvider.getClient().pods().inNamespace(namespace).withName(name).usingTimestamps().tailingLines(200).getLog().split("\n")).toList();
send(session, toLog(existingLogs));
LogWatch watch = clientProvider.getClient().pods().inNamespace(namespace).withName(name).usingTimestamps().tailingLines(0).watchLog();
sessions.put(session, watch);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(watch.getOutput())))
{
String line;
while ((line = reader.readLine()) != null && !Thread.currentThread().isInterrupted())
{
send(session, toLog(List.of(line)));
}
LOG.info("Ended");
}
catch (Exception e)
{
LOG.errorf("Error reading output of log watch: %s", e.getMessage());
}
});
Uni.createFrom().voidItem().invoke(() -> {
PodResource resource = clientProvider.getClient().pods().inNamespace(namespace).withName(name);
send(session, podService.getLogs(resource));
sessions.put(session, podService.watchLogs(resource, (logs) -> {
send(session, logs);
}));
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool()).subscribe().with(result -> {}, error -> {});
}
@OnClose
@ -91,7 +81,12 @@ public class LogWebsocket
}
}
private List<KubernetesLog> toLog(List<String> logs)
private List<KubernetesLog> toLog(String log, String podName)
{
return toLog(List.of(log), podName);
}
private List<KubernetesLog> toLog(List<String> logs, String podName)
{
List<KubernetesLog> result = new ArrayList<>();
for (String log : logs)
@ -103,7 +98,7 @@ public class LogWebsocket
try
{
String message = log.substring(indexFirstSpace + 1);
result.add(new KubernetesLog(LocalDateTime.parse(timestampRaw, DateTimeFormatter.ISO_DATE_TIME), message));
result.add(new KubernetesLog(LocalDateTime.parse(timestampRaw, DateTimeFormatter.ISO_DATE_TIME), message, podName));
}
catch (Exception e)
{

View File

@ -0,0 +1,29 @@
package dev.dinauer.inspect.utils;
import org.jboss.logging.Logger;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;
public class InputStreamWatcher
{
private static final Logger LOG = Logger.getLogger(InputStreamWatcher.class);
public static void watch(InputStream stream, Consumer<String> consume)
{
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream)))
{
String line;
while ((line = reader.readLine()) != null)
{
consume.accept(line);
}
}
catch (Exception e)
{
LOG.errorf("Error reading output of log watch: %s", e.getMessage());
}
}
}

View File

@ -1,4 +1,4 @@
package dev.dinauer;
package dev.dinauer.inspect.websocket;
public enum ResourceType
{

View File

@ -22,7 +22,6 @@ import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.WatcherException;
import dev.dinauer.ResourceType;
import dev.dinauer.inspect.TokenService;
import dev.dinauer.service.ResourceService;

View File

@ -6,7 +6,6 @@ import jakarta.ws.rs.BadRequestException;
import org.jboss.logging.Logger;
import dev.dinauer.ResourceType;
import dev.dinauer.service.*;
@ApplicationScoped
@ -103,7 +102,7 @@ public class ServiceFactory
{
return namespaceService;
}
default :
default:
{
LOG.errorf("Invalid resource type %s.", resourceType);
throw new BadRequestException();

View File

@ -8,7 +8,7 @@ import jakarta.inject.Inject;
import io.fabric8.kubernetes.api.model.Node;
import io.fabric8.kubernetes.api.model.Pod;
import dev.dinauer.ProcessRunner;
import dev.dinauer.utils.ProcessRunner;
import dev.dinauer.monitoring.nodes.MonitoredNode;
import dev.dinauer.monitoring.nodes.NodeMetrics;
import dev.dinauer.monitoring.nodes.client.NodeDiskMetrics;

View File

@ -9,7 +9,7 @@ import jakarta.inject.Inject;
import io.fabric8.kubernetes.api.model.Pod;
import dev.dinauer.ProcessRunner;
import dev.dinauer.utils.ProcessRunner;
import dev.dinauer.monitoring.MonitoringService;
import dev.dinauer.monitoring.entity.MonitoringConfig;
import dev.dinauer.monitoring.entity.MonitoringType;

View File

@ -10,7 +10,7 @@ import jakarta.inject.Inject;
import io.fabric8.kubernetes.api.model.Pod;
import io.quarkus.scheduler.Scheduled;
import dev.dinauer.ProcessRunner;
import dev.dinauer.utils.ProcessRunner;
@ApplicationScoped
public class PodMetricsService

View File

@ -15,7 +15,7 @@ import org.jboss.logging.Logger;
import io.fabric8.kubernetes.api.model.Pod;
import dev.dinauer.ProcessRunner;
import dev.dinauer.utils.ProcessRunner;
import dev.dinauer.monitoring.MonitoringService;
import dev.dinauer.monitoring.entity.MonitoringConfig;
import dev.dinauer.monitoring.entity.MonitoringType;

View File

@ -42,21 +42,6 @@ public class VolumeUsageRepo
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<>();

View File

@ -0,0 +1,47 @@
package dev.dinauer.service;
import dev.dinauer.inspect.log.KubernetesLog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class LogParser
{
@Inject
Logger LOG;
public List<KubernetesLog> toLog(String log, String podName)
{
return toLog(List.of(log), podName);
}
public List<KubernetesLog> toLog(List<String> logs, String podName)
{
List<KubernetesLog> result = new ArrayList<>();
for (String log : logs)
{
int indexFirstSpace = log.indexOf(" ");
if (indexFirstSpace != -1)
{
String timestampRaw = log.substring(0, indexFirstSpace);
try
{
String message = log.substring(indexFirstSpace + 1);
result.add(new KubernetesLog(LocalDateTime.parse(timestampRaw, DateTimeFormatter.ISO_DATE_TIME), message, podName));
}
catch (Exception e)
{
LOG.errorf("Error parsing log: %s", e.getMessage());
}
}
}
return result;
}
}

View File

@ -1,9 +1,16 @@
package dev.dinauer.service;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import dev.dinauer.inspect.log.KubernetesLog;
import dev.dinauer.inspect.utils.InputStreamWatcher;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.client.dsl.LogWatch;
import io.fabric8.kubernetes.client.dsl.PodResource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ -21,6 +28,12 @@ public class PodService implements ResourceService<Pod>
@Inject
ClientProvider clientProvider;
@Inject
LogParser logParser;
@Inject
dev.dinauer.PodResource podResource;
@Override
public void delete(String name, String namespace)
{
@ -60,6 +73,19 @@ public class PodService implements ResourceService<Pod>
}
}
public List<Pod> findByDeployment(String namespace, String name)
{
try (AppsAPIGroupDSL apps = clientProvider.getClient().apps())
{
Deployment set = apps.deployments().inNamespace(namespace).withName(name).get();
if (set != null)
{
return findByNamespaceAndLabels(namespace, set.getSpec().getSelector().getMatchLabels());
}
return null;
}
}
public List<Pod> findByNamespaceAndLabels(String namespace, Map<String, String> labels)
{
return clientProvider.getClient().pods().inNamespace(namespace).withLabels(labels).list().getItems();
@ -103,4 +129,17 @@ public class PodService implements ResourceService<Pod>
}
return Optional.empty();
}
public List<KubernetesLog> getLogs(PodResource resource)
{
List<String> existingLogs = Arrays.stream(resource.usingTimestamps().tailingLines(200).getLog().split("\n")).toList();
return logParser.toLog(existingLogs, resource.get().getMetadata().getName());
}
public LogWatch watchLogs(PodResource resource, Consumer<List<KubernetesLog>> consume)
{
LogWatch watch = resource.usingTimestamps().tailingLines(0).watchLog();
InputStreamWatcher.watch(watch.getOutput(), line -> consume.accept(logParser.toLog(line, resource.get().getMetadata().getName())));
return watch;
}
}

View File

@ -1,5 +0,0 @@
package dev.dinauer.settings;
public record Settings(Boolean kubeconfigDefaultPath, String kubeconfigPath, Integer refreshInterval)
{
}

View File

@ -1,27 +0,0 @@
package dev.dinauer.settings;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.dinauer.WorkdirProvider;
@ApplicationScoped
public class SettingsRepo
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Inject
WorkdirProvider workdirProvider;
public Settings find() throws IOException
{
return OBJECT_MAPPER.readValue(new File(workdirProvider.getWorkdir(Path.of("settings.json"))), Settings.class);
}
}

View File

@ -1,38 +0,0 @@
package dev.dinauer.settings;
import java.io.IOException;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.common.NotImplementedYet;
import io.quarkus.security.Authenticated;
@Path("/settings")
@ApplicationScoped
@Authenticated
public class SettingsResource
{
@Inject
SettingsRepo settingsRepo;
@GET
@Produces(MediaType.APPLICATION_JSON)
public Settings getSettings() throws IOException
{
return settingsRepo.find();
}
@PUT
@RolesAllowed("admin")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public void updateSettings()
{
throw new NotImplementedYet();
}
}

View File

@ -1,4 +1,4 @@
package dev.dinauer;
package dev.dinauer.utils;
import java.io.BufferedReader;
import java.io.IOException;