Статьи

Сборка игры Mancala в микросервисах с использованием Spring Boot (часть 2: реализация Mancala API)

Манкала для всех! 

В предыдущей статье « Создание игры Mancala на микросервисах с использованием Spring Boot (часть 1: Архитектура решения) » я объяснил общую архитектуру решения, необходимого для реализации игры Mancala с использованием подхода Microservices.


Вам также может понравиться:
Сборка игры Mancala в микросервисах с использованием Spring Boot (Часть 3: ٌ Разработка микросервиса веб-клиента с Vaadin)

В этой статье я собираюсь обсудить детальную реализацию проекта » mancala-api «, который считается бэкэнд-реализацией API для этой игры на основе Spring Boot. Дизайн микросервиса mancala-api ‘ основан на принципе SOLID,  поэтому вы ожидаете увидеть множество классов и интерфейсов, предназначенных для выполнения одной уникальной операции в приложении.

Полный исходный код этой статьи доступен в моем репозитории GitHub  .

Чтобы создать и запустить это приложение, пожалуйста, следуйте инструкциям, которые я предоставил здесь .

1 — пружинный инициализатор

Как всегда, первым шагом для создания загрузочного приложения Spring для многих разработчиков является использование инициализатора Spring с использованием start.spring.io :

  1. Группа: com.dzone.mancalagame
  2. Артефакт: mancala-API
  3. Выберите ниже зависимости:

Теперь давайте углубимся в детали реализации Game API :

2 — Модели данных

Вот классы домена, определенные для этого проекта:

2.1 — KalahaConstants

Этот класс содержит все константы, которые я определил для реализации ‘ mancala-api ‘:

public class KalahaConstants {
    public static final int leftPitHouseId = 14;
    public static final int totalPits = 14;
    public static final int rightPitHouseId = 7;
    public static final int emptyStone = 0;

    public static final int defaultPitStones = 6;
    public static final int firstPitPlayerA = 1;
    public static final int secondPitPlayerA = 2;
    public static final int thirdPitPlayerA = 3;
    public static final int forthPitPlayerA = 4;
    public static final int fifthPitPlayerA = 5;
    public static final int sixthPitPlayerA = 6;

    public static final int firstPitPlayerB = 8;
    public static final int secondPitPlayerB = 9;
    public static final int thirdPitPlayerB = 10;
    public static final int forthPitPlayerB = 11;
    public static final int fifthPitPlayerB = 12;
    public static final int sixthPitPlayerB = 13;

}

2.2 — KalahaGame

This class contains all information about Mancala game including the number of stones, player turn, and collection of KalahaPit which is corresponding to pits in the real game.


@Document(collection = "games")
@Setter
@Getter
public class KalahaGame implements Serializable{

    @Id
    private String id;

    private List<KalahaPit> pits;

    private PlayerTurns playerTurn;

    @JsonIgnore
    private int currentPitIndex;

    public KalahaGame() {
        this (defaultPitStones);
    }

    public KalahaGame(int pitStones) {
        this.pits = Arrays.asList(
                new KalahaPit(firstPitPlayerA, pitStones),
                new KalahaPit(secondPitPlayerA, pitStones),
                new KalahaPit(thirdPitPlayerA, pitStones),
                new KalahaPit(forthPitPlayerA, pitStones),
                new KalahaPit(fifthPitPlayerA, pitStones),
                new KalahaPit(sixthPitPlayerA, pitStones),
                new KalahaHouse(rightPitHouseId),
                new KalahaPit(firstPitPlayerB, pitStones),
                new KalahaPit(secondPitPlayerB, pitStones),
                new KalahaPit(thirdPitPlayerB, pitStones),
                new KalahaPit(forthPitPlayerB, pitStones),
                new KalahaPit(fifthPitPlayerB, pitStones),
                new KalahaPit(sixthPitPlayerB, pitStones),
                new KalahaHouse(leftPitHouseId));
    }

    public KalahaGame(String id, Integer pitStones) {
        this (pitStones);
        this.id = id;
    }

    // returns the corresponding pit of particular index
    public KalahaPit getPit(Integer pitIndex) throws MancalaApiException {
        try {
            return this.pits.get(pitIndex-1);
        }catch (Exception e){
            throw  new MancalaApiException("Invalid pitIndex:"+ pitIndex +" has given!");
        }
    }

    @Override
    public String toString() {
        return "KalahaGame{" +
                ", pits=" + pits +
                ", playerTurn=" + playerTurn +
                '}';
    }
}

2.3 — KalahaPit

This class holds information regarding each Pit including the pit index and the number of stones within that pit.


@AllArgsConstructor
@NoArgsConstructor
@Data
public class KalahaPit implements Serializable {

    private Integer id;
    private Integer stones;

    @JsonIgnore
    public Boolean isEmpty (){
        return this.stones == 0;
    }

    public void clear (){
        this.stones = 0;
    }

    public void sow () {
        this.stones++;
    }

    public void addStones (Integer stones){
        this.stones+= stones;
    }

    @Override
    public String toString() {
        return  id.toString() +
                ":" +
                stones.toString() ;
    }
}

2.4 — KalahaHouse

It’s a subclass of KalahaPit which indicates the current pit index is a House.


public class KalahaHouse extends KalahaPit {
    public KalahaHouse(Integer id) {
        super(id , 0);
    }
}

3 — API design

Below are two interfaces designed for this game to cover all operations for this microservice:

1 — KalahaGameApi is responsible for the creation of Mancala game instances based on the number of stones within each pit.


public interface KalahaGameApi {
    KalahaGame createGame(int stones);
}

2 — KalahaGameSowApi is responsible for providing sowing functionality of the game for a specific pit index.


public interface KalahaGameSowApi {
    KalahaGame sow (KalahaGame game, int pitIndex);
}

4 — Data Repositories With MongoDB

To persist game data, Document-based data storage such as MongoDB has been used. To enable MongoDB spring data we need to add below dependency to pom.xml file:

<!-- MongoDB -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

We also need to add @EnableMongodbRepositories to our Spring boot application startup class in order to use Spring Boot specific data repositories for MongoDB:

@EnableMongoRepositories
@EnableCaching
@EnableDiscoveryClient
@SpringBootApplication
public class MancalaGameApiApplication {

public static void main(String[] args) {
SpringApplication.run(MancalaGameApiApplication.class, args);
}
}

KalahaGameRepository

In order to use the Spring Boot implementation of MongoDB data repositories, we will need to define our interfaces to extend MongoRepository interface as below:


public interface KalahaGameRepository extends MongoRepository<KalahaGame, String> {
}

5 — GamePersistence Service Implementation

MancalaGameService class has designed to perform persisting operations with a MongoDB database and also Redisin-memory data storage as our caching mechanism:


@Service
public class MancalaGameService implements KalahaGameApi {

    @Autowired
    private KalahaGameRepository kalahaGameRepository;

    @Override
    public KalahaGame createGame(int pitStones) {

        KalahaGame kalahaGame = new KalahaGame(pitStones);

        kalahaGameRepository.save(kalahaGame);

        return kalahaGame;
    }

    // loads the game instance from the Cache if game instance was found
    @Cacheable (value = "kalahGames", key = "#id" , unless = "#result  == null")
    public KalahaGame loadGame (String id) throws ResourceNotFoundException {
        Optional<KalahaGame> gameOptional = kalahaGameRepository.findById(id);

        if (!gameOptional.isPresent())
            throw new ResourceNotFoundException("Game id " + id + " not found!");

        return gameOptional.get();
    }

    // put the updated game instance into cache as well as data store
    @CachePut(value = "kalahGames", key = "#kalahaGame.id")
    public KalahaGame updateGame (KalahaGame kalahaGame){
        kalahaGame = kalahaGameRepository.save(kalahaGame);
        return kalahaGame;
    }
}

The loadGame method forces Spring to first try finding the game instance within the cache and retrieves it if it’s found, otherwise, it will continue executing the logic for retrieving the game instance from our MongoDB database and then store the results into cache system before returning from this method.

The updateGame method updates the game data in both MongoDB as well as Redis cache systems. This method is invoked on every request to sow method from ‘mancal-api’ microservice endpoint, therefore it always keeps cache data updated for further fast retrieval.

6 — Mancala Game Logic Implementation

MancalaSowingService class has designed to implement the sowing functionality of the Mancala game. This class implements a simple interface designed for this purpose called KalahaGameSowApi:

public interface KalahaGameSowApi {    KalahaGame sow (KalahaGame game, int pitIndex);}

The implementation for the above interface provided as below:


@Service
public class MancalaSowingService implements KalahaGameSowApi {

