diff --git a/build.gradle b/build.gradle index c19b5f77d..8c1f8f79b 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'javax.cache:cache-api' implementation 'jakarta.xml.bind:jakarta.xml.bind-api' + implementation('io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:1.28.0') + implementation('io.opentelemetry:opentelemetry-api') + implementation 'com.squareup.okhttp3:okhttp' + implementation 'org.json:json:20171018' + runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator' runtimeOnly "org.webjars.npm:bootstrap:${webjarsBootstrapVersion}" runtimeOnly "org.webjars.npm:font-awesome:${webjarsFontawesomeVersion}" @@ -37,6 +42,17 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' } +tasks.named("bootRun") { + if (project.hasProperty('digma')) { + def tempDir = System.getProperty("java.io.tmpdir") + environment["JAVA_TOOL_OPTIONS"] = "-javaagent:${tempDir}/temp-digma-otel-jars/opentelemetry-javaagent.jar" + systemProperty 'otel.exporter.otlp.traces.endpoint', 'http://localhost:5050' + systemProperty 'otel.traces.exporter', 'otlp' + systemProperty 'otel.metrics.exporter', 'none' + systemProperty 'otel.service.name', "${project.name}" + systemProperty 'otel.javaagent.extensions', "${tempDir}/temp-digma-otel-jars/digma-otel-agent-extension.jar" + } +} tasks.named('test') { useJUnitPlatform() } diff --git a/digma-profile.gradle b/digma-profile.gradle new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/digma-profile.gradle @@ -0,0 +1 @@ + diff --git a/opentelemetry-agent.jar b/opentelemetry-agent.jar new file mode 100644 index 000000000..de24faaee Binary files /dev/null and b/opentelemetry-agent.jar differ diff --git a/otel/digma-otel-agent-extension.jar b/otel/digma-otel-agent-extension.jar new file mode 100644 index 000000000..a4a9ba051 Binary files /dev/null and b/otel/digma-otel-agent-extension.jar differ diff --git a/otel/opentelemetry-javaagent.jar b/otel/opentelemetry-javaagent.jar new file mode 100644 index 000000000..de24faaee Binary files /dev/null and b/otel/opentelemetry-javaagent.jar differ diff --git a/pom.xml b/pom.xml index b45f3a66c..68eb84d5e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,6 +5,8 @@ spring-petclinic 3.1.0-SNAPSHOT + + org.springframework.boot spring-boot-starter-parent @@ -67,6 +69,27 @@ test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + io.rest-assured + rest-assured + test + + com.h2database @@ -120,7 +143,12 @@ spring-boot-devtools true - + + com.github.javafaker + javafaker + 1.0.2 + test + jakarta.xml.bind jakarta.xml.bind-api @@ -135,9 +163,20 @@ android-json 0.0.20131108.vaadin1 - + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + @@ -183,28 +222,8 @@ checkstyle ${checkstyle.version} - - io.spring.nohttp - nohttp-checkstyle - ${nohttp-checkstyle.version} - + - - - nohttp-checkstyle-validation - validate - - src/checkstyle/nohttp-checkstyle.xml - src/checkstyle/nohttp-checkstyle-suppressions.xml - ${basedir} - **/* - **/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class - - - check - - - org.graalvm.buildtools @@ -311,6 +330,27 @@ + + digma + + + + org.springframework.boot + spring-boot-maven-plugin + + -javaagent:${env.TMPDIR}/temp-digma-otel-jars/opentelemetry-javaagent.jar + + http://localhost:5050 + otlp + none + ${pom.artifactId} + ${env.TMPDIR}/temp-digma-otel-jars/digma-otel-agent-extension.jar + + + + + + css diff --git a/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java b/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java index 2ac7e02e9..ad21a5984 100644 --- a/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java +++ b/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java @@ -16,8 +16,13 @@ package org.springframework.samples.petclinic; +import jakarta.servlet.ServletContext; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class PetClinicRuntimeHints implements RuntimeHintsRegistrar { diff --git a/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java index 737bb3616..5aa3c9b2d 100644 --- a/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java +++ b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java @@ -6,9 +6,11 @@ import org.json.JSONException; import java.io.IOException; public interface PetVaccinationService { + @WithSpan VaccinnationRecord[] AllVaccines() throws JSONException, IOException; @WithSpan VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException; + } diff --git a/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java index b1b1737ab..b65176313 100644 --- a/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java +++ b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java @@ -20,7 +20,7 @@ public class PetVaccinationServiceFacade implements PetVaccinationService { public static final String VACCINES_RECORDS_URL = "https://647f4bb4c246f166da9084c7.mockapi.io/api/vetcheck/vaccines"; - private String MakeHttpCall(String url) throws IOException{ + private String MakeHttpCall(String url) throws IOException { Request getAllVaccinesRequest = new Request.Builder().url(url).build(); OkHttpClient client = new OkHttpClient(); @@ -34,8 +34,7 @@ public class PetVaccinationServiceFacade implements PetVaccinationService { var vaccineListString = MakeHttpCall(VACCINES_RECORDS_URL); JSONArray jArr = new JSONArray(vaccineListString); - var vaccinnationRecords = - new ArrayList(); + var vaccinnationRecords = new ArrayList(); for (int i = 0; i < jArr.length(); i++) { @@ -69,6 +68,4 @@ public class PetVaccinationServiceFacade implements PetVaccinationService { return new VaccinnationRecord(id, petId, vaccineDate); } - - } diff --git a/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java b/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java index e11a19867..138760090 100644 --- a/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java +++ b/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java @@ -52,7 +52,7 @@ public class OwnerValidation { } - @WithSpan + // This function and classes were generated by ChatGPT public boolean ValidateUserAccess(String usr, String pswd, String sysCode) { @@ -62,7 +62,6 @@ public class OwnerValidation { return false; } - boolean vldPswd = pwdUtils.vldtPswd(usr, pswd); if (!vldPswd) { return false; diff --git a/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java b/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java index ca8b34f4b..12cbe425b 100644 --- a/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java +++ b/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java @@ -1,6 +1,7 @@ package org.springframework.samples.petclinic.domain; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.annotations.WithSpan; import org.json.JSONException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.samples.petclinic.adapters.PetVaccinationService; @@ -20,15 +21,16 @@ public class PetVaccinationStatusService { @Autowired private PetVaccinationService adapter; - public void UpdateVaccinationStatus(Pet[] pets){ + @WithSpan + public void UpdateVaccinationStatus(Pet[] pets) { - for (Pet pet: pets){ + for (Pet pet : pets) { try { var vaccinationRecords = this.adapter.AllVaccines(); - for (VaccinnationRecord record : vaccinationRecords){ + for (VaccinnationRecord record : vaccinationRecords) { var recordInfo = this.adapter.VaccineRecord(record.recordId()); - if (recordInfo.petId()==pet.getId()){ + if (recordInfo.petId() == pet.getId()) { var date = LocalDateTime.ofInstant(recordInfo.vaccineDate(), ZoneId.systemDefault()); PetVaccine petVaccine = new PetVaccine(); petVaccine.setDate(date.toLocalDate()); @@ -36,13 +38,14 @@ public class PetVaccinationStatusService { } } - } catch (JSONException |IOException e) { - //Fail silently + } + catch (JSONException | IOException e) { + // Fail silently Span.current().recordException(e); } } - } + } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java index f77ce4d28..c9684c751 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java @@ -129,11 +129,6 @@ class OwnerController { return addPaginationModel(page, model, ownersResults); } - - - - - private String addPaginationModel(int page, Model model, Page paginated) { model.addAttribute("listOwners", paginated); List listOwners = paginated.getContent(); @@ -151,11 +146,6 @@ class OwnerController { return owners.findByLastName(lastname, pageable); } - - - - - @GetMapping("/owners/{ownerId}/edit") public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) { Owner owner = this.owners.findById(ownerId); @@ -163,24 +153,6 @@ class OwnerController { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } - - - - - - - - - - - - - - - - - - @PostMapping("/owners/{ownerId}/edit") public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId) { @@ -199,10 +171,8 @@ class OwnerController { * @return a ModelMap with the model attributes for the view */ @GetMapping("/owners/{ownerId}") - public ModelAndView showOwner(@PathVariable("ownerId") - int ownerId) { - ModelAndView mav = - new ModelAndView("owners/ownerDetails"); + public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { + ModelAndView mav = new ModelAndView("owners/ownerDetails"); Owner owner = this.owners.findById(ownerId); mav.addObject(owner); return mav; diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java index f44449439..02df0e743 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java @@ -79,4 +79,8 @@ public interface OwnerRepository extends Repository { @Transactional(readOnly = true) Page findAll(Pageable pageable); + // @Query("DROP Table Owner") + // @Transactional(readOnly = true) + // void deleteAll(); + } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java index 5ab40f7bd..ac76af0a1 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java @@ -87,20 +87,9 @@ class PetController { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } - - - - - - - - - - - @PostMapping("/pets/new") public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model) { - if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) { + if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) { result.rejectValue("name", "duplicate", "already exists"); } @@ -110,19 +99,13 @@ class PetController { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } + this.owners.save(owner); petVaccinationStatus.UpdateVaccinationStatus(owner.getPets().toArray(Pet[]::new)); return "redirect:/owners/{ownerId}"; } - - - - - - - @GetMapping("/pets/{petId}/edit") public String initUpdateForm(Owner owner, @PathVariable("petId") int petId, ModelMap model) { Pet pet = owner.getPet(petId); diff --git a/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java b/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java index 9224015bc..a3074fb29 100644 --- a/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java +++ b/src/main/java/org/springframework/samples/petclinic/system/WelcomeController.java @@ -16,6 +16,7 @@ package org.springframework.samples.petclinic.system; +import io.opentelemetry.instrumentation.annotations.WithSpan; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -27,4 +28,6 @@ class WelcomeController { return "welcome"; } + + } diff --git a/src/main/resources/db/postgres/schema.sql b/src/main/resources/db/postgres/schema.sql index f09c06f4c..6cee046d9 100644 --- a/src/main/resources/db/postgres/schema.sql +++ b/src/main/resources/db/postgres/schema.sql @@ -54,5 +54,5 @@ CREATE INDEX ON visits (pet_id); CREATE TABLE IF NOT EXISTS pet_vaccines ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, pet_id INT REFERENCES pets (id), - vaccine_date DATE, + vaccine_date DATE ); diff --git a/src/test/java/org/springframework/samples/petclinic/integration/OwnerControllerTests.java b/src/test/java/org/springframework/samples/petclinic/integration/OwnerControllerTests.java new file mode 100644 index 000000000..b7b35a3fe --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/integration/OwnerControllerTests.java @@ -0,0 +1,154 @@ +package org.springframework.samples.petclinic.integration; + +import com.github.javafaker.Faker; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import jakarta.persistence.EntityManagerFactory; +import org.hamcrest.Matchers; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.samples.petclinic.owner.*; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasSize; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +@ActiveProfiles(value = "postgres") +public class OwnerControllerTests { + + @LocalServerPort + private Integer port; + + @BeforeAll + static void beforeAll() { + postgres.start(); + } + + @AfterAll + static void afterAll() { + postgres.stop(); + } + + @Container + @ServiceConnection + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); + + @BeforeEach + void setUp() { + // ownerRepository.deleteAll(); + + RestAssured.baseURI = "http://localhost:" + port; + } + + Faker faker = new Faker(); + + @Autowired + EntityManagerFactory emf; + + @Autowired + OwnerRepository ownerRepository; + + @Test + @WithSpan(kind = SpanKind.SERVER) + void shouldSaveNewOwnerPet() { + + Owner owner = CreateOwner(); + + for (int i = 0; i < 3; i++) { + + String newPetName = faker.dog().name(); + given().contentType("multipart/form-data") + .multiPart("id", "") + .multiPart("birthDate", "0222-02-02") + .multiPart("name", newPetName) + .multiPart("type", "dog") + .when() + .post(String.format("/owners/%s/pets/new", owner.getId())) + .then() + .statusCode(Matchers.not(Matchers.greaterThan(499))); + try { + Thread.sleep(800); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + + } + + // var updatedOwner = ownerRepository.findById(owner.getId()); + // assertThat(updatedOwner.getPets()) + // .hasSize(2) + // .extracting(Pet::getName) + // .contains(newPetName); + + } + + @Test + void shouldGetAllOwners() { + + Owner owner = CreateOwner(); + + var ownerLinkMatcher = + String.format("**.findAll { node -> node.@href=='/owners/%s'}", + owner.getId()); + + given() + .contentType(ContentType.JSON) + .when() + .get("/owners") + .then() + .contentType(ContentType.HTML) + .statusCode(200) + .body(ownerLinkMatcher,Matchers.notNullValue()); + + } + + @NotNull + private Owner CreateOwner() { + var owner = new Owner(); + owner.setFirstName(faker.name().firstName()); + owner.setLastName(faker.name().lastName()); + owner.setAddress(faker.address().streetAddress()); + owner.setTelephone("5555555"); + owner.setCity(faker.address().city()); + + Pet pet = new Pet(); + pet.setName(faker.dog().name()); + pet.setBirthDate(faker.date().birthday().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + PetType dog = new PetType(); + dog.setName(faker.dog().name()); + dog.setId(2); + pet.setType(dog); + PetVaccine vaccine = new PetVaccine(); + vaccine.setDate(faker.date() + .past(30, TimeUnit.DAYS, new java.util.Date()) + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate()); + pet.addVaccine(vaccine); + + owner.addPet(pet); + ownerRepository.save(owner); + return owner; + } + +}