Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Add more data for Telemetry #9345

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
848eb38
add more telemetry data
SimonEntholzer Sep 20, 2024
3a5d86f
disable on dev profile again
SimonEntholzer Sep 20, 2024
3829a34
improve coverage
SimonEntholzer Sep 20, 2024
e5c690a
simplify refactor and improve coverage
SimonEntholzer Sep 21, 2024
7c972b8
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Sep 23, 2024
8cb8520
improved service, incorporating feedback
SimonEntholzer Sep 26, 2024
8988d00
undo of disable disable
SimonEntholzer Sep 26, 2024
19c2db1
removed eureka querying until we have a better approach
SimonEntholzer Sep 28, 2024
97fa018
removed now unused EurekaClientService
SimonEntholzer Sep 28, 2024
c4091b8
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Sep 28, 2024
3e07931
Merge remote-tracking branch 'refs/remotes/origin/develop' into featu…
SimonEntholzer Sep 28, 2024
eb973ce
resolved merge conflict
SimonEntholzer Sep 28, 2024
389aba7
moved exception handling inside async function and added additional l…
SimonEntholzer Sep 29, 2024
fb396a2
re-added eurekaClientService, and schedule telemetry task 2 minutes a…
SimonEntholzer Oct 1, 2024
e9ec333
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 2, 2024
58f0212
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 8, 2024
5d35b48
remove Async as task is scheduled now
SimonEntholzer Oct 8, 2024
c28f7fb
increase delay
SimonEntholzer Oct 8, 2024
bf31151
add comment
SimonEntholzer Oct 8, 2024
8fea6c1
remove delay in tests
SimonEntholzer Oct 8, 2024
ea0c9c3
put parameters in constructor
SimonEntholzer Oct 9, 2024
49423d7
removed unused property
SimonEntholzer Oct 9, 2024
f18e15f
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 12, 2024
e51fa7c
removed multi node and build agent telemetry data for now
SimonEntholzer Oct 12, 2024
7efc0ac
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 12, 2024
3b7f759
Update src/main/java/de/tum/cit/aet/artemis/core/service/telemetry/Te…
SimonEntholzer Oct 12, 2024
655c571
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 13, 2024
4d16985
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 14, 2024
5d9f093
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 14, 2024
3de091e
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package de.tum.cit.aet.artemis.core.service.telemetry;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class EurekaClientService {
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

private static final Logger log = LoggerFactory.getLogger(EurekaClientService.class);

@Value("${eureka.client.service-url.defaultZone}")
private String eurekaServiceUrl;

private final RestTemplate restTemplate;

public EurekaClientService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

/**
* Retrieves the number of Artemis application instances registered with the Eureka server.
* <p>
* This method makes an HTTP GET request to the Eureka server's `/api/eureka/applications` endpoint to
* retrieve a list of all registered applications. It then filters out the Artemis application
* and returns the number of instances associated with it. If any error occurs during the request
* (e.g., network issues, parsing issues, invalid URI), the method returns 1 as the default value.
*
* @return the number of Artemis application instances, or 1 if an error occurs.
*/
public long getNumberOfReplicas() {
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
try {
var eurekaURI = new URI(eurekaServiceUrl);
HttpHeaders headers = createHeaders(eurekaURI.getUserInfo());
HttpEntity<String> request = new HttpEntity<>(headers);
var requestUrl = eurekaURI.getScheme() + "://" + eurekaURI.getAuthority() + "/api/eureka/applications";

ResponseEntity<String> response = restTemplate.exchange(requestUrl, HttpMethod.GET, request, String.class);

ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response.getBody());
JsonNode applicationsNode = rootNode.get("applications");
for (JsonNode application : applicationsNode) {
if (application.get("name").asText().equals("ARTEMIS")) {
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
JsonNode instancesNode = application.get("instances");
return instancesNode.size();
}
}
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
catch (RestClientException | JacksonException | URISyntaxException e) {
log.warn("Error while trying to retrieve number of replicas.");
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

return 1;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Creates HTTP headers with Basic Authentication and JSON content type.
*
* @param auth the user credentials in the format "username:password" to be encoded and included in the Authorization header.
* @return HttpHeaders with Basic Authentication and JSON content types.
*/
private HttpHeaders createHeaders(String auth) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.US_ASCII));
String authHeader = "Basic " + new String(encodedAuth);

headers.set("Authorization", authHeader);
return headers;
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -21,23 +21,32 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

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, String contact, List<String> profiles, String adminName) {
public record TelemetryData(String version, String serverUrl, String operator, List<String> profiles, boolean isProductionInstance, String dataSource, long numberOfNodes,
long buildAgentCount, String contact, String adminName) {
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

private final Environment env;

private final RestTemplate restTemplate;

public TelemetrySendingService(Environment env, RestTemplate restTemplate) {
private final EurekaClientService eurekaClientService;

private final ProfileService profileService;

public TelemetrySendingService(Environment env, RestTemplate restTemplate, EurekaClientService eurekaClientService, ProfileService profileService) {
this.env = env;
this.restTemplate = restTemplate;
this.eurekaClientService = eurekaClientService;
this.profileService = profileService;
}

@Value("${artemis.version}")
Expand All @@ -53,29 +62,41 @@ public TelemetrySendingService(Environment env, RestTemplate restTemplate) {
private String operatorAdminName;

@Value("${info.contact}")
private String contact;

@Value("${artemis.telemetry.sendAdminDetails}")
private boolean sendAdminDetails;
private String operatorContact;

SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
@Value("${artemis.telemetry.destination}")
private String destination;

@Value("${spring.datasource.url}")
private String datasourceUrl;

SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
@Value("${artemis.continuous-integration.concurrent-build-size}")
private long buildAgentCount;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
public void sendTelemetryByPostRequest(boolean eurekaEnabled, boolean sendAdminDetails) throws Exception {

SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
long numberOfInstances = 1;
if (eurekaEnabled) {
numberOfInstances = eurekaClientService.getNumberOfReplicas();
}

TelemetryData telemetryData;
var dataSource = datasourceUrl.startsWith("jdbc:mysql") ? "mysql" : "postgresql";
List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
String contact = null;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
String adminName = null;
if (sendAdminDetails) {
telemetryData = new TelemetryData(version, serverUrl, operator, contact, activeProfiles, operatorAdminName);
}
else {
telemetryData = new TelemetryData(version, serverUrl, operator, null, activeProfiles, null);
contact = operatorContact;
adminName = operatorAdminName;
}
telemetryData = new TelemetryData(version, serverUrl, operator, activeProfiles, profileService.isProductionActive(), dataSource, numberOfInstances, buildAgentCount,
contact, adminName);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,6 +12,8 @@

import com.fasterxml.jackson.core.JsonProcessingException;

import de.tum.cit.aet.artemis.core.service.ProfileService;

@Service
@Profile(PROFILE_SCHEDULING)
public class TelemetryService {
Expand All @@ -22,11 +24,17 @@ public class TelemetryService {

private final TelemetrySendingService telemetrySendingService;

@Value("${artemis.telemetry.destination}")
private String destination;

@Value("${artemis.telemetry.enabled}")
public boolean useTelemetry;

@Value("${artemis.telemetry.destination}")
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
private String destination;
@Value("${artemis.telemetry.sendAdminDetails}")
public boolean sendAdminDetails;

SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
@Value("${eureka.client.enabled}")
public boolean eurekaEnabled;

public TelemetryService(ProfileService profileService, TelemetrySendingService telemetrySendingService) {
this.profileService = profileService;
Expand All @@ -46,7 +54,7 @@ public void sendTelemetry() {

log.info("Sending telemetry information");
try {
telemetrySendingService.sendTelemetryByPostRequest();
telemetrySendingService.sendTelemetryByPostRequest(eurekaEnabled, sendAdminDetails);
}
catch (JsonProcessingException e) {
log.warn("JsonProcessingException in sendTelemetry.", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
package de.tum.cit.aet.artemis.telemetry;

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.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;

import java.net.URI;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import de.tum.cit.aet.artemis.AbstractSpringIntegrationIndependentTest;
import de.tum.cit.aet.artemis.core.service.TelemetryService;
import de.tum.cit.aet.artemis.core.service.telemetry.TelemetryService;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

@ExtendWith(MockitoExtension.class)
class TelemetryServiceTest extends AbstractSpringIntegrationIndependentTest {
Expand All @@ -46,19 +52,58 @@ class TelemetryServiceTest extends AbstractSpringIntegrationIndependentTest {
@Value("${artemis.telemetry.destination}")
private String destination;

@Value("${eureka.client.service-url.defaultZone}")
private String defaultZoneUrl;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

private String eurekaRequestUrl;

private byte[] applicationsBody;

@BeforeEach
void init() {
void init() throws JsonProcessingException {
try {
var eurekaURI = new URI(defaultZoneUrl);
eurekaRequestUrl = eurekaURI.getScheme() + "://" + eurekaURI.getAuthority() + "/api/eureka/applications";

}
catch (Exception ignored) {
// Exception can be ignored because defaultZoneUrl is guaranteed to be valid in the test environment
}
telemetryServiceSpy = spy(telemetryService);
mockServer = MockRestServiceServer.createServer(restTemplate);
applicationsBody = mapper.writeValueAsBytes(Map.of("applications", List.of(Map.of("name", "ARTEMIS", "instances", List.of(Map.of())))));
mockServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

telemetryServiceSpy.useTelemetry = true;
telemetryServiceSpy.eurekaEnabled = true;
}

@Test
void testSendTelemetry_TelemetryEnabled() throws Exception {
mockServer.expect(ExpectedCount.once(), requestTo(new URI(eurekaRequestUrl))).andExpect(method(HttpMethod.GET))
.andExpect(header(HttpHeaders.AUTHORIZATION, "Basic YWRtaW46YWRtaW4="))
.andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(applicationsBody));

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(1, SECONDS).untilAsserted(() -> mockServer.verify());

await().atMost(2, SECONDS).untilAsserted(() -> mockServer.verify());
}

@Test
void testSendTelemetry_TelemetryEnabledWithoutPersonalData() throws Exception {
telemetryServiceSpy.sendAdminDetails = false;
mockServer.expect(ExpectedCount.once(), requestTo(new URI(eurekaRequestUrl))).andExpect(method(HttpMethod.GET))
.andExpect(header(HttpHeaders.AUTHORIZATION, "Basic YWRtaW46YWRtaW4="))
.andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(applicationsBody));

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!")));
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
telemetryServiceSpy.sendTelemetry();

SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
await().atMost(2, SECONDS).untilAsserted(() -> mockServer.verify());
}

@Test
Expand All @@ -72,9 +117,12 @@ void testSendTelemetry_TelemetryDisabled() throws Exception {

@Test
void testSendTelemetry_ExceptionHandling() throws Exception {
mockServer.expect(ExpectedCount.once(), requestTo(new URI(eurekaRequestUrl))).andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(applicationsBody));
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());
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
}
1 change: 1 addition & 0 deletions src/test/resources/config/application-artemis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ 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
Expand Down
8 changes: 8 additions & 0 deletions src/test/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ spring:
application:
name: Artemis
datasource:
url: jdbc:mysql://artemis-mysql:3306/
type: com.zaxxer.hikari.HikariDataSource
name:
username:
Expand Down Expand Up @@ -288,3 +289,10 @@ jhipster:

aeolus:
url: http://mock-aeolus-url:8090

# Eureka configuration
eureka:
client:
enabled: false
service-url:
defaultZone: http://admin:admin@localhost:8761/eureka/
Loading