    // This method perform sowing the game on specific pit index
    @Override
    public KalahaGame sow(KalahaGame game, int requestedPitId) {

        // No movement on House pits
        if (requestedPitId == KalahaConstants.rightPitHouseId || requestedPitId == KalahaConstants.leftPitHouseId)
            return game;

        // we set the player turn for the first move of the game based on the pit id
        if (game.getPlayerTurn() == null) {
            if (requestedPitId < KalahaConstants.rightPitHouseId)
                game.setPlayerTurn(PlayerTurns.PlayerA);
            else
                game.setPlayerTurn(PlayerTurns.PlayerB);
        }

        // we need to check if request comes from the right player otherwise we do not sow the game. In other words,
        // we keep the turn for the correct player
        if (game.getPlayerTurn() == PlayerTurns.PlayerA && requestedPitId > KalahaConstants.rightPitHouseId ||
                game.getPlayerTurn() == PlayerTurns.PlayerB && requestedPitId < KalahaConstants.rightPitHouseId)
            return game;

        KalahaPit selectedPit = game.getPit(requestedPitId);

        int stones = selectedPit.getStones();

        // No movement for empty Pits
        if (stones == KalahaConstants.emptyStone)
            return game;

        selectedPit.setStones(KalahaConstants.emptyStone);

        // keep the pit index, used for sowing the stones in right pits
        game.setCurrentPitIndex(requestedPitId);

        // simply sow all stones except the last one
        for (int i = 0; i < stones - 1; i++) {
            sowRight(game,false);
        }

        // simply the last stone
        sowRight(game,true);

        int currentPitIndex = game.getCurrentPitIndex();

        // we switch the turn if the last sow was not on any of pit houses (left or right)
        if (currentPitIndex !=KalahaConstants. rightPitHouseId && currentPitIndex != KalahaConstants.leftPitHouseId)
            game.setPlayerTurn(nextTurn(game.getPlayerTurn()));

        return game;
    }

    // sow the game one pit to the right
    private void sowRight(KalahaGame game, Boolean lastStone) {
        int currentPitIndex = game.getCurrentPitIndex() % KalahaConstants.totalPits + 1;

        PlayerTurns playerTurn = game.getPlayerTurn();

        if ((currentPitIndex == KalahaConstants.rightPitHouseId && playerTurn == PlayerTurns.PlayerB) ||
                (currentPitIndex == KalahaConstants.leftPitHouseId && playerTurn == PlayerTurns.PlayerA))
            currentPitIndex = currentPitIndex % KalahaConstants.totalPits + 1;

        game.setCurrentPitIndex(currentPitIndex);

        KalahaPit targetPit = game.getPit(currentPitIndex);
        if (!lastStone || currentPitIndex == KalahaConstants.rightPitHouseId || currentPitIndex == KalahaConstants.leftPitHouseId) {
            targetPit.sow();
            return;
        }

        // It's the last stone and we need to check the opposite player's pit status
        KalahaPit oppositePit = game.getPit(KalahaConstants.totalPits - currentPitIndex);

        // we are sowing the last stone and the current player's pit is empty but the opposite pit is not empty, therefore,
        // we collect the opposite's Pit stones plus the last stone and add them to the House Pit of current player and
        // make the opposite Pit empty
        if (targetPit.isEmpty() && !oppositePit.isEmpty()) {
            Integer oppositeStones = oppositePit.getStones();
            oppositePit.clear();
            Integer pitHouseIndex = currentPitIndex < KalahaConstants.rightPitHouseId ? KalahaConstants.rightPitHouseId : KalahaConstants.leftPitHouseId;
            KalahaPit pitHouse = game.getPit(pitHouseIndex);
            pitHouse.addStones(oppositeStones + 1);
            return;
        }

        targetPit.sow();
    }

    public PlayerTurns nextTurn(PlayerTurns currentTurn) {
        if (currentTurn == PlayerTurns.PlayerA)
            return PlayerTurns.PlayerB;
        return PlayerTurns.PlayerA;
    }
}

7- Redis In-Memory Cache

Loading Mancala games instances from MongoDB storage on each request could be a very resource-intensive process especially in a large production deployment with millions of online users. Therefore, we will need to apply the Data Locality pattern in our implementation keeping our data as closest possible to the processing thread. There are many options available such as Facebook Mcrouter, yahoo pistachio, and Redis. In order to use Spring Redis Data to our project, we need to add below dependency to pom.xml file:

<!-- Redis -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

and having Redis configuration in the application.properties file as below:


# Redis pool configurations
spring.redis.jedis.pool.max-active=7
spring.redis.jedis.pool.max-idle=7
spring.redis.jedis.pool.min-idle=2

#Redis Cache specific configurations
spring.cache.redis.cache-null-values=false
spring.cache.redis.time-to-live=6000000
spring.cache.redis.use-key-prefix=true

# Redis data source configuration
spring.redis.port=6379
spring.redis.host=localhost

# Spring cache system
spring.cache.type=redis

as you can see, we have set the default spring caching system to use Redis as its underlying caching data structure. for more information, you can read Spring boot documentation here.

8 — MancalGame Controller

There are three endpoints defined for ‘mancala-api‘ microservice:

@Slf4j
@RestController
@RequestMapping("/games")
@Api(value = "Mancala game API. Set of endpoints for Creating and Sowing the Game")
public class MancalaController {

    @Autowired
    private MancalaGameService mancalaGameService;

    @Autowired
    private MancalaSowingService mancalaSowingService;

    @Value("${mancala.pit.stones}")
    private Integer pitStones;

    @PostMapping
    @ApiOperation(value = "Endpoint for creating new Mancala game instance. It returns a KalahaGame object with unique GameId used for sowing the game",
            produces = "Application/JSON", response = KalahaGame.class, httpMethod = "POST")
    public ResponseEntity<KalahaGame> createGame() throws Exception {

        log.info("Invoking create() endpoint... ");

        KalahaGame game = mancalaGameService.createGame(pitStones);

        log.info("Game instance created. Id=" + game.getId());

        log.info(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(game));

        mancalaGameService.updateGame(game);

        return ResponseEntity.ok(game);
    }

    @PutMapping(value = "{gameId}/pits/{pitId}")
    @ApiOperation(value = "Endpoint for sowing the game. It keeps the history of the Game instance for consecutive requests. ",
            produces = "Application/JSON", response = KalahaGame.class, httpMethod = "PUT")
    public ResponseEntity<KalahaGame> sowGame(
            @ApiParam(value = "The id of game created by calling createGame() method. It can't be empty or null", required = true)
            @PathVariable(value = "gameId") String gameId,
            @PathVariable(value = "pitId") Integer pitId) throws Exception {

        log.info("Invoking sow() endpoint. GameId: " + gameId + "  , pit Index: " + pitId);

        if (pitId == null || pitId < 1 || pitId >= KalahaConstants.leftPitHouseId || pitId == KalahaConstants.rightPitHouseId)
            throw new MancalaApiException("Invalid pit Index!. It should be between 1..6 or 8..13");

        KalahaGame kalahaGame = mancalaGameService.loadGame(gameId);

        kalahaGame = mancalaSowingService.sow(kalahaGame, pitId);

        mancalaGameService.updateGame(kalahaGame);

        log.info("sow is called for Game id:" + gameId + " , pitIndex:" + pitId);

        log.info(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(kalahaGame));

        return ResponseEntity.ok(kalahaGame);
    }

    @GetMapping("{id}")
    @ApiOperation(value = "Endpoint for returning the latest status of the Game",
            produces = "Application/JSON", response = KalahaGame.class, httpMethod = "GET")
    public ResponseEntity<KalahaGame> gameStatus(
            @ApiParam(value = "The id of game created by calling createGame() method. It's an String e.g. 5d34968590fcbd35b086bc21. It can't be empty or null",
                    required = true)
            @PathVariable(value = "id") String gameId) throws Exception {

        return ResponseEntity.ok(mancalaGameService.loadGame(gameId));
    }


}

9 — Service Discovery Using Consul

Consul, is an open source Service Registry/Discovery used to connect and secure services across any runtime platform. In order to use Consul in your Spring Boot application, you will need to add below dependency to your pom.xml file:

<!-- Consul -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

and add @EnableDiscoveryClient annotation to your Spring Boot startup class:

@EnableMongoRepositories
@EnableCaching
@EnableDiscoveryClient
@SpringBootApplication
public class MancalaGameApiApplication {

public static void main(String[] args) {
SpringApplication.run(MancalaGameApiApplication.class, args);
}
}

and set necessary Consul configurations in your application.properties file:

#consul configurations
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.preferIpAddress=true
spring.cloud.consul.discovery.instanceId=${spring.application.name}:${spring.application.instance_id:${random.value}}

and add below configuration in your bootstrap.properties file:

spring.application.name=mancala-api

Please note that according to Spring Boot documentation, the ‘
spring.application.name‘ property is required to be placed in
bootstrap.properties file instead of
application.properties file in order to work properly.

We have already launched our Consul server on port 8500 using Docker-compose file provided within the project repository:

version: ‘3’
services:
consul-server:
image: consul:1.2.0
command: consul agent -dev -client 0.0.0.0
ports:
— «8500:8500»

— «8600:8600/udp»

10 — Swagger Documentation

Swagger, is an open-source project used widely by millions of developers to provide API documentation for REST services. It allows you to describe the structure of your API in a machine readable way. To use Swagger with your Spring Boot application, you need to add below dependencies to your pom.xml file:

