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

Merged
merged 32 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
afab06b
Merge branch 'develop' into feature/telemetry/add-additional-fields
krusche Oct 19, 2024
463c858
Merge branch 'develop' into feature/telemetry/add-additional-fields
SimonEntholzer Oct 19, 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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<String> 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;
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("${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.
*
* <p>
* 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));
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> 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);
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

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;
}
}
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 @@ -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)
Expand All @@ -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;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
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.
* <p>
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -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;

Expand All @@ -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());
}
}
2 changes: 2 additions & 0 deletions src/test/resources/config/application-artemis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading