diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd4f47..176bad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to the DevStack API Service project will be documented in th The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.2] - 2026-02-20 + +### Added + +#### External Service API Module (`external-service-api`) +- **New module** for standardizing external service integrations +- `ExternalService` interface providing common contract for all external services +- `isHealthy()` method for health check implementation across all external services +- `AbstractExternalServiceHealthIndicator` base class for standardized health monitoring + +#### Health Monitoring & Observability +- **Spring Boot Actuator** health indicators for all external services: + +### Changed +- Updated all external service implementations to extend `ExternalService` interface +- Use of lombok.extern.slf4j.Slf4j to remove boilerplate code. + +### Dependencies +- Updated Spring Boot version +- Added health indicator dependencies across external service modules + +--- + ## [0.0.1-SNAPSHOT] - 2025-11-27 ### Added diff --git a/Makefile b/Makefile index f495425..10c1dc4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Project variables PROJECT_NAME := devstack-api-service -VERSION := 0.0.1-SNAPSHOT +VERSION := 0.0.2-SNAPSHOT JAVA_VERSION := 21 MAIN_CLASS := org.opendevstack.apiservice.core.DevstackApiServiceApplication diff --git a/README.md b/README.md index cb59890..7ae7011 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Build a traditional Spring Boot JAR file: ```bash make jar ``` -- Output: `core/target/core-0.0.1-SNAPSHOT.jar` +- Output: `core/target/core-0.0.2-SNAPSHOT.jar` - Includes all dependencies - Standard Spring Boot startup time diff --git a/api-project-platform/openapi/api-project-platform.yaml b/api-project-platform/openapi/api-project-platform.yaml index f467736..ce4a357 100644 --- a/api-project-platform/openapi/api-project-platform.yaml +++ b/api-project-platform/openapi/api-project-platform.yaml @@ -3,8 +3,7 @@ info: title: ODS API Server description: API documentation for ODS (Open DevStack) API Service contact: - name: EDPCore Team - url: https://confluence.biscrum.com/pages/viewpage.action?spaceKey=EDP&title=Welcome + name: ODS Team version: v0.0.1 servers: - url: http://{baseurl}/api/v1 diff --git a/api-project-platform/pom.xml b/api-project-platform/pom.xml index b2b2b23..c9bd226 100644 --- a/api-project-platform/pom.xml +++ b/api-project-platform/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT api-project-platform @@ -139,4 +139,4 @@ - \ No newline at end of file + diff --git a/api-project-users/pom.xml b/api-project-users/pom.xml index 099cba8..c273fca 100644 --- a/api-project-users/pom.xml +++ b/api-project-users/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT api-project-users @@ -131,4 +131,4 @@ - \ No newline at end of file + diff --git a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/controller/ProjectUserController.java b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/controller/ProjectUserController.java index 08be2d0..92845aa 100644 --- a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/controller/ProjectUserController.java +++ b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/controller/ProjectUserController.java @@ -9,22 +9,21 @@ import org.opendevstack.apiservice.projectusers.exception.InvalidTokenException; import org.opendevstack.apiservice.projectusers.service.MembershipRequestStatusService; import org.opendevstack.apiservice.projectusers.service.ProjectUserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import lombok.extern.slf4j.Slf4j; + /** * REST Controller for managing project users and their roles. * Provides endpoints for adding, removing, updating, and querying users in projects. */ +@Slf4j @RestController @RequestMapping("/api/v1") public class ProjectUserController implements ProjectUsersApi{ - private static final Logger logger = LoggerFactory.getLogger(ProjectUserController.class); - private final ProjectUserService projectUserService; private final MembershipRequestStatusService statusService; @@ -46,7 +45,7 @@ public ResponseEntity triggerMembershipReq String projectKey, AddUserToProjectRequest addUserToProjectRequest) { - logger.info("Triggering membership request for account '{}' of user '{}' to project '{}' with role '{}'", + log.info("Triggering membership request for account '{}' of user '{}' to project '{}' with role '{}'", addUserToProjectRequest.getAccount(), addUserToProjectRequest.getUser(), projectKey, addUserToProjectRequest.getRole()); MembershipRequestResponse response = projectUserService.addUserToProject(projectKey, addUserToProjectRequest); @@ -66,7 +65,7 @@ public ResponseEntity getRequestStat String user, String requestId) { - logger.info("Getting status for request '{}' - project '{}', user '{}'", requestId, projectKey, user); + log.info("Getting status for request '{}' - project '{}', user '{}'", requestId, projectKey, user); // Check the request is valid for the given project and user if (!statusService.validateRequestToken(requestId, projectKey, user)) { diff --git a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandler.java b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandler.java index ab69f2b..c53ea14 100644 --- a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandler.java +++ b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandler.java @@ -8,8 +8,8 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -31,11 +31,10 @@ * Provides comprehensive error handling with detailed validation error * messages. */ +@Slf4j @ControllerAdvice public class GlobalExceptionHandler { - private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); - /** * Handles validation errors from @Valid annotations on request bodies. */ @@ -43,7 +42,7 @@ public class GlobalExceptionHandler { public ResponseEntity handleMethodArgumentNotValidException( MethodArgumentNotValidException ex) { - logger.warn("Validation failed for request: {}", ex.getMessage()); + log.warn("Validation failed for request: {}", ex.getMessage()); List fieldErrors = new ArrayList<>(); @@ -90,7 +89,7 @@ public ResponseEntity handleMethodArgumentNotValidExcep public ResponseEntity handleConstraintViolationException( ConstraintViolationException ex) { - logger.warn("Constraint violation: {}", ex.getMessage()); + log.warn("Constraint violation: {}", ex.getMessage()); List errors = ex.getConstraintViolations() .stream() @@ -114,7 +113,7 @@ public ResponseEntity handleConstraintViolationException( public ResponseEntity handleHttpMessageNotReadableException( HttpMessageNotReadableException ex) { - logger.warn("Invalid request body: {}", ex.getMessage()); + log.warn("Invalid request body: {}", ex.getMessage()); String errorMessage = ErrorMessages.INVALID_REQUEST_BODY; String errorCode = ErrorCodes.PROJECT_USER_ERROR; @@ -171,7 +170,7 @@ public ResponseEntity handleHttpMessageNotReadableException( public ResponseEntity handleMissingPathVariableException( MissingPathVariableException ex) { - logger.warn("Missing path variable: {}", ex.getMessage()); + log.warn("Missing path variable: {}", ex.getMessage()); String errorMessage = String.format( ErrorMessages.REQUIRED_PATH_PARAMETER_MISSING, @@ -191,7 +190,7 @@ public ResponseEntity handleMissingPathVariableException( public ResponseEntity handleMissingServletRequestParameterException( MissingServletRequestParameterException ex) { - logger.warn("Missing request parameter: {}", ex.getMessage()); + log.warn("Missing request parameter: {}", ex.getMessage()); String errorMessage = String.format( ErrorMessages.REQUIRED_REQUEST_PARAMETER_MISSING, @@ -211,7 +210,7 @@ public ResponseEntity handleMissingServletRequestParameterExcep public ResponseEntity handleMethodArgumentTypeMismatchException( MethodArgumentTypeMismatchException ex) { - logger.warn("Method argument type mismatch: {}", ex.getMessage()); + log.warn("Method argument type mismatch: {}", ex.getMessage()); String errorMessage = String.format( ErrorMessages.PARAMETER_TYPE_CONVERSION_FAILED, @@ -233,7 +232,7 @@ public ResponseEntity handleMethodArgumentTypeMismatchException public ResponseEntity handleProjectNotFoundException( ProjectNotFoundException ex) { - logger.warn("Project not found: {}", ex.getMessage()); + log.warn("Project not found: {}", ex.getMessage()); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(ex.getMessage()); @@ -249,7 +248,7 @@ public ResponseEntity handleProjectNotFoundException( public ResponseEntity handleUserNotFoundException( UserNotFoundException ex) { - logger.warn("User not found: {}", ex.getMessage()); + log.warn("User not found: {}", ex.getMessage()); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(ex.getMessage()); @@ -265,7 +264,7 @@ public ResponseEntity handleUserNotFoundException( public ResponseEntity handleUserNotAuthenticatedException( UserNotAuthenticatedException ex) { - logger.warn("User not authenticated: {}", ex.getMessage()); + log.warn("User not authenticated: {}", ex.getMessage()); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(ex.getMessage()); @@ -281,7 +280,7 @@ public ResponseEntity handleUserNotAuthenticatedException( public ResponseEntity handleUserNotAuthorizedException( UserNotAuthorizedException ex) { - logger.warn("User not authorized: {}", ex.getMessage()); + log.warn("User not authorized: {}", ex.getMessage()); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(ex.getMessage()); @@ -297,7 +296,7 @@ public ResponseEntity handleUserNotAuthorizedException( public ResponseEntity handleInvalidRoleException( InvalidRoleException ex) { - logger.warn("Invalid role: {}", ex.getMessage()); + log.warn("Invalid role: {}", ex.getMessage()); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(ex.getMessage()); @@ -313,7 +312,7 @@ public ResponseEntity handleInvalidRoleException( public ResponseEntity handleAutomationPlatformException( AutomationPlatformException ex) { - logger.error("Automation platform error: {}", ex.getMessage(), ex); + log.error("Automation platform error: {}", ex.getMessage(), ex); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(String.format(ErrorMessages.EXTERNAL_SERVICE_ERROR, ex.getMessage())); @@ -327,7 +326,7 @@ public ResponseEntity handleAutomationPlatformException( */ @ExceptionHandler(ProjectUserException.class) public ResponseEntity handleProjectUserException(ProjectUserException ex) { - logger.error("Project user operation failed: {}", ex.getMessage(), ex); + log.error("Project user operation failed: {}", ex.getMessage(), ex); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(String.format(ErrorMessages.OPERATION_FAILED, ex.getMessage())); @@ -341,7 +340,7 @@ public ResponseEntity handleProjectUserException(ProjectUserExc */ @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception ex) { - logger.error("Unexpected error occurred: {}", ex.getMessage(), ex); + log.error("Unexpected error occurred: {}", ex.getMessage(), ex); BaseApiResponse errorResponse = new BaseApiResponse(); errorResponse.setSuccess(false); errorResponse.setMessage(ErrorMessages.UNEXPECTED_ERROR); diff --git a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestStatusServiceImpl.java b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestStatusServiceImpl.java index 0e57a4c..a3e95b7 100644 --- a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestStatusServiceImpl.java +++ b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestStatusServiceImpl.java @@ -8,10 +8,10 @@ import org.opendevstack.apiservice.projectusers.model.MembershipRequestStatusResponse; import org.opendevstack.apiservice.projectusers.service.MembershipRequestStatusService; import org.opendevstack.apiservice.projectusers.service.MembershipRequestTokenService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + import java.util.Map; /** @@ -25,11 +25,10 @@ * The overall status is determined by checking both systems and ensuring both * have completed successfully. */ +@Slf4j @Service("membershipRequestStatusService") public class MembershipRequestStatusServiceImpl implements MembershipRequestStatusService { - private static final Logger logger = LoggerFactory.getLogger(MembershipRequestStatusServiceImpl.class); - private final MembershipRequestTokenService tokenService; private final AutomationPlatformService automationPlatformService; private final UiPathOrchestratorService uiPathService; @@ -44,7 +43,7 @@ public MembershipRequestStatusServiceImpl(MembershipRequestTokenService tokenSer @Override public MembershipRequestStatusResponse getRequestStatus(String requestId) { - logger.debug("Getting status for request ID: {}", requestId); + log.debug("Getting status for request ID: {}", requestId); // Decode and validate the request token Map tokenData = tokenService.decodeRequestToken(requestId); @@ -58,11 +57,11 @@ public MembershipRequestStatusResponse getRequestStatus(String requestId) { try { // Step 1: Check Ansible Automation Platform status AutomationJobStatus ansibleStatus = automationPlatformService.getWorkflowJobStatus(jobId); - logger.debug("Ansible job '{}' status: {}", jobId, ansibleStatus.getStatus()); + log.debug("Ansible job '{}' status: {}", jobId, ansibleStatus.getStatus()); // If AAP is not completed, return in-progress status if (!isAnsibleTerminalStatus(ansibleStatus.getStatus())) { - logger.debug("Ansible workflow still in progress"); + log.debug("Ansible workflow still in progress"); return createResponse(requestId, projectKey, user, environment, MembershipRequestStatusResponse.StatusEnum.IN_PROGRESS, false, false, @@ -72,7 +71,7 @@ public MembershipRequestStatusResponse getRequestStatus(String requestId) { // Step 2: If AAP has completed but failed, return failure status if (ansibleStatus.getStatus() != AutomationJobStatus.Status.SUCCESSFUL) { - logger.info("Ansible workflow completed with failure status: {}", ansibleStatus.getStatus()); + log.info("Ansible workflow completed with failure status: {}", ansibleStatus.getStatus()); return createResponse(requestId, projectKey, user, environment, MembershipRequestStatusResponse.StatusEnum.COMPLETED, true, false, @@ -81,15 +80,15 @@ public MembershipRequestStatusResponse getRequestStatus(String requestId) { } // Step 3: AAP succeeded, now check UIPath status - logger.debug("Ansible workflow completed successfully, checking UIPath status"); + log.debug("Ansible workflow completed successfully, checking UIPath status"); return checkUiPathAndCreateResponse(requestId, projectKey, user, environment, uipathReference); } catch (AutomationPlatformException e) { - logger.error("Failed to get job status for request '{}': {}", requestId, e.getMessage(), e); + log.error("Failed to get job status for request '{}': {}", requestId, e.getMessage(), e); return createErrorResponse(requestId, projectKey, user, environment, "Failed to retrieve request status", e.getMessage()); } catch (Exception e) { - logger.error("Unexpected error while getting request status for '{}': {}", requestId, e.getMessage(), e); + log.error("Unexpected error while getting request status for '{}': {}", requestId, e.getMessage(), e); return createErrorResponse(requestId, projectKey, user, environment, "Failed to retrieve request status", e.getMessage()); } @@ -102,7 +101,7 @@ private MembershipRequestStatusResponse checkUiPathAndCreateResponse(String requ String user, String environment, String uipathReference) { - logger.debug("Checking UIPath status for reference: '{}'", uipathReference); + log.debug("Checking UIPath status for reference: '{}'", uipathReference); // Use the generic service method to check the queue item status UiPathQueueItemResult result = uiPathService.checkQueueItemByReference(uipathReference); @@ -110,17 +109,17 @@ private MembershipRequestStatusResponse checkUiPathAndCreateResponse(String requ // Map the result to the appropriate response return switch (result.getResultStatus()) { case NO_REFERENCE -> { - logger.debug("No UIPath reference provided, marking request as completed successfully"); + log.debug("No UIPath reference provided, marking request as completed successfully"); yield createSuccessResponse(requestId, projectKey, user, environment, "Membership request completed"); } case NOT_FOUND -> { - logger.warn("UIPath queue item not found for reference: '{}'", uipathReference); + log.warn("UIPath queue item not found for reference: '{}'", uipathReference); yield createErrorResponse(requestId, projectKey, user, environment, result.getMessage(), result.getErrorDetails()); } case IN_PROGRESS -> { - logger.debug("UIPath process is still in progress"); + log.debug("UIPath process is still in progress"); yield createResponse(requestId, projectKey, user, environment, MembershipRequestStatusResponse.StatusEnum.IN_PROGRESS, false, false, @@ -128,17 +127,17 @@ yield createResponse(requestId, projectKey, user, environment, null); } case SUCCESS -> { - logger.debug("UIPath process completed successfully"); + log.debug("UIPath process completed successfully"); yield createSuccessResponse(requestId, projectKey, user, environment, "Membership request completed"); } case FAILURE -> { - logger.warn("UIPath process failed"); + log.warn("UIPath process failed"); yield createErrorResponse(requestId, projectKey, user, environment, result.getMessage(), result.getErrorDetails()); } case ERROR -> { - logger.error("Error checking UIPath status: {}", result.getMessage()); + log.error("Error checking UIPath status: {}", result.getMessage()); yield createErrorResponse(requestId, projectKey, user, environment, result.getMessage(), result.getErrorDetails()); } @@ -207,7 +206,7 @@ private boolean isAnsibleTerminalStatus(AutomationJobStatus.Status status) { @Override public boolean validateRequestToken(String requestId, String projectKey, String user) { - logger.debug("Validating request token for requestId: {}, projectKey: {}, user: {}", requestId, projectKey, user); + log.debug("Validating request token for requestId: {}, projectKey: {}, user: {}", requestId, projectKey, user); try { Map tokenData = tokenService.decodeRequestToken(requestId); String tokenProjectKey = (String) tokenData.get("projectKey"); @@ -215,11 +214,11 @@ public boolean validateRequestToken(String requestId, String projectKey, String boolean isValid = projectKey.equals(tokenProjectKey) && user.equals(tokenUser); if (!isValid) { - logger.warn("Request token validation failed: projectKey or user does not match"); + log.warn("Request token validation failed: projectKey or user does not match"); } return isValid; } catch (Exception e) { - logger.error("Error validating request token: {}", e.getMessage(), e); + log.error("Error validating request token: {}", e.getMessage(), e); return false; } } diff --git a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestTokenServiceImpl.java b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestTokenServiceImpl.java index 1fb6c49..cda3d98 100644 --- a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestTokenServiceImpl.java +++ b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/MembershipRequestTokenServiceImpl.java @@ -8,8 +8,8 @@ import org.opendevstack.apiservice.projectusers.service.JwtMembershipRequestClaims; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -24,11 +24,10 @@ * JWT-based implementation of MembershipRequestTokenService. * Provides stateless request tracking using signed JWT tokens. */ +@Slf4j @Service("membershipRequestTokenService") public class MembershipRequestTokenServiceImpl implements MembershipRequestTokenService { - private static final Logger logger = LoggerFactory.getLogger(MembershipRequestTokenServiceImpl.class); - private final SecretKey secretKey; private final long tokenExpirationHours; @@ -67,11 +66,11 @@ public String createRequestToken(String jobId, String uipathReference, String pr // Generate user-friendly request ID with prefix and embed the full token String requestId = "req_" + System.currentTimeMillis() + "_" + token; - logger.debug("Created request token for job '{}', project '{}', user '{}'", jobId, projectKey, user); + log.debug("Created request token for job '{}', project '{}', user '{}'", jobId, projectKey, user); return requestId; } catch (Exception e) { - logger.warn("Failed to create request token for job '{}': {}", jobId, e.getMessage(), e); + log.warn("Failed to create request token for job '{}': {}", jobId, e.getMessage(), e); throw new TokenCreationException("Failed to create request token", e); } } @@ -112,16 +111,16 @@ public Map decodeRequestToken(String token) { return result; } catch (ExpiredJwtException e) { - logger.warn("Request token expired: {}", e.getMessage()); + log.warn("Request token expired: {}", e.getMessage()); throw new TokenExpiredException("Request token has expired"); } catch (JwtException e) { - logger.warn("Invalid request token: {}", e.getMessage()); + log.warn("Invalid request token: {}", e.getMessage()); throw new InvalidTokenException("Invalid request token"); } catch (InvalidTokenException e) { // Re-throw InvalidTokenException as-is throw e; } catch (Exception e) { - logger.warn("Failed to decode request token: {}", e.getMessage(), e); + log.warn("Failed to decode request token: {}", e.getMessage(), e); throw new TokenDecodingException("Failed to decode request token", e); } } @@ -136,7 +135,7 @@ public boolean isValidToken(String token) { .parseSignedClaims(jwtToken); return true; } catch (Exception e) { - logger.error("Token validation failed: {}", e.getMessage(), e); + log.error("Token validation failed: {}", e.getMessage(), e); return false; } } @@ -152,7 +151,7 @@ public String extractJobId(String token) { .getPayload(); return claims.get(JwtMembershipRequestClaims.CLAIM_JOB_ID, String.class); } catch (Exception e) { - logger.error("Failed to extract job ID from token: {}", e.getMessage(), e); + log.error("Failed to extract job ID from token: {}", e.getMessage(), e); return null; } } diff --git a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/ProjectUserServiceImpl.java b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/ProjectUserServiceImpl.java index 343e27d..9cad520 100644 --- a/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/ProjectUserServiceImpl.java +++ b/api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/service/impl/ProjectUserServiceImpl.java @@ -8,21 +8,20 @@ import org.opendevstack.apiservice.projectusers.model.MembershipRequestResponse; import org.opendevstack.apiservice.projectusers.service.MembershipRequestTokenService; import org.opendevstack.apiservice.projectusers.service.ProjectUserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + import java.util.*; /** * Implementation of ProjectUserService that manages project users and integrates with automation platform. * This is a stateless implementation that uses the automation platform for persistence. */ +@Slf4j @Service("projectUserService") public class ProjectUserServiceImpl implements ProjectUserService { - - private static final Logger logger = LoggerFactory.getLogger(ProjectUserServiceImpl.class); @Value("${apis.project-users.ansible-workflow-name}") private String addUserWorkflow; @@ -38,7 +37,7 @@ public ProjectUserServiceImpl(AutomationPlatformService automationPlatformServic @Override public MembershipRequestResponse addUserToProject(String projectKey, AddUserToProjectRequest request) { - logger.info("Adding user '{}' to project '{}' with role '{}'", request.getUser(), projectKey, request.getRole()); + log.info("Adding user '{}' to project '{}' with role '{}'", request.getUser(), projectKey, request.getRole()); // Validate project exists (this would typically be a call to a project service) validateProject(projectKey); @@ -75,7 +74,7 @@ public MembershipRequestResponse addUserToProject(String projectKey, AddUserToPr getCurrentUser() ); - logger.info("Successfully triggered membership request for user '{}' to project '{}' with job ID: {} and request ID: {}", + log.info("Successfully triggered membership request for user '{}' to project '{}' with job ID: {} and request ID: {}", request.getUser(), projectKey, result.getJobId(), requestId); // Create response with request tracking information @@ -96,7 +95,7 @@ public MembershipRequestResponse addUserToProject(String projectKey, AddUserToPr } } catch (org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException e) { - logger.error("Failed to add user '{}' to project '{}': {}", request.getUser(), projectKey, e.getMessage(), e); + log.error("Failed to add user '{}' to project '{}': {}", request.getUser(), projectKey, e.getMessage(), e); throw new AutomationPlatformException( "Automation platform execution failed", e); } @@ -107,7 +106,7 @@ private void validateProject(String projectKey) { // In a real implementation, this would validate against a project service // In our case interact with Jira to check if the project exists. if (projectKey == null || projectKey.trim().isEmpty()) { - logger.error("Invalid project key: {}", projectKey); + log.error("Invalid project key: {}", projectKey); throw new ProjectNotFoundException(projectKey); } // Add additional validation logic as needed diff --git a/application.yaml b/application.yaml index 603ecc0..61afb62 100644 --- a/application.yaml +++ b/application.yaml @@ -16,6 +16,9 @@ management: show-values: always loggers: access: unrestricted + health: + show-details: always + show-components: always info: git: # Show all build-time generated file git.properties info on /actuator/info endpoint @@ -41,7 +44,7 @@ openapi: otel: service: name: devstack-api-service-dev - version: 0.0.1-SNAPSHOT + version: 0.0.2-SNAPSHOT exporter: otlp: endpoint: http://opentelemetry.example.com @@ -52,7 +55,7 @@ otel: metrics: exporter: none resource: - attributes: service.name=devstack-api-service,service.version=0.0.1-SNAPSHOT,deployment.environment=development + attributes: service.name=devstack-api-service,service.version=0.0.2-SNAPSHOT,deployment.environment=development instrumentation: jdbc: enabled: false diff --git a/core/pom.xml b/core/pom.xml index 455b992..db90d6d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT core @@ -41,6 +41,13 @@ spring-boot-starter-actuator + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springdoc @@ -226,4 +233,4 @@ - \ No newline at end of file + diff --git a/core/src/main/java/org/opendevstack/apiservice/core/config/SecurityConfig.java b/core/src/main/java/org/opendevstack/apiservice/core/config/SecurityConfig.java index d945b43..cd8b73e 100644 --- a/core/src/main/java/org/opendevstack/apiservice/core/config/SecurityConfig.java +++ b/core/src/main/java/org/opendevstack/apiservice/core/config/SecurityConfig.java @@ -1,6 +1,5 @@ package org.opendevstack.apiservice.core.config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -10,16 +9,15 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; - +import lombok.extern.slf4j.Slf4j; import java.util.Arrays; +@Slf4j @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { - private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class); - private final SecurityProperties securityProperties; private final FlowProperties flowProperties; diff --git a/external-service-aap/pom.xml b/external-service-aap/pom.xml index c44dbc7..967c813 100644 --- a/external-service-aap/pom.xml +++ b/external-service-aap/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT external-service-aap @@ -14,6 +14,13 @@ Service module for integrating with external automation platforms like Ansible + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springframework.boot @@ -32,6 +39,12 @@ spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + com.fasterxml.jackson.core @@ -71,4 +84,4 @@ - \ No newline at end of file + diff --git a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/config/ExternalServiceConfig.java b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/config/ExternalServiceConfig.java index a94349c..097b91a 100644 --- a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/config/ExternalServiceConfig.java +++ b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/config/ExternalServiceConfig.java @@ -1,7 +1,5 @@ package org.opendevstack.apiservice.externalservice.aap.config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -12,6 +10,8 @@ import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; +import lombok.extern.slf4j.Slf4j; + import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; @@ -28,10 +28,9 @@ @Configuration @EnableAsync @EnableConfigurationProperties(SslProperties.class) +@Slf4j public class ExternalServiceConfig { - private static final Logger logger = LoggerFactory.getLogger(ExternalServiceConfig.class); - private final SslProperties sslProperties; public ExternalServiceConfig(@Qualifier("aapSslProperties") SslProperties sslProperties) { @@ -46,10 +45,10 @@ public ExternalServiceConfig(@Qualifier("aapSslProperties") SslProperties sslPro @Bean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { if (!sslProperties.isVerifyCertificates()) { - logger.warn("SSL certificate verification is DISABLED - this should only be used in development environments"); + log.warn("SSL certificate verification is DISABLED - this should only be used in development environments"); return createInsecureRestTemplate(); } else { - logger.info("SSL certificate verification is ENABLED"); + log.info("SSL certificate verification is ENABLED"); return createSecureRestTemplate(); } } @@ -94,7 +93,7 @@ protected void prepareConnection(HttpURLConnection connection, String httpMethod return new RestTemplate(requestFactory); } catch (GeneralSecurityException e) { - logger.warn("Failed to create insecure RestTemplate, falling back to default: {}", e.getMessage()); + log.warn("Failed to create insecure RestTemplate, falling back to default: {}", e.getMessage()); return new RestTemplate(); } } @@ -103,7 +102,7 @@ private RestTemplate createSecureRestTemplate() { try { // If custom trust store is provided, configure it if (StringUtils.hasText(sslProperties.getTrustStorePath())) { - logger.info("Custom trust store specified: {} (custom trust store support can be added in future versions)", + log.info("Custom trust store specified: {} (custom trust store support can be added in future versions)", sslProperties.getTrustStorePath()); } @@ -111,7 +110,7 @@ private RestTemplate createSecureRestTemplate() { return new RestTemplate(); } catch (Exception e) { - logger.warn("Failed to create secure RestTemplate with custom trust store, using default: {}", e.getMessage()); + log.warn("Failed to create secure RestTemplate with custom trust store, using default: {}", e.getMessage()); return new RestTemplate(); } } diff --git a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/health/AapHealthIndicator.java b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/health/AapHealthIndicator.java new file mode 100644 index 0000000..1cbf277 --- /dev/null +++ b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/health/AapHealthIndicator.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.externalservice.aap.health; + +import org.opendevstack.apiservice.externalservice.aap.service.AutomationPlatformService; +import org.opendevstack.apiservice.externalservice.api.health.AbstractExternalServiceHealthIndicator; +import org.springframework.stereotype.Component; + +/** + * Health indicator for Ansible Automation Platform (AAP) service. + * Provides health status information for the actuator endpoint. + */ +@Component +public class AapHealthIndicator extends AbstractExternalServiceHealthIndicator { + + public AapHealthIndicator(AutomationPlatformService automationPlatformService) { + super(automationPlatformService, "Ansible Automation Platform"); + } +} diff --git a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java index 9bb4df2..57cecb7 100644 --- a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java +++ b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.externalservice.aap.service; +import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException; import org.opendevstack.apiservice.externalservice.aap.model.AutomationExecutionResult; import org.opendevstack.apiservice.externalservice.aap.model.AutomationJobStatus; @@ -11,7 +12,7 @@ * Generic service interface for integrating with automation platforms like Ansible Automation Platform. * This interface provides a generic way to call different workflows and modules. */ -public interface AutomationPlatformService { +public interface AutomationPlatformService extends ExternalService { /** * Executes a workflow on the automation platform synchronously. diff --git a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/impl/AnsibleAutomationPlatformService.java b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/impl/AnsibleAutomationPlatformService.java index b58c469..7a3395d 100644 --- a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/impl/AnsibleAutomationPlatformService.java +++ b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/impl/AnsibleAutomationPlatformService.java @@ -4,8 +4,6 @@ import org.opendevstack.apiservice.externalservice.aap.model.AutomationExecutionResult; import org.opendevstack.apiservice.externalservice.aap.model.AutomationJobStatus; import org.opendevstack.apiservice.externalservice.aap.service.AutomationPlatformService; -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; @@ -17,6 +15,8 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriUtils; +import lombok.extern.slf4j.Slf4j; + import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -28,10 +28,9 @@ * This service provides integration with Ansible AWX/Tower for executing workflows and modules. */ @Service("automationPlatformService") +@Slf4j public class AnsibleAutomationPlatformService implements AutomationPlatformService { - private static final Logger logger = LoggerFactory.getLogger(AnsibleAutomationPlatformService.class); - private final RestTemplate restTemplate; @Value("${automation.platform.ansible.base-url:http://localhost:8080/api/v2}") @@ -52,7 +51,7 @@ public AnsibleAutomationPlatformService(RestTemplate restTemplate) { @Override public AutomationExecutionResult executeWorkflow(String workflowName, Map parameters) throws AutomationPlatformException { - logger.info("Executing workflow '{}' with parameters: {}", workflowName, parameters); + log.info("Executing workflow '{}' with parameters: {}", workflowName, parameters); try { // Create headers with authentication @@ -77,14 +76,14 @@ public AutomationExecutionResult executeWorkflow(String workflowName, Map executeWorkflowAsync(String AutomationExecutionResult result = executeWorkflow(workflowName, parameters); return CompletableFuture.completedFuture(result); } catch (AutomationPlatformException e) { - logger.error("Async workflow execution failed: {}", e.getMessage(), e); + log.error("Async workflow execution failed: {}", e.getMessage(), e); AutomationExecutionResult errorResult = AutomationExecutionResult.failure( UUID.randomUUID().toString(), "Async execution failed: " + e.getMessage(), @@ -108,7 +107,7 @@ public CompletableFuture executeWorkflowAsync(String @Override public AutomationJobStatus getJobStatus(String jobId) throws AutomationPlatformException { - logger.debug("Checking status for job ID: {}", jobId); + log.debug("Checking status for job ID: {}", jobId); try { HttpHeaders headers = createAuthHeaders(); @@ -118,14 +117,14 @@ public AutomationJobStatus getJobStatus(String jobId) throws AutomationPlatformE String url = baseUrl + "/jobs/" + encodedJobId + "/"; return fetchJobStatus(jobId, url, request); } catch (RestClientException e) { - logger.error("Failed to get job status for ID '{}': {}", jobId, e.getMessage(), e); + log.error("Failed to get job status for ID '{}': {}", jobId, e.getMessage(), e); throw new AutomationPlatformException("Failed to get job status", e); } } @Override public AutomationJobStatus getWorkflowJobStatus(String workflowId) throws AutomationPlatformException { - logger.debug("Checking workflow status for job ID: {}", workflowId); + log.debug("Checking workflow status for job ID: {}", workflowId); try { HttpHeaders headers = createAuthHeaders(); @@ -135,7 +134,7 @@ public AutomationJobStatus getWorkflowJobStatus(String workflowId) throws Automa String url = baseUrl + "/workflow_jobs/" + encodedWorkflowId + "/"; return fetchJobStatus(workflowId, url, request); } catch (RestClientException e) { - logger.error("Failed to get workflow job status for ID '{}': {}", workflowId, e.getMessage(), e); + log.error("Failed to get workflow job status for ID '{}': {}", workflowId, e.getMessage(), e); throw new AutomationPlatformException("Failed to get workflow job status", e); } } @@ -158,7 +157,7 @@ private AutomationJobStatus fetchJobStatus(String jobId, String url, HttpEntity< throw new AutomationPlatformException.JobNotFoundException(jobId); } } catch (RestClientException e) { - logger.debug("Job not found at {}: {}", url, e.getMessage()); + log.debug("Job not found at {}: {}", url, e.getMessage()); throw new AutomationPlatformException.JobNotFoundException(jobId); } } @@ -173,11 +172,11 @@ public boolean validateConnection() { ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, request, Map.class); boolean isValid = response.getStatusCode().is2xxSuccessful(); - logger.debug("Connection validation: {}", isValid ? "successful" : "failed"); + log.debug("Connection validation: {}", isValid ? "successful" : "failed"); return isValid; } catch (Exception e) { - logger.warn("Connection validation failed: {}", e.getMessage()); + log.warn("Connection validation failed: {}", e.getMessage()); return false; } } @@ -189,7 +188,7 @@ public boolean isHealthy() { // as health checks are frequent and failures are expected to be handled by the health indicator return validateConnection(); } catch (Exception e) { - logger.debug("Health check failed: {}", e.getMessage()); + log.debug("Health check failed: {}", e.getMessage()); return false; } } diff --git a/external-service-api/pom.xml b/external-service-api/pom.xml new file mode 100644 index 0000000..af29c09 --- /dev/null +++ b/external-service-api/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.opendevstack.apiservice + devstack-api-service + 0.0.2-SNAPSHOT + + + external-service-api + External Service API + Base interface definitions for external services + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.fasterxml.jackson.core + jackson-annotations + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + diff --git a/external-service-api/src/main/java/org/opendevstack/apiservice/externalservice/api/ExternalService.java b/external-service-api/src/main/java/org/opendevstack/apiservice/externalservice/api/ExternalService.java new file mode 100644 index 0000000..08a8edc --- /dev/null +++ b/external-service-api/src/main/java/org/opendevstack/apiservice/externalservice/api/ExternalService.java @@ -0,0 +1,16 @@ +package org.opendevstack.apiservice.externalservice.api; + +/** + * Base interface for all external services. + * All external service integrations should implement this interface. + */ +public interface ExternalService { + + /** + * Checks if the external service is healthy and reachable. + * This method is used by health indicators and should not throw exceptions. + * + * @return true if the service is healthy, false otherwise + */ + boolean isHealthy(); +} diff --git a/external-service-api/src/main/java/org/opendevstack/apiservice/externalservice/api/health/AbstractExternalServiceHealthIndicator.java b/external-service-api/src/main/java/org/opendevstack/apiservice/externalservice/api/health/AbstractExternalServiceHealthIndicator.java new file mode 100644 index 0000000..cad78ee --- /dev/null +++ b/external-service-api/src/main/java/org/opendevstack/apiservice/externalservice/api/health/AbstractExternalServiceHealthIndicator.java @@ -0,0 +1,69 @@ +package org.opendevstack.apiservice.externalservice.api.health; + +import org.opendevstack.apiservice.externalservice.api.ExternalService; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; + +import lombok.extern.slf4j.Slf4j; + +/** + * Abstract base class for external service health indicators. + * Provides standard health check implementation that can be reused by all external services. + * + * Subclasses should extend this class and provide the service name and external service instance. + * + */ +@Slf4j +public abstract class AbstractExternalServiceHealthIndicator implements HealthIndicator { + + private static final String DETAIL_SERVICE = "service"; + private static final String DETAIL_STATUS = "status"; + private static final String STATUS_CONNECTED = "connected"; + private static final String STATUS_DISCONNECTED = "disconnected"; + private static final String STATUS_ERROR = "error"; + private static final String DETAIL_REASON = "reason"; + private static final String DETAIL_ERROR = "error"; + private static final String HEALTH_CHECK_FAILED = "Health check failed"; + + private final ExternalService externalService; + private final String serviceName; + + /** + * Constructor for the abstract health indicator. + * + * @param externalService the external service to check health for + * @param serviceName the display name of the service for health reporting + */ + protected AbstractExternalServiceHealthIndicator(ExternalService externalService, String serviceName) { + this.externalService = externalService; + this.serviceName = serviceName; + } + + @Override + public Health health() { + try { + boolean isHealthy = externalService.isHealthy(); + if (isHealthy) { + log.debug("{} service is healthy", serviceName); + return Health.up() + .withDetail(DETAIL_SERVICE, serviceName) + .withDetail(DETAIL_STATUS, STATUS_CONNECTED) + .build(); + } else { + log.warn("{} service is not healthy", serviceName); + return Health.down() + .withDetail(DETAIL_SERVICE, serviceName) + .withDetail(DETAIL_STATUS, STATUS_DISCONNECTED) + .withDetail(DETAIL_REASON, HEALTH_CHECK_FAILED) + .build(); + } + } catch (Exception e) { + log.error("Error checking {} health: {}", serviceName, e.getMessage(), e); + return Health.down() + .withDetail(DETAIL_SERVICE, serviceName) + .withDetail(DETAIL_STATUS, STATUS_ERROR) + .withDetail(DETAIL_ERROR, e.getMessage()) + .build(); + } + } +} diff --git a/external-service-api/src/test/java/org/opendevstack/apiservice/externalservice/api/health/AbstractExternalServiceHealthIndicatorTest.java b/external-service-api/src/test/java/org/opendevstack/apiservice/externalservice/api/health/AbstractExternalServiceHealthIndicatorTest.java new file mode 100644 index 0000000..f5db952 --- /dev/null +++ b/external-service-api/src/test/java/org/opendevstack/apiservice/externalservice/api/health/AbstractExternalServiceHealthIndicatorTest.java @@ -0,0 +1,114 @@ +package org.opendevstack.apiservice.externalservice.api.health; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.externalservice.api.ExternalService; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link AbstractExternalServiceHealthIndicator}. + * Tests the health check functionality when external services are healthy, unhealthy, or throws exceptions. + */ +@ExtendWith(MockitoExtension.class) +class AbstractExternalServiceHealthIndicatorTest { + + @Mock + private ExternalService externalService; + + /** + * Concrete implementation of the abstract health indicator for testing purposes. + */ + private static class TestHealthIndicator extends AbstractExternalServiceHealthIndicator { + public TestHealthIndicator(ExternalService externalService, String serviceName) { + super(externalService, serviceName); + } + } + + @Test + void testHealthWhenServiceIsHealthy() { + // Arrange + when(externalService.isHealthy()).thenReturn(true); + AbstractExternalServiceHealthIndicator indicator = + new TestHealthIndicator(externalService, "TestService"); + + // Act + Health health = indicator.health(); + + // Assert + assertNotNull(health); + assertEquals(Status.UP, health.getStatus()); + } + + @Test + void testHealthWhenServiceIsUnhealthy() { + // Arrange + when(externalService.isHealthy()).thenReturn(false); + AbstractExternalServiceHealthIndicator indicator = + new TestHealthIndicator(externalService, "TestService"); + + // Act + Health health = indicator.health(); + + // Assert + assertNotNull(health); + assertEquals(Status.DOWN, health.getStatus()); + } + + @Test + void testHealthWhenServiceThrowsException() { + // Arrange + String errorMessage = "Connection timeout"; + when(externalService.isHealthy()).thenThrow(new RuntimeException(errorMessage)); + AbstractExternalServiceHealthIndicator indicator = + new TestHealthIndicator(externalService, "TestService"); + + // Act + Health health = indicator.health(); + + // Assert + assertNotNull(health); + assertEquals(Status.DOWN, health.getStatus()); + } + + @Test + void testHealthWhenServiceThrowsCheckedException() { + // Arrange + String errorMessage = "Database connection error"; + when(externalService.isHealthy()).thenThrow(new IllegalStateException(errorMessage)); + AbstractExternalServiceHealthIndicator indicator = + new TestHealthIndicator(externalService, "DatabaseService"); + + // Act + Health health = indicator.health(); + + // Assert + assertNotNull(health); + assertEquals(Status.DOWN, health.getStatus()); + } + + @Test + void testHealthStatusIsUp() { + // Arrange + when(externalService.isHealthy()).thenReturn(true); + AbstractExternalServiceHealthIndicator indicator = + new TestHealthIndicator(externalService, "TestService"); + + // Act + Health health = indicator.health(); + + // Assert - Verify status is UP and not null + assertNotNull(health); + assertNotNull(health.getStatus()); + assertEquals(Status.UP, health.getStatus()); + } + +} + diff --git a/external-service-bitbucket/pom.xml b/external-service-bitbucket/pom.xml index bb645ea..8cb800d 100644 --- a/external-service-bitbucket/pom.xml +++ b/external-service-bitbucket/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT external-service-bitbucket @@ -14,6 +14,13 @@ Service module for integrating with Bitbucket + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springframework.boot @@ -26,6 +33,12 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + com.fasterxml.jackson.core diff --git a/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/health/BitbucketHealthIndicator.java b/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/health/BitbucketHealthIndicator.java new file mode 100644 index 0000000..4db9040 --- /dev/null +++ b/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/health/BitbucketHealthIndicator.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.externalservice.bitbucket.health; + +import org.opendevstack.apiservice.externalservice.api.health.AbstractExternalServiceHealthIndicator; +import org.opendevstack.apiservice.externalservice.bitbucket.service.BitbucketService; +import org.springframework.stereotype.Component; + +/** + * Health indicator for Bitbucket service. + * Provides health status information for the actuator endpoint. + */ +@Component +public class BitbucketHealthIndicator extends AbstractExternalServiceHealthIndicator { + + public BitbucketHealthIndicator(BitbucketService bitbucketService) { + super(bitbucketService, "Bitbucket"); + } +} diff --git a/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/BitbucketService.java b/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/BitbucketService.java index 7415b1b..1f4224d 100644 --- a/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/BitbucketService.java +++ b/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/BitbucketService.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.externalservice.bitbucket.service; +import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.bitbucket.exception.BitbucketException; import java.util.Set; @@ -8,7 +9,7 @@ * Service interface for interacting with Bitbucket repositories. * Provides high-level methods to work with branches in Bitbucket repositories across multiple instances. */ -public interface BitbucketService { +public interface BitbucketService extends ExternalService { /** * Get the default branch for a repository in a specific Bitbucket instance @@ -47,4 +48,13 @@ public interface BitbucketService { * @return true if configured, false otherwise */ boolean hasInstance(String instanceName); + + /** + * Checks if the Bitbucket service is healthy and reachable. + * This method is used by health indicators and should not throw exceptions. + * + * @return true if the service is healthy, false otherwise + */ + @Override + boolean isHealthy(); } diff --git a/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/impl/BitbucketServiceImpl.java b/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/impl/BitbucketServiceImpl.java index 5a1f104..294ad4c 100644 --- a/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/impl/BitbucketServiceImpl.java +++ b/external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/impl/BitbucketServiceImpl.java @@ -138,4 +138,21 @@ public Set getAvailableInstances() { public boolean hasInstance(String instanceName) { return clientFactory.hasInstance(instanceName); } + + @Override + public boolean isHealthy() { + try { + // Check if at least one instance is available + Set instances = getAvailableInstances(); + if (instances.isEmpty()) { + return false; + } + // Try to access the first instance + String instanceName = instances.iterator().next(); + return hasInstance(instanceName); + } catch (Exception e) { + log.debug("Health check failed", e); + return false; + } + } } diff --git a/external-service-ocp/pom.xml b/external-service-ocp/pom.xml index c2bd052..27c9f8f 100644 --- a/external-service-ocp/pom.xml +++ b/external-service-ocp/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT external-service-ocp @@ -14,6 +14,13 @@ Service module for integrating with OpenShift Container Platform + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springframework.boot @@ -26,6 +33,12 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + com.fasterxml.jackson.core @@ -65,4 +78,4 @@ - \ No newline at end of file + diff --git a/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/health/OpenshiftHealthIndicator.java b/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/health/OpenshiftHealthIndicator.java new file mode 100644 index 0000000..0954627 --- /dev/null +++ b/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/health/OpenshiftHealthIndicator.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.externalservice.ocp.health; + +import org.opendevstack.apiservice.externalservice.api.health.AbstractExternalServiceHealthIndicator; +import org.opendevstack.apiservice.externalservice.ocp.service.OpenshiftService; +import org.springframework.stereotype.Component; + +/** + * Health indicator for OpenShift service. + * Provides health status information for the actuator endpoint. + */ +@Component +public class OpenshiftHealthIndicator extends AbstractExternalServiceHealthIndicator { + + public OpenshiftHealthIndicator(OpenshiftService openshiftService) { + super(openshiftService, "OpenShift"); + } +} diff --git a/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/OpenshiftService.java b/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/OpenshiftService.java index 1b90796..a49f5a1 100644 --- a/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/OpenshiftService.java +++ b/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/OpenshiftService.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.externalservice.ocp.service; +import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.ocp.exception.OpenshiftException; import java.util.Map; @@ -9,7 +10,7 @@ * Service interface for interacting with OpenShift clusters. * Provides high-level methods to retrieve secrets and other resources from multiple OpenShift instances. */ -public interface OpenshiftService { +public interface OpenshiftService extends ExternalService { /** * Get a secret from a specific OpenShift instance @@ -88,5 +89,14 @@ public interface OpenshiftService { * @return true if configured, false otherwise */ boolean hasInstance(String instanceName); + + /** + * Checks if the OpenShift service is healthy and reachable. + * This method is used by health indicators and should not throw exceptions. + * + * @return true if the service is healthy, false otherwise + */ + @Override + boolean isHealthy(); } diff --git a/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/impl/OpenshiftServiceImpl.java b/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/impl/OpenshiftServiceImpl.java index 2f2c5a4..2e0604e 100644 --- a/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/impl/OpenshiftServiceImpl.java +++ b/external-service-ocp/src/main/java/org/opendevstack/apiservice/externalservice/ocp/service/impl/OpenshiftServiceImpl.java @@ -96,6 +96,23 @@ public Set getAvailableInstances() { public boolean hasInstance(String instanceName) { return clientFactory.hasInstance(instanceName); } + + @Override + public boolean isHealthy() { + try { + // Check if at least one instance is available + Set instances = getAvailableInstances(); + if (instances.isEmpty()) { + return false; + } + // Try to access the first instance + String instanceName = instances.iterator().next(); + return hasInstance(instanceName); + } catch (Exception e) { + log.debug("Health check failed", e); + return false; + } + } } \ No newline at end of file diff --git a/external-service-projects-info-service/pom.xml b/external-service-projects-info-service/pom.xml index cd427a6..50c3b84 100644 --- a/external-service-projects-info-service/pom.xml +++ b/external-service-projects-info-service/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT external-service-projects-info-service @@ -14,6 +14,13 @@ Service module for integrating with projects info service + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springframework.boot @@ -32,6 +39,12 @@ spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + com.fasterxml.jackson.core @@ -190,4 +203,4 @@ - \ No newline at end of file + diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/health/ProjectsInfoServiceHealthIndicator.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/health/ProjectsInfoServiceHealthIndicator.java new file mode 100644 index 0000000..37ca013 --- /dev/null +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/health/ProjectsInfoServiceHealthIndicator.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.externalservice.projectsinfoservice.health; + +import org.opendevstack.apiservice.externalservice.api.health.AbstractExternalServiceHealthIndicator; +import org.opendevstack.apiservice.externalservice.projectsinfoservice.service.ProjectsInfoService; +import org.springframework.stereotype.Component; + +/** + * Health indicator for Projects Info Service. + * Provides health status information for the actuator endpoint. + */ +@Component +public class ProjectsInfoServiceHealthIndicator extends AbstractExternalServiceHealthIndicator { + + public ProjectsInfoServiceHealthIndicator(ProjectsInfoService projectsInfoService) { + super(projectsInfoService, "Projects Info Service"); + } +} diff --git a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java index 4935365..9c24d18 100644 --- a/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java +++ b/external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/service/ProjectsInfoService.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.externalservice.projectsinfoservice.service; +import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.projectsinfoservice.exception.ProjectsInfoServiceException; import org.opendevstack.apiservice.externalservice.projectsinfoservice.model.Platforms; @@ -7,7 +8,7 @@ * Service interface for integrating with projects info service * This interface provides a generic way to consume the service. */ -public interface ProjectsInfoService { +public interface ProjectsInfoService extends ExternalService { /** * Retrieves the platforms associated with a given project. diff --git a/external-service-uipath/pom.xml b/external-service-uipath/pom.xml index 162190b..867ebf4 100644 --- a/external-service-uipath/pom.xml +++ b/external-service-uipath/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT external-service-uipath @@ -14,6 +14,13 @@ Service module for integrating with UiPath automation platform + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springframework.boot @@ -26,6 +33,12 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + com.fasterxml.jackson.core @@ -65,4 +78,4 @@ - \ No newline at end of file + diff --git a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/config/UiPathServiceConfig.java b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/config/UiPathServiceConfig.java index 7084c41..cec3df2 100644 --- a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/config/UiPathServiceConfig.java +++ b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/config/UiPathServiceConfig.java @@ -1,7 +1,5 @@ package org.opendevstack.apiservice.externalservice.uipath.config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,6 +16,7 @@ import java.net.HttpURLConnection; import java.security.GeneralSecurityException; import java.security.cert.X509Certificate; +import lombok.extern.slf4j.Slf4j; /** * Configuration class for UIPath service components. @@ -25,10 +24,9 @@ @Configuration @EnableAsync @EnableConfigurationProperties(UiPathProperties.class) +@Slf4j public class UiPathServiceConfig { - private static final Logger logger = LoggerFactory.getLogger(UiPathServiceConfig.class); - private final UiPathProperties uiPathProperties; public UiPathServiceConfig(@org.springframework.beans.factory.annotation.Qualifier("uiPathOrchestratorProperties") UiPathProperties uiPathProperties) { @@ -44,10 +42,10 @@ public UiPathServiceConfig(@org.springframework.beans.factory.annotation.Qualifi @Bean(name = "uiPathRestTemplate") public RestTemplate uiPathRestTemplate() { if (!uiPathProperties.getSsl().isVerifyCertificates()) { - logger.warn("UIPath SSL certificate verification is DISABLED - this should only be used in development environments"); + log.warn("UIPath SSL certificate verification is DISABLED - this should only be used in development environments"); return createInsecureRestTemplate(); } else { - logger.info("UIPath SSL certificate verification is ENABLED"); + log.info("UIPath SSL certificate verification is ENABLED"); return createSecureRestTemplate(); } } @@ -95,7 +93,7 @@ protected void prepareConnection(HttpURLConnection connection, String httpMethod return new RestTemplate(requestFactory); } catch (GeneralSecurityException e) { - logger.warn("Failed to create insecure RestTemplate, falling back to default: {}", e.getMessage()); + log.warn("Failed to create insecure RestTemplate, falling back to default: {}", e.getMessage()); return createSecureRestTemplate(); } } diff --git a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/health/UiPathHealthIndicator.java b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/health/UiPathHealthIndicator.java new file mode 100644 index 0000000..4aded26 --- /dev/null +++ b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/health/UiPathHealthIndicator.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.externalservice.uipath.health; + +import org.opendevstack.apiservice.externalservice.api.health.AbstractExternalServiceHealthIndicator; +import org.opendevstack.apiservice.externalservice.uipath.service.UiPathOrchestratorService; +import org.springframework.stereotype.Component; + +/** + * Health indicator for UIPath Orchestrator service. + * Provides health status information for the actuator endpoint. + */ +@Component +public class UiPathHealthIndicator extends AbstractExternalServiceHealthIndicator { + + public UiPathHealthIndicator(UiPathOrchestratorService uiPathOrchestratorService) { + super(uiPathOrchestratorService, "UIPath Orchestrator"); + } +} diff --git a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/UiPathOrchestratorService.java b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/UiPathOrchestratorService.java index c272dec..5ea516f 100644 --- a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/UiPathOrchestratorService.java +++ b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/UiPathOrchestratorService.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.externalservice.uipath.service; +import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.uipath.exception.UiPathException; import org.opendevstack.apiservice.externalservice.uipath.model.UiPathQueueItem; import org.opendevstack.apiservice.externalservice.uipath.model.UiPathQueueItemRequest; @@ -13,7 +14,7 @@ * Service interface for integrating with UIPath Orchestrator. * Provides methods for authentication, queue item management, and status checking. */ -public interface UiPathOrchestratorService { +public interface UiPathOrchestratorService extends ExternalService { /** * Authenticates to UIPath Orchestrator and returns a bearer token. @@ -111,6 +112,7 @@ boolean hasQueueItemFinalizedById(Long queueItemId) * * @return true if the platform is healthy, false otherwise */ + @Override boolean isHealthy(); /** diff --git a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/impl/UiPathOrchestratorServiceImpl.java b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/impl/UiPathOrchestratorServiceImpl.java index 26084cf..e6d6a04 100644 --- a/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/impl/UiPathOrchestratorServiceImpl.java +++ b/external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/service/impl/UiPathOrchestratorServiceImpl.java @@ -10,8 +10,6 @@ import org.opendevstack.apiservice.externalservice.uipath.model.UiPathQueueItemRequest; import org.opendevstack.apiservice.externalservice.uipath.model.UiPathQueueItemResult; import org.opendevstack.apiservice.externalservice.uipath.service.UiPathOrchestratorService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; @@ -25,6 +23,8 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import lombok.extern.slf4j.Slf4j; + import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -36,10 +36,9 @@ * and checking robot execution status. */ @Service("uiPathOrchestratorService") +@Slf4j public class UiPathOrchestratorServiceImpl implements UiPathOrchestratorService { - private static final Logger logger = LoggerFactory.getLogger(UiPathOrchestratorServiceImpl.class); - private final RestTemplate restTemplate; private final UiPathProperties properties; @@ -52,7 +51,7 @@ public UiPathOrchestratorServiceImpl( @Override public String authenticate() throws UiPathException.AuthenticationException { - logger.debug("Authenticating to UIPath Orchestrator at {}", properties.getHost()); + log.debug("Authenticating to UIPath Orchestrator at {}", properties.getHost()); try { UiPathAuthRequest authRequest = new UiPathAuthRequest( @@ -76,7 +75,7 @@ public String authenticate() throws UiPathException.AuthenticationException { UiPathAuthResponse authResponse = response.getBody(); if (authResponse.isSuccess() && StringUtils.hasText(authResponse.getToken())) { - logger.debug("Successfully authenticated to UIPath Orchestrator"); + log.debug("Successfully authenticated to UIPath Orchestrator"); return authResponse.getToken(); } else { String errorMsg = authResponse.getError() != null ? authResponse.getError() : "Unknown authentication error"; @@ -89,7 +88,7 @@ public String authenticate() throws UiPathException.AuthenticationException { } } catch (RestClientException e) { - logger.error("Failed to authenticate to UIPath Orchestrator: {}", e.getMessage(), e); + log.error("Failed to authenticate to UIPath Orchestrator: {}", e.getMessage(), e); throw new UiPathException.AuthenticationException("Authentication failed", e); } } @@ -99,7 +98,7 @@ public UiPathQueueItem addQueueItem(UiPathQueueItemRequest request) throws UiPathException.QueueItemCreationException { String reference = request.getItemData() != null ? request.getItemData().getReference() : "unknown"; - logger.info("Adding queue item with reference '{}'", reference); + log.info("Adding queue item with reference '{}'", reference); try { // Authenticate first @@ -118,7 +117,7 @@ public UiPathQueueItem addQueueItem(UiPathQueueItemRequest request) if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { UiPathQueueItem queueItem = response.getBody(); - logger.info("Successfully created queue item with ID {} and reference '{}'", + log.info("Successfully created queue item with ID {} and reference '{}'", queueItem.getId(), reference); return queueItem; } else { @@ -129,10 +128,10 @@ public UiPathQueueItem addQueueItem(UiPathQueueItemRequest request) } } catch (UiPathException.AuthenticationException e) { - logger.error("Failed to authenticate before adding queue item: {}", e.getMessage()); + log.error("Failed to authenticate before adding queue item: {}", e.getMessage(), e); throw new UiPathException.QueueItemCreationException(reference, e); } catch (RestClientException e) { - logger.error("Failed to add queue item with reference '{}': {}", reference, e.getMessage(), e); + log.error("Failed to add queue item with reference '{}': {}", reference, e.getMessage(), e); throw new UiPathException.QueueItemCreationException(reference, e); } } @@ -144,7 +143,7 @@ public CompletableFuture addQueueItemAsync(UiPathQueueItemReque UiPathQueueItem result = addQueueItem(request); return CompletableFuture.completedFuture(result); } catch (UiPathException.QueueItemCreationException e) { - logger.error("Async queue item creation failed: {}", e.getMessage(), e); + log.error("Async queue item creation failed: {}", e.getMessage(), e); return CompletableFuture.failedFuture(e); } } @@ -153,7 +152,7 @@ public CompletableFuture addQueueItemAsync(UiPathQueueItemReque public UiPathQueueItem getQueueItemById(Long queueItemId) throws UiPathException.QueueItemNotFoundException, UiPathException.StatusCheckException { - logger.debug("Getting queue item by ID: {}", queueItemId); + log.debug("Getting queue item by ID: {}", queueItemId); try { String token = authenticate(); @@ -171,17 +170,17 @@ public UiPathQueueItem getQueueItemById(Long queueItemId) if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { UiPathQueueItem queueItem = response.getBody(); - logger.debug("Found queue item {} with status: {}", queueItemId, queueItem.getStatus()); + log.debug("Found queue item {} with status: {}", queueItemId, queueItem.getStatus()); return queueItem; } else { throw new UiPathException.QueueItemNotFoundException(queueItemId.toString()); } } catch (UiPathException.AuthenticationException e) { - logger.error("Authentication failed while getting queue item: {}", e.getMessage()); + log.error("Authentication failed while getting queue item: {}", e.getMessage()); throw new UiPathException.StatusCheckException(queueItemId.toString(), e); } catch (RestClientException e) { - logger.debug("Queue item not found: {}", queueItemId); + log.debug("Queue item not found: {}", queueItemId); throw new UiPathException.QueueItemNotFoundException(queueItemId.toString()); } } @@ -190,7 +189,7 @@ public UiPathQueueItem getQueueItemById(Long queueItemId) public List getQueueItemsByReference(String reference) throws UiPathException.StatusCheckException { - logger.debug("Getting queue items by reference: '{}'", reference); + log.debug("Getting queue items by reference: '{}'", reference); try { String token = authenticate(); @@ -214,20 +213,20 @@ public List getQueueItemsByReference(String reference) UiPathODataResponse odataResponse = response.getBody(); List items = odataResponse.getValue(); - logger.debug("Found {} queue item(s) with reference '{}'", items != null ? items.size() : 0, reference); + log.debug("Found {} queue item(s) with reference '{}'", items != null ? items.size() : 0, reference); return items != null ? items : List.of(); } else { - logger.warn("Unexpected response when querying by reference '{}': {}", + log.warn("Unexpected response when querying by reference '{}': {}", reference, response.getStatusCode()); return List.of(); } } catch (UiPathException.AuthenticationException e) { - logger.error("Authentication failed while querying by reference: {}", e.getMessage()); + log.error("Authentication failed while querying by reference: {}", e.getMessage()); throw new UiPathException.StatusCheckException(reference, e); } catch (RestClientException e) { - logger.error("Failed to query queue items by reference '{}': {}", reference, e.getMessage(), e); + log.error("Failed to query queue items by reference '{}': {}", reference, e.getMessage(), e); throw new UiPathException.StatusCheckException(reference, e); } } @@ -236,12 +235,12 @@ public List getQueueItemsByReference(String reference) public Optional getLatestQueueItemByReference(String reference) throws UiPathException.StatusCheckException { - logger.debug("Getting latest queue item by reference: '{}'", reference); + log.debug("Getting latest queue item by reference: '{}'", reference); List items = getQueueItemsByReference(reference); if (items.isEmpty()) { - logger.debug("No queue items found with reference '{}'", reference); + log.debug("No queue items found with reference '{}'", reference); return Optional.empty(); } @@ -250,7 +249,7 @@ public Optional getLatestQueueItemByReference(String reference) .max(Comparator.comparing(UiPathQueueItem::getId)); latestItem.ifPresent(item -> - logger.debug("Latest queue item for reference '{}' is ID {} with status: {}", + log.debug("Latest queue item for reference '{}' is ID {} with status: {}", reference, item.getId(), item.getStatus()) ); @@ -261,7 +260,7 @@ public Optional getLatestQueueItemByReference(String reference) public boolean hasQueueItemFinalized(String reference) throws UiPathException.QueueItemNotFoundException, UiPathException.StatusCheckException { - logger.debug("Checking if queue item with reference '{}' has finalized", reference); + log.debug("Checking if queue item with reference '{}' has finalized", reference); Optional latestItem = getLatestQueueItemByReference(reference); @@ -272,7 +271,7 @@ public boolean hasQueueItemFinalized(String reference) UiPathQueueItem item = latestItem.get(); boolean finalized = item.isFinalized(); - logger.debug("Queue item {} (reference '{}') finalized status: {} (status: {})", + log.debug("Queue item {} (reference '{}') finalized status: {} (status: {})", item.getId(), reference, finalized, item.getStatus()); return finalized; @@ -282,12 +281,12 @@ public boolean hasQueueItemFinalized(String reference) public boolean hasQueueItemFinalizedById(Long queueItemId) throws UiPathException.QueueItemNotFoundException, UiPathException.StatusCheckException { - logger.debug("Checking if queue item {} has finalized", queueItemId); + log.debug("Checking if queue item {} has finalized", queueItemId); UiPathQueueItem item = getQueueItemById(queueItemId); boolean finalized = item.isFinalized(); - logger.debug("Queue item {} finalized status: {} (status: {})", + log.debug("Queue item {} finalized status: {} (status: {})", queueItemId, finalized, item.getStatus()); return finalized; @@ -298,10 +297,10 @@ public boolean validateConnection() { try { String token = authenticate(); boolean isValid = StringUtils.hasText(token); - logger.debug("Connection validation: {}", isValid ? "successful" : "failed"); + log.debug("Connection validation: {}", isValid ? "successful" : "failed"); return isValid; } catch (Exception e) { - logger.warn("Connection validation failed: {}", e.getMessage()); + log.warn("Connection validation failed: {}", e.getMessage()); return false; } } @@ -311,7 +310,7 @@ public boolean isHealthy() { try { return validateConnection(); } catch (Exception e) { - logger.debug("Health check failed: {}", e.getMessage()); + log.debug("Health check failed: {}", e.getMessage()); return false; } } @@ -320,44 +319,44 @@ public boolean isHealthy() { public UiPathQueueItemResult checkQueueItemByReference(String reference) { // If no UIPath reference, consider the process complete and successful if (reference == null || reference.isEmpty()) { - logger.debug("No UIPath reference provided, returning NO_REFERENCE result"); + log.debug("No UIPath reference provided, returning NO_REFERENCE result"); return UiPathQueueItemResult.noReference(); } try { - logger.debug("Checking UIPath queue item status for reference: '{}'", reference); + log.debug("Checking UIPath queue item status for reference: '{}'", reference); Optional queueItem = getLatestQueueItemByReference(reference); if (queueItem.isEmpty()) { - logger.warn("UIPath queue item not found for reference: '{}'", reference); + log.warn("UIPath queue item not found for reference: '{}'", reference); return UiPathQueueItemResult.notFound(reference); } UiPathQueueItem item = queueItem.get(); QueueItemStatus status = item.getStatusEnum(); - logger.debug("UIPath queue item '{}' status: {}", reference, status); + log.debug("UIPath queue item '{}' status: {}", reference, status); // If UIPath is not in final state, return in-progress if (!status.isFinalState()) { - logger.debug("UIPath queue item '{}' is still in progress with status: {}", reference, status); + log.debug("UIPath queue item '{}' is still in progress with status: {}", reference, status); return UiPathQueueItemResult.inProgress(item); } // If UIPath failed, return failure if (!status.isSuccessful()) { - logger.warn("UIPath queue item '{}' failed with status: {}", reference, status); + log.warn("UIPath queue item '{}' failed with status: {}", reference, status); return UiPathQueueItemResult.failure(item); } // UIPath succeeded - logger.debug("UIPath queue item '{}' completed successfully", reference); + log.debug("UIPath queue item '{}' completed successfully", reference); return UiPathQueueItemResult.success(item); } catch (UiPathException.StatusCheckException e) { - logger.error("Failed to check UIPath status for reference '{}': {}", reference, e.getMessage(), e); + log.error("Failed to check UIPath status for reference '{}': {}", reference, e.getMessage(), e); return UiPathQueueItemResult.error("Failed to check UIPath status", e.getMessage()); } catch (Exception e) { - logger.error("Unexpected error checking UIPath status for reference '{}': {}", reference, e.getMessage(), e); + log.error("Unexpected error checking UIPath status for reference '{}': {}", reference, e.getMessage(), e); return UiPathQueueItemResult.error("Unexpected error checking UIPath", e.getMessage()); } } diff --git a/external-service-webhookproxy/pom.xml b/external-service-webhookproxy/pom.xml index f17beaa..0454c6b 100644 --- a/external-service-webhookproxy/pom.xml +++ b/external-service-webhookproxy/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT external-service-webhookproxy @@ -14,6 +14,13 @@ Service module for integrating with Webhook Proxy + + + org.opendevstack.apiservice + external-service-api + ${project.version} + + org.springframework.boot @@ -26,6 +33,12 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + com.fasterxml.jackson.core @@ -65,4 +78,4 @@ - \ No newline at end of file + diff --git a/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/health/WebhookProxyHealthIndicator.java b/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/health/WebhookProxyHealthIndicator.java new file mode 100644 index 0000000..522bb1d --- /dev/null +++ b/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/health/WebhookProxyHealthIndicator.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.externalservice.webhookproxy.health; + +import org.opendevstack.apiservice.externalservice.api.health.AbstractExternalServiceHealthIndicator; +import org.opendevstack.apiservice.externalservice.webhookproxy.service.WebhookProxyService; +import org.springframework.stereotype.Component; + +/** + * Health indicator for WebhookProxy service. + * Provides health status information for the actuator endpoint. + */ +@Component +public class WebhookProxyHealthIndicator extends AbstractExternalServiceHealthIndicator { + + public WebhookProxyHealthIndicator(WebhookProxyService webhookProxyService) { + super(webhookProxyService, "WebhookProxy"); + } +} diff --git a/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/WebhookProxyService.java b/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/WebhookProxyService.java index dfdcb7c..3ccea7a 100644 --- a/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/WebhookProxyService.java +++ b/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/WebhookProxyService.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.externalservice.webhookproxy.service; +import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.webhookproxy.dto.WebhookProxyBuildRequest; import org.opendevstack.apiservice.externalservice.webhookproxy.dto.WebhookProxyBuildResponse; import org.opendevstack.apiservice.externalservice.webhookproxy.exception.WebhookProxyException; @@ -10,7 +11,7 @@ * Service interface for triggering builds via the ODS Webhook Proxy. * Provides high-level methods to trigger release manager builds across different clusters. */ -public interface WebhookProxyService { +public interface WebhookProxyService extends ExternalService { /** * Trigger a release manager build via webhook proxy @@ -67,4 +68,13 @@ WebhookProxyBuildResponse triggerBuild(String clusterName, String projectKey, * @throws WebhookProxyException.ConfigurationException if the cluster is not configured */ String getWebhookProxyUrl(String clusterName, String projectKey) throws WebhookProxyException.ConfigurationException; + + /** + * Checks if the webhook proxy is healthy and reachable. + * This method is used by health indicators and should not throw exceptions. + * + * @return true if the service is healthy, false otherwise + */ + @Override + boolean isHealthy(); } diff --git a/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/impl/WebhookProxyServiceImpl.java b/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/impl/WebhookProxyServiceImpl.java index 952f7a8..8994431 100644 --- a/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/impl/WebhookProxyServiceImpl.java +++ b/external-service-webhookproxy/src/main/java/org/opendevstack/apiservice/externalservice/webhookproxy/service/impl/WebhookProxyServiceImpl.java @@ -107,4 +107,16 @@ public String getWebhookProxyUrl(String clusterName, String projectKey) return clusterConfig.buildWebhookProxyUrl(projectKey); } + + @Override + public boolean isHealthy() { + try { + // Check if at least one cluster is configured + Set clusters = getAvailableClusters(); + return !clusters.isEmpty(); + } catch (Exception e) { + log.debug("Health check failed", e); + return false; + } + } } diff --git a/pom.xml b/pom.xml index 75fa5b7..ae2cd88 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.7 + 3.5.11 pom @@ -14,7 +14,7 @@ org.opendevstack.apiservice devstack-api-service Devstack API Service - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT 21 @@ -43,6 +43,7 @@ + external-service-api core external-service-aap external-service-projects-info-service
+ * Subclasses should extend this class and provide the service name and external service instance. + *