<!-- Swagger -->
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>2.9.2</version>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.9.2</version>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-bean-validators</artifactId>
  <version>2.8.0</version>
</dependency>

Then, you will need to provide your custom configuration implementation within your Spring boot application:


@EnableSwagger2
@Configuration
public class SwaggerConfiguration {
    @Bean
    public Docket apiDocket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.dzone.mancala.game.controller"))
                .paths(PathSelectors.any())
                .build()
                .apiInfo(getApiInfo());
    }

    private ApiInfo getApiInfo() {
        return new ApiInfo(
                "Manacala Game Api service",
                "This application provides api for building Mancala game application.",
                "1.0.0",
                "TERMS OF SERVICE URL",
                new Contact("Esfandiyar", "http://linkedin.com/in/esfandiyar", "esfand55@gmail.com"),
                "MIT License",
                "LICENSE URL",
                Collections.emptyList()
        );
    }
}

The operations within MancalaController class has been documented using @ApiOperation and @ApiParam annotations from Swagger:

 @PutMapping(value = "{gameId}/pits/{pitId}")
    @ApiOperation(value = "Endpoint for sowing the game. It keeps the history of the Game instance for consecutive requests. ",
            produces = "Application/JSON", response = KalahaGame.class, httpMethod = "PUT")
    public ResponseEntity<KalahaGame> sowGame(
            @ApiParam(value = "The id of game created by calling createGame() method. It can't be empty or null", required = true)
            @PathVariable(value = "gameId") String gameId,
            @PathVariable(value = "pitId") Integer pitId) throws Exception 

  @PostMapping
    @ApiOperation(value = "Endpoint for creating new Mancala game instance. It returns a KalahaGame object with unique GameId used for sowing the game",
            produces = "Application/JSON", response = KalahaGame.class, httpMethod = "POST")
    public ResponseEntity<KalahaGame> createGame() throws Exception

To see the online documentation for your REST services, you will need to have swagger-ui dependency in your pom.xml file as shown above and then navigate to below address after launching your service:http://localhost/mancala-api/swagger-ui.html

Please notice that the given URL shows the way we access our microservices through Consul registry while using Apache httpd to proxy requests and hide the underlying ports for our end users. To achieve this functionality, we are using a template provided by Hashicorp’s Consul here: https://releases.hashicorp.com/consul-template/0.18.0/consul-template_0.18.0_linux_amd64.zip.

For more detail about the configuration, please see the apache folder provided in my GitHub repository for this project: https://github.com/esfand55/mancala-game/tree/master/docker/apache

However, if you are running your Spring boot application as a standalone application for instance on port 8080, the URL will be like this: http://localhost:8080/swagger-ui.html

You can even test your API using the web interface provided by swagger.

11 — Spring Boot Actuator

In the world of microservices, it’s essential to have enough information in run time about how well each microservice operates in production through a comprehensive list of metrics and health checks collected from those microservice. Spring Boot Actuator lets you monitor and interact with your application at runtime through well-defined APIs. To enable Spring Boot Actuator, you will need to add below dependency into your pom.xml file:

<!-- Spring boot actuator to expose metrics endpoint -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

You can observe those metrics by navigating to the below address if you are running your Spring boot application for instance in port 8080: http://localhost:8080/actuator.

But in our implementation, since we are using Consul and Httpd proxy, the URL would be as below:

http://localhost/mancala-api/actuator

