diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/TelemetrySendingService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/TelemetrySendingService.java deleted file mode 100644 index f3d50f64722c..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/TelemetrySendingService.java +++ /dev/null @@ -1,89 +0,0 @@ -package de.tum.cit.aet.artemis.core.service; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; - -import java.util.Arrays; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Profile; -import org.springframework.core.env.Environment; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; - -@Service -@Profile(PROFILE_SCHEDULING) -public class TelemetrySendingService { - - private static final Logger log = LoggerFactory.getLogger(TelemetrySendingService.class); - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - public record TelemetryData(String version, String serverUrl, String operator, String contact, List profiles, String adminName) { - } - - private final Environment env; - - private final RestTemplate restTemplate; - - public TelemetrySendingService(Environment env, RestTemplate restTemplate) { - this.env = env; - this.restTemplate = restTemplate; - } - - @Value("${artemis.version}") - private String version; - - @Value("${server.url}") - private String serverUrl; - - @Value("${info.operatorName}") - private String operator; - - @Value("${info.operatorAdminName}") - private String operatorAdminName; - - @Value("${info.contact}") - private String contact; - - @Value("${artemis.telemetry.sendAdminDetails}") - private boolean sendAdminDetails; - - @Value("${artemis.telemetry.destination}") - private String destination; - - /** - * Assembles the telemetry data, and sends it to the external telemetry server. - * - * @throws Exception if the writing the telemetry data to a json format fails, or the connection to the telemetry server fails - */ - @Async - public void sendTelemetryByPostRequest() throws Exception { - List activeProfiles = Arrays.asList(env.getActiveProfiles()); - TelemetryData telemetryData; - if (sendAdminDetails) { - telemetryData = new TelemetryData(version, serverUrl, operator, contact, activeProfiles, operatorAdminName); - } - else { - telemetryData = new TelemetryData(version, serverUrl, operator, null, activeProfiles, null); - } - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - ObjectWriter objectWriter = new ObjectMapper().writer().withDefaultPrettyPrinter(); - - var telemetryJson = objectWriter.writeValueAsString(telemetryData); - HttpEntity requestEntity = new HttpEntity<>(telemetryJson, headers); - var response = restTemplate.postForEntity(destination + "/api/telemetry", requestEntity, String.class); - log.info("Successfully sent telemetry data. {}", response.getBody()); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/TelemetrySendingService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/TelemetrySendingService.java new file mode 100644 index 000000000000..265b96192ad0 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/TelemetrySendingService.java @@ -0,0 +1,128 @@ +package de.tum.cit.aet.artemis.core.service.telemetry; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; + +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.core.service.ProfileService; + +@Service +@Profile(PROFILE_SCHEDULING) +public class TelemetrySendingService { + + private static final Logger log = LoggerFactory.getLogger(TelemetrySendingService.class); + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public record TelemetryData(String version, String serverUrl, String operator, List profiles, boolean isProductionInstance, boolean isTestServer, String dataSource, + String contact, String adminName) { + } + + private final Environment env; + + private final RestTemplate restTemplate; + + private final ProfileService profileService; + + public TelemetrySendingService(Environment env, RestTemplate restTemplate, ProfileService profileService) { + this.env = env; + this.restTemplate = restTemplate; + this.profileService = profileService; + } + + @Value("${artemis.version}") + private String version; + + @Value("${server.url}") + private String serverUrl; + + @Value("${info.operatorName}") + private String operator; + + @Value("${info.operatorAdminName}") + private String operatorAdminName; + + @Value("${info.contact}") + private String operatorContact; + + @Value("${artemis.telemetry.destination}") + private String destination; + + @Value("${spring.datasource.url}") + private String datasourceUrl; + + @Value("${info.test-server:false}") + private boolean isTestServer; + + /** + * Sends telemetry data to a specified destination via an HTTP POST request asynchronously. + * The telemetry includes information about the application version, environment, data source, + * and optionally, administrator details. If Eureka is enabled, the number of registered + * instances is also included. + * + *

+ * The method constructs the telemetry data object, converts it to JSON, and sends it to a + * telemetry collection server. The request is sent asynchronously due to the {@code @Async} annotation. + * + * @param sendAdminDetails a flag indicating whether to include administrator details in the + * telemetry data (such as contact information and admin name). + */ + @Async + public void sendTelemetryByPostRequest(boolean sendAdminDetails) { + + try { + String telemetryJson = new ObjectMapper().writer().withDefaultPrettyPrinter().writeValueAsString(buildTelemetryData(sendAdminDetails)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(telemetryJson, headers); + + log.info("Sending telemetry to {}", destination); + var response = restTemplate.postForEntity(destination + "/api/telemetry", requestEntity, String.class); + log.info("Successfully sent telemetry data. {}", response.getBody()); + } + catch (JsonProcessingException e) { + log.warn("JsonProcessingException in sendTelemetry.", e); + } + catch (Exception e) { + log.warn("Exception in sendTelemetry, with dst URI: {}", destination, e); + } + } + + /** + * Retrieves telemetry data for the current system configuration, including details + * about the active profiles, data source type, and optionally admin contact details. + * + * @param sendAdminDetails whether to include admin contact information in the telemetry data + * @return an instance of {@link TelemetryData} containing the gathered telemetry information + */ + private TelemetryData buildTelemetryData(boolean sendAdminDetails) { + TelemetryData telemetryData; + var dataSource = datasourceUrl.startsWith("jdbc:mysql") ? "mysql" : "postgresql"; + List activeProfiles = Arrays.asList(env.getActiveProfiles()); + + String contact = null; + String adminName = null; + if (sendAdminDetails) { + contact = operatorContact; + adminName = operatorAdminName; + } + telemetryData = new TelemetryData(version, serverUrl, operator, activeProfiles, profileService.isProductionActive(), isTestServer, dataSource, contact, adminName); + return telemetryData; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/TelemetryService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/TelemetryService.java similarity index 54% rename from src/main/java/de/tum/cit/aet/artemis/core/service/TelemetryService.java rename to src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/TelemetryService.java index 408e6c3dd514..d43f79aae256 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/TelemetryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/TelemetryService.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis.core.service; +package de.tum.cit.aet.artemis.core.service.telemetry; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; @@ -10,7 +10,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import com.fasterxml.jackson.core.JsonProcessingException; +import de.tum.cit.aet.artemis.core.service.ProfileService; @Service @Profile(PROFILE_SCHEDULING) @@ -22,37 +22,30 @@ public class TelemetryService { private final TelemetrySendingService telemetrySendingService; - @Value("${artemis.telemetry.enabled}") - public boolean useTelemetry; + private final boolean useTelemetry; - @Value("${artemis.telemetry.destination}") - private String destination; + private final boolean sendAdminDetails; - public TelemetryService(ProfileService profileService, TelemetrySendingService telemetrySendingService) { + public TelemetryService(ProfileService profileService, TelemetrySendingService telemetrySendingService, @Value("${artemis.telemetry.enabled}") boolean useTelemetry, + @Value("${artemis.telemetry.sendAdminDetails}") boolean sendAdminDetails) { this.profileService = profileService; this.telemetrySendingService = telemetrySendingService; + this.useTelemetry = useTelemetry; + this.sendAdminDetails = sendAdminDetails; } /** - * Sends telemetry to the server specified in artemis.telemetry.destination. - * This function runs once, at the startup of the application. - * If telemetry is disabled in artemis.telemetry.enabled, no data is sent. + * Sends telemetry data to the server after the application is ready. + * This method is triggered automatically when the application context is fully initialized. + *

+ * If telemetry is disabled (as specified by the {@code useTelemetry} flag), the task will not be executed. */ @EventListener(ApplicationReadyEvent.class) public void sendTelemetry() { if (!useTelemetry || profileService.isDevActive()) { return; } - - log.info("Sending telemetry information"); - try { - telemetrySendingService.sendTelemetryByPostRequest(); - } - catch (JsonProcessingException e) { - log.warn("JsonProcessingException in sendTelemetry.", e); - } - catch (Exception e) { - log.warn("Exception in sendTelemetry, with dst URI: {}", destination, e); - } + log.info("Start sending telemetry data asynchronously"); + telemetrySendingService.sendTelemetryByPostRequest(sendAdminDetails); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/service/TelemetryServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/core/service/TelemetryServiceTest.java index c820f3fb0bb0..1938c719ffa1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/service/TelemetryServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/service/TelemetryServiceTest.java @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.core.service; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.spy; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; @@ -25,6 +26,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import de.tum.cit.aet.artemis.core.service.telemetry.TelemetrySendingService; +import de.tum.cit.aet.artemis.core.service.telemetry.TelemetryService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; @ExtendWith(MockitoExtension.class) @@ -34,7 +37,10 @@ class TelemetryServiceTest extends AbstractSpringIntegrationIndependentTest { private RestTemplate restTemplate; @Autowired - private TelemetryService telemetryService; + private TelemetrySendingService telemetrySendingService; + + @Autowired + private ProfileService profileService; private MockRestServiceServer mockServer; @@ -46,34 +52,54 @@ class TelemetryServiceTest extends AbstractSpringIntegrationIndependentTest { private String destination; @BeforeEach - void init() { - telemetryServiceSpy = spy(telemetryService); - mockServer = MockRestServiceServer.createServer(restTemplate); - telemetryServiceSpy.useTelemetry = true; + void setUp() { + mockServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build(); } @Test void testSendTelemetry_TelemetryEnabled() throws Exception { + TelemetryService telemetryService = new TelemetryService(profileService, telemetrySendingService, true, true); + telemetryServiceSpy = spy(telemetryService); + mockServer.expect(ExpectedCount.once(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) + .andExpect(request -> assertThat(request.getBody().toString()).contains("adminName")) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(mapper.writeValueAsString("Success!"))); + telemetryServiceSpy.sendTelemetry(); + + await().atMost(2, SECONDS).untilAsserted(() -> mockServer.verify()); + } + + @Test + void testSendTelemetry_TelemetryEnabledWithoutPersonalData() throws Exception { + TelemetryService telemetryService = new TelemetryService(profileService, telemetrySendingService, true, false); + telemetryServiceSpy = spy(telemetryService); mockServer.expect(ExpectedCount.once(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) + .andExpect(request -> assertThat(request.getBody().toString()).doesNotContain("adminName")) .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(mapper.writeValueAsString("Success!"))); telemetryServiceSpy.sendTelemetry(); - await().atMost(1, SECONDS).untilAsserted(() -> mockServer.verify()); + + await().atMost(2, SECONDS).untilAsserted(() -> mockServer.verify()); } @Test void testSendTelemetry_TelemetryDisabled() throws Exception { + TelemetryService telemetryService = new TelemetryService(profileService, telemetrySendingService, false, true); + telemetryServiceSpy = spy(telemetryService); + mockServer.expect(ExpectedCount.never(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(mapper.writeValueAsString("Success!"))); - telemetryServiceSpy.useTelemetry = false; telemetryServiceSpy.sendTelemetry(); - await().atMost(1, SECONDS).untilAsserted(() -> mockServer.verify()); + await().atMost(2, SECONDS).untilAsserted(() -> mockServer.verify()); } @Test void testSendTelemetry_ExceptionHandling() throws Exception { + TelemetryService telemetryService = new TelemetryService(profileService, telemetrySendingService, true, true); + telemetryServiceSpy = spy(telemetryService); + mockServer.expect(ExpectedCount.once(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) .andRespond(withServerError().body(mapper.writeValueAsString("Failure!"))); + telemetryServiceSpy.sendTelemetry(); - await().atMost(1, SECONDS).untilAsserted(() -> mockServer.verify()); + await().atMost(2, SECONDS).untilAsserted(() -> mockServer.verify()); } } diff --git a/src/test/resources/config/application-artemis.yml b/src/test/resources/config/application-artemis.yml index d06190330e15..c43ee1c7f401 100644 --- a/src/test/resources/config/application-artemis.yml +++ b/src/test/resources/config/application-artemis.yml @@ -48,6 +48,8 @@ artemis: password: fake-password token: fake-token url: https://continuous-integration.fake.fake + concurrent-build-size: 1 + secret-push-token: fake-token-hash vcs-credentials: fake-key artemis-authentication-token-key: fake-key artemis-authentication-token-value: fake-token diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 670ef33f823f..16c56a619dd9 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -79,6 +79,7 @@ spring: application: name: Artemis datasource: + url: jdbc:mysql://artemis-mysql:3306/ type: com.zaxxer.hikari.HikariDataSource name: username: @@ -292,3 +293,10 @@ jhipster: aeolus: url: http://mock-aeolus-url:8090 + +# Eureka configuration +eureka: + client: + enabled: false + service-url: + defaultZone: http://admin:admin@localhost:8761/eureka/