Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.8</version>
<version>4.1.0</version>
</parent>
<groupId>com.obsidiandynamics.kafdrop</groupId>
<artifactId>kafdrop</artifactId>
Expand Down Expand Up @@ -156,16 +156,12 @@
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
Expand All @@ -176,10 +172,6 @@
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
Expand Down Expand Up @@ -211,7 +203,7 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.14</version>
<version>3.0.3</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
Expand All @@ -226,6 +218,16 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-restclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-resttestclient</artifactId>
<scope>test</scope>
</dependency>

<!-- Testcontainers -->
<dependency>
Expand Down Expand Up @@ -354,7 +356,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.5.8</version>
<version>4.1.0</version>
<configuration>
<mainClass>${start.class}</mainClass>
<layout>ZIP</layout>
Expand Down
61 changes: 21 additions & 40 deletions src/main/java/kafdrop/Kafdrop.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,25 @@

package kafdrop;

import com.google.common.base.Strings;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.DisallowedMethodsHandler;
import io.undertow.util.HttpString;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kafdrop.config.ini.IniFilePropertySource;
import kafdrop.config.ini.IniFileReader;
import org.jspecify.annotations.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.io.File;
import java.io.FileInputStream;
Expand All @@ -68,36 +63,22 @@ public static SpringApplicationBuilder createApplicationBuilder() {
}

@Bean
public WebServerFactoryCustomizer<UndertowServletWebServerFactory> deploymentCustomizer() {
return factory -> {
final UndertowDeploymentInfoCustomizer customizer = deploymentInfo -> {
var inf = new WebSocketDeploymentInfo();
inf.setBuffers(new DefaultByteBufferPool(false, 64));
deploymentInfo.addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, inf);
// see https://stackoverflow.com/a/54129696
deploymentInfo.addInitialHandlerChainWrapper(new HandlerWrapper() {
@Override
public HttpHandler wrap(HttpHandler handler) {
HttpString[] disallowedHttpMethods = {
HttpString.tryFromString("TRACE"),
HttpString.tryFromString("TRACK")
};
return new DisallowedMethodsHandler(handler, disallowedHttpMethods);
}
});
};
factory.addDeploymentInfoCustomizers(customizer);
};
}

@Bean
public WebMvcConfigurer webConfig() {
return new WebMvcConfigurer() {
public FilterRegistrationBean<OncePerRequestFilter> blockTrackFilter() {
FilterRegistrationBean<OncePerRequestFilter> registration = new FilterRegistrationBean<>();
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
registration.setFilter(new OncePerRequestFilter() {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String method = request.getMethod();
if ("TRACK".equals(method)) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "TRACK method is not allowed");
Comment thread
Bert-R marked this conversation as resolved.
return;
}
filterChain.doFilter(request, response);
}
};
});
return registration;
}

private static final class LoggingConfigurationListener
Expand Down
52 changes: 32 additions & 20 deletions src/main/java/kafdrop/config/HealthCheckConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@

package kafdrop.config;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.health.actuate.endpoint.CompositeHealthDescriptor;
import org.springframework.boot.health.actuate.endpoint.HealthDescriptor;
import org.springframework.boot.health.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.health.actuate.endpoint.IndicatedHealthDescriptor;
import org.springframework.boot.health.contributor.Status;
import org.springframework.context.annotation.Configuration;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
public class HealthCheckConfiguration {
Expand All @@ -43,28 +44,39 @@ public HealthCheck(HealthEndpoint healthEndpoint) {

@ManagedAttribute
public Map<String, Object> getHealth() {
final var health = (Health) healthEndpoint.health();
final var health = healthEndpoint.health();
final var healthMap = new LinkedHashMap<String, Object>();
healthMap.put("status", getStatus(health));
healthMap.put("detail", getDetails(health.getDetails()));
healthMap.put("status", getStatus(health.getStatus()));
healthMap.put("detail", getDetails(health));
return healthMap;
}

private Map<String, Object> getDetails(Map<String, Object> details) {
return details.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> {
final var health = (Health) e.getValue();
final var detail = new LinkedHashMap<String, Object>();
final var healthy = Status.UP.equals(health.getStatus());
detail.put("healthy", healthy);
detail.put("message", health.getDetails().toString());
return detail;
}));
private Map<String, Object> getDetails(HealthDescriptor healthDescriptor) {
if (healthDescriptor instanceof CompositeHealthDescriptor composite) {
final var result = new LinkedHashMap<String, Object>();
composite.getComponents().forEach((key, component) -> result.put(key, describeComponent(component)));
return result;
}
if (healthDescriptor instanceof IndicatedHealthDescriptor indicated) {

return indicated.getDetails();
}
return Map.of();
}

private Object describeComponent(HealthDescriptor component) {
if (component instanceof CompositeHealthDescriptor composite) {
return getDetails(composite);
}
final var detail = new LinkedHashMap<String, Object>();
detail.put("healthy", Status.UP.equals(component.getStatus()));
detail.put("message", component instanceof IndicatedHealthDescriptor indicated
? indicated.getDetails().toString()
: component.getStatus().toString());
return detail;
}

private String getStatus(Health health) {
final var status = health.getStatus();
private String getStatus(Status status) {
if (Status.UP.equals(status) || Status.DOWN.equals(status)) {
return status.toString();
} else {
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/kafdrop/config/ObjectMapperConfig.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package kafdrop.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import tools.jackson.databind.json.JsonMapper;

import java.util.TimeZone;

@Configuration
public class ObjectMapperConfig {

@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
public JsonMapper objectMapper(JsonMapper.Builder builder) {
return builder
.timeZone(TimeZone.getDefault())
.defaultTimeZone(TimeZone.getDefault())
.build();
}
}
4 changes: 2 additions & 2 deletions src/main/java/kafdrop/controller/BasicErrorController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.webmvc.autoconfigure.error.AbstractErrorController;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/kafdrop/service/KafkaHighLevelAdminClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import kafdrop.config.KafkaConfiguration;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.Config;
import org.apache.kafka.clients.admin.ConsumerGroupListing;
import org.apache.kafka.clients.admin.DeleteTopicsOptions;
import org.apache.kafka.clients.admin.GroupListing;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
Expand Down Expand Up @@ -84,13 +84,13 @@ ClusterDescription describeCluster() {
}

Set<String> listConsumerGroups() {
final Collection<ConsumerGroupListing> groupListing;
final Collection<GroupListing> groupListing;
try {
groupListing = adminClient.listConsumerGroups().valid().get();
groupListing = adminClient.listGroups().valid().get();
} catch (InterruptedException | ExecutionException e) {
throw new KafkaAdminClientException(e);
}
return groupListing.stream().map(ConsumerGroupListing::groupId).collect(Collectors.toSet());
return groupListing.stream().map(GroupListing::groupId).collect(Collectors.toSet());
}

Map<TopicPartition, OffsetAndMetadata> listConsumerGroupOffsetsIfAuthorized(String groupId) {
Expand Down
7 changes: 3 additions & 4 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ spring:
jackson:
deserialization:
fail_on_unknown_properties: false
read_unknown_enum_values_as_null: true
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datatype:
enum:
read_unknown_enum_values_as_null: true

springdoc:
enable-kotlin: false # Kafdrop does not use Kotlin; enabling it causes #803
Expand Down
4 changes: 3 additions & 1 deletion src/test/java/kafdrop/KafdropTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;
Expand All @@ -18,6 +19,7 @@

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate
public class KafdropTest extends AbstractIntegrationTest {

@LocalServerPort
Expand Down