{"_links":{"self":{"href":"http://localhost/actuator","templated":false},
           "archaius":{"href":"http://localhost/actuator/archaius","templated":false},
           "auditevents":{"href":"http://localhost/actuator/auditevents","templated":false},
           "beans":{"href":"http://localhost/actuator/beans","templated":false},
           "caches":{"href":"http://localhost/actuator/caches","templated":false},
           "caches-cache":{"href":"http://localhost/actuator/caches/{cache}","templated":true},
           "health":{"href":"http://localhost/actuator/health","templated":false},
           "health-component":{"href":"http://localhost/actuator/health/{component}","templated":true},
           "health-component-instance":{"href":"http://localhost/actuator/health/{component}/{instance}","templated":true},
           "conditions":{"href":"http://localhost/actuator/conditions","templated":false},
           "configprops":{"href":"http://localhost/actuator/configprops","templated":false},
           "env-toMatch":{"href":"http://localhost/actuator/env/{toMatch}","templated":true},
           "env":{"href":"http://localhost/actuator/env","templated":false},
           "info":{"href":"http://localhost/actuator/info","templated":false},
           "logfile":{"href":"http://localhost/actuator/logfile","templated":false},
           "loggers-name":{"href":"http://localhost/actuator/loggers/{name}","templated":true},
           "loggers":{"href":"http://localhost/actuator/loggers","templated":false},
           "heapdump":{"href":"http://localhost/actuator/heapdump","templated":false},
           "threaddump":{"href":"http://localhost/actuator/threaddump","templated":false},
           "prometheus":{"href":"http://localhost/actuator/prometheus","templated":false},
           "metrics-requiredMetricName":{"href":"http://localhost/actuator/metrics/{requiredMetricName}","templated":true},
           "metrics":{"href":"http://localhost/actuator/metrics","templated":false},
           "scheduledtasks":{"href":"http://localhost/actuator/scheduledtasks","templated":false},
           "httptrace":{"href":"http://localhost/actuator/httptrace","templated":false},
           "mappings":{"href":"http://localhost/actuator/mappings","templated":false},
           "refresh":{"href":"http://localhost/actuator/refresh","templated":false},
           "features":{"href":"http://localhost/actuator/features","templated":false},
           "service-registry":{"href":"http://localhost/actuator/service-registry","templated":false},
           "consul":{"href":"http://localhost/actuator/consul","templated":false}}}

You can define which endpoints to be exposed in your application.properties file:

management.endpoints.jmx.exposure.include=*
management.endpoints.jmx.exposure.exclude=

To see the details information about any of above endpoints, you will need to add the name of that metric to the end of the above URL. For instance, to see the metrics information check this URL:

http://localhost/mancala-api/actuator/metrics

{"names":["jvm.memory.max","jvm.threads.states","process.files.max","jvm.gc.memory.promoted","system.load.average.1m","jvm.memory.used","jvm.gc.max.data.size","jvm.gc.pause","jvm.memory.committed","system.cpu.count","logback.events","http.server.requests","tomcat.global.sent","jvm.buffer.memory.used","tomcat.sessions.created","jvm.threads.daemon","system.cpu.usage","jvm.gc.memory.allocated","tomcat.global.request.max","tomcat.global.request","tomcat.sessions.expired","jvm.threads.live","jvm.threads.peak","tomcat.global.received","process.uptime","tomcat.sessions.rejected","process.cpu.usage","tomcat.threads.config.max","jvm.classes.loaded","jvm.classes.unloaded","tomcat.global.error","tomcat.sessions.active.current","tomcat.sessions.alive.max","jvm.gc.live.data.size","tomcat.threads.current","process.files.open","jvm.buffer.count","jvm.buffer.total.capacity","tomcat.sessions.active.max","tomcat.threads.busy","process.start.time"]}

To see the health of microservice, check this URL: http://localhost/mancala-api/actuator/health

{"status":"UP"}

This is the default implementation of Spring Boot for microservice health checking. You can always customize the default implementation and provide your own definition of how to provide health checks for your microservices. For that you will need to implement HealthIndicator interface and provide your own implementation for health() method:

@Component
public class ServiceAHealthIndicator implements HealthIndicator {
    private final String message_key = "mancala-api Service is Available";

    @Override
    public Health health() {
        if (!isServiceRunning()) {
            return Health.down().withDetail(message_key, "Not Available").build();
        }
        return Health.up().withDetail(message_key, "Available").build();
    }

    private Boolean isServiceRunning() {
        Boolean isRunning = true;
        // Logic Skipped. Provide your own logic

        return isRunning;
    }
}

12 — Micrometer and Prometheus Server

Micrometer is the most popular open source monitoring system used widely by many developers which allows you to include different libraries into your application to collect and ship metrics to different backend systems such as Prometheus.

In order to enable Micrometer to send your metrics to Prometheus server from your Spring Boot application, you will need below dependencies to be added to your pom.xml file:


<!-- Micormeter core   -->
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-core</artifactId>
</dependency>

<!-- Micrometer Prometheus registry  -->
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

You will also need to provide below configurations in your application.properties file within your Spring boot application:


# Prometheus configuration
management.metrics.export.prometheus.enabled=true
management.endpoint.prometheus.enabled=true

In this implementation, we have used Docker-compose to build and run our containers. You can find the docker-compose.yml file designed for this purpose at Github repository for this project here.

You can access to Prometheus server and play with various metrics collected: http://localhost:9090/graph

13 — Distributed Tracing Using Spring Cloud Sleuth and Zipkin

In the Microservices ecosystem where a typical request might span multiple services, we would need efficient tools to help us diagnose if there is any performance degradation or failure in that process chain and give us required runtime information to debug and fix those issues. Spring Cloud Sleuth is a project by the Spring Boot team designed to address this issue.

