├── .github └── FUNDING.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.adoc ├── build-docker-images.sh ├── categorizer-service ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── categorizerservice │ │ │ ├── CategorizerServiceApplication.java │ │ │ ├── config │ │ │ └── SchemaRegistryConfig.java │ │ │ └── news │ │ │ ├── CategoryService.java │ │ │ └── NewsEventProcessor.java │ └── resources │ │ ├── application.yml │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── categorizerservice │ └── CategorizerServiceApplicationTests.java ├── collector-service ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── collectorservice │ │ │ ├── CollectorServiceApplication.java │ │ │ ├── config │ │ │ └── SchemaRegistryConfig.java │ │ │ └── news │ │ │ ├── News.java │ │ │ ├── NewsEventProcessor.java │ │ │ └── NewsRepository.java │ └── resources │ │ ├── application.yml │ │ ├── banner.txt │ │ └── news-es-analysis.json │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── collectorservice │ └── CollectorServiceApplicationTests.java ├── commons-news ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── ivanfranchin │ │ └── commonsnews │ │ └── avro │ │ └── NewsEvent.java │ └── resources │ └── avro │ └── news-event.avsc ├── docker-compose.yml ├── documentation ├── eureka.jpg ├── project-diagram.excalidraw ├── project-diagram.jpeg ├── websocket-operation.gif └── zipkin.jpg ├── eureka-server ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── eurekaservice │ │ │ └── EurekaServiceApplication.java │ └── resources │ │ ├── application.yml │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── eurekaservice │ └── EurekaServiceApplicationTests.java ├── mvnw ├── mvnw.cmd ├── news-client ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── newsclient │ │ │ ├── NewsClientApplication.java │ │ │ ├── config │ │ │ ├── SchemaRegistryConfig.java │ │ │ └── WebSocketConfig.java │ │ │ ├── news │ │ │ ├── NewsController.java │ │ │ ├── NewsEventListener.java │ │ │ ├── client │ │ │ │ └── PublisherApiClient.java │ │ │ └── dto │ │ │ │ ├── MyPage.java │ │ │ │ ├── News.java │ │ │ │ └── SearchRequest.java │ │ │ └── util │ │ │ └── DateTimeUtil.java │ └── resources │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── public │ │ └── img │ │ │ ├── Entertainment.jpg │ │ │ ├── Health.jpg │ │ │ ├── Science.jpg │ │ │ ├── Sport.jpg │ │ │ └── World.jpg │ │ ├── static │ │ └── app.js │ │ └── templates │ │ └── news.html │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── newsclient │ └── NewsClientApplicationTests.java ├── pom.xml ├── producer-api ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── producerapi │ │ │ ├── ProducerApiApplication.java │ │ │ ├── config │ │ │ ├── ErrorAttributesConfig.java │ │ │ ├── SchemaRegistryConfig.java │ │ │ └── SwaggerConfig.java │ │ │ └── news │ │ │ ├── CreateNewsRequest.java │ │ │ ├── News.java │ │ │ ├── NewsController.java │ │ │ └── NewsEventEmitter.java │ └── resources │ │ ├── application.yml │ │ ├── avro │ │ └── news-event.avsc │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── producerapi │ └── ProducerApiApplicationTests.java ├── publisher-api ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── publisherapi │ │ │ ├── PublisherApiApplication.java │ │ │ ├── config │ │ │ ├── ErrorAttributesConfig.java │ │ │ ├── SpringDataWebSupportConfig.java │ │ │ └── SwaggerConfig.java │ │ │ └── news │ │ │ ├── NewsController.java │ │ │ ├── NewsRepository.java │ │ │ ├── NewsService.java │ │ │ ├── dto │ │ │ └── SearchRequest.java │ │ │ ├── exception │ │ │ └── NewsNotFoundException.java │ │ │ └── model │ │ │ └── News.java │ └── resources │ │ ├── application.yml │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── publisherapi │ └── PublisherApiApplicationTests.java ├── remove-docker-images.sh ├── scripts └── my-functions.sh ├── start-apps.sh └── stop-apps.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ivangfr 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### MAC OS ### 35 | *.DS_Store 36 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | distributionSha256Sum=4ec3f26fb1a692473aea0235c300bd20f0f9fe741947c82c1234cefd76ac3a3c 21 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = spring-cloud-stream-kafka-elasticsearch 2 | 3 | The goal of this project is to implement a "News" processing pipeline composed of five https://docs.spring.io/spring-boot/index.html[`Spring Boot`] applications: `producer-api`, `categorizer-service`, `collector-service`, `publisher-api` and `news-client`. 4 | 5 | == Proof-of-Concepts & Articles 6 | 7 | On https://ivangfr.github.io[ivangfr.github.io], I have compiled my Proof-of-Concepts (PoCs) and articles. You can easily search for the technology you are interested in by using the filter. Who knows, perhaps I have already implemented a PoC or written an article about what you are looking for. 8 | 9 | == Additional Readings 10 | 11 | * [Medium]: https://medium.com/@ivangfr/implementing-a-kafka-producer-and-consumer-using-spring-cloud-stream-d4b9a6a9eab1[**Implementing a Kafka Producer and Consumer using Spring Cloud Stream**] 12 | * [Medium]: https://medium.com/@ivangfr/implementing-unit-tests-for-a-kafka-producer-and-consumer-that-uses-spring-cloud-stream-f7a98a89fcf2[**Implementing Unit Tests for a Kafka Producer and Consumer that uses Spring Cloud Stream**] 13 | * [Medium]: https://medium.com/@ivangfr/implementing-end-to-end-testing-for-a-kafka-producer-and-consumer-that-uses-spring-cloud-stream-fbf5e666899e[**Implementing End-to-End testing for a Kafka Producer and Consumer that uses Spring Cloud Stream**] 14 | * [Medium]: https://medium.com/@ivangfr/configuring-distributed-tracing-with-zipkin-in-a-kafka-producer-and-consumer-that-uses-spring-cloud-9f1e55468b9e[**Configuring Distributed Tracing with Zipkin in a Kafka Producer and Consumer that uses Spring Cloud Stream**] 15 | * [Medium]: https://medium.com/@ivangfr/using-cloudevents-in-a-kafka-producer-and-consumer-that-uses-spring-cloud-stream-9c51670b5566[**Using CloudEvents in a Kafka Producer and Consumer that uses Spring Cloud Stream**] 16 | 17 | == Technologies used 18 | 19 | * https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/[`Spring Cloud Stream`] to build highly scalable event-driven applications connected with shared messaging systems; 20 | * https://docs.spring.io/spring-cloud-schema-registry/docs/current/reference/html/spring-cloud-schema-registry.html[`Spring Cloud Schema Registry`] that supports schema evolution so that the data can be evolved over time; besides, it lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format; 21 | * https://docs.spring.io/spring-data/elasticsearch/reference/[`Spring Data Elasticsearch`] to persist data in https://www.elastic.co/elasticsearch[`Elasticsearch`]; 22 | * https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/[`Spring Cloud OpenFeign`] to write web service clients easily; 23 | * https://www.thymeleaf.org/[`Thymeleaf`] as HTML template; 24 | * https://zipkin.io[`Zipkin`] to visualize traces between and within applications; 25 | * https://github.com/Netflix/eureka[`Eureka`] as service registration and discovery. 26 | 27 | NOTE: In https://github.com/ivangfr/docker-swarm-environment[`docker-swarm-environment`] repository, it is shown how to deploy this project into a cluster of Docker Engines in swarm mode. 28 | 29 | == Project Architecture 30 | 31 | image::documentation/project-diagram.jpeg[] 32 | 33 | == Applications 34 | 35 | * *producer-api* 36 | + 37 | `Spring Boot` Web Java application that creates news and pushes news events to `producer.news` topic in `Kafka`. 38 | 39 | * *categorizer-service* 40 | + 41 | `Spring Boot` Web Java application that listens to news events in `producer.news` topic in `Kafka`, categorizes and pushes them to `categorizer.news` topic. 42 | 43 | * *collector-service* 44 | + 45 | `Spring Boot` Web Java application that listens for news events in `categorizer.news` topic in `Kafka`, saves them in `Elasticsearch` and pushes the news events to `collector.news` topic. 46 | 47 | * *publisher-api* 48 | + 49 | `Spring Boot` Web Java application that reads directly from `Elasticsearch` and exposes a REST API. It doesn't listen from `Kafka`. 50 | 51 | * *news-client* 52 | + 53 | `Spring Boot` Web java application that provides a User Interface to see the news. It implements a `Websocket` that consumes news events from the topic `collector.news`. So, news are updated on the fly on the main page. Besides, `news-client` communicates directly with `publisher-api` whenever search for a specific news or news update are needed. 54 | + 55 | The `Websocket` operation is shown in the short gif below. News is created in `producer-api` and, immediately, it is shown in `news-client`. 56 | + 57 | image::documentation/websocket-operation.gif[] 58 | 59 | == Prerequisites 60 | 61 | * https://www.oracle.com/java/technologies/downloads/#java21[`Java 21+`] 62 | * Some containerization tool https://www.docker.com[`Docker`], https://podman.io[`Podman`], etc. 63 | 64 | == Generate NewsEvent 65 | 66 | * Open a terminal and navigate to the `spring-cloud-stream-kafka-elasticsearch` root folder; 67 | 68 | * Run the following command to generate `NewsEvent`: 69 | + 70 | [source] 71 | ---- 72 | ./mvnw clean install --projects commons-news 73 | ---- 74 | + 75 | It will install `commons-news-1.0.0.jar` in you local `Maven` repository, so that it can be visible by all services. 76 | 77 | == Start Environment 78 | 79 | * In a terminal, navigate to the `spring-cloud-stream-kafka-elasticsearch` root folder, and run: 80 | + 81 | [source] 82 | ---- 83 | docker compose up -d 84 | ---- 85 | 86 | * Wait for Docker containers to be up and running. To check it, run: 87 | + 88 | [source] 89 | ---- 90 | docker ps -a 91 | ---- 92 | 93 | == Running Applications with Maven 94 | 95 | Inside the `spring-cloud-stream-kafka-elasticsearch` root folder, run the following `Maven` commands in different terminals: 96 | 97 | * *eureka-server* 98 | + 99 | [source] 100 | ---- 101 | ./mvnw clean spring-boot:run --projects eureka-server 102 | ---- 103 | 104 | * *producer-api* 105 | + 106 | [source] 107 | ---- 108 | ./mvnw clean spring-boot:run --projects producer-api -Dspring-boot.run.jvmArguments="-Dserver.port=9080" 109 | ---- 110 | 111 | * *categorizer-service* 112 | + 113 | [source] 114 | ---- 115 | ./mvnw clean spring-boot:run --projects categorizer-service -Dspring-boot.run.jvmArguments="-Dserver.port=9081" 116 | ---- 117 | 118 | * *collector-service* 119 | + 120 | [source] 121 | ---- 122 | ./mvnw clean spring-boot:run --projects collector-service -Dspring-boot.run.jvmArguments="-Dserver.port=9082" 123 | ---- 124 | 125 | * *publisher-api* 126 | + 127 | [source] 128 | ---- 129 | ./mvnw clean spring-boot:run --projects publisher-api -Dspring-boot.run.jvmArguments="-Dserver.port=9083" 130 | ---- 131 | 132 | * *news-client* 133 | + 134 | [source] 135 | ---- 136 | ./mvnw clean spring-boot:run --projects news-client 137 | ---- 138 | 139 | == Running Applications as Docker containers 140 | 141 | === Build Application's Docker Image 142 | 143 | * In a terminal, make sure you are in the `spring-cloud-stream-kafka-elasticsearch` root folder; 144 | 145 | * In order to build the application's docker images, run the following script: 146 | + 147 | [source] 148 | ---- 149 | ./build-docker-images.sh 150 | ---- 151 | 152 | === Application's Environment Variables 153 | 154 | * *producer-api* 155 | + 156 | |=== 157 | |Environment Variable | Description 158 | 159 | |`KAFKA_HOST` 160 | |Specify host of the `Kafka` message broker to use (default `localhost`) 161 | 162 | |`KAFKA_PORT` 163 | |Specify port of the `Kafka` message broker to use (default `29092`) 164 | 165 | |`SCHEMA_REGISTRY_HOST` 166 | |Specify host of the `Schema Registry` to use (default `localhost`) 167 | 168 | |`SCHEMA_REGISTRY_PORT` 169 | |Specify port of the `Schema Registry` to use (default `8081`) 170 | 171 | |`EUREKA_HOST` 172 | |Specify host of the `Eureka` service discovery to use (default `localhost`) 173 | 174 | |`EUREKA_PORT` 175 | |Specify port of the `Eureka` service discovery to use (default `8761`) 176 | 177 | |`ZIPKIN_HOST` 178 | |Specify host of the `Zipkin` distributed tracing system to use (default `localhost`) 179 | 180 | |`ZIPKIN_PORT` 181 | |Specify port of the `Zipkin` distributed tracing system to use (default `9411`) 182 | 183 | |=== 184 | 185 | * *categorizer-service* 186 | + 187 | |=== 188 | |Environment Variable | Description 189 | 190 | |`KAFKA_HOST` 191 | |Specify host of the `Kafka` message broker to use (default `localhost`) 192 | 193 | |`KAFKA_PORT` 194 | |Specify port of the `Kafka` message broker to use (default `29092`) 195 | 196 | |`SCHEMA_REGISTRY_HOST` 197 | |Specify host of the `Schema Registry` to use (default `localhost`) 198 | 199 | |`SCHEMA_REGISTRY_PORT` 200 | |Specify port of the `Schema Registry` to use (default `8081`) 201 | 202 | |`EUREKA_HOST` 203 | |Specify host of the `Eureka` service discovery to use (default `localhost`) 204 | 205 | |`EUREKA_PORT` 206 | |Specify port of the `Eureka` service discovery to use (default `8761`) 207 | 208 | |`ZIPKIN_HOST` 209 | |Specify host of the `Zipkin` distributed tracing system to use (default `localhost`) 210 | 211 | |`ZIPKIN_PORT` 212 | |Specify port of the `Zipkin` distributed tracing system to use (default `9411`) 213 | 214 | |=== 215 | 216 | * *collector-service* 217 | + 218 | |=== 219 | |Environment Variable | Description 220 | 221 | |`ELASTICSEARCH_HOST` 222 | |Specify host of the `Elasticsearch` search engine to use (default `localhost`) 223 | 224 | |`ELASTICSEARCH_NODES_PORT` 225 | |Specify nodes port of the `Elasticsearch` search engine to use (default `9300`) 226 | 227 | |`ELASTICSEARCH_REST_PORT` 228 | |Specify rest port of the `Elasticsearch` search engine to use (default `9200`) 229 | 230 | |`KAFKA_HOST` 231 | |Specify host of the `Kafka` message broker to use (default `localhost`) 232 | 233 | |`KAFKA_PORT` 234 | |Specify port of the `Kafka` message broker to use (default `29092`) 235 | 236 | |`SCHEMA_REGISTRY_HOST` 237 | |Specify host of the `Schema Registry` to use (default `localhost`) 238 | 239 | |`SCHEMA_REGISTRY_PORT` 240 | |Specify port of the `Schema Registry` to use (default `8081`) 241 | 242 | |`EUREKA_HOST` 243 | |Specify host of the `Eureka` service discovery to use (default `localhost`) 244 | 245 | |`EUREKA_PORT` 246 | |Specify port of the `Eureka` service discovery to use (default `8761`) 247 | 248 | |`ZIPKIN_HOST` 249 | |Specify host of the `Zipkin` distributed tracing system to use (default `localhost`) 250 | 251 | |`ZIPKIN_PORT` 252 | |Specify port of the `Zipkin` distributed tracing system to use (default `9411`) 253 | 254 | |=== 255 | 256 | * *publisher-api* 257 | + 258 | |=== 259 | |Environment Variable | Description 260 | 261 | |`ELASTICSEARCH_HOST` 262 | |Specify host of the `Elasticsearch` search engine to use (default `localhost`) 263 | 264 | |`ELASTICSEARCH_NODES_PORT` 265 | |Specify nodes port of the `Elasticsearch` search engine to use (default `9300`) 266 | 267 | |`ELASTICSEARCH_REST_PORT` 268 | |Specify rest port of the `Elasticsearch` search engine to use (default `9200`) 269 | 270 | |`EUREKA_HOST` 271 | |Specify host of the `Eureka` service discovery to use (default `localhost`) 272 | 273 | |`EUREKA_PORT` 274 | |Specify port of the `Eureka` service discovery to use (default `8761`) 275 | 276 | |`ZIPKIN_HOST` 277 | |Specify host of the `Zipkin` distributed tracing system to use (default `localhost`) 278 | 279 | |`ZIPKIN_PORT` 280 | |Specify port of the `Zipkin` distributed tracing system to use (default `9411`) 281 | 282 | |=== 283 | 284 | * *news-client* 285 | + 286 | |=== 287 | |Environment Variable | Description 288 | 289 | |`KAFKA_HOST` 290 | |Specify host of the `Kafka` message broker to use (default `localhost`) 291 | 292 | |`KAFKA_PORT` 293 | |Specify port of the `Kafka` message broker to use (default `29092`) 294 | 295 | |`SCHEMA_REGISTRY_HOST` 296 | |Specify host of the `Schema Registry` to use (default `localhost`) 297 | 298 | |`SCHEMA_REGISTRY_PORT` 299 | |Specify port of the `Schema Registry` to use (default `8081`) 300 | 301 | |`EUREKA_HOST` 302 | |Specify host of the `Eureka` service discovery to use (default `localhost`) 303 | 304 | |`EUREKA_PORT` 305 | |Specify port of the `Eureka` service discovery to use (default `8761`) 306 | 307 | |`ZIPKIN_HOST` 308 | |Specify host of the `Zipkin` distributed tracing system to use (default `localhost`) 309 | 310 | |`ZIPKIN_PORT` 311 | |Specify port of the `Zipkin` distributed tracing system to use (default `9411`) 312 | 313 | |=== 314 | 315 | === Run Application's Docker Container 316 | 317 | * In a terminal, make sure you are inside the `spring-cloud-stream-kafka-elasticsearch` root folder; 318 | 319 | * Run following script: 320 | + 321 | [source] 322 | ---- 323 | ./start-apps.sh 324 | ---- 325 | 326 | == Applications URLs 327 | 328 | |=== 329 | |Application |URL 330 | 331 | |producer-api 332 | |http://localhost:9080/swagger-ui.html 333 | 334 | |publisher-api 335 | |http://localhost:9083/swagger-ui.html 336 | 337 | |news-client 338 | |http://localhost:8080 339 | 340 | |=== 341 | 342 | == Useful links 343 | 344 | * *Eureka* 345 | + 346 | `Eureka` can be accessed at http://localhost:8761 347 | + 348 | image::documentation/eureka.jpg[] 349 | 350 | * *Zipkin* 351 | + 352 | `Zipkin` can be accessed at http://localhost:9411 353 | + 354 | image::documentation/zipkin.jpg[] 355 | 356 | * *Kafka Topics UI* 357 | + 358 | `Kafka Topics UI` can be accessed at http://localhost:8085 359 | 360 | * *Kafka Manager* 361 | + 362 | `Kafka Manager` can be accessed at http://localhost:9001 363 | + 364 | _Configuration_ 365 | + 366 | - First, you must create a new cluster. Click on `Cluster` (dropdown button on the header) and then on `Add Cluster` 367 | - Type the name of your cluster in `Cluster Name` field, for example: `MyCluster` 368 | - Type `zookeeper:2181` in `Cluster Zookeeper Hosts` field 369 | - Enable checkbox `Poll consumer information (Not recommended for large # of consumers if ZK is used for offsets tracking on older Kafka versions)` 370 | - Click on `Save` button at the bottom of the page. 371 | 372 | * *Schema Registry UI* 373 | + 374 | `Schema Registry UI` can be accessed at http://localhost:8001 375 | 376 | * *Elasticsearch REST API* 377 | + 378 | Check ES is up and running 379 | + 380 | [source] 381 | ---- 382 | curl localhost:9200 383 | ---- 384 | + 385 | Check indexes 386 | + 387 | [source] 388 | ---- 389 | curl "localhost:9200/_cat/indices?v" 390 | ---- 391 | + 392 | Check _news_ index mapping 393 | + 394 | [source] 395 | ---- 396 | curl "localhost:9200/news/_mapping?pretty" 397 | ---- 398 | + 399 | Simple search 400 | + 401 | [source] 402 | ---- 403 | curl "localhost:9200/news/_search?pretty" 404 | ---- 405 | + 406 | Delete _news_ index 407 | + 408 | [source] 409 | ---- 410 | curl -X DELETE localhost:9200/news 411 | ---- 412 | 413 | == Shutdown 414 | 415 | * To stop applications: 416 | ** If they were started with `Maven`, go to the terminals where they are running and press `Ctrl+C`; 417 | ** If they were started as Docker containers, in a terminal and inside the `spring-cloud-stream-kafka-elasticsearch` root folder, run the script below: 418 | + 419 | [source] 420 | ---- 421 | ./stop-apps.sh 422 | ---- 423 | 424 | * To stop and remove docker compose containers, network and volumes, in a terminal, navigate to the `spring-cloud-stream-kafka-elasticsearch` root folder, and run the following command: 425 | + 426 | [source] 427 | ---- 428 | docker compose down -v 429 | ---- 430 | 431 | == Cleanup 432 | 433 | To remove the Docker images created by this project, in a terminal and inside the `spring-cloud-stream-kafka-elasticsearch` root folder, run the script below: 434 | [source] 435 | ---- 436 | ./remove-docker-images.sh 437 | ---- -------------------------------------------------------------------------------- /build-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DOCKER_IMAGE_PREFIX="ivanfranchin" 4 | APP_VERSION="1.0.0" 5 | 6 | EUREKA_SERVER_APP_NAME="eureka-server" 7 | PRODUCER_API_APP_NAME="producer-api" 8 | CATEGORIZER_SERVICE_APP_NAME="categorizer-service" 9 | COLLECTOR_SERVICE_APP_NAME="collector-service" 10 | PUBLISHER_API_APP_NAME="publisher-api" 11 | NEWS_CLIENT_APP_NAME="news-client" 12 | 13 | EUREKA_SERVER_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${EUREKA_SERVER_APP_NAME}:${APP_VERSION}" 14 | PRODUCER_API_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${PRODUCER_API_APP_NAME}:${APP_VERSION}" 15 | CATEGORIZER_SERVICE_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${CATEGORIZER_SERVICE_APP_NAME}:${APP_VERSION}" 16 | COLLECTOR_SERVICE_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${COLLECTOR_SERVICE_APP_NAME}:${APP_VERSION}" 17 | PUBLISHER_API_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${PUBLISHER_API_APP_NAME}:${APP_VERSION}" 18 | NEWS_CLIENT_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${NEWS_CLIENT_APP_NAME}:${APP_VERSION}" 19 | 20 | SKIP_TESTS="true" 21 | 22 | ./mvnw clean compile jib:dockerBuild \ 23 | --projects "$EUREKA_SERVER_APP_NAME" \ 24 | -DskipTests="$SKIP_TESTS" \ 25 | -Dimage="$EUREKA_SERVER_DOCKER_IMAGE_NAME" 26 | 27 | ./mvnw clean compile jib:dockerBuild \ 28 | --projects "$PRODUCER_API_APP_NAME" \ 29 | -DskipTests="$SKIP_TESTS" \ 30 | -Dimage="$PRODUCER_API_DOCKER_IMAGE_NAME" 31 | 32 | ./mvnw clean compile jib:dockerBuild \ 33 | --projects "$CATEGORIZER_SERVICE_APP_NAME" \ 34 | -DskipTests="$SKIP_TESTS" \ 35 | -Dimage="$CATEGORIZER_SERVICE_DOCKER_IMAGE_NAME" 36 | 37 | ./mvnw clean compile jib:dockerBuild \ 38 | --projects "$COLLECTOR_SERVICE_APP_NAME" \ 39 | -DskipTests="$SKIP_TESTS" \ 40 | -Dimage="$COLLECTOR_SERVICE_DOCKER_IMAGE_NAME" 41 | 42 | ./mvnw clean compile jib:dockerBuild \ 43 | --projects "$PUBLISHER_API_APP_NAME" \ 44 | -DskipTests="$SKIP_TESTS" \ 45 | -Dimage="$PUBLISHER_API_DOCKER_IMAGE_NAME" 46 | 47 | ./mvnw clean compile jib:dockerBuild \ 48 | --projects "$NEWS_CLIENT_APP_NAME" \ 49 | -DskipTests="$SKIP_TESTS" \ 50 | -Dimage="$NEWS_CLIENT_DOCKER_IMAGE_NAME" 51 | -------------------------------------------------------------------------------- /categorizer-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | spring-cloud-stream-kafka-elasticsearch 8 | 1.0.0 9 | ../pom.xml 10 | 11 | categorizer-service 12 | categorizer-service 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 1.1.5 29 | 1.0.0 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-actuator 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-web 39 | 40 | 41 | io.micrometer 42 | micrometer-tracing-bridge-brave 43 | 44 | 45 | io.zipkin.reporter2 46 | zipkin-reporter-brave 47 | 48 | 49 | org.springframework.cloud 50 | spring-cloud-starter-netflix-eureka-client 51 | 52 | 53 | org.springframework.cloud 54 | spring-cloud-stream-binder-kafka 55 | 56 | 57 | org.springframework.kafka 58 | spring-kafka 59 | 60 | 61 | 62 | 63 | org.springframework.cloud 64 | spring-cloud-schema-registry-client 65 | ${spring-cloud-schema-registry-client.version} 66 | 67 | 68 | 69 | 70 | com.ivanfranchin 71 | commons-news 72 | ${ivanfranchin-commons-news.version} 73 | 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | true 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-test 83 | test 84 | 85 | 86 | org.springframework.cloud 87 | spring-cloud-stream-test-binder 88 | test 89 | 90 | 91 | org.springframework.kafka 92 | spring-kafka-test 93 | test 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-compiler-plugin 102 | 103 | 104 | 105 | org.projectlombok 106 | lombok 107 | 108 | 109 | 110 | 111 | 112 | org.springframework.boot 113 | spring-boot-maven-plugin 114 | 115 | 116 | 117 | org.projectlombok 118 | lombok 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /categorizer-service/src/main/java/com/ivanfranchin/categorizerservice/CategorizerServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.categorizerservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @EnableDiscoveryClient 8 | @SpringBootApplication 9 | public class CategorizerServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(CategorizerServiceApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /categorizer-service/src/main/java/com/ivanfranchin/categorizerservice/config/SchemaRegistryConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.categorizerservice.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.cloud.schema.registry.avro.AvroSchemaMessageConverter; 5 | import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl; 6 | import org.springframework.cloud.schema.registry.client.ConfluentSchemaRegistryClient; 7 | import org.springframework.cloud.schema.registry.client.SchemaRegistryClient; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.messaging.converter.MessageConverter; 11 | import org.springframework.util.MimeType; 12 | 13 | @Configuration 14 | public class SchemaRegistryConfig { 15 | 16 | @Bean 17 | SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.schema-registry-client.endpoint}") String endpoint) { 18 | ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(); 19 | client.setEndpoint(endpoint); 20 | return client; 21 | } 22 | 23 | @Bean 24 | MessageConverter avroSchemaMessageConverter() { 25 | return new AvroSchemaMessageConverter(MimeType.valueOf("application/*+avro"), new AvroSchemaServiceManagerImpl()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /categorizer-service/src/main/java/com/ivanfranchin/categorizerservice/news/CategoryService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.categorizerservice.news; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.security.SecureRandom; 6 | import java.util.Random; 7 | 8 | @Service 9 | public class CategoryService { 10 | 11 | private final String[] categories = new String[]{"Sport", "World", "Science", "Entertainment", "Health"}; 12 | private final Random random = new SecureRandom(); 13 | 14 | public String categorize(String title, String text) { 15 | return categories[random.nextInt(categories.length)]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /categorizer-service/src/main/java/com/ivanfranchin/categorizerservice/news/NewsEventProcessor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.categorizerservice.news; 2 | 3 | import com.ivanfranchin.commonsnews.avro.NewsEvent; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.integration.IntegrationMessageHeaderAccessor; 8 | import org.springframework.kafka.support.KafkaHeaders; 9 | import org.springframework.messaging.Message; 10 | import org.springframework.messaging.MessageHeaders; 11 | import org.springframework.messaging.support.MessageBuilder; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | import java.util.function.Function; 16 | 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | @Component 20 | public class NewsEventProcessor { 21 | 22 | private final CategoryService categoryService; 23 | 24 | @Bean 25 | Function, Message> news() { 26 | return message -> { 27 | NewsEvent newsEvent = message.getPayload(); 28 | MessageHeaders messageHeaders = message.getHeaders(); 29 | log.info("NewsEvent with id '{}' and title '{}' received from bus. topic: {}, partition: {}, offset: {}, deliveryAttempt: {}", 30 | newsEvent.getId(), 31 | newsEvent.getTitle(), 32 | messageHeaders.get(KafkaHeaders.RECEIVED_TOPIC, String.class), 33 | messageHeaders.get(KafkaHeaders.RECEIVED_PARTITION, Integer.class), 34 | messageHeaders.get(KafkaHeaders.OFFSET, Long.class), 35 | messageHeaders.get(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, AtomicInteger.class)); 36 | 37 | String category = categoryService.categorize(newsEvent.getTitle().toString(), newsEvent.getText().toString()); 38 | newsEvent.setCategory(category); 39 | 40 | return MessageBuilder.withPayload(newsEvent) 41 | .setHeader("partitionKey", newsEvent.getId().toString()) 42 | .build(); 43 | }; 44 | } 45 | } -------------------------------------------------------------------------------- /categorizer-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: categorizer-service 4 | cloud: 5 | schema-registry-client: 6 | endpoint: http://${SCHEMA_REGISTRY_HOST:localhost}:${SCHEMA_REGISTRY_PORT:8081} 7 | stream: 8 | kafka: 9 | binder: 10 | brokers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:29092} 11 | auto-add-partitions: true 12 | min-partition-count: 2 13 | enable-observation: true 14 | bindings: 15 | news-in-0: 16 | destination: com.ivanfranchin.newspipeline.producer.news 17 | content-type: application/*+avro 18 | group: categorigerGroup 19 | consumer: 20 | max-attempts: 4 21 | back-off-initial-interval: 10000 22 | news-out-0: 23 | destination: com.ivanfranchin.newspipeline.categorizer.news 24 | content-type: application/*+avro 25 | producer: 26 | partition-key-expression: headers['partitionKey'] 27 | partition-count: 2 28 | main: 29 | allow-bean-definition-overriding: true 30 | 31 | management: 32 | endpoints: 33 | web: 34 | exposure.include: beans, env, health, metrics, mappings 35 | endpoint: 36 | health: 37 | show-details: always 38 | tracing: 39 | sampling: 40 | probability: 1.0 41 | zipkin: 42 | tracing: 43 | endpoint: http://${ZIPKIN_HOST:localhost}:${ZIPKIN_PORT:9411}/api/v2/spans 44 | 45 | eureka: 46 | client: 47 | serviceUrl: 48 | defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka 49 | instance: 50 | preferIpAddress: true 51 | -------------------------------------------------------------------------------- /categorizer-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | ___ __ _| |_ ___ __ _ ___ _ __(_)_______ _ __ ___ ___ _ ____ _(_) ___ ___ 3 | / __/ _` | __/ _ \/ _` |/ _ \| '__| |_ / _ \ '__|____/ __|/ _ \ '__\ \ / / |/ __/ _ \ 4 | | (_| (_| | || __/ (_| | (_) | | | |/ / __/ | |_____\__ \ __/ | \ V /| | (_| __/ 5 | \___\__,_|\__\___|\__, |\___/|_| |_/___\___|_| |___/\___|_| \_/ |_|\___\___| 6 | |___/ 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /categorizer-service/src/test/java/com/ivanfranchin/categorizerservice/CategorizerServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.categorizerservice; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class CategorizerServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /collector-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | spring-cloud-stream-kafka-elasticsearch 8 | 1.0.0 9 | ../pom.xml 10 | 11 | collector-service 12 | collector-service 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 1.1.5 29 | 1.0.0 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-actuator 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-elasticsearch 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | io.micrometer 46 | micrometer-tracing-bridge-brave 47 | 48 | 49 | io.zipkin.reporter2 50 | zipkin-reporter-brave 51 | 52 | 53 | org.springframework.cloud 54 | spring-cloud-starter-netflix-eureka-client 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-stream-binder-kafka 59 | 60 | 61 | org.springframework.kafka 62 | spring-kafka 63 | 64 | 65 | 66 | 67 | org.springframework.cloud 68 | spring-cloud-schema-registry-client 69 | ${spring-cloud-schema-registry-client.version} 70 | 71 | 72 | 73 | 74 | com.ivanfranchin 75 | commons-news 76 | ${ivanfranchin-commons-news.version} 77 | 78 | 79 | 80 | org.projectlombok 81 | lombok 82 | true 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-starter-test 87 | test 88 | 89 | 90 | org.springframework.cloud 91 | spring-cloud-stream-test-binder 92 | test 93 | 94 | 95 | org.springframework.kafka 96 | spring-kafka-test 97 | test 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-compiler-plugin 106 | 107 | 108 | 109 | org.projectlombok 110 | lombok 111 | 112 | 113 | 114 | 115 | 116 | org.springframework.boot 117 | spring-boot-maven-plugin 118 | 119 | 120 | 121 | org.projectlombok 122 | lombok 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /collector-service/src/main/java/com/ivanfranchin/collectorservice/CollectorServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.collectorservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @EnableDiscoveryClient 8 | @SpringBootApplication 9 | public class CollectorServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(CollectorServiceApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /collector-service/src/main/java/com/ivanfranchin/collectorservice/config/SchemaRegistryConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.collectorservice.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.cloud.schema.registry.avro.AvroSchemaMessageConverter; 5 | import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl; 6 | import org.springframework.cloud.schema.registry.client.ConfluentSchemaRegistryClient; 7 | import org.springframework.cloud.schema.registry.client.SchemaRegistryClient; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.messaging.converter.MessageConverter; 11 | import org.springframework.util.MimeType; 12 | 13 | @Configuration 14 | public class SchemaRegistryConfig { 15 | 16 | @Bean 17 | SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.schema-registry-client.endpoint}") String endpoint) { 18 | ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(); 19 | client.setEndpoint(endpoint); 20 | return client; 21 | } 22 | 23 | @Bean 24 | MessageConverter avroSchemaMessageConverter() { 25 | return new AvroSchemaMessageConverter(MimeType.valueOf("application/*+avro"), new AvroSchemaServiceManagerImpl()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /collector-service/src/main/java/com/ivanfranchin/collectorservice/news/News.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.collectorservice.news; 2 | 3 | import com.ivanfranchin.commonsnews.avro.NewsEvent; 4 | import lombok.Data; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.elasticsearch.annotations.Document; 7 | import org.springframework.data.elasticsearch.annotations.Field; 8 | import org.springframework.data.elasticsearch.annotations.FieldType; 9 | import org.springframework.data.elasticsearch.annotations.Setting; 10 | 11 | @Data 12 | @Setting(settingPath = "news-es-analysis.json") 13 | @Document(indexName = "news") 14 | public class News { 15 | 16 | @Id 17 | private String id; 18 | 19 | @Field(type = FieldType.Text, analyzer = "my_analyzer", searchAnalyzer = "my_search_analyzer") 20 | private String title; 21 | 22 | @Field(type = FieldType.Text, analyzer = "my_analyzer", searchAnalyzer = "my_search_analyzer") 23 | private String text; 24 | 25 | @Field(type = FieldType.Text, analyzer = "my_analyzer", searchAnalyzer = "my_search_analyzer") 26 | private String category; 27 | 28 | @Field(type = FieldType.Date) 29 | private String datetime; 30 | 31 | public static News from(NewsEvent newsEvent) { 32 | News news = new News(); 33 | news.setId(newsEvent.getId().toString()); 34 | news.setTitle(newsEvent.getTitle().toString()); 35 | news.setText(newsEvent.getText().toString()); 36 | news.setCategory(newsEvent.getCategory().toString()); 37 | news.setDatetime(newsEvent.getDatetime().toString()); 38 | return news; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /collector-service/src/main/java/com/ivanfranchin/collectorservice/news/NewsEventProcessor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.collectorservice.news; 2 | 3 | import com.ivanfranchin.commonsnews.avro.NewsEvent; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.integration.IntegrationMessageHeaderAccessor; 8 | import org.springframework.kafka.support.KafkaHeaders; 9 | import org.springframework.messaging.Message; 10 | import org.springframework.messaging.MessageHeaders; 11 | import org.springframework.messaging.support.MessageBuilder; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | import java.util.function.Function; 16 | 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | @Component 20 | public class NewsEventProcessor { 21 | 22 | private final NewsRepository newsRepository; 23 | 24 | @Bean 25 | Function, Message> news() { 26 | return message -> { 27 | NewsEvent newsEvent = message.getPayload(); 28 | MessageHeaders messageHeaders = message.getHeaders(); 29 | log.info("NewsEvent with id '{}' and title '{}' received from bus. topic: {}, partition: {}, offset: {}, deliveryAttempt: {}", 30 | newsEvent.getId(), 31 | newsEvent.getTitle(), 32 | messageHeaders.get(KafkaHeaders.RECEIVED_TOPIC, String.class), 33 | messageHeaders.get(KafkaHeaders.RECEIVED_PARTITION, Integer.class), 34 | messageHeaders.get(KafkaHeaders.OFFSET, Long.class), 35 | messageHeaders.get(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, AtomicInteger.class)); 36 | 37 | News news = News.from(newsEvent); 38 | news = newsRepository.save(news); 39 | log.info("News with id {} saved in Elasticsearch.", news.getId()); 40 | 41 | return MessageBuilder.withPayload(newsEvent) 42 | .setHeader("partitionKey", newsEvent.getId().toString()) 43 | .build(); 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /collector-service/src/main/java/com/ivanfranchin/collectorservice/news/NewsRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.collectorservice.news; 2 | 3 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface NewsRepository extends ElasticsearchRepository { 8 | } 9 | -------------------------------------------------------------------------------- /collector-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: collector-service 4 | cloud: 5 | schema-registry-client: 6 | endpoint: http://${SCHEMA_REGISTRY_HOST:localhost}:${SCHEMA_REGISTRY_PORT:8081} 7 | stream: 8 | kafka: 9 | binder: 10 | brokers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:29092} 11 | auto-add-partitions: true 12 | min-partition-count: 2 13 | enable-observation: true 14 | bindings: 15 | news-in-0: 16 | destination: com.ivanfranchin.newspipeline.categorizer.news 17 | content-type: application/*+avro 18 | group: collectorGroup 19 | consumer: 20 | max-attempts: 4 21 | back-off-initial-interval: 10000 22 | news-out-0: 23 | destination: com.ivanfranchin.newspipeline.collector.news 24 | content-type: application/*+avro 25 | producer: 26 | partition-key-expression: headers['partitionKey'] 27 | partition-count: 2 28 | elasticsearch: 29 | uris: http://${ELASTICSEARCH_HOST:localhost}:${ELASTICSEARCH_REST_PORT:9200} 30 | main: 31 | allow-bean-definition-overriding: true 32 | 33 | management: 34 | endpoints: 35 | web: 36 | exposure.include: beans, env, health, metrics, mappings 37 | endpoint: 38 | health: 39 | show-details: always 40 | tracing: 41 | sampling: 42 | probability: 1.0 43 | zipkin: 44 | tracing: 45 | endpoint: http://${ZIPKIN_HOST:localhost}:${ZIPKIN_PORT:9411}/api/v2/spans 46 | 47 | eureka: 48 | client: 49 | serviceUrl: 50 | defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka 51 | instance: 52 | preferIpAddress: true 53 | -------------------------------------------------------------------------------- /collector-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ _ 2 | ___ ___ | | | ___ ___| |_ ___ _ __ ___ ___ _ ____ _(_) ___ ___ 3 | / __/ _ \| | |/ _ \/ __| __/ _ \| '__|____/ __|/ _ \ '__\ \ / / |/ __/ _ \ 4 | | (_| (_) | | | __/ (__| || (_) | | |_____\__ \ __/ | \ V /| | (_| __/ 5 | \___\___/|_|_|\___|\___|\__\___/|_| |___/\___|_| \_/ |_|\___\___| 6 | :: Spring Boot :: ${spring-boot.formatted-version} 7 | -------------------------------------------------------------------------------- /collector-service/src/main/resources/news-es-analysis.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysis": { 3 | "analyzer": { 4 | "my_analyzer": { 5 | "type": "custom", 6 | "tokenizer": "standard", 7 | "filter": [ 8 | "lowercase", 9 | "edge_ngram_filter" 10 | ] 11 | }, 12 | "my_search_analyzer": { 13 | "type": "custom", 14 | "tokenizer": "standard", 15 | "filter": [ 16 | "lowercase" 17 | ] 18 | } 19 | }, 20 | "filter": { 21 | "edge_ngram_filter": { 22 | "type": "edge_ngram", 23 | "min_gram": 1, 24 | "max_gram": 20 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /collector-service/src/test/java/com/ivanfranchin/collectorservice/CollectorServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.collectorservice; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class CollectorServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /commons-news/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.ivanfranchin 6 | commons-news 7 | 1.0.0 8 | jar 9 | 10 | UTF-8 11 | 21 12 | 21 13 | 1.12.0 14 | 15 | 16 | 17 | org.apache.avro 18 | avro 19 | ${avro.version} 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.apache.avro 27 | avro-maven-plugin 28 | ${avro.version} 29 | 30 | 31 | generate-sources 32 | 33 | schema 34 | 35 | 36 | ${project.basedir}/src/main/resources/avro 37 | ${project.basedir}/src/main/java 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /commons-news/src/main/java/com/ivanfranchin/commonsnews/avro/NewsEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Autogenerated by Avro 3 | * 4 | * DO NOT EDIT DIRECTLY 5 | */ 6 | package com.ivanfranchin.commonsnews.avro; 7 | 8 | import org.apache.avro.generic.GenericArray; 9 | import org.apache.avro.specific.SpecificData; 10 | import org.apache.avro.util.Utf8; 11 | import org.apache.avro.message.BinaryMessageEncoder; 12 | import org.apache.avro.message.BinaryMessageDecoder; 13 | import org.apache.avro.message.SchemaStore; 14 | 15 | @org.apache.avro.specific.AvroGenerated 16 | public class NewsEvent extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { 17 | private static final long serialVersionUID = -5013785730745098233L; 18 | 19 | 20 | public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"NewsEvent\",\"namespace\":\"com.ivanfranchin.commonsnews.avro\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"title\",\"type\":\"string\"},{\"name\":\"text\",\"type\":\"string\"},{\"name\":\"datetime\",\"type\":\"string\"},{\"name\":\"category\",\"type\":[\"null\",\"string\"],\"default\":null}]}"); 21 | public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } 22 | 23 | private static final SpecificData MODEL$ = new SpecificData(); 24 | 25 | private static final BinaryMessageEncoder ENCODER = 26 | new BinaryMessageEncoder<>(MODEL$, SCHEMA$); 27 | 28 | private static final BinaryMessageDecoder DECODER = 29 | new BinaryMessageDecoder<>(MODEL$, SCHEMA$); 30 | 31 | /** 32 | * Return the BinaryMessageEncoder instance used by this class. 33 | * @return the message encoder used by this class 34 | */ 35 | public static BinaryMessageEncoder getEncoder() { 36 | return ENCODER; 37 | } 38 | 39 | /** 40 | * Return the BinaryMessageDecoder instance used by this class. 41 | * @return the message decoder used by this class 42 | */ 43 | public static BinaryMessageDecoder getDecoder() { 44 | return DECODER; 45 | } 46 | 47 | /** 48 | * Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}. 49 | * @param resolver a {@link SchemaStore} used to find schemas by fingerprint 50 | * @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore 51 | */ 52 | public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { 53 | return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); 54 | } 55 | 56 | /** 57 | * Serializes this NewsEvent to a ByteBuffer. 58 | * @return a buffer holding the serialized data for this instance 59 | * @throws java.io.IOException if this instance could not be serialized 60 | */ 61 | public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { 62 | return ENCODER.encode(this); 63 | } 64 | 65 | /** 66 | * Deserializes a NewsEvent from a ByteBuffer. 67 | * @param b a byte buffer holding serialized data for an instance of this class 68 | * @return a NewsEvent instance decoded from the given buffer 69 | * @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class 70 | */ 71 | public static NewsEvent fromByteBuffer( 72 | java.nio.ByteBuffer b) throws java.io.IOException { 73 | return DECODER.decode(b); 74 | } 75 | 76 | private java.lang.CharSequence id; 77 | private java.lang.CharSequence title; 78 | private java.lang.CharSequence text; 79 | private java.lang.CharSequence datetime; 80 | private java.lang.CharSequence category; 81 | 82 | /** 83 | * Default constructor. Note that this does not initialize fields 84 | * to their default values from the schema. If that is desired then 85 | * one should use newBuilder(). 86 | */ 87 | public NewsEvent() {} 88 | 89 | /** 90 | * All-args constructor. 91 | * @param id The new value for id 92 | * @param title The new value for title 93 | * @param text The new value for text 94 | * @param datetime The new value for datetime 95 | * @param category The new value for category 96 | */ 97 | public NewsEvent(java.lang.CharSequence id, java.lang.CharSequence title, java.lang.CharSequence text, java.lang.CharSequence datetime, java.lang.CharSequence category) { 98 | this.id = id; 99 | this.title = title; 100 | this.text = text; 101 | this.datetime = datetime; 102 | this.category = category; 103 | } 104 | 105 | @Override 106 | public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; } 107 | 108 | @Override 109 | public org.apache.avro.Schema getSchema() { return SCHEMA$; } 110 | 111 | // Used by DatumWriter. Applications should not call. 112 | @Override 113 | public java.lang.Object get(int field$) { 114 | switch (field$) { 115 | case 0: return id; 116 | case 1: return title; 117 | case 2: return text; 118 | case 3: return datetime; 119 | case 4: return category; 120 | default: throw new IndexOutOfBoundsException("Invalid index: " + field$); 121 | } 122 | } 123 | 124 | // Used by DatumReader. Applications should not call. 125 | @Override 126 | @SuppressWarnings(value="unchecked") 127 | public void put(int field$, java.lang.Object value$) { 128 | switch (field$) { 129 | case 0: id = (java.lang.CharSequence)value$; break; 130 | case 1: title = (java.lang.CharSequence)value$; break; 131 | case 2: text = (java.lang.CharSequence)value$; break; 132 | case 3: datetime = (java.lang.CharSequence)value$; break; 133 | case 4: category = (java.lang.CharSequence)value$; break; 134 | default: throw new IndexOutOfBoundsException("Invalid index: " + field$); 135 | } 136 | } 137 | 138 | /** 139 | * Gets the value of the 'id' field. 140 | * @return The value of the 'id' field. 141 | */ 142 | public java.lang.CharSequence getId() { 143 | return id; 144 | } 145 | 146 | 147 | /** 148 | * Sets the value of the 'id' field. 149 | * @param value the value to set. 150 | */ 151 | public void setId(java.lang.CharSequence value) { 152 | this.id = value; 153 | } 154 | 155 | /** 156 | * Gets the value of the 'title' field. 157 | * @return The value of the 'title' field. 158 | */ 159 | public java.lang.CharSequence getTitle() { 160 | return title; 161 | } 162 | 163 | 164 | /** 165 | * Sets the value of the 'title' field. 166 | * @param value the value to set. 167 | */ 168 | public void setTitle(java.lang.CharSequence value) { 169 | this.title = value; 170 | } 171 | 172 | /** 173 | * Gets the value of the 'text' field. 174 | * @return The value of the 'text' field. 175 | */ 176 | public java.lang.CharSequence getText() { 177 | return text; 178 | } 179 | 180 | 181 | /** 182 | * Sets the value of the 'text' field. 183 | * @param value the value to set. 184 | */ 185 | public void setText(java.lang.CharSequence value) { 186 | this.text = value; 187 | } 188 | 189 | /** 190 | * Gets the value of the 'datetime' field. 191 | * @return The value of the 'datetime' field. 192 | */ 193 | public java.lang.CharSequence getDatetime() { 194 | return datetime; 195 | } 196 | 197 | 198 | /** 199 | * Sets the value of the 'datetime' field. 200 | * @param value the value to set. 201 | */ 202 | public void setDatetime(java.lang.CharSequence value) { 203 | this.datetime = value; 204 | } 205 | 206 | /** 207 | * Gets the value of the 'category' field. 208 | * @return The value of the 'category' field. 209 | */ 210 | public java.lang.CharSequence getCategory() { 211 | return category; 212 | } 213 | 214 | 215 | /** 216 | * Sets the value of the 'category' field. 217 | * @param value the value to set. 218 | */ 219 | public void setCategory(java.lang.CharSequence value) { 220 | this.category = value; 221 | } 222 | 223 | /** 224 | * Creates a new NewsEvent RecordBuilder. 225 | * @return A new NewsEvent RecordBuilder 226 | */ 227 | public static com.ivanfranchin.commonsnews.avro.NewsEvent.Builder newBuilder() { 228 | return new com.ivanfranchin.commonsnews.avro.NewsEvent.Builder(); 229 | } 230 | 231 | /** 232 | * Creates a new NewsEvent RecordBuilder by copying an existing Builder. 233 | * @param other The existing builder to copy. 234 | * @return A new NewsEvent RecordBuilder 235 | */ 236 | public static com.ivanfranchin.commonsnews.avro.NewsEvent.Builder newBuilder(com.ivanfranchin.commonsnews.avro.NewsEvent.Builder other) { 237 | if (other == null) { 238 | return new com.ivanfranchin.commonsnews.avro.NewsEvent.Builder(); 239 | } else { 240 | return new com.ivanfranchin.commonsnews.avro.NewsEvent.Builder(other); 241 | } 242 | } 243 | 244 | /** 245 | * Creates a new NewsEvent RecordBuilder by copying an existing NewsEvent instance. 246 | * @param other The existing instance to copy. 247 | * @return A new NewsEvent RecordBuilder 248 | */ 249 | public static com.ivanfranchin.commonsnews.avro.NewsEvent.Builder newBuilder(com.ivanfranchin.commonsnews.avro.NewsEvent other) { 250 | if (other == null) { 251 | return new com.ivanfranchin.commonsnews.avro.NewsEvent.Builder(); 252 | } else { 253 | return new com.ivanfranchin.commonsnews.avro.NewsEvent.Builder(other); 254 | } 255 | } 256 | 257 | /** 258 | * RecordBuilder for NewsEvent instances. 259 | */ 260 | @org.apache.avro.specific.AvroGenerated 261 | public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase 262 | implements org.apache.avro.data.RecordBuilder { 263 | 264 | private java.lang.CharSequence id; 265 | private java.lang.CharSequence title; 266 | private java.lang.CharSequence text; 267 | private java.lang.CharSequence datetime; 268 | private java.lang.CharSequence category; 269 | 270 | /** Creates a new Builder */ 271 | private Builder() { 272 | super(SCHEMA$, MODEL$); 273 | } 274 | 275 | /** 276 | * Creates a Builder by copying an existing Builder. 277 | * @param other The existing Builder to copy. 278 | */ 279 | private Builder(com.ivanfranchin.commonsnews.avro.NewsEvent.Builder other) { 280 | super(other); 281 | if (isValidValue(fields()[0], other.id)) { 282 | this.id = data().deepCopy(fields()[0].schema(), other.id); 283 | fieldSetFlags()[0] = other.fieldSetFlags()[0]; 284 | } 285 | if (isValidValue(fields()[1], other.title)) { 286 | this.title = data().deepCopy(fields()[1].schema(), other.title); 287 | fieldSetFlags()[1] = other.fieldSetFlags()[1]; 288 | } 289 | if (isValidValue(fields()[2], other.text)) { 290 | this.text = data().deepCopy(fields()[2].schema(), other.text); 291 | fieldSetFlags()[2] = other.fieldSetFlags()[2]; 292 | } 293 | if (isValidValue(fields()[3], other.datetime)) { 294 | this.datetime = data().deepCopy(fields()[3].schema(), other.datetime); 295 | fieldSetFlags()[3] = other.fieldSetFlags()[3]; 296 | } 297 | if (isValidValue(fields()[4], other.category)) { 298 | this.category = data().deepCopy(fields()[4].schema(), other.category); 299 | fieldSetFlags()[4] = other.fieldSetFlags()[4]; 300 | } 301 | } 302 | 303 | /** 304 | * Creates a Builder by copying an existing NewsEvent instance 305 | * @param other The existing instance to copy. 306 | */ 307 | private Builder(com.ivanfranchin.commonsnews.avro.NewsEvent other) { 308 | super(SCHEMA$, MODEL$); 309 | if (isValidValue(fields()[0], other.id)) { 310 | this.id = data().deepCopy(fields()[0].schema(), other.id); 311 | fieldSetFlags()[0] = true; 312 | } 313 | if (isValidValue(fields()[1], other.title)) { 314 | this.title = data().deepCopy(fields()[1].schema(), other.title); 315 | fieldSetFlags()[1] = true; 316 | } 317 | if (isValidValue(fields()[2], other.text)) { 318 | this.text = data().deepCopy(fields()[2].schema(), other.text); 319 | fieldSetFlags()[2] = true; 320 | } 321 | if (isValidValue(fields()[3], other.datetime)) { 322 | this.datetime = data().deepCopy(fields()[3].schema(), other.datetime); 323 | fieldSetFlags()[3] = true; 324 | } 325 | if (isValidValue(fields()[4], other.category)) { 326 | this.category = data().deepCopy(fields()[4].schema(), other.category); 327 | fieldSetFlags()[4] = true; 328 | } 329 | } 330 | 331 | /** 332 | * Gets the value of the 'id' field. 333 | * @return The value. 334 | */ 335 | public java.lang.CharSequence getId() { 336 | return id; 337 | } 338 | 339 | 340 | /** 341 | * Sets the value of the 'id' field. 342 | * @param value The value of 'id'. 343 | * @return This builder. 344 | */ 345 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder setId(java.lang.CharSequence value) { 346 | validate(fields()[0], value); 347 | this.id = value; 348 | fieldSetFlags()[0] = true; 349 | return this; 350 | } 351 | 352 | /** 353 | * Checks whether the 'id' field has been set. 354 | * @return True if the 'id' field has been set, false otherwise. 355 | */ 356 | public boolean hasId() { 357 | return fieldSetFlags()[0]; 358 | } 359 | 360 | 361 | /** 362 | * Clears the value of the 'id' field. 363 | * @return This builder. 364 | */ 365 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder clearId() { 366 | id = null; 367 | fieldSetFlags()[0] = false; 368 | return this; 369 | } 370 | 371 | /** 372 | * Gets the value of the 'title' field. 373 | * @return The value. 374 | */ 375 | public java.lang.CharSequence getTitle() { 376 | return title; 377 | } 378 | 379 | 380 | /** 381 | * Sets the value of the 'title' field. 382 | * @param value The value of 'title'. 383 | * @return This builder. 384 | */ 385 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder setTitle(java.lang.CharSequence value) { 386 | validate(fields()[1], value); 387 | this.title = value; 388 | fieldSetFlags()[1] = true; 389 | return this; 390 | } 391 | 392 | /** 393 | * Checks whether the 'title' field has been set. 394 | * @return True if the 'title' field has been set, false otherwise. 395 | */ 396 | public boolean hasTitle() { 397 | return fieldSetFlags()[1]; 398 | } 399 | 400 | 401 | /** 402 | * Clears the value of the 'title' field. 403 | * @return This builder. 404 | */ 405 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder clearTitle() { 406 | title = null; 407 | fieldSetFlags()[1] = false; 408 | return this; 409 | } 410 | 411 | /** 412 | * Gets the value of the 'text' field. 413 | * @return The value. 414 | */ 415 | public java.lang.CharSequence getText() { 416 | return text; 417 | } 418 | 419 | 420 | /** 421 | * Sets the value of the 'text' field. 422 | * @param value The value of 'text'. 423 | * @return This builder. 424 | */ 425 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder setText(java.lang.CharSequence value) { 426 | validate(fields()[2], value); 427 | this.text = value; 428 | fieldSetFlags()[2] = true; 429 | return this; 430 | } 431 | 432 | /** 433 | * Checks whether the 'text' field has been set. 434 | * @return True if the 'text' field has been set, false otherwise. 435 | */ 436 | public boolean hasText() { 437 | return fieldSetFlags()[2]; 438 | } 439 | 440 | 441 | /** 442 | * Clears the value of the 'text' field. 443 | * @return This builder. 444 | */ 445 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder clearText() { 446 | text = null; 447 | fieldSetFlags()[2] = false; 448 | return this; 449 | } 450 | 451 | /** 452 | * Gets the value of the 'datetime' field. 453 | * @return The value. 454 | */ 455 | public java.lang.CharSequence getDatetime() { 456 | return datetime; 457 | } 458 | 459 | 460 | /** 461 | * Sets the value of the 'datetime' field. 462 | * @param value The value of 'datetime'. 463 | * @return This builder. 464 | */ 465 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder setDatetime(java.lang.CharSequence value) { 466 | validate(fields()[3], value); 467 | this.datetime = value; 468 | fieldSetFlags()[3] = true; 469 | return this; 470 | } 471 | 472 | /** 473 | * Checks whether the 'datetime' field has been set. 474 | * @return True if the 'datetime' field has been set, false otherwise. 475 | */ 476 | public boolean hasDatetime() { 477 | return fieldSetFlags()[3]; 478 | } 479 | 480 | 481 | /** 482 | * Clears the value of the 'datetime' field. 483 | * @return This builder. 484 | */ 485 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder clearDatetime() { 486 | datetime = null; 487 | fieldSetFlags()[3] = false; 488 | return this; 489 | } 490 | 491 | /** 492 | * Gets the value of the 'category' field. 493 | * @return The value. 494 | */ 495 | public java.lang.CharSequence getCategory() { 496 | return category; 497 | } 498 | 499 | 500 | /** 501 | * Sets the value of the 'category' field. 502 | * @param value The value of 'category'. 503 | * @return This builder. 504 | */ 505 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder setCategory(java.lang.CharSequence value) { 506 | validate(fields()[4], value); 507 | this.category = value; 508 | fieldSetFlags()[4] = true; 509 | return this; 510 | } 511 | 512 | /** 513 | * Checks whether the 'category' field has been set. 514 | * @return True if the 'category' field has been set, false otherwise. 515 | */ 516 | public boolean hasCategory() { 517 | return fieldSetFlags()[4]; 518 | } 519 | 520 | 521 | /** 522 | * Clears the value of the 'category' field. 523 | * @return This builder. 524 | */ 525 | public com.ivanfranchin.commonsnews.avro.NewsEvent.Builder clearCategory() { 526 | category = null; 527 | fieldSetFlags()[4] = false; 528 | return this; 529 | } 530 | 531 | @Override 532 | @SuppressWarnings("unchecked") 533 | public NewsEvent build() { 534 | try { 535 | NewsEvent record = new NewsEvent(); 536 | record.id = fieldSetFlags()[0] ? this.id : (java.lang.CharSequence) defaultValue(fields()[0]); 537 | record.title = fieldSetFlags()[1] ? this.title : (java.lang.CharSequence) defaultValue(fields()[1]); 538 | record.text = fieldSetFlags()[2] ? this.text : (java.lang.CharSequence) defaultValue(fields()[2]); 539 | record.datetime = fieldSetFlags()[3] ? this.datetime : (java.lang.CharSequence) defaultValue(fields()[3]); 540 | record.category = fieldSetFlags()[4] ? this.category : (java.lang.CharSequence) defaultValue(fields()[4]); 541 | return record; 542 | } catch (org.apache.avro.AvroMissingFieldException e) { 543 | throw e; 544 | } catch (java.lang.Exception e) { 545 | throw new org.apache.avro.AvroRuntimeException(e); 546 | } 547 | } 548 | } 549 | 550 | @SuppressWarnings("unchecked") 551 | private static final org.apache.avro.io.DatumWriter 552 | WRITER$ = (org.apache.avro.io.DatumWriter)MODEL$.createDatumWriter(SCHEMA$); 553 | 554 | @Override public void writeExternal(java.io.ObjectOutput out) 555 | throws java.io.IOException { 556 | WRITER$.write(this, SpecificData.getEncoder(out)); 557 | } 558 | 559 | @SuppressWarnings("unchecked") 560 | private static final org.apache.avro.io.DatumReader 561 | READER$ = (org.apache.avro.io.DatumReader)MODEL$.createDatumReader(SCHEMA$); 562 | 563 | @Override public void readExternal(java.io.ObjectInput in) 564 | throws java.io.IOException { 565 | READER$.read(this, SpecificData.getDecoder(in)); 566 | } 567 | 568 | @Override protected boolean hasCustomCoders() { return true; } 569 | 570 | @Override public void customEncode(org.apache.avro.io.Encoder out) 571 | throws java.io.IOException 572 | { 573 | out.writeString(this.id); 574 | 575 | out.writeString(this.title); 576 | 577 | out.writeString(this.text); 578 | 579 | out.writeString(this.datetime); 580 | 581 | if (this.category == null) { 582 | out.writeIndex(0); 583 | out.writeNull(); 584 | } else { 585 | out.writeIndex(1); 586 | out.writeString(this.category); 587 | } 588 | 589 | } 590 | 591 | @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) 592 | throws java.io.IOException 593 | { 594 | org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); 595 | if (fieldOrder == null) { 596 | this.id = in.readString(this.id instanceof Utf8 ? (Utf8)this.id : null); 597 | 598 | this.title = in.readString(this.title instanceof Utf8 ? (Utf8)this.title : null); 599 | 600 | this.text = in.readString(this.text instanceof Utf8 ? (Utf8)this.text : null); 601 | 602 | this.datetime = in.readString(this.datetime instanceof Utf8 ? (Utf8)this.datetime : null); 603 | 604 | if (in.readIndex() != 1) { 605 | in.readNull(); 606 | this.category = null; 607 | } else { 608 | this.category = in.readString(this.category instanceof Utf8 ? (Utf8)this.category : null); 609 | } 610 | 611 | } else { 612 | for (int i = 0; i < 5; i++) { 613 | switch (fieldOrder[i].pos()) { 614 | case 0: 615 | this.id = in.readString(this.id instanceof Utf8 ? (Utf8)this.id : null); 616 | break; 617 | 618 | case 1: 619 | this.title = in.readString(this.title instanceof Utf8 ? (Utf8)this.title : null); 620 | break; 621 | 622 | case 2: 623 | this.text = in.readString(this.text instanceof Utf8 ? (Utf8)this.text : null); 624 | break; 625 | 626 | case 3: 627 | this.datetime = in.readString(this.datetime instanceof Utf8 ? (Utf8)this.datetime : null); 628 | break; 629 | 630 | case 4: 631 | if (in.readIndex() != 1) { 632 | in.readNull(); 633 | this.category = null; 634 | } else { 635 | this.category = in.readString(this.category instanceof Utf8 ? (Utf8)this.category : null); 636 | } 637 | break; 638 | 639 | default: 640 | throw new java.io.IOException("Corrupt ResolvingDecoder."); 641 | } 642 | } 643 | } 644 | } 645 | } 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | -------------------------------------------------------------------------------- /commons-news/src/main/resources/avro/news-event.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "com.ivanfranchin.commonsnews.avro", 3 | "type": "record", 4 | "name": "NewsEvent", 5 | "fields": [ 6 | {"name": "id", "type": "string"}, 7 | {"name": "title", "type": "string"}, 8 | {"name": "text", "type": "string"}, 9 | {"name": "datetime", "type": "string"}, 10 | {"name": "category", "type": ["null", "string"], "default": null} 11 | ] 12 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | elasticsearch: 4 | image: 'docker.elastic.co/elasticsearch/elasticsearch:8.15.4' 5 | container_name: 'elasticsearch' 6 | restart: 'unless-stopped' 7 | ports: 8 | - '9200:9200' 9 | - '9300:9300' 10 | environment: 11 | - 'discovery.type=single-node' 12 | - 'xpack.security.enabled=false' 13 | - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' 14 | healthcheck: 15 | test: 'curl -f http://localhost:9200 || exit 1' 16 | 17 | zookeeper: 18 | image: 'confluentinc/cp-zookeeper:7.8.0' 19 | container_name: 'zookeeper' 20 | restart: 'unless-stopped' 21 | ports: 22 | - '2181:2181' 23 | environment: 24 | - 'ZOOKEEPER_CLIENT_PORT=2181' 25 | healthcheck: 26 | test: 'echo stat | nc localhost $$ZOOKEEPER_CLIENT_PORT' 27 | 28 | kafka: 29 | image: 'confluentinc/cp-kafka:7.8.0' 30 | container_name: 'kafka' 31 | restart: 'unless-stopped' 32 | depends_on: 33 | - 'zookeeper' 34 | ports: 35 | - '29092:29092' 36 | environment: 37 | - 'KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181' 38 | - 'KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' 39 | - 'KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092' 40 | - 'KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1' 41 | healthcheck: 42 | test: [ "CMD", "nc", "-z", "localhost", "9092" ] 43 | 44 | schema-registry: 45 | image: 'confluentinc/cp-schema-registry:7.8.0' 46 | container_name: 'schema-registry' 47 | restart: 'unless-stopped' 48 | depends_on: 49 | - 'kafka' 50 | ports: 51 | - '8081:8081' 52 | environment: 53 | - 'SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=kafka:9092' 54 | - 'SCHEMA_REGISTRY_HOST_NAME=schema-registry' 55 | - 'SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081' 56 | healthcheck: 57 | test: 'curl -f http://localhost:8081 || exit 1' 58 | 59 | schema-registry-ui: 60 | image: 'landoop/schema-registry-ui:0.9.5' 61 | container_name: 'kafka-schema-registry-ui' 62 | depends_on: 63 | - 'schema-registry' 64 | ports: 65 | - '8001:8000' 66 | environment: 67 | - 'SCHEMAREGISTRY_URL=http://schema-registry:8081' 68 | - 'PROXY=true' 69 | healthcheck: 70 | test: 'wget --quiet --tries=1 --spider http://localhost:8000 || exit 1' 71 | 72 | kafka-rest-proxy: 73 | image: 'confluentinc/cp-kafka-rest:7.8.0' 74 | container_name: 'kafka-rest-proxy' 75 | restart: 'unless-stopped' 76 | depends_on: 77 | - 'zookeeper' 78 | - 'kafka' 79 | ports: 80 | - '8082:8082' 81 | environment: 82 | - 'KAFKA_REST_BOOTSTRAP_SERVERS=PLAINTEXT://kafka:9092' 83 | - 'KAFKA_REST_ZOOKEEPER_CONNECT=zookeeper:2181' 84 | - 'KAFKA_REST_HOST_NAME=kafka-rest-proxy' 85 | - 'KAFKA_REST_LISTENERS=http://0.0.0.0:8082' 86 | - 'KAFKA_REST_CONSUMER_REQUEST_TIMEOUT_MS=30000' 87 | healthcheck: 88 | test: 'curl -f http://localhost:8082 || exit 1' 89 | 90 | kafka-topics-ui: 91 | image: 'landoop/kafka-topics-ui:0.9.4' 92 | container_name: 'kafka-topics-ui' 93 | restart: 'unless-stopped' 94 | depends_on: 95 | - 'kafka-rest-proxy' 96 | ports: 97 | - '8085:8000' 98 | environment: 99 | - 'KAFKA_REST_PROXY_URL=http://kafka-rest-proxy:8082' 100 | - 'PROXY=true' 101 | healthcheck: 102 | test: 'wget --quiet --tries=1 --spider http://localhost:8000 || exit 1' 103 | 104 | kafka-manager: 105 | image: 'hlebalbau/kafka-manager:3.0.0.5' 106 | container_name: 'kafka-manager' 107 | restart: 'unless-stopped' 108 | depends_on: 109 | - 'zookeeper' 110 | ports: 111 | - '9001:9000' 112 | environment: 113 | - 'ZK_HOSTS=zookeeper:2181' 114 | - 'APPLICATION_SECRET=random-secret' 115 | command: '-Dpidfile.path=/dev/null' 116 | healthcheck: 117 | test: 'curl -f http://localhost:9000 || exit 1' 118 | 119 | zipkin: 120 | image: 'openzipkin/zipkin:3.4.1' 121 | container_name: 'zipkin' 122 | restart: 'unless-stopped' 123 | ports: 124 | - '9411:9411' 125 | healthcheck: 126 | test: [ "CMD", "nc", "-z", "localhost", "9411" ] 127 | -------------------------------------------------------------------------------- /documentation/eureka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/documentation/eureka.jpg -------------------------------------------------------------------------------- /documentation/project-diagram.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "type": "rectangle", 8 | "version": 2058, 9 | "versionNonce": 37924673, 10 | "isDeleted": false, 11 | "id": "NKmNZxYxWMCKh3prRiPwX", 12 | "fillStyle": "hachure", 13 | "strokeWidth": 1, 14 | "strokeStyle": "solid", 15 | "roughness": 1, 16 | "opacity": 100, 17 | "angle": 0, 18 | "x": -60.80828370227391, 19 | "y": -344.01692817015993, 20 | "strokeColor": "#000000", 21 | "backgroundColor": "#be4bdb", 22 | "width": 246, 23 | "height": 100, 24 | "seed": 1239189380, 25 | "groupIds": [], 26 | "roundness": { 27 | "type": 3 28 | }, 29 | "boundElements": [ 30 | { 31 | "type": "text", 32 | "id": "GrVT2PZuYu1cRv3sXwFoI" 33 | }, 34 | { 35 | "id": "cA_hWJzSnGig-8CoXOII4", 36 | "type": "arrow" 37 | }, 38 | { 39 | "id": "cL-cANaVTTmnsXByEgrMg", 40 | "type": "arrow" 41 | }, 42 | { 43 | "id": "kkrWMTS6TyAOSsiNmeg_R", 44 | "type": "arrow" 45 | } 46 | ], 47 | "updated": 1678886274691, 48 | "link": null, 49 | "locked": false 50 | }, 51 | { 52 | "type": "text", 53 | "version": 1012, 54 | "versionNonce": 884726927, 55 | "isDeleted": false, 56 | "id": "GrVT2PZuYu1cRv3sXwFoI", 57 | "fillStyle": "hachure", 58 | "strokeWidth": 1, 59 | "strokeStyle": "solid", 60 | "roughness": 0, 61 | "opacity": 100, 62 | "angle": 0, 63 | "x": -20.170237437625474, 64 | "y": -310.81692817015994, 65 | "strokeColor": "#000000", 66 | "backgroundColor": "transparent", 67 | "width": 164.72390747070312, 68 | "height": 33.6, 69 | "seed": 294979727, 70 | "groupIds": [], 71 | "roundness": null, 72 | "boundElements": [], 73 | "updated": 1678885886736, 74 | "link": null, 75 | "locked": false, 76 | "fontSize": 28, 77 | "fontFamily": 1, 78 | "text": "producer-api", 79 | "textAlign": "center", 80 | "verticalAlign": "middle", 81 | "containerId": "NKmNZxYxWMCKh3prRiPwX", 82 | "originalText": "producer-api" 83 | }, 84 | { 85 | "type": "rectangle", 86 | "version": 2532, 87 | "versionNonce": 6206369, 88 | "isDeleted": false, 89 | "id": "GdyILsCD4rTRfB2LlEajF", 90 | "fillStyle": "hachure", 91 | "strokeWidth": 1, 92 | "strokeStyle": "solid", 93 | "roughness": 1, 94 | "opacity": 100, 95 | "angle": 0, 96 | "x": 243.43162379627438, 97 | "y": -343.8463785049814, 98 | "strokeColor": "#000000", 99 | "backgroundColor": "#12b886", 100 | "width": 246, 101 | "height": 100, 102 | "seed": 661190404, 103 | "groupIds": [], 104 | "roundness": { 105 | "type": 3 106 | }, 107 | "boundElements": [ 108 | { 109 | "type": "text", 110 | "id": "Pjp8Tj_wsaL6aNumfA2-8" 111 | }, 112 | { 113 | "id": "cfuk6WWFkUXZV3zLpvYmv", 114 | "type": "arrow" 115 | }, 116 | { 117 | "id": "Z5AdI__Yt6_dDTzbNQTGp", 118 | "type": "arrow" 119 | }, 120 | { 121 | "id": "FDwW0dczTV9sPyxO0rm_x", 122 | "type": "arrow" 123 | } 124 | ], 125 | "updated": 1678886330767, 126 | "link": null, 127 | "locked": false 128 | }, 129 | { 130 | "type": "text", 131 | "version": 1504, 132 | "versionNonce": 1498843873, 133 | "isDeleted": false, 134 | "id": "Pjp8Tj_wsaL6aNumfA2-8", 135 | "fillStyle": "hachure", 136 | "strokeWidth": 1, 137 | "strokeStyle": "solid", 138 | "roughness": 0, 139 | "opacity": 100, 140 | "angle": 0, 141 | "x": 288.9556563890478, 142 | "y": -327.4463785049814, 143 | "strokeColor": "#000000", 144 | "backgroundColor": "transparent", 145 | "width": 154.95193481445312, 146 | "height": 67.2, 147 | "seed": 341736561, 148 | "groupIds": [], 149 | "roundness": null, 150 | "boundElements": [], 151 | "updated": 1678886284124, 152 | "link": null, 153 | "locked": false, 154 | "fontSize": 28, 155 | "fontFamily": 1, 156 | "text": "categorizer\n-service", 157 | "textAlign": "center", 158 | "verticalAlign": "middle", 159 | "containerId": "GdyILsCD4rTRfB2LlEajF", 160 | "originalText": "categorizer\n-service" 161 | }, 162 | { 163 | "type": "rectangle", 164 | "version": 1076, 165 | "versionNonce": 2071662319, 166 | "isDeleted": false, 167 | "id": "gwGOOW99YxclikCNuOiyC", 168 | "fillStyle": "hachure", 169 | "strokeWidth": 1, 170 | "strokeStyle": "solid", 171 | "roughness": 1, 172 | "opacity": 100, 173 | "angle": 0, 174 | "x": -55.746658149114865, 175 | "y": -41.15568200221696, 176 | "strokeColor": "#000000", 177 | "backgroundColor": "#ced4da", 178 | "width": 1469.7182125515399, 179 | "height": 280.73980422247024, 180 | "seed": 146503300, 181 | "groupIds": [ 182 | "sa3Ax-WgNGnA-_zmnjbX3" 183 | ], 184 | "roundness": { 185 | "type": 3 186 | }, 187 | "boundElements": [], 188 | "updated": 1678886259575, 189 | "link": null, 190 | "locked": false 191 | }, 192 | { 193 | "type": "text", 194 | "version": 421, 195 | "versionNonce": 2016640047, 196 | "isDeleted": false, 197 | "id": "lv-AGSCax4dA7SdjKXS01", 198 | "fillStyle": "hachure", 199 | "strokeWidth": 1, 200 | "strokeStyle": "solid", 201 | "roughness": 1, 202 | "opacity": 100, 203 | "angle": 0, 204 | "x": -32.528237787399235, 205 | "y": -27.280384673813032, 206 | "strokeColor": "#000000", 207 | "backgroundColor": "transparent", 208 | "width": 81.75994873046875, 209 | "height": 33.6, 210 | "seed": 36037695, 211 | "groupIds": [], 212 | "roundness": null, 213 | "boundElements": [], 214 | "updated": 1678886254054, 215 | "link": null, 216 | "locked": false, 217 | "fontSize": 28, 218 | "fontFamily": 1, 219 | "text": "Kafka", 220 | "textAlign": "left", 221 | "verticalAlign": "top", 222 | "containerId": null, 223 | "originalText": "Kafka" 224 | }, 225 | { 226 | "type": "rectangle", 227 | "version": 2611, 228 | "versionNonce": 760620193, 229 | "isDeleted": false, 230 | "id": "umf7pq1g-JfSQD3V1WxDT", 231 | "fillStyle": "hachure", 232 | "strokeWidth": 1, 233 | "strokeStyle": "solid", 234 | "roughness": 1, 235 | "opacity": 100, 236 | "angle": 0, 237 | "x": 547.8419065738404, 238 | "y": -343.8465528911421, 239 | "strokeColor": "#000000", 240 | "backgroundColor": "#228be6", 241 | "width": 246, 242 | "height": 100, 243 | "seed": 840188111, 244 | "groupIds": [], 245 | "roundness": { 246 | "type": 3 247 | }, 248 | "boundElements": [ 249 | { 250 | "type": "text", 251 | "id": "cslQVbx0_uAoJYqF3XpK8" 252 | }, 253 | { 254 | "id": "vFUg49T2Ao35trTckk354", 255 | "type": "arrow" 256 | }, 257 | { 258 | "id": "49nTZMcdBYc1Kiao3IOS1", 259 | "type": "arrow" 260 | }, 261 | { 262 | "id": "OrW60dJMx1ubu6K8fEiFZ", 263 | "type": "arrow" 264 | }, 265 | { 266 | "id": "vB-m6btdWL7J-5_aUJ0z4", 267 | "type": "arrow" 268 | } 269 | ], 270 | "updated": 1678886316160, 271 | "link": null, 272 | "locked": false 273 | }, 274 | { 275 | "type": "text", 276 | "version": 1597, 277 | "versionNonce": 773758529, 278 | "isDeleted": false, 279 | "id": "cslQVbx0_uAoJYqF3XpK8", 280 | "fillStyle": "hachure", 281 | "strokeWidth": 1, 282 | "strokeStyle": "solid", 283 | "roughness": 0, 284 | "opacity": 100, 285 | "angle": 0, 286 | "x": 612.3219251895631, 287 | "y": -327.4465528911421, 288 | "strokeColor": "#000000", 289 | "backgroundColor": "transparent", 290 | "width": 117.03996276855469, 291 | "height": 67.2, 292 | "seed": 1491613601, 293 | "groupIds": [], 294 | "roundness": null, 295 | "boundElements": null, 296 | "updated": 1678886288195, 297 | "link": null, 298 | "locked": false, 299 | "fontSize": 28, 300 | "fontFamily": 1, 301 | "text": "collector\n-service", 302 | "textAlign": "center", 303 | "verticalAlign": "middle", 304 | "containerId": "umf7pq1g-JfSQD3V1WxDT", 305 | "originalText": "collector\n-service" 306 | }, 307 | { 308 | "type": "rectangle", 309 | "version": 2556, 310 | "versionNonce": 1517911105, 311 | "isDeleted": false, 312 | "id": "_XVaZa0RKrBuvLhhaAr-6", 313 | "fillStyle": "hachure", 314 | "strokeWidth": 1, 315 | "strokeStyle": "solid", 316 | "roughness": 1, 317 | "opacity": 100, 318 | "angle": 0, 319 | "x": 852.0830347755139, 320 | "y": -343.84528859147684, 321 | "strokeColor": "#000000", 322 | "backgroundColor": "#15aabf", 323 | "width": 246, 324 | "height": 100, 325 | "seed": 769280975, 326 | "groupIds": [], 327 | "roundness": { 328 | "type": 3 329 | }, 330 | "boundElements": [ 331 | { 332 | "type": "text", 333 | "id": "8vnOBPoBDdOsXYUbuqjGP" 334 | }, 335 | { 336 | "id": "XIJnxCtHrLXi_ryGGQEmU", 337 | "type": "arrow" 338 | } 339 | ], 340 | "updated": 1678886319380, 341 | "link": null, 342 | "locked": false 343 | }, 344 | { 345 | "type": "text", 346 | "version": 1542, 347 | "versionNonce": 1570151439, 348 | "isDeleted": false, 349 | "id": "8vnOBPoBDdOsXYUbuqjGP", 350 | "fillStyle": "hachure", 351 | "strokeWidth": 1, 352 | "strokeStyle": "solid", 353 | "roughness": 0, 354 | "opacity": 100, 355 | "angle": 0, 356 | "x": 893.1130869605724, 357 | "y": -310.64528859147686, 358 | "strokeColor": "#000000", 359 | "backgroundColor": "transparent", 360 | "width": 163.9398956298828, 361 | "height": 33.6, 362 | "seed": 1822186657, 363 | "groupIds": [], 364 | "roundness": null, 365 | "boundElements": null, 366 | "updated": 1678886317348, 367 | "link": null, 368 | "locked": false, 369 | "fontSize": 28, 370 | "fontFamily": 1, 371 | "text": "publisher-api", 372 | "textAlign": "center", 373 | "verticalAlign": "middle", 374 | "containerId": "_XVaZa0RKrBuvLhhaAr-6", 375 | "originalText": "publisher-api" 376 | }, 377 | { 378 | "type": "rectangle", 379 | "version": 2553, 380 | "versionNonce": 1993135041, 381 | "isDeleted": false, 382 | "id": "JKpeXhwdrNYXIaUyA2iwS", 383 | "fillStyle": "hachure", 384 | "strokeWidth": 1, 385 | "strokeStyle": "solid", 386 | "roughness": 1, 387 | "opacity": 100, 388 | "angle": 0, 389 | "x": 1156.3288714035264, 390 | "y": -344.01692817015993, 391 | "strokeColor": "#000000", 392 | "backgroundColor": "#fab005", 393 | "width": 246, 394 | "height": 100, 395 | "seed": 876785953, 396 | "groupIds": [], 397 | "roundness": { 398 | "type": 3 399 | }, 400 | "boundElements": [ 401 | { 402 | "type": "text", 403 | "id": "Z0ITH8A5wxC4jxnDNvk-m" 404 | }, 405 | { 406 | "id": "7mDoXq8CqYd8_0mg2hFvm", 407 | "type": "arrow" 408 | }, 409 | { 410 | "id": "LjntQQpGvJGbNev3eraj3", 411 | "type": "arrow" 412 | } 413 | ], 414 | "updated": 1678886324821, 415 | "link": null, 416 | "locked": false 417 | }, 418 | { 419 | "type": "text", 420 | "version": 1552, 421 | "versionNonce": 1842397807, 422 | "isDeleted": false, 423 | "id": "Z0ITH8A5wxC4jxnDNvk-m", 424 | "fillStyle": "hachure", 425 | "strokeWidth": 1, 426 | "strokeStyle": "solid", 427 | "roughness": 0, 428 | "opacity": 100, 429 | "angle": 0, 430 | "x": 1207.3689104660264, 431 | "y": -310.81692817015994, 432 | "strokeColor": "#000000", 433 | "backgroundColor": "transparent", 434 | "width": 143.919921875, 435 | "height": 33.6, 436 | "seed": 727002479, 437 | "groupIds": [], 438 | "roundness": null, 439 | "boundElements": null, 440 | "updated": 1678886321066, 441 | "link": null, 442 | "locked": false, 443 | "fontSize": 28, 444 | "fontFamily": 1, 445 | "text": "news-client", 446 | "textAlign": "center", 447 | "verticalAlign": "middle", 448 | "containerId": "JKpeXhwdrNYXIaUyA2iwS", 449 | "originalText": "news-client" 450 | }, 451 | { 452 | "type": "rectangle", 453 | "version": 2875, 454 | "versionNonce": 1064289121, 455 | "isDeleted": false, 456 | "id": "lSFZOqZs-2cpHDqEqLKwG", 457 | "fillStyle": "hachure", 458 | "strokeWidth": 1, 459 | "strokeStyle": "solid", 460 | "roughness": 1, 461 | "opacity": 100, 462 | "angle": 0, 463 | "x": 701.551213758437, 464 | "y": -162.99801876681192, 465 | "strokeColor": "#000000", 466 | "backgroundColor": "#82c91e", 467 | "width": 246, 468 | "height": 100, 469 | "seed": 674165409, 470 | "groupIds": [], 471 | "roundness": { 472 | "type": 3 473 | }, 474 | "boundElements": [ 475 | { 476 | "type": "text", 477 | "id": "AzwwW-bFPOumHVKnqDD6Z" 478 | }, 479 | { 480 | "id": "OrW60dJMx1ubu6K8fEiFZ", 481 | "type": "arrow" 482 | }, 483 | { 484 | "id": "XIJnxCtHrLXi_ryGGQEmU", 485 | "type": "arrow" 486 | } 487 | ], 488 | "updated": 1678886270483, 489 | "link": null, 490 | "locked": false 491 | }, 492 | { 493 | "type": "text", 494 | "version": 1877, 495 | "versionNonce": 1553848193, 496 | "isDeleted": false, 497 | "id": "AzwwW-bFPOumHVKnqDD6Z", 498 | "fillStyle": "hachure", 499 | "strokeWidth": 1, 500 | "strokeStyle": "solid", 501 | "roughness": 0, 502 | "opacity": 100, 503 | "angle": 0, 504 | "x": 731.9272513560933, 505 | "y": -129.79801876681188, 506 | "strokeColor": "#000000", 507 | "backgroundColor": "transparent", 508 | "width": 185.2479248046875, 509 | "height": 33.6, 510 | "seed": 1818739695, 511 | "groupIds": [], 512 | "roundness": null, 513 | "boundElements": null, 514 | "updated": 1678886239307, 515 | "link": null, 516 | "locked": false, 517 | "fontSize": 28, 518 | "fontFamily": 1, 519 | "text": "Elasticsearch", 520 | "textAlign": "center", 521 | "verticalAlign": "middle", 522 | "containerId": "lSFZOqZs-2cpHDqEqLKwG", 523 | "originalText": "Elasticsearch" 524 | }, 525 | { 526 | "type": "rectangle", 527 | "version": 2809, 528 | "versionNonce": 35546305, 529 | "isDeleted": false, 530 | "id": "1CgHRN2NXzOixDHRW6ZBt", 531 | "fillStyle": "hachure", 532 | "strokeWidth": 1, 533 | "strokeStyle": "solid", 534 | "roughness": 1, 535 | "opacity": 100, 536 | "angle": 0, 537 | "x": 547.8481844756262, 538 | "y": -574.7250826270241, 539 | "strokeColor": "#000000", 540 | "backgroundColor": "#fd7e14", 541 | "width": 246, 542 | "height": 100, 543 | "seed": 1268226511, 544 | "groupIds": [], 545 | "roundness": { 546 | "type": 3 547 | }, 548 | "boundElements": [ 549 | { 550 | "type": "text", 551 | "id": "85huIoKa6Gb8_M7_-Jl8t" 552 | }, 553 | { 554 | "id": "kkrWMTS6TyAOSsiNmeg_R", 555 | "type": "arrow" 556 | }, 557 | { 558 | "id": "FDwW0dczTV9sPyxO0rm_x", 559 | "type": "arrow" 560 | }, 561 | { 562 | "id": "vB-m6btdWL7J-5_aUJ0z4", 563 | "type": "arrow" 564 | }, 565 | { 566 | "id": "LjntQQpGvJGbNev3eraj3", 567 | "type": "arrow" 568 | } 569 | ], 570 | "updated": 1678886307179, 571 | "link": null, 572 | "locked": false 573 | }, 574 | { 575 | "type": "text", 576 | "version": 1821, 577 | "versionNonce": 1076732207, 578 | "isDeleted": false, 579 | "id": "85huIoKa6Gb8_M7_-Jl8t", 580 | "fillStyle": "hachure", 581 | "strokeWidth": 1, 582 | "strokeStyle": "solid", 583 | "roughness": 0, 584 | "opacity": 100, 585 | "angle": 0, 586 | "x": 560.2062319609778, 587 | "y": -541.5250826270241, 588 | "strokeColor": "#000000", 589 | "backgroundColor": "transparent", 590 | "width": 221.28390502929688, 591 | "height": 33.6, 592 | "seed": 245558945, 593 | "groupIds": [], 594 | "roundness": null, 595 | "boundElements": null, 596 | "updated": 1678886303611, 597 | "link": null, 598 | "locked": false, 599 | "fontSize": 28, 600 | "fontFamily": 1, 601 | "text": "Schema Registry", 602 | "textAlign": "center", 603 | "verticalAlign": "middle", 604 | "containerId": "1CgHRN2NXzOixDHRW6ZBt", 605 | "originalText": "Schema Registry" 606 | }, 607 | { 608 | "id": "tjtMT0KITH4Ln7OFsBxLT", 609 | "type": "rectangle", 610 | "x": -32.67560160149566, 611 | "y": 29.160053568567946, 612 | "width": 1424.009660993303, 613 | "height": 41.964111328125, 614 | "angle": 0, 615 | "strokeColor": "#000000", 616 | "backgroundColor": "#868e96", 617 | "fillStyle": "hachure", 618 | "strokeWidth": 1, 619 | "strokeStyle": "solid", 620 | "roughness": 1, 621 | "opacity": 100, 622 | "groupIds": [], 623 | "roundness": { 624 | "type": 3 625 | }, 626 | "seed": 1286735201, 627 | "version": 213, 628 | "versionNonce": 1945428545, 629 | "isDeleted": false, 630 | "boundElements": [ 631 | { 632 | "type": "text", 633 | "id": "No9CGErnuw-ljVEOpU6vR" 634 | }, 635 | { 636 | "id": "cL-cANaVTTmnsXByEgrMg", 637 | "type": "arrow" 638 | }, 639 | { 640 | "id": "cfuk6WWFkUXZV3zLpvYmv", 641 | "type": "arrow" 642 | } 643 | ], 644 | "updated": 1678886254054, 645 | "link": null, 646 | "locked": false 647 | }, 648 | { 649 | "id": "No9CGErnuw-ljVEOpU6vR", 650 | "type": "text", 651 | "x": -27.675601601495657, 652 | "y": 38.142109232630446, 653 | "width": 160.6598663330078, 654 | "height": 24, 655 | "angle": 0, 656 | "strokeColor": "#000000", 657 | "backgroundColor": "transparent", 658 | "fillStyle": "hachure", 659 | "strokeWidth": 1, 660 | "strokeStyle": "solid", 661 | "roughness": 1, 662 | "opacity": 100, 663 | "groupIds": [], 664 | "roundness": null, 665 | "seed": 424206977, 666 | "version": 64, 667 | "versionNonce": 2029546305, 668 | "isDeleted": false, 669 | "boundElements": null, 670 | "updated": 1678886385853, 671 | "link": null, 672 | "locked": false, 673 | "text": " producer.news", 674 | "fontSize": 20, 675 | "fontFamily": 1, 676 | "textAlign": "left", 677 | "verticalAlign": "middle", 678 | "containerId": "tjtMT0KITH4Ln7OFsBxLT", 679 | "originalText": " producer.news" 680 | }, 681 | { 682 | "type": "rectangle", 683 | "version": 302, 684 | "versionNonce": 967941601, 685 | "isDeleted": false, 686 | "id": "IGe7ziqsitN9OSWMLhiLf", 687 | "fillStyle": "hachure", 688 | "strokeWidth": 1, 689 | "strokeStyle": "solid", 690 | "roughness": 1, 691 | "opacity": 100, 692 | "angle": 0, 693 | "x": -29.140009386093766, 694 | "y": 101.47947669914834, 695 | "strokeColor": "#000000", 696 | "backgroundColor": "#868e96", 697 | "width": 1424.009660993303, 698 | "height": 41.964111328125, 699 | "seed": 1033488687, 700 | "groupIds": [], 701 | "roundness": { 702 | "type": 3 703 | }, 704 | "boundElements": [ 705 | { 706 | "type": "text", 707 | "id": "opxm69Dn6mvUQTwmg1eZG" 708 | }, 709 | { 710 | "id": "Z5AdI__Yt6_dDTzbNQTGp", 711 | "type": "arrow" 712 | }, 713 | { 714 | "id": "vFUg49T2Ao35trTckk354", 715 | "type": "arrow" 716 | } 717 | ], 718 | "updated": 1678886254055, 719 | "link": null, 720 | "locked": false 721 | }, 722 | { 723 | "type": "text", 724 | "version": 164, 725 | "versionNonce": 483122575, 726 | "isDeleted": false, 727 | "id": "opxm69Dn6mvUQTwmg1eZG", 728 | "fillStyle": "hachure", 729 | "strokeWidth": 1, 730 | "strokeStyle": "solid", 731 | "roughness": 1, 732 | "opacity": 100, 733 | "angle": 0, 734 | "x": -24.140009386093766, 735 | "y": 110.46153236321084, 736 | "strokeColor": "#000000", 737 | "backgroundColor": "transparent", 738 | "width": 189.47984313964844, 739 | "height": 24, 740 | "seed": 1487267137, 741 | "groupIds": [], 742 | "roundness": null, 743 | "boundElements": null, 744 | "updated": 1678886389027, 745 | "link": null, 746 | "locked": false, 747 | "fontSize": 20, 748 | "fontFamily": 1, 749 | "text": " categorizer.news", 750 | "textAlign": "left", 751 | "verticalAlign": "middle", 752 | "containerId": "IGe7ziqsitN9OSWMLhiLf", 753 | "originalText": " categorizer.news" 754 | }, 755 | { 756 | "type": "rectangle", 757 | "version": 354, 758 | "versionNonce": 1000373633, 759 | "isDeleted": false, 760 | "id": "ucx_nOj_6dPlU0s7mSha2", 761 | "fillStyle": "hachure", 762 | "strokeWidth": 1, 763 | "strokeStyle": "solid", 764 | "roughness": 1, 765 | "opacity": 100, 766 | "angle": 0, 767 | "x": -31.480794821361656, 768 | "y": 178.9209701422286, 769 | "strokeColor": "#000000", 770 | "backgroundColor": "#868e96", 771 | "width": 1424.009660993303, 772 | "height": 41.964111328125, 773 | "seed": 475359009, 774 | "groupIds": [], 775 | "roundness": { 776 | "type": 3 777 | }, 778 | "boundElements": [ 779 | { 780 | "type": "text", 781 | "id": "6i42caLCnnh6sr-SUwxhz" 782 | }, 783 | { 784 | "id": "49nTZMcdBYc1Kiao3IOS1", 785 | "type": "arrow" 786 | }, 787 | { 788 | "id": "7mDoXq8CqYd8_0mg2hFvm", 789 | "type": "arrow" 790 | } 791 | ], 792 | "updated": 1678886254055, 793 | "link": null, 794 | "locked": false 795 | }, 796 | { 797 | "type": "text", 798 | "version": 224, 799 | "versionNonce": 1722303137, 800 | "isDeleted": false, 801 | "id": "6i42caLCnnh6sr-SUwxhz", 802 | "fillStyle": "hachure", 803 | "strokeWidth": 1, 804 | "strokeStyle": "solid", 805 | "roughness": 1, 806 | "opacity": 100, 807 | "angle": 0, 808 | "x": -26.480794821361656, 809 | "y": 187.9030258062911, 810 | "strokeColor": "#000000", 811 | "backgroundColor": "transparent", 812 | "width": 162.39987182617188, 813 | "height": 24, 814 | "seed": 191944559, 815 | "groupIds": [], 816 | "roundness": null, 817 | "boundElements": null, 818 | "updated": 1678886391633, 819 | "link": null, 820 | "locked": false, 821 | "fontSize": 20, 822 | "fontFamily": 1, 823 | "text": " collector.news", 824 | "textAlign": "left", 825 | "verticalAlign": "middle", 826 | "containerId": "ucx_nOj_6dPlU0s7mSha2", 827 | "originalText": " collector.news" 828 | }, 829 | { 830 | "id": "cL-cANaVTTmnsXByEgrMg", 831 | "type": "arrow", 832 | "x": 101.84803804188175, 833 | "y": -243.01692817015993, 834 | "width": 2.943832393500543, 835 | "height": 264.45315710145184, 836 | "angle": 0, 837 | "strokeColor": "#000000", 838 | "backgroundColor": "#82c91e", 839 | "fillStyle": "hachure", 840 | "strokeWidth": 1, 841 | "strokeStyle": "solid", 842 | "roughness": 1, 843 | "opacity": 100, 844 | "groupIds": [], 845 | "roundness": { 846 | "type": 2 847 | }, 848 | "seed": 1997044417, 849 | "version": 158, 850 | "versionNonce": 1786835489, 851 | "isDeleted": false, 852 | "boundElements": null, 853 | "updated": 1678886254054, 854 | "link": null, 855 | "locked": false, 856 | "points": [ 857 | [ 858 | 0, 859 | 0 860 | ], 861 | [ 862 | -2.943832393500543, 863 | 264.45315710145184 864 | ] 865 | ], 866 | "lastCommittedPoint": null, 867 | "startBinding": { 868 | "elementId": "NKmNZxYxWMCKh3prRiPwX", 869 | "focus": -0.3255402297422068, 870 | "gap": 1 871 | }, 872 | "endBinding": { 873 | "elementId": "tjtMT0KITH4Ln7OFsBxLT", 874 | "focus": -0.8153794743922477, 875 | "gap": 7.723824637276039 876 | }, 877 | "startArrowhead": null, 878 | "endArrowhead": "arrow" 879 | }, 880 | { 881 | "id": "cfuk6WWFkUXZV3zLpvYmv", 882 | "type": "arrow", 883 | "x": 302.3460735094663, 884 | "y": 22.353761715890144, 885 | "width": 5.423901254219572, 886 | "height": 260.48393031529014, 887 | "angle": 0, 888 | "strokeColor": "#000000", 889 | "backgroundColor": "#82c91e", 890 | "fillStyle": "hachure", 891 | "strokeWidth": 1, 892 | "strokeStyle": "solid", 893 | "roughness": 1, 894 | "opacity": 100, 895 | "groupIds": [], 896 | "roundness": { 897 | "type": 2 898 | }, 899 | "seed": 1892250465, 900 | "version": 198, 901 | "versionNonce": 1390708417, 902 | "isDeleted": false, 903 | "boundElements": null, 904 | "updated": 1678886284125, 905 | "link": null, 906 | "locked": false, 907 | "points": [ 908 | [ 909 | 0, 910 | 0 911 | ], 912 | [ 913 | 5.423901254219572, 914 | -260.48393031529014 915 | ] 916 | ], 917 | "lastCommittedPoint": null, 918 | "startBinding": { 919 | "elementId": "tjtMT0KITH4Ln7OFsBxLT", 920 | "focus": -0.5299543679751451, 921 | "gap": 6.806291852677802 922 | }, 923 | "endBinding": { 924 | "elementId": "GdyILsCD4rTRfB2LlEajF", 925 | "focus": 0.4635680782506194, 926 | "gap": 5.7162099055813655 927 | }, 928 | "startArrowhead": "dot", 929 | "endArrowhead": "arrow" 930 | }, 931 | { 932 | "id": "Z5AdI__Yt6_dDTzbNQTGp", 933 | "type": "arrow", 934 | "x": 418.70044918070664, 935 | "y": -242.2363523326589, 936 | "width": 2.6608469861869253, 937 | "height": 341.5564923967633, 938 | "angle": 0, 939 | "strokeColor": "#000000", 940 | "backgroundColor": "#82c91e", 941 | "fillStyle": "hachure", 942 | "strokeWidth": 1, 943 | "strokeStyle": "solid", 944 | "roughness": 1, 945 | "opacity": 100, 946 | "groupIds": [], 947 | "roundness": { 948 | "type": 2 949 | }, 950 | "seed": 2081281071, 951 | "version": 289, 952 | "versionNonce": 84412065, 953 | "isDeleted": false, 954 | "boundElements": null, 955 | "updated": 1678886284125, 956 | "link": null, 957 | "locked": false, 958 | "points": [ 959 | [ 960 | 0, 961 | 0 962 | ], 963 | [ 964 | -2.6608469861869253, 965 | 341.5564923967633 966 | ] 967 | ], 968 | "lastCommittedPoint": null, 969 | "startBinding": { 970 | "elementId": "GdyILsCD4rTRfB2LlEajF", 971 | "focus": -0.42686679674955885, 972 | "gap": 1.6100261723224776 973 | }, 974 | "endBinding": { 975 | "elementId": "IGe7ziqsitN9OSWMLhiLf", 976 | "focus": -0.3749191069668723, 977 | "gap": 2.1593366350439283 978 | }, 979 | "startArrowhead": null, 980 | "endArrowhead": "arrow" 981 | }, 982 | { 983 | "id": "vFUg49T2Ao35trTckk354", 984 | "type": "arrow", 985 | "x": 565.5054847336945, 986 | "y": 93.0683090094169, 987 | "width": 5.062023271121802, 988 | "height": 329.6763916015624, 989 | "angle": 0, 990 | "strokeColor": "#000000", 991 | "backgroundColor": "#82c91e", 992 | "fillStyle": "hachure", 993 | "strokeWidth": 1, 994 | "strokeStyle": "solid", 995 | "roughness": 1, 996 | "opacity": 100, 997 | "groupIds": [], 998 | "roundness": { 999 | "type": 2 1000 | }, 1001 | "seed": 2024705519, 1002 | "version": 148, 1003 | "versionNonce": 806563361, 1004 | "isDeleted": false, 1005 | "boundElements": null, 1006 | "updated": 1678886288196, 1007 | "link": null, 1008 | "locked": false, 1009 | "points": [ 1010 | [ 1011 | 0, 1012 | 0 1013 | ], 1014 | [ 1015 | 5.062023271121802, 1016 | -329.6763916015624 1017 | ] 1018 | ], 1019 | "lastCommittedPoint": null, 1020 | "startBinding": { 1021 | "elementId": "IGe7ziqsitN9OSWMLhiLf", 1022 | "focus": -0.16537371595666883, 1023 | "gap": 8.411167689731428 1024 | }, 1025 | "endBinding": { 1026 | "elementId": "umf7pq1g-JfSQD3V1WxDT", 1027 | "focus": 0.8030811617046224, 1028 | "gap": 7.238470298996617 1029 | }, 1030 | "startArrowhead": "dot", 1031 | "endArrowhead": "arrow" 1032 | }, 1033 | { 1034 | "id": "49nTZMcdBYc1Kiao3IOS1", 1035 | "type": "arrow", 1036 | "x": 665.8861624762802, 1037 | "y": -240.51132443087323, 1038 | "width": 0.6533210448222917, 1039 | "height": 417.4794311523438, 1040 | "angle": 0, 1041 | "strokeColor": "#000000", 1042 | "backgroundColor": "#82c91e", 1043 | "fillStyle": "hachure", 1044 | "strokeWidth": 1, 1045 | "strokeStyle": "solid", 1046 | "roughness": 1, 1047 | "opacity": 100, 1048 | "groupIds": [], 1049 | "roundness": { 1050 | "type": 2 1051 | }, 1052 | "seed": 896074945, 1053 | "version": 184, 1054 | "versionNonce": 1219150337, 1055 | "isDeleted": false, 1056 | "boundElements": null, 1057 | "updated": 1678886288196, 1058 | "link": null, 1059 | "locked": false, 1060 | "points": [ 1061 | [ 1062 | 0, 1063 | 0 1064 | ], 1065 | [ 1066 | -0.6533210448222917, 1067 | 417.4794311523438 1068 | ] 1069 | ], 1070 | "lastCommittedPoint": null, 1071 | "startBinding": { 1072 | "elementId": "umf7pq1g-JfSQD3V1WxDT", 1073 | "focus": 0.039578561243246685, 1074 | "gap": 3.3352284602688655 1075 | }, 1076 | "endBinding": { 1077 | "elementId": "ucx_nOj_6dPlU0s7mSha2", 1078 | "focus": -0.02152566675656283, 1079 | "gap": 1.9528634207580353 1080 | }, 1081 | "startArrowhead": null, 1082 | "endArrowhead": "arrow" 1083 | }, 1084 | { 1085 | "id": "7mDoXq8CqYd8_0mg2hFvm", 1086 | "type": "arrow", 1087 | "x": 1269.4239537922572, 1088 | "y": 171.18825181075624, 1089 | "width": 3.163834906050397, 1090 | "height": 409.7439226422991, 1091 | "angle": 0, 1092 | "strokeColor": "#000000", 1093 | "backgroundColor": "#82c91e", 1094 | "fillStyle": "hachure", 1095 | "strokeWidth": 1, 1096 | "strokeStyle": "solid", 1097 | "roughness": 1, 1098 | "opacity": 100, 1099 | "groupIds": [], 1100 | "roundness": { 1101 | "type": 2 1102 | }, 1103 | "seed": 2113789103, 1104 | "version": 219, 1105 | "versionNonce": 1921817743, 1106 | "isDeleted": false, 1107 | "boundElements": null, 1108 | "updated": 1678886321073, 1109 | "link": null, 1110 | "locked": false, 1111 | "points": [ 1112 | [ 1113 | 0, 1114 | 0 1115 | ], 1116 | [ 1117 | -3.163834906050397, 1118 | -409.7439226422991 1119 | ] 1120 | ], 1121 | "lastCommittedPoint": null, 1122 | "startBinding": { 1123 | "elementId": "ucx_nOj_6dPlU0s7mSha2", 1124 | "focus": 0.8272303469686978, 1125 | "gap": 7.73271833147237 1126 | }, 1127 | "endBinding": { 1128 | "elementId": "JKpeXhwdrNYXIaUyA2iwS", 1129 | "focus": 0.10938832664170174, 1130 | "gap": 5.461257338617088 1131 | }, 1132 | "startArrowhead": "dot", 1133 | "endArrowhead": "arrow" 1134 | }, 1135 | { 1136 | "id": "OrW60dJMx1ubu6K8fEiFZ", 1137 | "type": "arrow", 1138 | "x": 724.4422819159001, 1139 | "y": -240.07505385330643, 1140 | "width": 89.9217871934618, 1141 | "height": 73.77314976283486, 1142 | "angle": 0, 1143 | "strokeColor": "#000000", 1144 | "backgroundColor": "#82c91e", 1145 | "fillStyle": "hachure", 1146 | "strokeWidth": 1, 1147 | "strokeStyle": "solid", 1148 | "roughness": 1, 1149 | "opacity": 100, 1150 | "groupIds": [], 1151 | "roundness": { 1152 | "type": 2 1153 | }, 1154 | "seed": 174511023, 1155 | "version": 166, 1156 | "versionNonce": 887413217, 1157 | "isDeleted": false, 1158 | "boundElements": [ 1159 | { 1160 | "type": "text", 1161 | "id": "zlam_TX4bFkuYywoWqukg" 1162 | } 1163 | ], 1164 | "updated": 1678886288196, 1165 | "link": null, 1166 | "locked": false, 1167 | "points": [ 1168 | [ 1169 | 0, 1170 | 0 1171 | ], 1172 | [ 1173 | 89.9217871934618, 1174 | 73.77314976283486 1175 | ] 1176 | ], 1177 | "lastCommittedPoint": null, 1178 | "startBinding": { 1179 | "elementId": "umf7pq1g-JfSQD3V1WxDT", 1180 | "focus": 0.06501328282059994, 1181 | "gap": 3.7714990378356674 1182 | }, 1183 | "endBinding": { 1184 | "elementId": "lSFZOqZs-2cpHDqEqLKwG", 1185 | "focus": 0.2978325169859957, 1186 | "gap": 3.3038853236596424 1187 | }, 1188 | "startArrowhead": null, 1189 | "endArrowhead": "arrow" 1190 | }, 1191 | { 1192 | "id": "zlam_TX4bFkuYywoWqukg", 1193 | "type": "text", 1194 | "x": 759.6974681616871, 1195 | "y": -203.16396463734668, 1196 | "width": 14.919998168945312, 1197 | "height": 24, 1198 | "angle": 0, 1199 | "strokeColor": "#000000", 1200 | "backgroundColor": "#82c91e", 1201 | "fillStyle": "hachure", 1202 | "strokeWidth": 1, 1203 | "strokeStyle": "solid", 1204 | "roughness": 1, 1205 | "opacity": 100, 1206 | "groupIds": [], 1207 | "roundness": null, 1208 | "seed": 1047317441, 1209 | "version": 8, 1210 | "versionNonce": 35105295, 1211 | "isDeleted": false, 1212 | "boundElements": null, 1213 | "updated": 1678886144097, 1214 | "link": null, 1215 | "locked": false, 1216 | "text": "W", 1217 | "fontSize": 20, 1218 | "fontFamily": 1, 1219 | "textAlign": "center", 1220 | "verticalAlign": "middle", 1221 | "containerId": "OrW60dJMx1ubu6K8fEiFZ", 1222 | "originalText": "W" 1223 | }, 1224 | { 1225 | "id": "XIJnxCtHrLXi_ryGGQEmU", 1226 | "type": "arrow", 1227 | "x": 867.1139571562085, 1228 | "y": -168.9670477846679, 1229 | "width": 99.43260949141745, 1230 | "height": 67.30368477957586, 1231 | "angle": 0, 1232 | "strokeColor": "#000000", 1233 | "backgroundColor": "#82c91e", 1234 | "fillStyle": "hachure", 1235 | "strokeWidth": 1, 1236 | "strokeStyle": "solid", 1237 | "roughness": 1, 1238 | "opacity": 100, 1239 | "groupIds": [], 1240 | "roundness": { 1241 | "type": 2 1242 | }, 1243 | "seed": 808449889, 1244 | "version": 120, 1245 | "versionNonce": 1269187119, 1246 | "isDeleted": false, 1247 | "boundElements": [ 1248 | { 1249 | "type": "text", 1250 | "id": "_pK5HYqCLFu8_-DyV9KNR" 1251 | } 1252 | ], 1253 | "updated": 1678886317348, 1254 | "link": null, 1255 | "locked": false, 1256 | "points": [ 1257 | [ 1258 | 0, 1259 | 0 1260 | ], 1261 | [ 1262 | 99.43260949141745, 1263 | -67.30368477957586 1264 | ] 1265 | ], 1266 | "lastCommittedPoint": null, 1267 | "startBinding": { 1268 | "elementId": "lSFZOqZs-2cpHDqEqLKwG", 1269 | "focus": -0.20289726524833537, 1270 | "gap": 5.9690290178559735 1271 | }, 1272 | "endBinding": { 1273 | "elementId": "_XVaZa0RKrBuvLhhaAr-6", 1274 | "focus": -0.38869878648466843, 1275 | "gap": 7.574556027233086 1276 | }, 1277 | "startArrowhead": null, 1278 | "endArrowhead": "arrow" 1279 | }, 1280 | { 1281 | "id": "_pK5HYqCLFu8_-DyV9KNR", 1282 | "type": "text", 1283 | "x": 908.1846060924211, 1284 | "y": -202.59500798974614, 1285 | "width": 13.55999755859375, 1286 | "height": 24, 1287 | "angle": 0, 1288 | "strokeColor": "#000000", 1289 | "backgroundColor": "#82c91e", 1290 | "fillStyle": "hachure", 1291 | "strokeWidth": 1, 1292 | "strokeStyle": "solid", 1293 | "roughness": 1, 1294 | "opacity": 100, 1295 | "groupIds": [], 1296 | "roundness": null, 1297 | "seed": 409153871, 1298 | "version": 4, 1299 | "versionNonce": 1204550369, 1300 | "isDeleted": false, 1301 | "boundElements": null, 1302 | "updated": 1678886141125, 1303 | "link": null, 1304 | "locked": false, 1305 | "text": "R", 1306 | "fontSize": 20, 1307 | "fontFamily": 1, 1308 | "textAlign": "center", 1309 | "verticalAlign": "middle", 1310 | "containerId": "XIJnxCtHrLXi_ryGGQEmU", 1311 | "originalText": "R" 1312 | }, 1313 | { 1314 | "id": "kkrWMTS6TyAOSsiNmeg_R", 1315 | "type": "arrow", 1316 | "x": 113.66865411837003, 1317 | "y": -357.26752385888665, 1318 | "width": 454.06317574193235, 1319 | "height": 104.90713936941967, 1320 | "angle": 0, 1321 | "strokeColor": "#000000", 1322 | "backgroundColor": "#82c91e", 1323 | "fillStyle": "hachure", 1324 | "strokeWidth": 1, 1325 | "strokeStyle": "solid", 1326 | "roughness": 1, 1327 | "opacity": 100, 1328 | "groupIds": [], 1329 | "roundness": { 1330 | "type": 2 1331 | }, 1332 | "seed": 203096673, 1333 | "version": 147, 1334 | "versionNonce": 190817103, 1335 | "isDeleted": false, 1336 | "boundElements": [ 1337 | { 1338 | "type": "text", 1339 | "id": "jluJou4jkrbRJhCDZpa_V" 1340 | } 1341 | ], 1342 | "updated": 1678886303620, 1343 | "link": null, 1344 | "locked": false, 1345 | "points": [ 1346 | [ 1347 | 0, 1348 | 0 1349 | ], 1350 | [ 1351 | 454.06317574193235, 1352 | -104.90713936941967 1353 | ] 1354 | ], 1355 | "lastCommittedPoint": null, 1356 | "startBinding": { 1357 | "elementId": "NKmNZxYxWMCKh3prRiPwX", 1358 | "focus": -0.6548060722527196, 1359 | "gap": 13.250595688726719 1360 | }, 1361 | "endBinding": { 1362 | "elementId": "1CgHRN2NXzOixDHRW6ZBt", 1363 | "focus": -0.4938447554482164, 1364 | "gap": 12.550419398717793 1365 | }, 1366 | "startArrowhead": "arrow", 1367 | "endArrowhead": "arrow" 1368 | }, 1369 | { 1370 | "id": "jluJou4jkrbRJhCDZpa_V", 1371 | "type": "text", 1372 | "x": 298.99273444935477, 1373 | "y": -451.0159849010183, 1374 | "width": 58.47999572753906, 1375 | "height": 24, 1376 | "angle": 0, 1377 | "strokeColor": "#000000", 1378 | "backgroundColor": "#82c91e", 1379 | "fillStyle": "hachure", 1380 | "strokeWidth": 1, 1381 | "strokeStyle": "solid", 1382 | "roughness": 1, 1383 | "opacity": 100, 1384 | "groupIds": [], 1385 | "roundness": null, 1386 | "seed": 526019329, 1387 | "version": 8, 1388 | "versionNonce": 992854543, 1389 | "isDeleted": false, 1390 | "boundElements": null, 1391 | "updated": 1678886207384, 1392 | "link": null, 1393 | "locked": false, 1394 | "text": "W / R", 1395 | "fontSize": 20, 1396 | "fontFamily": 1, 1397 | "textAlign": "center", 1398 | "verticalAlign": "middle", 1399 | "containerId": "kkrWMTS6TyAOSsiNmeg_R", 1400 | "originalText": "W / R" 1401 | }, 1402 | { 1403 | "id": "FDwW0dczTV9sPyxO0rm_x", 1404 | "type": "arrow", 1405 | "x": 648.1466088980474, 1406 | "y": -465.30210463455626, 1407 | "width": 272.545161457908, 1408 | "height": 109.94001116071422, 1409 | "angle": 0, 1410 | "strokeColor": "#000000", 1411 | "backgroundColor": "#82c91e", 1412 | "fillStyle": "hachure", 1413 | "strokeWidth": 1, 1414 | "strokeStyle": "solid", 1415 | "roughness": 1, 1416 | "opacity": 100, 1417 | "groupIds": [], 1418 | "roundness": { 1419 | "type": 2 1420 | }, 1421 | "seed": 931071151, 1422 | "version": 158, 1423 | "versionNonce": 1095524719, 1424 | "isDeleted": false, 1425 | "boundElements": [ 1426 | { 1427 | "type": "text", 1428 | "id": "iNItmDa4NmU4T3N6sN7HF" 1429 | } 1430 | ], 1431 | "updated": 1678886303621, 1432 | "link": null, 1433 | "locked": false, 1434 | "points": [ 1435 | [ 1436 | 0, 1437 | 0 1438 | ], 1439 | [ 1440 | -272.545161457908, 1441 | 109.94001116071422 1442 | ] 1443 | ], 1444 | "lastCommittedPoint": null, 1445 | "startBinding": { 1446 | "elementId": "1CgHRN2NXzOixDHRW6ZBt", 1447 | "focus": -0.5046537673543661, 1448 | "gap": 9.42297799246785 1449 | }, 1450 | "endBinding": { 1451 | "elementId": "GdyILsCD4rTRfB2LlEajF", 1452 | "focus": -0.5803959456311888, 1453 | "gap": 11.515714968860664 1454 | }, 1455 | "startArrowhead": null, 1456 | "endArrowhead": "arrow" 1457 | }, 1458 | { 1459 | "id": "iNItmDa4NmU4T3N6sN7HF", 1460 | "type": "text", 1461 | "x": 477.50909950062436, 1462 | "y": -423.49835010051606, 1463 | "width": 13.55999755859375, 1464 | "height": 24, 1465 | "angle": 0, 1466 | "strokeColor": "#000000", 1467 | "backgroundColor": "#82c91e", 1468 | "fillStyle": "hachure", 1469 | "strokeWidth": 1, 1470 | "strokeStyle": "solid", 1471 | "roughness": 1, 1472 | "opacity": 100, 1473 | "groupIds": [], 1474 | "roundness": null, 1475 | "seed": 133301793, 1476 | "version": 4, 1477 | "versionNonce": 1063753903, 1478 | "isDeleted": false, 1479 | "boundElements": null, 1480 | "updated": 1678886209917, 1481 | "link": null, 1482 | "locked": false, 1483 | "text": "R", 1484 | "fontSize": 20, 1485 | "fontFamily": 1, 1486 | "textAlign": "center", 1487 | "verticalAlign": "middle", 1488 | "containerId": "FDwW0dczTV9sPyxO0rm_x", 1489 | "originalText": "R" 1490 | }, 1491 | { 1492 | "id": "vB-m6btdWL7J-5_aUJ0z4", 1493 | "type": "arrow", 1494 | "x": 683.161273675161, 1495 | "y": -470.3126549972795, 1496 | "width": 0.637065864129454, 1497 | "height": 119.22498430524547, 1498 | "angle": 0, 1499 | "strokeColor": "#000000", 1500 | "backgroundColor": "#82c91e", 1501 | "fillStyle": "hachure", 1502 | "strokeWidth": 1, 1503 | "strokeStyle": "solid", 1504 | "roughness": 1, 1505 | "opacity": 100, 1506 | "groupIds": [], 1507 | "roundness": { 1508 | "type": 2 1509 | }, 1510 | "seed": 270056303, 1511 | "version": 139, 1512 | "versionNonce": 859593615, 1513 | "isDeleted": false, 1514 | "boundElements": [ 1515 | { 1516 | "type": "text", 1517 | "id": "PafJzLeltNFkUhNquHwGn" 1518 | } 1519 | ], 1520 | "updated": 1678886303621, 1521 | "link": null, 1522 | "locked": false, 1523 | "points": [ 1524 | [ 1525 | 0, 1526 | 0 1527 | ], 1528 | [ 1529 | 0.637065864129454, 1530 | 119.22498430524547 1531 | ] 1532 | ], 1533 | "lastCommittedPoint": null, 1534 | "startBinding": { 1535 | "elementId": "1CgHRN2NXzOixDHRW6ZBt", 1536 | "focus": -0.09772304900017086, 1537 | "gap": 4.412427629744627 1538 | }, 1539 | "endBinding": { 1540 | "elementId": "umf7pq1g-JfSQD3V1WxDT", 1541 | "focus": 0.10758983722445196, 1542 | "gap": 7.241117800891914 1543 | }, 1544 | "startArrowhead": null, 1545 | "endArrowhead": "arrow" 1546 | }, 1547 | { 1548 | "id": "PafJzLeltNFkUhNquHwGn", 1549 | "type": "text", 1550 | "x": 650.0941781487827, 1551 | "y": -423.52110749448934, 1552 | "width": 13.55999755859375, 1553 | "height": 24, 1554 | "angle": 0, 1555 | "strokeColor": "#000000", 1556 | "backgroundColor": "#82c91e", 1557 | "fillStyle": "hachure", 1558 | "strokeWidth": 1, 1559 | "strokeStyle": "solid", 1560 | "roughness": 1, 1561 | "opacity": 100, 1562 | "groupIds": [], 1563 | "roundness": null, 1564 | "seed": 258910927, 1565 | "version": 4, 1566 | "versionNonce": 724121953, 1567 | "isDeleted": false, 1568 | "boundElements": null, 1569 | "updated": 1678886212438, 1570 | "link": null, 1571 | "locked": false, 1572 | "text": "R", 1573 | "fontSize": 20, 1574 | "fontFamily": 1, 1575 | "textAlign": "center", 1576 | "verticalAlign": "middle", 1577 | "containerId": "vB-m6btdWL7J-5_aUJ0z4", 1578 | "originalText": "R" 1579 | }, 1580 | { 1581 | "id": "LjntQQpGvJGbNev3eraj3", 1582 | "type": "arrow", 1583 | "x": 754.8491288579188, 1584 | "y": -461.91615754331747, 1585 | "width": 495.8822993806085, 1586 | "height": 106.37331281389504, 1587 | "angle": 0, 1588 | "strokeColor": "#000000", 1589 | "backgroundColor": "#82c91e", 1590 | "fillStyle": "hachure", 1591 | "strokeWidth": 1, 1592 | "strokeStyle": "solid", 1593 | "roughness": 1, 1594 | "opacity": 100, 1595 | "groupIds": [], 1596 | "roundness": { 1597 | "type": 2 1598 | }, 1599 | "seed": 875342881, 1600 | "version": 123, 1601 | "versionNonce": 1124610735, 1602 | "isDeleted": false, 1603 | "boundElements": [ 1604 | { 1605 | "type": "text", 1606 | "id": "3PQRZujoB2ZtOJOC8-d4f" 1607 | } 1608 | ], 1609 | "updated": 1678886321074, 1610 | "link": null, 1611 | "locked": false, 1612 | "points": [ 1613 | [ 1614 | 0, 1615 | 0 1616 | ], 1617 | [ 1618 | 495.8822993806085, 1619 | 106.37331281389504 1620 | ] 1621 | ], 1622 | "lastCommittedPoint": null, 1623 | "startBinding": { 1624 | "elementId": "1CgHRN2NXzOixDHRW6ZBt", 1625 | "focus": 0.58623769949796, 1626 | "gap": 12.80892508370664 1627 | }, 1628 | "endBinding": { 1629 | "elementId": "JKpeXhwdrNYXIaUyA2iwS", 1630 | "focus": 0.725159318853772, 1631 | "gap": 11.525916559262441 1632 | }, 1633 | "startArrowhead": null, 1634 | "endArrowhead": "arrow" 1635 | }, 1636 | { 1637 | "id": "3PQRZujoB2ZtOJOC8-d4f", 1638 | "type": "text", 1639 | "x": 1020.9844630957691, 1640 | "y": -452.7244461175362, 1641 | "width": 13.55999755859375, 1642 | "height": 24, 1643 | "angle": 0, 1644 | "strokeColor": "#000000", 1645 | "backgroundColor": "#82c91e", 1646 | "fillStyle": "hachure", 1647 | "strokeWidth": 1, 1648 | "strokeStyle": "solid", 1649 | "roughness": 1, 1650 | "opacity": 100, 1651 | "groupIds": [], 1652 | "roundness": null, 1653 | "seed": 1667065153, 1654 | "version": 4, 1655 | "versionNonce": 1201275791, 1656 | "isDeleted": false, 1657 | "boundElements": null, 1658 | "updated": 1678886214987, 1659 | "link": null, 1660 | "locked": false, 1661 | "text": "R", 1662 | "fontSize": 20, 1663 | "fontFamily": 1, 1664 | "textAlign": "center", 1665 | "verticalAlign": "middle", 1666 | "containerId": "LjntQQpGvJGbNev3eraj3", 1667 | "originalText": "R" 1668 | } 1669 | ], 1670 | "appState": { 1671 | "gridSize": null, 1672 | "viewBackgroundColor": "#ffffff" 1673 | }, 1674 | "files": {} 1675 | } -------------------------------------------------------------------------------- /documentation/project-diagram.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/documentation/project-diagram.jpeg -------------------------------------------------------------------------------- /documentation/websocket-operation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/documentation/websocket-operation.gif -------------------------------------------------------------------------------- /documentation/zipkin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/documentation/zipkin.jpg -------------------------------------------------------------------------------- /eureka-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | spring-cloud-stream-kafka-elasticsearch 8 | 1.0.0 9 | ../pom.xml 10 | 11 | eureka-server 12 | eureka-server 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | org.springframework.cloud 34 | spring-cloud-starter-netflix-eureka-server 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /eureka-server/src/main/java/com/ivanfranchin/eurekaservice/EurekaServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.eurekaservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | @EnableEurekaServer 8 | @SpringBootApplication 9 | public class EurekaServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(EurekaServiceApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server.port: 8761 2 | 3 | spring: 4 | application: 5 | name: eureka-server 6 | 7 | eureka: 8 | client: 9 | registerWithEureka: false 10 | fetchRegistry: false 11 | 12 | management: 13 | endpoints: 14 | web: 15 | exposure.include: beans, env, health, metrics, mappings 16 | endpoint: 17 | health: 18 | show-details: always -------------------------------------------------------------------------------- /eureka-server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ 2 | ___ _ _ _ __ ___| | ____ _ ___ ___ _ ____ _____ _ __ 3 | / _ \ | | | '__/ _ \ |/ / _` |_____/ __|/ _ \ '__\ \ / / _ \ '__| 4 | | __/ |_| | | | __/ < (_| |_____\__ \ __/ | \ V / __/ | 5 | \___|\__,_|_| \___|_|\_\__,_| |___/\___|_| \_/ \___|_| 6 | :: Spring Boot :: ${spring-boot.formatted-version} 7 | -------------------------------------------------------------------------------- /eureka-server/src/test/java/com/ivanfranchin/eurekaservice/EurekaServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.eurekaservice; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class EurekaServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /news-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | spring-cloud-stream-kafka-elasticsearch 8 | 1.0.0 9 | ../pom.xml 10 | 11 | news-client 12 | news-client 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 1.1.5 29 | 1.0.0 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-actuator 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-thymeleaf 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-websocket 47 | 48 | 49 | io.micrometer 50 | micrometer-tracing-bridge-brave 51 | 52 | 53 | io.zipkin.reporter2 54 | zipkin-reporter-brave 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-starter-netflix-eureka-client 59 | 60 | 61 | org.springframework.cloud 62 | spring-cloud-starter-openfeign 63 | 64 | 65 | org.springframework.cloud 66 | spring-cloud-stream-binder-kafka 67 | 68 | 69 | org.springframework.kafka 70 | spring-kafka 71 | 72 | 73 | 74 | 75 | org.springframework.cloud 76 | spring-cloud-schema-registry-client 77 | ${spring-cloud-schema-registry-client.version} 78 | 79 | 80 | 81 | 82 | com.ivanfranchin 83 | commons-news 84 | ${ivanfranchin-commons-news.version} 85 | 86 | 87 | 88 | org.projectlombok 89 | lombok 90 | true 91 | 92 | 93 | org.springframework.boot 94 | spring-boot-starter-test 95 | test 96 | 97 | 98 | org.springframework.cloud 99 | spring-cloud-stream-test-binder 100 | test 101 | 102 | 103 | org.springframework.kafka 104 | spring-kafka-test 105 | test 106 | 107 | 108 | 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-compiler-plugin 114 | 115 | 116 | 117 | org.projectlombok 118 | lombok 119 | 120 | 121 | 122 | 123 | 124 | org.springframework.boot 125 | spring-boot-maven-plugin 126 | 127 | 128 | 129 | org.projectlombok 130 | lombok 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/NewsClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | 8 | @EnableFeignClients 9 | @EnableDiscoveryClient 10 | @SpringBootApplication 11 | public class NewsClientApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(NewsClientApplication.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/config/SchemaRegistryConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.cloud.schema.registry.avro.AvroSchemaMessageConverter; 5 | import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl; 6 | import org.springframework.cloud.schema.registry.client.ConfluentSchemaRegistryClient; 7 | import org.springframework.cloud.schema.registry.client.SchemaRegistryClient; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.messaging.converter.MessageConverter; 11 | import org.springframework.util.MimeType; 12 | 13 | @Configuration 14 | public class SchemaRegistryConfig { 15 | 16 | @Bean 17 | SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.schema-registry-client.endpoint}") String endpoint) { 18 | ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(); 19 | client.setEndpoint(endpoint); 20 | return client; 21 | } 22 | 23 | @Bean 24 | MessageConverter avroSchemaMessageConverter() { 25 | return new AvroSchemaMessageConverter(MimeType.valueOf("application/*+avro"), new AvroSchemaServiceManagerImpl()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/news-websocket").withSockJS(); 16 | } 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry registry) { 20 | registry.setApplicationDestinationPrefixes("/app"); 21 | registry.enableSimpleBroker("/topic"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/news/NewsController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.news; 2 | 3 | import com.ivanfranchin.newsclient.news.client.PublisherApiClient; 4 | import com.ivanfranchin.newsclient.news.dto.MyPage; 5 | import com.ivanfranchin.newsclient.news.dto.News; 6 | import com.ivanfranchin.newsclient.news.dto.SearchRequest; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.ModelAttribute; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | @Controller 19 | public class NewsController { 20 | 21 | private final PublisherApiClient publisherApiClient; 22 | 23 | @GetMapping(value = {"/"}) 24 | public String getNews(@RequestParam(required = false) Integer page, 25 | @RequestParam(required = false) Integer size, 26 | @RequestParam(required = false, defaultValue = "datetime,desc") String sort, 27 | Model model) { 28 | model.addAttribute("searchRequest", new SearchRequest()); 29 | model.addAttribute("newsList", publisherApiClient.listNewsByPage(page, size, sort)); 30 | return "news"; 31 | } 32 | 33 | @PostMapping("/search") 34 | public String searchNews(@RequestParam(required = false) Integer page, 35 | @RequestParam(required = false) Integer size, 36 | @RequestParam(required = false, defaultValue = "datetime,desc") String sort, 37 | @ModelAttribute SearchRequest searchRequest, 38 | Model model) { 39 | MyPage result; 40 | if (searchRequest.getText().trim().isEmpty()) { 41 | result = publisherApiClient.listNewsByPage(page, size, sort); 42 | } else { 43 | result = publisherApiClient.searchNewsByPage(searchRequest, page, size, sort); 44 | } 45 | model.addAttribute("newsList", result); 46 | return "news"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/news/NewsEventListener.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.news; 2 | 3 | import com.ivanfranchin.commonsnews.avro.NewsEvent; 4 | import com.ivanfranchin.newsclient.news.dto.News; 5 | import com.ivanfranchin.newsclient.util.DateTimeUtil; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.integration.IntegrationMessageHeaderAccessor; 10 | import org.springframework.kafka.support.KafkaHeaders; 11 | import org.springframework.messaging.Message; 12 | import org.springframework.messaging.MessageHeaders; 13 | import org.springframework.messaging.simp.SimpMessagingTemplate; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | import java.util.function.Consumer; 18 | 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | @Component 22 | public class NewsEventListener { 23 | 24 | private final SimpMessagingTemplate simpMessagingTemplate; 25 | 26 | @Bean 27 | Consumer> news() { 28 | return message -> { 29 | NewsEvent newsEvent = message.getPayload(); 30 | MessageHeaders messageHeaders = message.getHeaders(); 31 | log.info("NewsEvent with id '{}' and title '{}' received from bus. topic: {}, partition: {}, offset: {}, deliveryAttempt: {}", 32 | newsEvent.getId(), 33 | newsEvent.getTitle(), 34 | messageHeaders.get(KafkaHeaders.RECEIVED_TOPIC, String.class), 35 | messageHeaders.get(KafkaHeaders.RECEIVED_PARTITION, Integer.class), 36 | messageHeaders.get(KafkaHeaders.OFFSET, Long.class), 37 | messageHeaders.get(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, AtomicInteger.class)); 38 | 39 | simpMessagingTemplate.convertAndSend("/topic/news", createNews(newsEvent)); 40 | }; 41 | } 42 | 43 | private News createNews(NewsEvent newsEvent) { 44 | return new News( 45 | newsEvent.getId().toString(), 46 | newsEvent.getTitle().toString(), 47 | newsEvent.getText().toString(), 48 | DateTimeUtil.fromStringToDate(newsEvent.getDatetime().toString()), 49 | newsEvent.getCategory().toString() 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/news/client/PublisherApiClient.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.news.client; 2 | 3 | import com.ivanfranchin.newsclient.news.dto.MyPage; 4 | import com.ivanfranchin.newsclient.news.dto.News; 5 | import com.ivanfranchin.newsclient.news.dto.SearchRequest; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PutMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | 12 | @FeignClient("publisher-api") 13 | public interface PublisherApiClient { 14 | 15 | @GetMapping("/api/news") 16 | MyPage listNewsByPage(@RequestParam Integer page, @RequestParam Integer size, 17 | @RequestParam String sort); 18 | 19 | @PutMapping("/api/news/search") 20 | MyPage searchNewsByPage(@RequestBody SearchRequest searchRequest, @RequestParam Integer page, 21 | @RequestParam Integer size, @RequestParam String sort); 22 | } 23 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/news/dto/MyPage.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.news.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class MyPage { 9 | 10 | private List content; 11 | private Integer totalElements; 12 | private Integer totalPages; 13 | private Integer size; 14 | private Integer numberOfElements; 15 | private Boolean first; 16 | private Boolean last; 17 | } 18 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/news/dto/News.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.news.dto; 2 | 3 | import java.util.Date; 4 | 5 | public record News(String id, String title, String text, Date datetime, String category) { 6 | } 7 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/news/dto/SearchRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.news.dto; 2 | 3 | import lombok.Data; 4 | 5 | /* This class cannot be converted to a record because it is passed as an argument to the UI model 6 | to be updated in the Thymeleaf template. */ 7 | @Data 8 | public class SearchRequest { 9 | 10 | private String text; 11 | } 12 | -------------------------------------------------------------------------------- /news-client/src/main/java/com/ivanfranchin/newsclient/util/DateTimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient.util; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.ZoneId; 5 | import java.time.ZonedDateTime; 6 | import java.time.format.DateTimeFormatter; 7 | import java.util.Date; 8 | 9 | public final class DateTimeUtil { 10 | 11 | private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ssX"; 12 | private static final ZoneId ZONE_ID = ZoneId.of("UTC"); 13 | private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern(PATTERN); 14 | 15 | private DateTimeUtil() { 16 | throw new UnsupportedOperationException("Utility class cannot be instantiated"); 17 | } 18 | 19 | public static Date fromStringToDate(String dateTimeString) { 20 | if (dateTimeString == null || dateTimeString.isEmpty()) { 21 | throw new IllegalArgumentException("Input string cannot be null or empty"); 22 | } 23 | LocalDateTime localDateTime = LocalDateTime.parse(dateTimeString, DTF); 24 | ZonedDateTime zonedDateTime = localDateTime.atZone(ZONE_ID); 25 | return Date.from(zonedDateTime.toInstant()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /news-client/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: news-client 4 | cloud: 5 | schema-registry-client: 6 | endpoint: http://${SCHEMA_REGISTRY_HOST:localhost}:${SCHEMA_REGISTRY_PORT:8081} 7 | stream: 8 | kafka: 9 | binder: 10 | brokers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:29092} 11 | auto-add-partitions: true 12 | min-partition-count: 2 13 | enable-observation: true 14 | bindings: 15 | news-in-0: 16 | destination: com.ivanfranchin.newspipeline.collector.news 17 | content-type: application/*+avro 18 | group: newsClientGroup 19 | consumer: 20 | max-attempts: 4 21 | back-off-initial-interval: 10000 22 | main: 23 | allow-bean-definition-overriding: true 24 | 25 | management: 26 | endpoints: 27 | web: 28 | exposure.include: beans, env, health, metrics, mappings 29 | endpoint: 30 | health: 31 | show-details: always 32 | tracing: 33 | sampling: 34 | probability: 1.0 35 | zipkin: 36 | tracing: 37 | endpoint: http://${ZIPKIN_HOST:localhost}:${ZIPKIN_PORT:9411}/api/v2/spans 38 | 39 | eureka: 40 | client: 41 | serviceUrl: 42 | defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka 43 | instance: 44 | preferIpAddress: true 45 | -------------------------------------------------------------------------------- /news-client/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | _ __ _____ _____ ___| (_) ___ _ __ | |_ 3 | | '_ \ / _ \ \ /\ / / __|_____ / __| | |/ _ \ '_ \| __| 4 | | | | | __/\ V V /\__ \_____| (__| | | __/ | | | |_ 5 | |_| |_|\___| \_/\_/ |___/ \___|_|_|\___|_| |_|\__| 6 | :: Spring Boot :: ${spring-boot.formatted-version} 7 | -------------------------------------------------------------------------------- /news-client/src/main/resources/public/img/Entertainment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/news-client/src/main/resources/public/img/Entertainment.jpg -------------------------------------------------------------------------------- /news-client/src/main/resources/public/img/Health.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/news-client/src/main/resources/public/img/Health.jpg -------------------------------------------------------------------------------- /news-client/src/main/resources/public/img/Science.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/news-client/src/main/resources/public/img/Science.jpg -------------------------------------------------------------------------------- /news-client/src/main/resources/public/img/Sport.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/news-client/src/main/resources/public/img/Sport.jpg -------------------------------------------------------------------------------- /news-client/src/main/resources/public/img/World.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/spring-cloud-stream-kafka-elasticsearch/baef5c86bf95963081c885e9b78338d4c3ac5c48/news-client/src/main/resources/public/img/World.jpg -------------------------------------------------------------------------------- /news-client/src/main/resources/static/app.js: -------------------------------------------------------------------------------- 1 | let stompClient = null 2 | 3 | function connect() { 4 | const socket = new SockJS('/news-websocket') 5 | stompClient = Stomp.over(socket) 6 | 7 | const $webSocketSwitch = $('#webSocketSwitch') 8 | 9 | stompClient.connect({}, 10 | function (frame) { 11 | console.log('Connected: ' + frame) 12 | stompClient.subscribe('/topic/news', function (news) { 13 | const newsBody = JSON.parse(news.body) 14 | const newsItem = '
' + 15 | '
' + 16 | '' + 17 | '
' + 18 | '
' + 19 | '
' + 20 | ''+moment(newsBody.datetime).format("DD-MMM-YYYY HH:mm:ss")+'' + 21 | '
' + 22 | '
'+newsBody.category.toUpperCase()+'
' + 23 | '
' + 24 | '
'+newsBody.title+'
' + 25 | '
' + 26 | '

