From bf071f8e56c7ebf9fb8d095f232874d4c54ed95c Mon Sep 17 00:00:00 2001 From: Paul Rangger <48455539+PaRangger@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:01:31 +0100 Subject: [PATCH] Development: Add health indicator api (#15) --- .../artemis/push/apns/ApnsSendService.java | 17 +++++++++++++- .../cit/artemis/push/common/SendService.java | 2 ++ .../push/firebase/FirebaseSendService.java | 19 +++++++++++++++ hermes/build.gradle | 4 ++++ .../cit/artemis/push/HealthController.java | 20 ++++++++++++++++ .../de/tum/cit/artemis/push/HealthReport.java | 4 ++++ .../push/NotificationHealthService.java | 23 +++++++++++++++++++ 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 hermes/src/main/java/de/tum/cit/artemis/push/HealthController.java create mode 100644 hermes/src/main/java/de/tum/cit/artemis/push/HealthReport.java create mode 100644 hermes/src/main/java/de/tum/cit/artemis/push/NotificationHealthService.java diff --git a/apns/src/main/java/de/tum/cit/artemis/push/apns/ApnsSendService.java b/apns/src/main/java/de/tum/cit/artemis/push/apns/ApnsSendService.java index d096972..0a45d12 100644 --- a/apns/src/main/java/de/tum/cit/artemis/push/apns/ApnsSendService.java +++ b/apns/src/main/java/de/tum/cit/artemis/push/apns/ApnsSendService.java @@ -1,6 +1,7 @@ package de.tum.cit.artemis.push.apns; import com.eatthepath.pushy.apns.*; +import com.eatthepath.pushy.apns.server.RejectionReason; import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder; import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture; @@ -20,6 +21,7 @@ import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.List; import java.util.concurrent.ExecutionException; @Service @@ -38,6 +40,8 @@ public class ApnsSendService implements SendService { private ApnsClient apnsClient; + private boolean isConnected; + @EventListener(ApplicationReadyEvent.class) public void applicationReady() { log.info("apnsCertificatePwd: {}", apnsCertificatePwd); @@ -52,8 +56,10 @@ public void applicationReady() { .setApnsServer(apnsProdEnvironment ? ApnsClientBuilder.PRODUCTION_APNS_HOST : ApnsClientBuilder.DEVELOPMENT_APNS_HOST) .setClientCredentials(new File(apnsCertificatePath), apnsCertificatePwd) .build(); + isConnected = true; log.info("Started APNS client successfully!"); } catch (IOException e) { + isConnected = false; log.error("Could not init APNS service", e); } } @@ -94,10 +100,14 @@ private ResponseEntity sendApnsRequest(NotificationRequest request) { final PushNotificationResponse pushNotificationResponse = responsePushNotificationFuture.get(); if (pushNotificationResponse.isAccepted()) { log.info("Send notification to {}", request.token()); + isConnected = true; return ResponseEntity.ok().build(); } else { + var rejectionReasons = List.of(RejectionReason.BAD_CERTIFICATE.toString(), RejectionReason.BAD_CERTIFICATE_ENVIRONMENT.toString()); + if(pushNotificationResponse.getRejectionReason().isPresent() && rejectionReasons.contains(pushNotificationResponse.getRejectionReason().get())) { + isConnected = false; + } log.error("Notification rejected by the APNs gateway: {}", pushNotificationResponse.getRejectionReason()); - pushNotificationResponse.getTokenInvalidationTimestamp().ifPresent(timestamp -> log.error("\t... and the token is invalid as of {}", timestamp)); return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).build(); } @@ -106,4 +116,9 @@ private ResponseEntity sendApnsRequest(NotificationRequest request) { return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).build(); } } + + @Override + public boolean isHealthy() { + return isConnected; + } } diff --git a/common/src/main/java/de/tum/cit/artemis/push/common/SendService.java b/common/src/main/java/de/tum/cit/artemis/push/common/SendService.java index 985529e..c405619 100644 --- a/common/src/main/java/de/tum/cit/artemis/push/common/SendService.java +++ b/common/src/main/java/de/tum/cit/artemis/push/common/SendService.java @@ -5,4 +5,6 @@ public interface SendService { ResponseEntity send(T request); + + boolean isHealthy(); } diff --git a/firebase/src/main/java/de/tum/cit/artemis/push/firebase/FirebaseSendService.java b/firebase/src/main/java/de/tum/cit/artemis/push/firebase/FirebaseSendService.java index 5a434ce..57c8ab6 100644 --- a/firebase/src/main/java/de/tum/cit/artemis/push/firebase/FirebaseSendService.java +++ b/firebase/src/main/java/de/tum/cit/artemis/push/firebase/FirebaseSendService.java @@ -6,6 +6,7 @@ import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; import de.tum.cit.artemis.push.common.NotificationRequest; import de.tum.cit.artemis.push.common.SendService; import org.slf4j.Logger; @@ -25,6 +26,8 @@ public class FirebaseSendService implements SendService firebaseApp = Optional.empty(); + private boolean isConnected; + public FirebaseSendService() { try { FirebaseOptions options = FirebaseOptions @@ -34,8 +37,11 @@ public FirebaseSendService() { .build(); firebaseApp = Optional.of(FirebaseApp.initializeApp(options)); + + isConnected = true; } catch (IOException e) { log.error("Exception while loading Firebase credentials", e); + isConnected = false; } } @@ -61,11 +67,24 @@ public ResponseEntity send(List requests) { try { FirebaseMessaging.getInstance(firebaseApp.get()).sendEach(batch); + isConnected = true; } catch (FirebaseMessagingException e) { + // In case the certificate is invalid, the THIRD_PARTY_AUTH_ERROR error code will be returned + var errorCodes = List.of(MessagingErrorCode.THIRD_PARTY_AUTH_ERROR); + + if(errorCodes.contains(e.getMessagingErrorCode())) { + isConnected = false; + } + return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).build(); } } return ResponseEntity.ok().build(); } + + @Override + public boolean isHealthy() { + return isConnected; + } } diff --git a/hermes/build.gradle b/hermes/build.gradle index 3448b98..22f8824 100644 --- a/hermes/build.gradle +++ b/hermes/build.gradle @@ -27,3 +27,7 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +springBoot { + buildInfo() +} diff --git a/hermes/src/main/java/de/tum/cit/artemis/push/HealthController.java b/hermes/src/main/java/de/tum/cit/artemis/push/HealthController.java new file mode 100644 index 0000000..0a99ea8 --- /dev/null +++ b/hermes/src/main/java/de/tum/cit/artemis/push/HealthController.java @@ -0,0 +1,20 @@ +package de.tum.cit.artemis.push; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class HealthController { + private final NotificationHealthService healthService; + + public HealthController(NotificationHealthService healthService) { + this.healthService = healthService; + } + + @GetMapping("/health") + public HealthReport getHealth() { + return healthService.getHealthReport(); + } +} diff --git a/hermes/src/main/java/de/tum/cit/artemis/push/HealthReport.java b/hermes/src/main/java/de/tum/cit/artemis/push/HealthReport.java new file mode 100644 index 0000000..c598eb1 --- /dev/null +++ b/hermes/src/main/java/de/tum/cit/artemis/push/HealthReport.java @@ -0,0 +1,4 @@ +package de.tum.cit.artemis.push; + +public record HealthReport(boolean isApnsConnected, boolean isFirebaseConnected, String versionNumber) { +} diff --git a/hermes/src/main/java/de/tum/cit/artemis/push/NotificationHealthService.java b/hermes/src/main/java/de/tum/cit/artemis/push/NotificationHealthService.java new file mode 100644 index 0000000..fa4580f --- /dev/null +++ b/hermes/src/main/java/de/tum/cit/artemis/push/NotificationHealthService.java @@ -0,0 +1,23 @@ +package de.tum.cit.artemis.push; + +import de.tum.cit.artemis.push.apns.ApnsSendService; +import de.tum.cit.artemis.push.firebase.FirebaseSendService; +import org.springframework.boot.info.BuildProperties; +import org.springframework.stereotype.Service; + +@Service +public class NotificationHealthService { + private final FirebaseSendService firebaseSendService; + private final ApnsSendService apnsSendService; + private final BuildProperties buildProperties; + + NotificationHealthService(FirebaseSendService firebaseSendService, ApnsSendService apnsSendService, BuildProperties buildProperties) { + this.firebaseSendService = firebaseSendService; + this.apnsSendService = apnsSendService; + this.buildProperties = buildProperties; + } + + public HealthReport getHealthReport() { + return new HealthReport(apnsSendService.isHealthy(), firebaseSendService.isHealthy(), buildProperties.getVersion()); + } +} \ No newline at end of file