To enable this feature in your Spring boot application, you will need to add below dependency into your pom.xml file:

<!-- Sleuth-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

and you will provide below configuration in your application.properties file:

#Sleuth configurations
spring.sleuth.sampler.probability=1

Zipkin is a distributed tracing system. It helps gather timing data needed to troubleshoot latency problems in service architectures. Features include both the collection and lookup of this data. To enable Zipkin in your Spring Boot application you will need to add below dependency to your pom.xml file:

<!-- Zipkin -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

and provide below configuration in your application.properties file:

spring.zipkin.base-url=http://localhost:9411/

We have used Docker-compose to build and run our containers. Here is the docker-compose.yml file designed for this purpose:

version: '3'
services:
  consul-server:
    image: consul:1.2.0
    command: consul agent -dev -client 0.0.0.0
    ports:
      - "8500:8500"
      - "8600:8600/udp"
  mongodb:
    image: mongo:latest
    container_name: "dzone_mancala_mongodb"
    ports:
      - "27017:27017"
    command: mongod --smallfiles --logpath=/dev/null # --quiet

  redisdb:
    restart: always
    container_name: "dzone_mancala_redis"
    image: redis
    ports:
      - "6379:6379"

  mancala-api:
    build: ../mancala-microservice/mancala-api
    links:
      - consul-server
      - zipkin-server
    depends_on:
      - redisdb
      - mongodb
    environment:
      - MANCALA_PIT_STONES=6
      - SPRING_CLOUD_CONSUL_HOST=consul-server
      - SPRING_APPLICATION_NAME=mancala-api
      - SPRING_DATA_MONGODB_HOST=mongodb
      - SPRING_DATA_MONGODB_PORT=27017
      - SPRING_REDIS_HOST=redisdb
      - SPRING_REDIS_PORT=6379
      - MANCALA_API_SERVICE_ID= mancala-api
      - SPRING_ZIPKIN_BASE_URL=http://zipkin-server:9411/

  mancala-web:
    build: ../mancala-microservice/mancala-web
    links:
      - consul-server
      - zipkin-server
    environment:
      - SPRING_CLOUD_CONSUL_HOST=consul-server
      - SPRING_APPLICATION_NAME=mancala-web
      - MANCALA_API_SERVICE_ID= mancala-api
      - SPRING_ZIPKIN_BASE_URL=http://zipkin-server:9411/

  apache:
    build: apache
    links:
      - consul-server
    depends_on:
      - consul-server
    ports:
      - "80:80"

  zipkin-storage:
    image: openzipkin/zipkin-cassandra
    container_name: cassandra
    ports:
      - "9042:9042"

  zipkin-server:
    image: openzipkin/zipkin
    ports:
      - "9411:9411"
    environment:
      - STORAGE_TYPE=cassandra3
      - CASSANDRA_ENSURE_SCHEMA=false
      - CASSANDRA_CONTACT_POINTS=cassandra
    depends_on:
      - zipkin-storage

  grafana:
    image: grafana/grafana:6.2.5
    container_name: grafana
    ports:
      - 3000:3000
    depends_on:
      - prometheus
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin

  prometheus:
    build: prometheus
    links:
      - mancala-api
    ports:
      - "9090:9090"

To build the images execute the below command:

docker-compose -f docker-compose-zipkin-cassandra.yml build

and to run them execute :

docker-compose -f docker-compose-zipkin-cassandra.yml up -d

Then you can access the Zipkin web interface to see all tracking information about calling our microservices at: http://localhost:9411

14 — ELK-Stack 

ELK-Stack, «ELK» is the acronym for three open source projects: Elasticsearch, Logstash, and Kibana. Elasticsearch is a search and analytics engine. Logstash is a server‑side data processing pipeline that ingests data from multiple sources simultaneously, transforms it, and then sends it to a «stash» like Elasticsearch. Kibana lets users visualize data with charts and graphs in Elasticsearch.

We are using java logger API to log all operations within MancalaController class along with their results in order to ingest them into Elasticsearchengine through Filebeat component. These log data are stored in the logs folder as we have described in our logger-spring.xml file. This file is located under the resources folder of our spring boot application:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE"
  value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}" />
<include
resource="org/springframework/boot/logging/logback/console-appender.xml" />

<appender name="JSON_FILE"
  class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/mancala-api-${PID}.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>mancala-api-${PID}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder
