Monitoring Using Spring Boot 2.0, Prometheus, and Grafana (Part 1 — REST API)
Read this tutorial in order to learn how to create a REST API for CRUD by using Spring Boot 2.0, Prometheus, and Grafana.
Join the DZone community and get the full member experience.
Join For FreeIn part 1, we will be creating a REST API for CRUD operations using Spring Boot 2.0, JPA, H2 Database, and SWAGGER UI for documentation.
We will be creating a simple application offering CRUD operations over REST for a person entity we shall be using
H2: as our underlying database
Spring Boot Web: for creating REST API
Spring Data JPA: for JPA implementation
SWAGGER UI: for documenting API's
So, let's get started by creating a new project.
Create a spring starter project in Eclipse (I am using STS) or you can use Spring Initializer to get started. Add dependencies for Web, Lombok, Actuator, H2, and JPA.
At this point, our project structure should look like this:
Now, let's add controller, entity, model, and service classes.
pom .xml: Maven pom file containing all the dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.satish.monitoring</groupId>
<artifactId>person-application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>person-application</name>
<description>Sample application to be monitored</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<springfox-version>2.5.0</springfox-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- DB-JPA boot related -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--SpringFox swagger dependencies -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
PersonEntity: JPA entity class representing person table in the database.
package com.satish.monitoring.db.entities;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import com.satish.monitoring.web.models.Person;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@Table(name = "PERSON")
@NoArgsConstructor
public class PersonEntity implements Serializable{
private static final long serialVersionUID = -8003246612943943723L;
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private int personId;
private String firstName;
private String lastName;
private String email;
public PersonEntity( String firstName, String lastName, String email) {
super();
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public PersonEntity(int personId, String firstName, String lastName, String email) {
super();
this.personId = personId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
}
package com.satish.monitoring.db.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.satish.monitoring.db.entities.PersonEntity;
@Repository
public interface PersonRepository extends JpaRepository<PersonEntity, Integer>{
}
package com.satish.monitoring.services;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.satish.monitoring.web.models.Person;
@Service
public interface PersonService {
/**
*
* @param personId
* @return {@link Optional} {@link Person} objects if present in database
* for supplied person ID
*/
public Optional<Person> getPersonById(int personId);
/**
*
* @return {@link List} of {@link Person} model class fo rall available
* entities
*/
public List<Person> getAllPersons();
/**
*
* @param personId
* @return Delete the person from database for supplied id
*/
public boolean removePerson(int personId);
/**
*
* @param person
* @return {@link Optional} {@link Person} objects after save or update Save
* if no personId present else update
*/
public Optional<Person> saveUpdatePerson(Person person);
}
PersonServiceImpl: The implementation class to interact with Database using repository interface.
package com.satish.monitoring.services;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.satish.monitoring.db.entities.PersonEntity;
import com.satish.monitoring.db.repositories.PersonRepository;
import com.satish.monitoring.web.models.Person;
@Component
public class PersonServiceImpl implements PersonService {
private final PersonRepository personRepo;
@Autowired
PersonServiceImpl(PersonRepository personRepo) {
this.personRepo = personRepo;
}
/**
* Convert {@link Person} Object to {@link PersonEntity} object Set the
* personId if present else return object with id null/0
*/
private final Function<Person, PersonEntity> personToEntity = new Function<Person, PersonEntity>() {
@Override
public PersonEntity apply(Person person) {
if (person.getPersonId() == 0) {
return new PersonEntity(person.getFirstName(), person.getLastName(), person.getEmail());
} else {
return new PersonEntity(person.getPersonId(), person.getFirstName(), person.getLastName(),
person.getEmail());
}
}
};
/**
* Convert {@link PersonEntity} to {@link Person} object
*/
private final Function<PersonEntity, Person> entityToPerson = new Function<PersonEntity, Person>() {
@Override
public Person apply(PersonEntity entity) {
return new Person(entity.getPersonId(), entity.getFirstName(), entity.getLastName(), entity.getEmail());
}
};
/**
* If record is present then convert the record else return the empty {@link Optional}
*/
@Override
public Optional<Person> getPersonById(int personId) {
return personRepo.findById(personId).map(s -> entityToPerson.apply(s));
}
@Override
public List<Person> getAllPersons() {
return personRepo.findAll().parallelStream()
.map(s -> entityToPerson.apply(s))
.collect(Collectors.toList());
}
@Override
public boolean removePerson(int personId) {
personRepo.deleteById(personId);
return true;
}
@Override
public Optional<Person> saveUpdatePerson(Person person) {
if(person.getPersonId() == 0 || personRepo.existsById(person.getPersonId())){
PersonEntity entity = personRepo.save(personToEntity.apply(person));
return Optional.of(entityToPerson.apply(entity));
}else{
return Optional.empty();
}
}
}
package com.satish.monitoring.web.rest;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.satish.monitoring.services.PersonService;
import com.satish.monitoring.web.models.Person;
@RestController
@RequestMapping("/person")
public class PersonResource {
private final PersonService personService;
/**
* Constructor to autowire PersonService instance.
* Look we have declared personService as final without initialization
*/
@Autowired
PersonResource(PersonService personService) {
this.personService = personService;
}
/**
*
* @return expose GET endpoint to return {@link List} of all available persons
*/
@GetMapping
public List<Person> getAllPerson() {
return personService.getAllPersons();
}
/**
*
* @param personId supplied as path variable
* @return expose GET endpoint to return {@link Person} for the supplied person id
* return HTTP 404 in case person is not found in database
*/
@GetMapping(value = "/{personId}")
public ResponseEntity<Person> getPerson(@PathVariable("personId") int personId) {
return personService.getPersonById(personId).map(person -> {
return ResponseEntity.ok(person);
}).orElseGet(() -> {
return new ResponseEntity<Person>(HttpStatus.NOT_FOUND);
});
}
/**
*
* @param person JSON body
* @return expose POST mapping and return newly created person in case of successful operation
* return HTTP 417 in case of failure
*/
@PostMapping
public ResponseEntity<Person> addNewPerson(@RequestBody Person person) {
return personService.saveUpdatePerson(person).map(p -> {
return ResponseEntity.ok(p);
}).orElseGet(() -> {
return new ResponseEntity<Person>(HttpStatus.EXPECTATION_FAILED);
});
}
/**
*
* @param person JSON body
* @return expose PUT mapping and return newly created or updated person in case of successful operation
* return HTTP 417 in case of failure
*
*/
@PutMapping
public ResponseEntity<Person> updatePerson(@RequestBody Person person) {
return personService.saveUpdatePerson(person).map(p -> {
return ResponseEntity.ok(p);
}).orElseGet(() -> {
return new ResponseEntity<Person>(HttpStatus.EXPECTATION_FAILED);
});
}
/**
*
* @param personId person id to be deleted
* @return expose DELETE mapping and return success message if operation was successful.
* return HTTP 417 in case of failure
*
*/
@DeleteMapping(value = "/{personId}")
public ResponseEntity<String> deletePerson(@PathVariable("personId") int personId) {
if (personService.removePerson(personId)) {
return ResponseEntity.ok("Person with id : " + personId + " removed");
} else {
return new ResponseEntity<String>("Error deleting enitty ", HttpStatus.EXPECTATION_FAILED);
}
}
}
package com.satish.monitoring.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
*
* @author Satish Sharma
*
*/
@EnableSwagger2
@Configuration
public class SwaggerAPIDocumentationConfig {
ApiInfo apiInfo() {
return new ApiInfoBuilder().title("Person REST CRUD operations API in Spring-Boot 2")
.description(
"Sample REST API for monitoring using Spring Boot, Prometheus and Graphana ")
.termsOfServiceUrl("").version("0.0.1-SNAPSHOT").contact(new Contact("Satish Sharma", "https://github.com/hellosatish/monitoring/person", "https://github.com/hellosatish")).build();
}
@Bean
public Docket configureControllerPackageAndConvertors() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.basePackage("com.satish.monitoring")).build()
.directModelSubstitute(org.joda.time.LocalDate.class, java.sql.Date.class)
.directModelSubstitute(org.joda.time.DateTime.class, java.util.Date.class)
.apiInfo(apiInfo());
}
}
package com.satish.monitoring.web.rest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class SwaggerUIController {
@RequestMapping(value = "/")
public String index() {
return "redirect:swagger-ui.html";
}
}
application.properties: Configure properties. Notice we have configured the application to run on port 9000.
# Server configurations.
server.port=9000
logging.level.com.satish.monitoring=debug
logging.file=logs/monitoring.log
# Database configurations.
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./db/target/person_db;DB_CLOSE_DELAY=-1
spring.datasource.username=satish
spring.datasource.data=classpath:/db-scripts/init-script.sql
spring.h2.console.enabled=true
spring.h2.console.path=/db-console
spring.jpa.show-sql=true
# change the below to none in production
spring.jpa.hibernate.ddl-auto=create-drop
mvn clean spring-boot:run
Now browse the URL http:localhost:9000 and you should be able to see the SWAGGER UI
You have successfully created the REST API for CRUD operations. You can have a look/download the code from this GitHub Repo.
In the next part, we shall be enabling endpoint to expose metrics as JSON.
Opinions expressed by DZone contributors are their own.
Comments