Kstreamplify Framework

repository·main·Indexed 41 days ago

https://github.com/michelin/kstreamplify

A framework to accelerate Kafka Streams application development with automated bootstrapping, Avro support via SerdesUtils, and integrated testing utilities. It provides built-in error handling for routing failures to Dead Letter Queues (DLQ) using Processing, Deserialization, and Production Exception Handlers, as well as Kubernetes integration for readiness and liveness probes.

Tokens
6.5K
Snippets
18
Records
25
Agent score
0%

What's inside Kstreamplify

  1. Overview of Kstreamplify features

    main

    Kstreamplify is a library designed to simplify the development of Kafka Streams applications by providing high-level abstractions and automated setup. Key features include:

    • Bootstrapping: Automatic startup, configuration, and initialization of Kafka Streams.
    • Avro Support: Out-of-the-box Avro serializers and deserializers via SerdesUtils.
    • Error Handling: Built-in mechanisms to catch and route errors to a Dead Letter Queue (DLQ) topic.
    • Kubernetes Integration: Built-in readiness and liveness probes.
    • Interactive Queries: Simplified access to Kafka Streams state stores.
    • Deduplication: Utilities to remove duplicate events from streams.
    • Testing: Automated setup for the Topology Test Driver.
  2. Use the Processing Exception Handler for error routing

    main

    The recommended way to handle processing errors is using the ProcessingExceptionHandler. This implementation (based on KIP-1033/KIP-1034) forwards failed records to your configured DLQ topic as a KafkaError Avro object and resumes processing.

    To enable it, configure the following Kafka property:

    kafka:
      properties:
        processing.exception.handler: 'com.michelin.kstreamplify.error.DlqProcessingExceptionHandler'
  3. Get started with Kstreamplify in plain Java

    main

    For framework-independent Java applications:

    1. Add the dependency: Include kstreamplify-core.
    2. Implement the starter: Create a class extending KafkaStreamsStarter and override topology(StreamsBuilder streamsBuilder) and dlqTopic().
    3. Initialize and start: In your main method, wrap your starter in a KafkaStreamsInitializer and call .start().
    4. Configure properties: Place your Kafka settings in src/main/resources/application.yml under the kafka.properties key.

    Important Notes:

    • To enable web services (like Topology or Interactive Queries), you must define server.port in your configuration.
    • The core dependency does not include a logger; you must add one to your project manually.
    <!-- Dependency -->
    <dependency>
        <groupId>com.michelin</groupId>
        <artifactId>kstreamplify-core</artifactId>
        <version>${kstreamplify.version}</version>
    </dependency>
    
    <!-- Implementation and Startup -->
    public class MyKafkaStreams extends KafkaStreamsStarter {
        @Override
        public void topology(StreamsBuilder streamsBuilder) {
            // Define your topology here
        }
    
        @Override
        public String dlqTopic() {
            return "dlq_topic";
        }
    }
    
    public class MainKstreamplify {
        public static void main(String[] args) {
            KafkaStreamsInitializer initializer = new KafkaStreamsInitializer(new MyKafkaStreams());
            initializer.start();
        }
    }
    
    <!-- Configuration in application.yml -->
    kafka:
      properties:
        application.id: 'myKafkaStreams'
        bootstrap.servers: 'localhost:9092'
        schema.registry.url: 'http://localhost:8081'
    server:
      port: 8080
  4. Integrate OpenTelemetry with Kstreamplify

    main

    Kstreamplify simplifies the integration between OpenTelemetry and Kafka Streams by binding all Kafka Streams metrics to the Spring Boot registry.

    To run your application with the OpenTelemetry Java agent, include the following JVM options:

    -javaagent:/opentelemetry-javaagent.jar -Dotel.traces.exporter=otlp -Dotel.logs.exporter=otlp -Dotel.metrics.exporter=otlp

    Note for OpenTelemetry Java agent version 2+: The Micrometer metrics bridge is disabled by default in version 2. You must manually enable it by adding the following JVM option: -Dotel.instrumentation.micrometer.enabled=true.

    Kstreamplify also supports the OpenTelemetry BOM and Spring Boot starter dependency approach.

    -javaagent:/opentelemetry-javaagent.jar -Dotel.traces.exporter=otlp -Dotel.logs.exporter=otlp -Dotel.metrics.exporter=otlp -Dotel.instrumentation.micrometer.enabled=true
  5. Access Swagger and OpenAPI documentation

    main

    The Kstreamplify Spring Boot module integrates with Springdoc to automatically generate API documentation for your Kafka Streams application.

    By default, the documentation is available at:

    • Swagger UI: http://host:port/swagger-ui/index.html
    • OpenAPI documentation: http://host:port/v3/api-docs

    You can customize these endpoints using standard Springdoc properties.

  6. Migrate from Processing Result API to Processing Exception Handler

    main

    To migrate to the modern ProcessingExceptionHandler approach:

    1. Update method signatures: Change methods returning ProcessingResult<V, V2> to return the raw value V.
    2. Remove explicit error handling: Remove ProcessingResult.success() and ProcessingResult.fail(). Allow exceptions to propagate naturally so the handler can catch them.
    3. Clean up the topology: Remove TopologyErrorHandler.catchErrors() from your DSL chain.
    4. Update Processors: In ContextualProcessor, stop wrapping records in ProcessingResult. Simply call context().forward(record) and let exceptions propagate to trigger the DLQ mechanism.
  7. Set up a Dead Letter Queue (DLQ) topic

    main

    To enable routing of failed records to a Dead Letter Queue (DLQ), override the dlqTopic() method in your class that extends KafkaStreamsStarter and return the desired topic name.

    @Component
    public class MyKafkaStreams extends KafkaStreamsStarter {
        @Override
        public void topology(StreamsBuilder streamsBuilder) {
            // Define your topology here
        }
    
        @Override
        public String dlqTopic() {
            return "dlq_topic";
        }
    }
  8. Unit testing Kafka Streams with Kstreamplify

    main

    Kstreamplify simplifies testing by providing KafkaStreamsStarterTest, which wraps the Kafka Topology Test Driver.

    1. Add the test dependency: Include kstreamplify-core-test with test scope.
    2. Create a test class: Extend KafkaStreamsStarterTest and override getKafkaStreamsStarter() to return your application's starter implementation.
    3. Setup topics: In a @BeforeEach method, use testDriver.createInputTopic and testDriver.createOutputTopic to define your test streams. Use SerdesUtils to obtain the correct Serdes for your Avro types.
    4. Override properties: If you need to change default test properties (e.g., the state directory), override getSpecificProperties().

    Example test case for verifying business logic and DLQ routing:

    <!-- Dependency -->
    <dependency>
        <groupId>com.michelin</groupId>
        <artifactId>kstreamplify-core-test</artifactId>
        <version>${kstreamplify.version}</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Test Implementation -->
    public class MyKafkaStreamsTest extends KafkaStreamsStarterTest {
        private TestInputTopic<String, KafkaUser> inputTopic;
        private TestOutputTopic<String, KafkaUser> outputTopic;
    
        @Override
        protected KafkaStreamsStarter getKafkaStreamsStarter() {
            return new MyKafkaStreams();
        }
    
        @BeforeEach
        void setUp() {
            inputTopic = testDriver.createInputTopic("input_topic", new StringSerializer(),
                SerdesUtils.<KafkaUser>getValueSerdes().serializer());
    
            outputTopic = testDriver.createOutputTopic("output_topic", new StringDeserializer(),
                SerdesUtils.<KafkaUser>getValueSerdes().deserializer());
        }
    
        @Test
        void shouldUpperCase() {
            inputTopic.pipeInput("1", user);
            List<KeyValue<String, KafkaUser>> results = outputTopic.readKeyValuesToList();
            assertEquals("FIRST NAME", results.get(0).value.getFirstName());
        }
    
        @Test
        void shouldFailAndRouteToDlqTopic() {
            inputTopic.pipeInput("1", user);
            List<KeyValue<String, KafkaError>> errors = dlqTopic.readKeyValuesToList();
            assertEquals("1", errors.get(0).key);
        }
    
        @Override
        protected Map<String, String> getSpecificProperties() {
            return Map.of(STATE_DIR_CONFIG, "/tmp/kafka-streams");
        }
    }
  9. Get started with Kstreamplify in Spring Boot

    main

    To use Kstreamplify in a Spring Boot application, follow these steps:

    1. Add the dependency: Include kstreamplify-spring-boot. You may need to override the kafka.version property in your pom.xml to ensure compatibility with the Spring Boot Starter Parent.
    2. Implement the starter: Create a class that extends KafkaStreamsStarter and override the topology(StreamsBuilder streamsBuilder) method to define your business logic. You should also override dlqTopic() to specify your error topic.
    3. Configure properties: Define your Kafka settings in application.yml under the kafka.properties key.

    Note: The application will automatically bootstrap the Kafka Streams instance.

    <!-- Dependency setup -->
    <properties>
        <kafka.version>4.3.1</kafka.version>
    </properties>
    
    <dependency>
        <groupId>com.michelin</groupId>
        <artifactId>kstreamplify-spring-boot</artifactId>
        <version>${kstreamplify.version}</version>
    </dependency>
    
    <!-- Implementation -->
    @Component
    public class MyKafkaStreams extends KafkaStreamsStarter {
        @Override
        public void topology(StreamsBuilder streamsBuilder) {
            // Define your topology here
        }
    
        @Override
        public String dlqTopic() {
            return "dlq_topic";
        }
    }
    
    <!-- Configuration in application.yml -->
    kafka:
      properties:
        application.id: 'myKafkaStreams'
        bootstrap.servers: 'localhost:9092'
        schema.registry.url: 'http://localhost:8081'
  10. Configure Web Service endpoints (Topology, Interactive Queries, Kubernetes)

    main

    Kstreamplify exposes several web services for monitoring and querying your application.

    Topology Endpoint

    Returns the Kafka Streams topology description. Customize the path via:

    topology:
      path: 'custom-topology'

    Interactive Queries

    Provides endpoints to query state stores (Key-Value, Timestamped Key-Value, Window, and Timestamped Window stores). Note that only state stores with String keys are supported.

    Kubernetes Probes

    Exposes readiness and liveness probes for Kubernetes. Customize the paths via:

    kubernetes:
      liveness:
        path: 'custom-liveness'
      readiness:
        path: 'custom-readiness'
  11. Configure Deserialization Error handling

    main

    Kstreamplify provides a DlqDeserializationExceptionHandler to forward deserialization errors to the DLQ as KafkaError Avro objects.

    Enable the handler:

    kafka:
      properties:
        deserialization.exception.handler: 'com.michelin.kstreamplify.error.DlqDeserializationExceptionHandler'

    Optional advanced routing properties:

    PropertyDescription
    dlq.deserialization-handler.forward-restclient-exceptionForwards RestClientException from the Schema Registry (e.g., when a schema is not found)
    dlq.deserialization-handler.continue-on-unhandled-errorsRoutes all unhandled deserialization errors to DLQ and continues processing instead of failing

    Example Config:

    kafka:
      properties:
        dlq:
          deserialization-handler:
            forward-restclient-exception: true
            continue-on-unhandled-errors: true
  12. Remap topic names dynamically

    main

    If you need to reuse the same topology with different input or output topics, you can use dynamic remapping in your application.yml. This allows you to map a fixed topic name used in your code to a different name in a specific environment.

    Configuration in application.yml:

    kafka:
      properties:
        topic:
          remap:
            oldTopicName: newTopicName
            foo: bar

    This works for both input and output topics.