class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<arguments />
<stackTrace />
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern>
{
"severity": "%level",
"service": "mancala-api",
"pid": ${PID:-},
"thread": "%thread",
"logger": "%logger",
"message": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_FILE" />
<appender-ref ref="CONSOLE" />
</root>
</configuration>

We also need to configure our Filebeat module to read these log data from the proper location. This configuration has been set in filebeat.yml used within Filebeat docker file:

filebeat.prospectors:
        - input_type: log
          paths:
            - /logs/*.json
          json:
            keys_under_root: true

        output.elasticsearch:
          hosts:
            - elasticsearch:9200

in this application, we are using docker-compose to build and run our containers. The docker-compose.yml file for the above configuration is provided here:

version: '3'
services:
  consul-server:
    image: consul:1.2.0
    command: consul agent -dev -client 0.0.0.0
    ports:
      - "8500:8500"
      - "8600:8600/udp"
  mongodb:
    image: mongo:latest
    container_name: "dzone_mancala_mongodb"
    ports:
      - "27017:27017"
    command: mongod --smallfiles --logpath=/dev/null # --quiet

  redisdb:
    restart: always
    container_name: "dzone_mancala_redis"
    image: redis
    ports:
      - "6379:6379"

  mancala-api:
    build: ../mancala-microservice/mancala-api
    volumes:
      - logs:/logs
    links:
      - consul-server
      - zipkin-server
    depends_on:
      - redisdb
      - mongodb
    environment:
      - MANCALA_PIT_STONES=6
      - SPRING_CLOUD_CONSUL_HOST=consul-server
      - SPRING_APPLICATION_NAME=mancala-api
      - SPRING_DATA_MONGODB_HOST=mongodb
      - SPRING_DATA_MONGODB_PORT=27017
      - SPRING_REDIS_HOST=redisdb
      - SPRING_REDIS_PORT=6379
      - MANCALA_API_SERVICE_ID= mancala-api
      - SPRING_ZIPKIN_BASE_URL=http://zipkin-server:9411/

  mancala-web:
    build: ../mancala-microservice/mancala-web
    volumes:
      - logs:/logs
    links:
      - consul-server
      - zipkin-server
    environment:
      - SPRING_CLOUD_CONSUL_HOST=consul-server
      - SPRING_APPLICATION_NAME=mancala-web
      - MANCALA_API_SERVICE_ID= mancala-api
      - SPRING_ZIPKIN_BASE_URL=http://zipkin-server:9411/

  apache:
    build: apache
    links:
      - consul-server
    depends_on:
      - consul-server
    ports:
      - "80:80"

  zipkin-storage:
    image: openzipkin/zipkin-cassandra
    container_name: cassandra
    ports:
      - "9042:9042"

  zipkin-server:
      image: openzipkin/zipkin
      ports:
        - "9411:9411"
      environment:
        - STORAGE_TYPE=cassandra3
        - CASSANDRA_ENSURE_SCHEMA=false
        - CASSANDRA_CONTACT_POINTS=cassandra
      depends_on:
        - zipkin-storage

  prometheus:
    build: prometheus
    links:
      - mancala-api
    ports:
      - "9090:9090"

  node-exporter:
    image: prom/node-exporter:latest
    ports:
      - '9100:9100'

  grafana:
    image: grafana/grafana:6.2.5
    container_name: grafana
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin

  filebeat:
    build: filebeat
    volumes:
      - logs:/logs
    links:
      - elasticsearch

  elasticsearch:
    build: elasticsearch
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.security.enabled=false
      - xpack.monitoring.enabled=false

  kibana:
    image: docker.elastic.co/kibana/kibana:6.3.1
    ports:
      - "5601:5601"
    links:
      - elasticsearch
    environment:
      - ELASTICSEARCH_URL=http://elasticsearch:9200

volumes:
  logs:

The above docker-compose.yml file includes all the features provided by this game which are discussed in this article.

You can then access the Kibana web interface at http://localhost:5601/

15- Better API With TDD

Developing production-ready robust APIs is essential in Microservices architecture and requires a lot of attention and solid principles to follow. 

16- Source Code

The complete source code for this project is available in my GitHub repository.

17- How to Build and Run

To build and run this application, please follow the instructions I have provided here.

In my next article (Building Mancala Game in Microservices Using Spring Boot (Web Client Microservice Implementation), I am explaining how to develop a simple web application using Spring Boot and Vaadin and use ‘mancala-api‘ microservice using Consul service registry.

Your valuable comments and feedbacks are highly appreciated.

Further Reading

Microservices Using Spring Boot Applications

Deploying a Spring Boot Microservices to Docker: A Quick Guide