'+newsBody.text+'

' + 27 | '
' + 28 | '
' + 29 | '
' 30 | 31 | $('#newsList').prepend(newsItem) 32 | }) 33 | $webSocketSwitch.removeClass('disabled') 34 | }, 35 | function() { 36 | console.log('Unable to connect to Websocket!') 37 | $webSocketSwitch.addClass('disabled') 38 | } 39 | ) 40 | } 41 | 42 | function disconnect() { 43 | if (stompClient !== null) { 44 | stompClient.disconnect() 45 | } 46 | console.log("Disconnected") 47 | } 48 | 49 | $(function () { 50 | $('#webSocketSwitch').checkbox({ 51 | onChecked: function() { connect() }, 52 | onUnchecked: function() { disconnect() } 53 | }) 54 | }) 55 | 56 | connect() -------------------------------------------------------------------------------- /news-client/src/main/resources/templates/news.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | News-Client 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 39 |
40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | Datetime 51 |
52 |
Category
53 |
54 |
Title
55 |
56 |

Text

57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /news-client/src/test/java/com/ivanfranchin/newsclient/NewsClientApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.newsclient; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class NewsClientApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 3.4.0 8 | 9 | 10 | com.ivanfranchin 11 | spring-cloud-stream-kafka-elasticsearch 12 | 1.0.0 13 | pom 14 | spring-cloud-stream-kafka-elasticsearch 15 | News Pipeline 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 21 31 | 2024.0.0 32 | 3.4.4 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-stream 38 | 39 | 40 | 41 | 42 | 43 | org.springframework.cloud 44 | spring-cloud-dependencies 45 | ${spring-cloud.version} 46 | pom 47 | import 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | com.google.cloud.tools 56 | jib-maven-plugin 57 | ${jib-maven-plugin.version} 58 | 59 | 60 | 61 | 62 | 63 | eureka-server 64 | producer-api 65 | categorizer-service 66 | collector-service 67 | publisher-api 68 | news-client 69 | commons-news 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /producer-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | spring-cloud-stream-kafka-elasticsearch 8 | 1.0.0 9 | ../pom.xml 10 | 11 | producer-api 12 | producer-api 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 1.1.5 29 | 1.12.0 30 | 1.0.0 31 | 2.7.0 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-actuator 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-validation 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-web 45 | 46 | 47 | io.micrometer 48 | micrometer-tracing-bridge-brave 49 | 50 | 51 | io.zipkin.reporter2 52 | zipkin-reporter-brave 53 | 54 | 55 | org.springframework.cloud 56 | spring-cloud-starter-netflix-eureka-client 57 | 58 | 59 | org.springframework.cloud 60 | spring-cloud-stream-binder-kafka 61 | 62 | 63 | org.springframework.kafka 64 | spring-kafka 65 | 66 | 67 | 68 | 69 | org.springframework.cloud 70 | spring-cloud-schema-registry-client 71 | ${spring-cloud-schema-registry-client.version} 72 | 73 | 74 | 75 | 76 | org.apache.avro 77 | avro 78 | ${avro.version} 79 | 80 | 81 | 82 | 83 | com.ivanfranchin 84 | commons-news 85 | ${ivanfranchin-commons-news.version} 86 | 87 | 88 | 89 | 90 | org.springdoc 91 | springdoc-openapi-starter-webmvc-ui 92 | ${springdoc-openapi.version} 93 | 94 | 95 | 96 | org.projectlombok 97 | lombok 98 | true 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-starter-test 103 | test 104 | 105 | 106 | org.springframework.cloud 107 | spring-cloud-stream-test-binder 108 | test 109 | 110 | 111 | org.springframework.kafka 112 | spring-kafka-test 113 | test 114 | 115 | 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-compiler-plugin 122 | 123 | 124 | 125 | org.projectlombok 126 | lombok 127 | 128 | 129 | 130 | 131 | 132 | org.springframework.boot 133 | spring-boot-maven-plugin 134 | 135 | 136 | 137 | org.projectlombok 138 | lombok 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/ProducerApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @EnableDiscoveryClient 8 | @SpringBootApplication 9 | public class ProducerApiApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ProducerApiApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, 22 | options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 23 | } 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/config/SchemaRegistryConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.cloud.schema.registry.avro.AvroSchemaMessageConverter; 5 | import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl; 6 | import org.springframework.cloud.schema.registry.client.ConfluentSchemaRegistryClient; 7 | import org.springframework.cloud.schema.registry.client.SchemaRegistryClient; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.messaging.converter.MessageConverter; 11 | import org.springframework.util.MimeType; 12 | 13 | @Configuration 14 | public class SchemaRegistryConfig { 15 | 16 | @Bean 17 | SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.schema-registry-client.endpoint}") String endpoint) { 18 | ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(); 19 | client.setEndpoint(endpoint); 20 | return client; 21 | } 22 | 23 | @Bean 24 | MessageConverter avroSchemaMessageConverter() { 25 | return new AvroSchemaMessageConverter(MimeType.valueOf("application/*+avro"), new AvroSchemaServiceManagerImpl()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springdoc.core.models.GroupedOpenApi; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | 14 | @Value("${spring.application.name}") 15 | private String applicationName; 16 | 17 | @Bean 18 | OpenAPI customOpenAPI() { 19 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 20 | } 21 | 22 | @Bean 23 | GroupedOpenApi customApi() { 24 | return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build(); 25 | } 26 | 27 | @Bean 28 | GroupedOpenApi actuatorApi() { 29 | return GroupedOpenApi.builder().group("actuator").pathsToMatch("/actuator/**").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/news/CreateNewsRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.news; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record CreateNewsRequest( 7 | @Schema(title = "the title of the news", example = "Brazil") @NotBlank String title, 8 | @Schema(title = "the text of the news", example = "This news is about Brasilia") @NotBlank String text) { 9 | } 10 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/news/News.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.news; 2 | 3 | import java.time.Instant; 4 | import java.util.UUID; 5 | 6 | public record News(String id, String title, String text, String datetime) { 7 | 8 | public static News from(CreateNewsRequest createNewsRequest) { 9 | return new News( 10 | UUID.randomUUID().toString(), 11 | createNewsRequest.title(), 12 | createNewsRequest.text(), 13 | Instant.ofEpochSecond(Instant.now().getEpochSecond()).toString()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/news/NewsController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.news; 2 | 3 | import jakarta.validation.Valid; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RequiredArgsConstructor 13 | @RestController 14 | @RequestMapping("/api/news") 15 | public class NewsController { 16 | 17 | private final NewsEventEmitter newsEventEmitter; 18 | 19 | @ResponseStatus(HttpStatus.CREATED) 20 | @PostMapping 21 | public News createNew(@Valid @RequestBody CreateNewsRequest createNewsRequest) { 22 | News news = News.from(createNewsRequest); 23 | newsEventEmitter.newsCreated(news); 24 | return news; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /producer-api/src/main/java/com/ivanfranchin/producerapi/news/NewsEventEmitter.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi.news; 2 | 3 | import com.ivanfranchin.commonsnews.avro.NewsEvent; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.cloud.stream.function.StreamBridge; 8 | import org.springframework.messaging.Message; 9 | import org.springframework.messaging.support.MessageBuilder; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.util.MimeType; 12 | 13 | import java.time.Instant; 14 | import java.util.UUID; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | @Component 19 | public class NewsEventEmitter { 20 | 21 | private final StreamBridge streamBridge; 22 | 23 | @Value("${spring.cloud.stream.bindings.news-out-0.content-type}") 24 | private String newsOutMimeType; 25 | 26 | public void newsCreated(News news) { 27 | NewsEvent newsEvent = NewsEvent.newBuilder() 28 | .setId(news.id()) 29 | .setTitle(news.title()) 30 | .setText(news.text()) 31 | .setDatetime(news.datetime()) 32 | .build(); 33 | 34 | Message message = MessageBuilder.withPayload(newsEvent) 35 | .setHeader("partitionKey", newsEvent.getId().toString()) 36 | .build(); 37 | streamBridge.send("news-out-0", message, MimeType.valueOf(newsOutMimeType)); 38 | 39 | log.info("NewsEvent with id '{}' and title '{}' sent to bus.", message.getPayload().getId(), message.getPayload().getTitle()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /producer-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: producer-api 4 | cloud: 5 | schema-registry-client: 6 | endpoint: http://${SCHEMA_REGISTRY_HOST:localhost}:${SCHEMA_REGISTRY_PORT:8081} 7 | schema: 8 | avro: 9 | schema-locations: 10 | - classpath:avro/news-event.avsc 11 | stream: 12 | kafka: 13 | binder: 14 | brokers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:29092} 15 | auto-add-partitions: true 16 | enable-observation: true 17 | output-bindings: news-out-0 18 | bindings: 19 | news-out-0: 20 | destination: com.ivanfranchin.newspipeline.producer.news 21 | content-type: application/*+avro 22 | producer: 23 | partition-key-expression: headers['partitionKey'] 24 | partition-count: 2 25 | main: 26 | allow-bean-definition-overriding: true 27 | 28 | management: 29 | endpoints: 30 | web: 31 | exposure.include: beans, env, health, metrics, mappings 32 | endpoint: 33 | health: 34 | show-details: always 35 | tracing: 36 | sampling: 37 | probability: 1.0 38 | zipkin: 39 | tracing: 40 | endpoint: http://${ZIPKIN_HOST:localhost}:${ZIPKIN_PORT:9411}/api/v2/spans 41 | 42 | eureka: 43 | client: 44 | serviceUrl: 45 | defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka 46 | instance: 47 | preferIpAddress: true 48 | 49 | springdoc: 50 | show-actuator: true 51 | swagger-ui: 52 | groups-order: DESC 53 | disable-swagger-default-url: true 54 | -------------------------------------------------------------------------------- /producer-api/src/main/resources/avro/news-event.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "com.ivanfranchin.commonsnews.avro", 3 | "type": "record", 4 | "name": "NewsEvent", 5 | "fields": [ 6 | {"name": "id", "type": "string"}, 7 | {"name": "title", "type": "string"}, 8 | {"name": "text", "type": "string"}, 9 | {"name": "datetime", "type": "string"}, 10 | {"name": "category", "type": ["null", "string"], "default": null} 11 | ] 12 | } -------------------------------------------------------------------------------- /producer-api/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | _ __ _ __ ___ __| |_ _ ___ ___ _ __ __ _ _ __ (_) 3 | | '_ \| '__/ _ \ / _` | | | |/ __/ _ \ '__|____ / _` | '_ \| | 4 | | |_) | | | (_) | (_| | |_| | (_| __/ | |_____| (_| | |_) | | 5 | | .__/|_| \___/ \__,_|\__,_|\___\___|_| \__,_| .__/|_| 6 | |_| |_| 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /producer-api/src/test/java/com/ivanfranchin/producerapi/ProducerApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.producerapi; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class ProducerApiApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /publisher-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | spring-cloud-stream-kafka-elasticsearch 8 | 1.0.0 9 | ../pom.xml 10 | 11 | publisher-api 12 | publisher-api 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 2.7.0 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-elasticsearch 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-validation 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-web 46 | 47 | 48 | io.micrometer 49 | micrometer-tracing-bridge-brave 50 | 51 | 52 | io.zipkin.reporter2 53 | zipkin-reporter-brave 54 | 55 | 56 | org.springframework.cloud 57 | spring-cloud-starter-netflix-eureka-client 58 | 59 | 60 | 61 | 62 | org.springdoc 63 | springdoc-openapi-starter-webmvc-ui 64 | ${springdoc-openapi.version} 65 | 66 | 67 | 68 | org.projectlombok 69 | lombok 70 | true 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-starter-test 75 | test 76 | 77 | 78 | org.springframework.cloud 79 | spring-cloud-stream-test-binder 80 | test 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-compiler-plugin 89 | 90 | 91 | 92 | org.projectlombok 93 | lombok 94 | 95 | 96 | 97 | 98 | 99 | org.springframework.boot 100 | spring-boot-maven-plugin 101 | 102 | 103 | 104 | org.projectlombok 105 | lombok 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/PublisherApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @EnableDiscoveryClient 8 | @SpringBootApplication 9 | public class PublisherApiApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(PublisherApiApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, 22 | options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 23 | } 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/config/SpringDataWebSupportConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 5 | 6 | import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO; 7 | 8 | /* 9 | The reason this configuration class was added is to prevent the following WARN message from occurring. 10 | 11 | WARN Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure! 12 | For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)) 13 | or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables. 14 | */ 15 | @Configuration 16 | @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) 17 | public class SpringDataWebSupportConfig { 18 | } 19 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springdoc.core.models.GroupedOpenApi; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | 14 | @Value("${spring.application.name}") 15 | private String applicationName; 16 | 17 | @Bean 18 | OpenAPI customOpenAPI() { 19 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 20 | } 21 | 22 | @Bean 23 | GroupedOpenApi customApi() { 24 | return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build(); 25 | } 26 | 27 | @Bean 28 | GroupedOpenApi actuatorApi() { 29 | return GroupedOpenApi.builder().group("actuator").pathsToMatch("/actuator/**").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/news/NewsController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.news; 2 | 3 | import com.ivanfranchin.publisherapi.news.dto.SearchRequest; 4 | import com.ivanfranchin.publisherapi.news.model.News; 5 | import io.swagger.v3.oas.annotations.Operation; 6 | import jakarta.validation.Valid; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springdoc.core.annotations.ParameterObject; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RequiredArgsConstructor 19 | @RestController 20 | @RequestMapping("/api/news") 21 | public class NewsController { 22 | 23 | private final NewsService newsService; 24 | 25 | @GetMapping 26 | public Page getNews(@ParameterObject Pageable pageable) { 27 | return newsService.listAllNewsByPage(pageable); 28 | } 29 | 30 | @GetMapping("/{id}") 31 | public News getNewsById(@PathVariable String id) { 32 | return newsService.validateAndGetNewsById(id); 33 | } 34 | 35 | @Operation(description = "This endpoint queries the 'string' provided in the fields 'title', 'text', and 'category'.") 36 | @PutMapping("/search") 37 | public Page searchNews(@Valid @RequestBody SearchRequest searchRequest, @ParameterObject Pageable pageable) { 38 | return newsService.search(searchRequest.text(), pageable); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/news/NewsRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.news; 2 | 3 | import com.ivanfranchin.publisherapi.news.model.News; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface NewsRepository extends ElasticsearchRepository { 11 | 12 | Page findByTitleOrTextOrCategoryAllIgnoreCase(String title, String text, String category, Pageable pageable); 13 | } 14 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/news/NewsService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.news; 2 | 3 | import com.ivanfranchin.publisherapi.news.exception.NewsNotFoundException; 4 | import com.ivanfranchin.publisherapi.news.model.News; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class NewsService { 13 | 14 | private final NewsRepository newsRepository; 15 | 16 | public News validateAndGetNewsById(String id) { 17 | return newsRepository.findById(id).orElseThrow(() -> new NewsNotFoundException(id)); 18 | } 19 | 20 | public Page listAllNewsByPage(Pageable pageable) { 21 | return newsRepository.findAll(pageable); 22 | } 23 | 24 | public Page search(String text, Pageable pageable) { 25 | return newsRepository.findByTitleOrTextOrCategoryAllIgnoreCase(text, text, text, pageable); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/news/dto/SearchRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.news.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record SearchRequest(@Schema(title = "text to be searched", example = "Brazil") @NotBlank String text) { 7 | } 8 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/news/exception/NewsNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.news.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class NewsNotFoundException extends RuntimeException { 8 | 9 | public NewsNotFoundException(String id) { 10 | super(String.format("News with id '%s' doesn't exist", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /publisher-api/src/main/java/com/ivanfranchin/publisherapi/news/model/News.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi.news.model; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.elasticsearch.annotations.Document; 6 | 7 | @Data 8 | @Document(indexName = "news", createIndex = false) 9 | public class News { 10 | 11 | @Id 12 | private String id; 13 | private String title; 14 | private String text; 15 | private String category; 16 | private String datetime; 17 | } 18 | -------------------------------------------------------------------------------- /publisher-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: publisher-api 4 | elasticsearch: 5 | uris: http://${ELASTICSEARCH_HOST:localhost}:${ELASTICSEARCH_REST_PORT:9200} 6 | 7 | management: 8 | endpoints: 9 | web: 10 | exposure.include: beans, env, health, metrics, mappings 11 | endpoint: 12 | health: 13 | show-details: always 14 | tracing: 15 | sampling: 16 | probability: 1.0 17 | zipkin: 18 | tracing: 19 | endpoint: http://${ZIPKIN_HOST:localhost}:${ZIPKIN_PORT:9411}/api/v2/spans 20 | 21 | eureka: 22 | client: 23 | serviceUrl: 24 | defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka 25 | instance: 26 | preferIpAddress: true 27 | 28 | springdoc: 29 | show-actuator: true 30 | swagger-ui: 31 | groups-order: DESC 32 | disable-swagger-default-url: true 33 | -------------------------------------------------------------------------------- /publisher-api/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ _ _ 2 | _ __ _ _| |__ | (_)___| |__ ___ _ __ __ _ _ __ (_) 3 | | '_ \| | | | '_ \| | / __| '_ \ / _ \ '__|____ / _` | '_ \| | 4 | | |_) | |_| | |_) | | \__ \ | | | __/ | |_____| (_| | |_) | | 5 | | .__/ \__,_|_.__/|_|_|___/_| |_|\___|_| \__,_| .__/|_| 6 | |_| |_| 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /publisher-api/src/test/java/com/ivanfranchin/publisherapi/PublisherApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.publisherapi; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class PublisherApiApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /remove-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker rmi ivanfranchin/eureka-server:1.0.0 4 | docker rmi ivanfranchin/producer-api:1.0.0 5 | docker rmi ivanfranchin/categorizer-service:1.0.0 6 | docker rmi ivanfranchin/collector-service:1.0.0 7 | docker rmi ivanfranchin/publisher-api:1.0.0 8 | docker rmi ivanfranchin/news-client:1.0.0 9 | -------------------------------------------------------------------------------- /scripts/my-functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TIMEOUT=120 4 | 5 | # -- wait_for_container_log -- 6 | # $1: docker container name 7 | # S2: spring value to wait to appear in container logs 8 | function wait_for_container_log() { 9 | local log_waiting="Waiting for string '$2' in the $1 logs ..." 10 | echo "${log_waiting} It will timeout in ${TIMEOUT}s" 11 | SECONDS=0 12 | 13 | while true ; do 14 | local log=$(docker logs $1 2>&1 | grep "$2") 15 | if [ -n "$log" ] ; then 16 | echo $log 17 | break 18 | fi 19 | 20 | if [ $SECONDS -ge $TIMEOUT ] ; then 21 | echo "${log_waiting} TIMEOUT" 22 | break; 23 | fi 24 | sleep 1 25 | done 26 | } 27 | -------------------------------------------------------------------------------- /start-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source scripts/my-functions.sh 4 | 5 | echo 6 | echo "Starting eureka..." 7 | 8 | docker run -d --rm --name eureka -p 8761:8761 \ 9 | --network spring-cloud-stream-kafka-elasticsearch_default \ 10 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/8761)" ] || exit 1' \ 11 | ivanfranchin/eureka-server:1.0.0 12 | 13 | wait_for_container_log "eureka" "Started" 14 | 15 | echo 16 | echo "Starting producer-api..." 17 | 18 | docker run -d --rm --name producer-api -p 9080:8080 \ 19 | -e KAFKA_HOST=kafka -e KAFKA_PORT=9092 -e SCHEMA_REGISTRY_HOST=schema-registry -e EUREKA_HOST=eureka -e ZIPKIN_HOST=zipkin \ 20 | --network=spring-cloud-stream-kafka-elasticsearch_default \ 21 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9080)" ] || exit 1' \ 22 | ivanfranchin/producer-api:1.0.0 23 | 24 | wait_for_container_log "producer-api" "Started" 25 | 26 | echo 27 | echo "Starting categorizer-service..." 28 | 29 | docker run -d --rm --name categorizer-service -p 9081:8080 \ 30 | -e KAFKA_HOST=kafka -e KAFKA_PORT=9092 -e SCHEMA_REGISTRY_HOST=schema-registry -e EUREKA_HOST=eureka -e ZIPKIN_HOST=zipkin \ 31 | --network=spring-cloud-stream-kafka-elasticsearch_default \ 32 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9081)" ] || exit 1' \ 33 | ivanfranchin/categorizer-service:1.0.0 34 | 35 | wait_for_container_log "categorizer-service" "Started" 36 | 37 | echo 38 | echo "Starting collector-service..." 39 | 40 | docker run -d --rm --name collector-service -p 9082:8080 \ 41 | -e ELASTICSEARCH_HOST=elasticsearch -e KAFKA_HOST=kafka -e KAFKA_PORT=9092 -e SCHEMA_REGISTRY_HOST=schema-registry -e EUREKA_HOST=eureka -e ZIPKIN_HOST=zipkin \ 42 | --network=spring-cloud-stream-kafka-elasticsearch_default \ 43 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9082)" ] || exit 1' \ 44 | ivanfranchin/collector-service:1.0.0 45 | 46 | wait_for_container_log "collector-service" "Started" 47 | 48 | echo 49 | echo "Starting publisher-api..." 50 | 51 | docker run -d --rm --name publisher-api -p 9083:8080 \ 52 | -e ELASTICSEARCH_HOST=elasticsearch -e EUREKA_HOST=eureka -e ZIPKIN_HOST=zipkin \ 53 | --network=spring-cloud-stream-kafka-elasticsearch_default \ 54 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9083)" ] || exit 1' \ 55 | ivanfranchin/publisher-api:1.0.0 56 | 57 | wait_for_container_log "publisher-api" "Started" 58 | 59 | echo 60 | echo "Starting news-client..." 61 | 62 | docker run -d --rm --name news-client -p 8080:8080 \ 63 | -e KAFKA_HOST=kafka -e KAFKA_PORT=9092 -e SCHEMA_REGISTRY_HOST=schema-registry -e EUREKA_HOST=eureka -e ZIPKIN_HOST=zipkin \ 64 | --network=spring-cloud-stream-kafka-elasticsearch_default \ 65 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/8080)" ] || exit 1' \ 66 | ivanfranchin/news-client:1.0.0 67 | 68 | wait_for_container_log "news-client" "Started" 69 | 70 | printf "\n" 71 | printf "%14s | %37s |\n" "Application" "URL" 72 | printf "%14s + %37s |\n" "--------------" "-------------------------------------" 73 | printf "%14s | %37s |\n" "producer-api" "http://localhost:9080/swagger-ui.html" 74 | printf "%14s | %37s |\n" "publisher-api" "http://localhost:9083/swagger-ui.html" 75 | printf "%14s | %37s |\n" "news-client" "http://localhost:8080" 76 | printf "\n" 77 | -------------------------------------------------------------------------------- /stop-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker stop producer-api categorizer-service collector-service publisher-api news-client eureka 4 | --------------------------------------------------------------------------------