Keptn for Quality Gates in Java: Implementing Automated Quality Assurance

Keptn is a cloud-native application lifecycle management platform that provides automated quality gates for continuous delivery. This guide shows how to integrate Keptn quality gates into Java applications.


Core Concepts

What are Keptn Quality Gates?

  • Automated quality checks in your delivery pipeline
  • Decision points that prevent bad deployments
  • Based on SLOs (Service Level Objectives) and SLIs (Service Level Indicators)
  • Integrates with monitoring tools like Prometheus, Dynatrace, Datadog

Key Components:

  • SLOs: Service Level Objectives (e.g., 99.9% availability)
  • SLIs: Service Level Indicators (metrics that measure SLOs)
  • Quality Gate: Evaluation of SLIs against SLOs
  • Keptn API: REST API for triggering and querying evaluations

Dependencies and Setup

Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <micrometer.version>1.11.0</micrometer.version> <okhttp.version>4.11.0</okhttp.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>${micrometer.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>${micrometer.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <!-- JSON --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies>
Keptn Configuration
# keptn-config.yaml apiVersion: v1 kind: ConfigMap metadata: name: keptn-config data: prometheus-url: "http://prometheus:9090" sli-provider: "prometheus" quality-gates: - name: "response-time" objectives: - sli: "response_time_p95" pass: ["<=1000"] warning: ["<=1500"] weight: 1 - name: "error-rate" objectives: - sli: "error_rate" pass: ["<=0.05"] warning: ["<=0.1"] weight: 2

Core Implementation

1. Keptn Data Models
@Data @Builder @JsonIgnoreProperties(ignoreUnknown = true) public class KeptnEvaluationRequest { @JsonProperty("data") private EvaluationData data; @Data @Builder public static class EvaluationData { @JsonProperty("project") private String project; @JsonProperty("stage") private String stage; @JsonProperty("service") private String service; @JsonProperty("labels") private Map<String, String> labels; @JsonProperty("start") private String start; // ISO timestamp @JsonProperty("end") private String end; // ISO timestamp @JsonProperty("testStrategy") private String testStrategy; } } @Data @Builder @JsonIgnoreProperties(ignoreUnknown = true) public class KeptnEvaluationResponse { @JsonProperty("keptnContext") private String keptnContext; @JsonProperty("data") private EvaluationResultData data; @Data @Builder public static class EvaluationResultData { @JsonProperty("project") private String project; @JsonProperty("stage") private String stage; @JsonProperty("service") private String service; @JsonProperty("evaluation") private EvaluationDetails evaluation; } @Data @Builder public static class EvaluationDetails { @JsonProperty("score") private double score; @JsonProperty("sloFileContent") private String sloFileContent; @JsonProperty("timeStart") private String timeStart; @JsonProperty("timeEnd") private String timeEnd; @JsonProperty("result") private String result; // "pass" or "fail" @JsonProperty("indicatorResults") private List<IndicatorResult> indicatorResults; } @Data @Builder public static class IndicatorResult { @JsonProperty("score") private double score; @JsonProperty("value") private Object value; @JsonProperty("displayName") private String displayName; @JsonProperty("status") private String status; // "pass", "warning", "fail" @JsonProperty("targets") private List<Target> targets; @JsonProperty("keySli") private boolean keySli; } @Data @Builder public static class Target { @JsonProperty("criteria") private String criteria; @JsonProperty("targetValue") private double targetValue; @JsonProperty("violated") private boolean violated; } } @Data @Builder public class QualityGateResult { private String keptnContext; private String project; private String stage; private String service; private QualityGateStatus status; private double score; private String message; private Instant evaluationStart; private Instant evaluationEnd; private List<SLIResult> sliResults; public boolean isPassed() { return status == QualityGateStatus.PASS; } public boolean hasWarnings() { return sliResults.stream().anyMatch(sli -> sli.getStatus() == QualityGateStatus.WARNING); } } @Data @Builder public class SLIResult { private String name; private String displayName; private double value; private double score; private QualityGateStatus status; private String criteria; private boolean keySli; private String message; } public enum QualityGateStatus { PASS, WARNING, FAIL, UNKNOWN }
2. Keptn Client
@Component @Slf4j public class KeptnClient { private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final KeptnConfig keptnConfig; public KeptnClient(KeptnConfig keptnConfig, ObjectMapper objectMapper) { this.keptnConfig = keptnConfig; this.objectMapper = objectMapper; this.httpClient = new OkHttpClient.Builder() .connectTimeout(Duration.ofSeconds(30)) .readTimeout(Duration.ofSeconds(60)) .build(); } /** * Trigger a quality gate evaluation */ public String triggerEvaluation(KeptnEvaluationRequest request) throws IOException { String url = keptnConfig.getApiUrl() + "/v1/evaluation"; String jsonBody = objectMapper.writeValueAsString(request); Request httpRequest = new Request.Builder() .url(url) .post(RequestBody.create(jsonBody, MediaType.get("application/json"))) .header("x-token", keptnConfig.getApiToken()) .build(); try (Response response = httpClient.newCall(httpRequest).execute()) { if (!response.isSuccessful()) { throw new IOException("Keptn API request failed: " + response.code() + " - " + response.message()); } String responseBody = response.body().string(); KeptnEvaluationResponse evaluationResponse = objectMapper.readValue( responseBody, KeptnEvaluationResponse.class); log.info("Triggered Keptn evaluation with context: {}", evaluationResponse.getKeptnContext()); return evaluationResponse.getKeptnContext(); } } /** * Get evaluation results by Keptn context */ public QualityGateResult getEvaluationResult(String keptnContext) throws IOException { String url = keptnConfig.getApiUrl() + "/v1/event?keptnContext=" + keptnContext + "&type=sh.keptn.event.evaluation.finished"; Request request = new Request.Builder() .url(url) .get() .header("x-token", keptnConfig.getApiToken()) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("Failed to fetch evaluation results: " + response.code()); } String responseBody = response.body().string(); KeptnEvaluationResponse evaluationResponse = objectMapper.readValue( responseBody, KeptnEvaluationResponse.class); return convertToQualityGateResult(evaluationResponse); } } /** * Wait for evaluation completion with timeout */ public QualityGateResult waitForEvaluation(String keptnContext, Duration timeout) throws IOException, InterruptedException, TimeoutException { Instant start = Instant.now(); Duration pollInterval = Duration.ofSeconds(5); while (Duration.between(start, Instant.now()).compareTo(timeout) < 0) { try { QualityGateResult result = getEvaluationResult(keptnContext); if (result != null) { return result; } } catch (IOException e) { log.debug("Evaluation not ready yet: {}", e.getMessage()); } Thread.sleep(pollInterval.toMillis()); } throw new TimeoutException("Evaluation did not complete within timeout: " + timeout); } private QualityGateResult convertToQualityGateResult(KeptnEvaluationResponse response) { if (response.getData() == null || response.getData().getEvaluation() == null) { return null; } EvaluationDetails evaluation = response.getData().getEvaluation(); List<SLIResult> sliResults = evaluation.getIndicatorResults().stream() .map(this::convertToSLIResult) .collect(Collectors.toList()); QualityGateStatus status = determineOverallStatus(evaluation.getResult(), sliResults); return QualityGateResult.builder() .keptnContext(response.getKeptnContext()) .project(response.getData().getProject()) .stage(response.getData().getStage()) .service(response.getData().getService()) .status(status) .score(evaluation.getScore()) .evaluationStart(parseTimestamp(evaluation.getTimeStart())) .evaluationEnd(parseTimestamp(evaluation.getTimeEnd())) .sliResults(sliResults) .message(buildStatusMessage(status, evaluation.getScore(), sliResults)) .build(); } private SLIResult convertToSLIResult(IndicatorResult indicator) { return SLIResult.builder() .name(indicator.getDisplayName()) .displayName(indicator.getDisplayName()) .value(parseValue(indicator.getValue())) .score(indicator.getScore()) .status(parseStatus(indicator.getStatus())) .criteria(buildCriteriaString(indicator.getTargets())) .keySli(indicator.isKeySli()) .message(buildSLIMessage(indicator)) .build(); } private double parseValue(Object value) { if (value instanceof Number) { return ((Number) value).doubleValue(); } else if (value instanceof String) { try { return Double.parseDouble((String) value); } catch (NumberFormatException e) { return 0.0; } } return 0.0; } private QualityGateStatus parseStatus(String status) { if ("pass".equalsIgnoreCase(status)) return QualityGateStatus.PASS; if ("warning".equalsIgnoreCase(status)) return QualityGateStatus.WARNING; if ("fail".equalsIgnoreCase(status)) return QualityGateStatus.FAIL; return QualityGateStatus.UNKNOWN; } private QualityGateStatus determineOverallStatus(String result, List<SLIResult> sliResults) { if ("fail".equalsIgnoreCase(result)) { return QualityGateStatus.FAIL; } // Check if any key SLI failed boolean keySliFailed = sliResults.stream() .filter(SLIResult::isKeySli) .anyMatch(sli -> sli.getStatus() == QualityGateStatus.FAIL); if (keySliFailed) { return QualityGateStatus.FAIL; } return QualityGateStatus.PASS; } private String buildCriteriaString(List<Target> targets) { if (targets == null || targets.isEmpty()) { return "No criteria defined"; } return targets.stream() .map(target -> target.getCriteria() + " (target: " + target.getTargetValue() + ")") .collect(Collectors.joining(", ")); } private String buildSLIMessage(IndicatorResult indicator) { String status = indicator.getStatus(); double value = parseValue(indicator.getValue()); return String.format("%s: %.2f (%s)", indicator.getDisplayName(), value, status); } private String buildStatusMessage(QualityGateStatus status, double score, List<SLIResult> sliResults) { long failedSLIs = sliResults.stream() .filter(sli -> sli.getStatus() == QualityGateStatus.FAIL) .count(); long warningSLIs = sliResults.stream() .filter(sli -> sli.getStatus() == QualityGateStatus.WARNING) .count(); return String.format("Quality Gate %s - Score: %.1f/100 (Failed: %d, Warnings: %d)", status, score, failedSLIs, warningSLIs); } private Instant parseTimestamp(String timestamp) { if (timestamp == null) return null; try { return Instant.parse(timestamp); } catch (Exception e) { log.warn("Failed to parse timestamp: {}", timestamp); return null; } } }
3. Quality Gate Service
@Service @Slf4j public class QualityGateService { private final KeptnClient keptnClient; private final MetricsService metricsService; private final QualityGateConfig qualityGateConfig; public QualityGateService(KeptnClient keptnClient, MetricsService metricsService, QualityGateConfig qualityGateConfig) { this.keptnClient = keptnClient; this.metricsService = metricsService; this.qualityGateConfig = qualityGateConfig; } /** * Execute quality gate for a deployment */ public QualityGateResult executeQualityGate(String project, String stage, String service, String deploymentId) { log.info("Executing quality gate for {}/{}/{} - Deployment: {}", project, stage, service, deploymentId); try { // Trigger evaluation String keptnContext = triggerEvaluation(project, stage, service, deploymentId); // Wait for results QualityGateResult result = keptnClient.waitForEvaluation( keptnContext, qualityGateConfig.getEvaluationTimeout()); // Record metrics recordQualityGateMetrics(result); log.info("Quality gate completed: {} - Score: {}/100", result.getStatus(), result.getScore()); return result; } catch (Exception e) { log.error("Quality gate execution failed", e); return createErrorResult(project, stage, service, e.getMessage()); } } /** * Execute quality gate with custom time range */ public QualityGateResult executeQualityGateWithTimeRange(String project, String stage, String service, String deploymentId, Instant startTime, Instant endTime) { KeptnEvaluationRequest request = KeptnEvaluationRequest.builder() .data(KeptnEvaluationRequest.EvaluationData.builder() .project(project) .stage(stage) .service(service) .start(startTime.toString()) .end(endTime.toString()) .labels(Map.of( "deploymentId", deploymentId, "triggeredBy", "java-client" )) .build()) .build(); try { String keptnContext = keptnClient.triggerEvaluation(request); QualityGateResult result = keptnClient.waitForEvaluation( keptnContext, qualityGateConfig.getEvaluationTimeout()); recordQualityGateMetrics(result); return result; } catch (Exception e) { log.error("Quality gate with custom time range failed", e); return createErrorResult(project, stage, service, e.getMessage()); } } /** * Check if deployment should proceed based on quality gate result */ public boolean shouldProceedWithDeployment(QualityGateResult result) { if (result == null) { log.warn("No quality gate result available"); return qualityGateConfig.isProceedOnFailure(); } boolean shouldProceed = result.isPassed() || (result.getStatus() == QualityGateStatus.WARNING && qualityGateConfig.isProceedOnWarning()); if (!shouldProceed) { log.warn("Deployment blocked by quality gate: {}", result.getMessage()); } return shouldProceed; } /** * Generate quality gate report */ public QualityGateReport generateReport(QualityGateResult result) { Map<String, SLIResult> sliResults = result.getSliResults().stream() .collect(Collectors.toMap(SLIResult::getName, Function.identity())); long passedSLIs = result.getSliResults().stream() .filter(sli -> sli.getStatus() == QualityGateStatus.PASS) .count(); long warningSLIs = result.getSliResults().stream() .filter(sli -> sli.getStatus() == QualityGateStatus.WARNING) .count(); long failedSLIs = result.getSliResults().stream() .filter(sli -> sli.getStatus() == QualityGateStatus.FAIL) .count(); return QualityGateReport.builder() .timestamp(Instant.now()) .project(result.getProject()) .stage(result.getStage()) .service(result.getService()) .overallStatus(result.getStatus()) .overallScore(result.getScore()) .passedSLIs(passedSLIs) .warningSLIs(warningSLIs) .failedSLIs(failedSLIs) .sliResults(sliResults) .message(result.getMessage()) .evaluationStart(result.getEvaluationStart()) .evaluationEnd(result.getEvaluationEnd()) .build(); } private String triggerEvaluation(String project, String stage, String service, String deploymentId) throws IOException { // Calculate evaluation time window (last 15 minutes by default) Instant endTime = Instant.now(); Instant startTime = endTime.minus(qualityGateConfig.getEvaluationWindow()); KeptnEvaluationRequest request = KeptnEvaluationRequest.builder() .data(KeptnEvaluationRequest.EvaluationData.builder() .project(project) .stage(stage) .service(service) .start(startTime.toString()) .end(endTime.toString()) .labels(Map.of( "deploymentId", deploymentId, "triggeredBy", "java-client", "application", service )) .testStrategy("real-user") .build()) .build(); return keptnClient.triggerEvaluation(request); } private void recordQualityGateMetrics(QualityGateResult result) { metricsService.recordQualityGateResult( result.getProject(), result.getStage(), result.getService(), result.getStatus(), result.getScore()); // Record individual SLI metrics result.getSliResults().forEach(sli -> { metricsService.recordSLIMetric( result.getProject(), result.getStage(), result.getService(), sli.getName(), sli.getValue(), sli.getStatus()); }); } private QualityGateResult createErrorResult(String project, String stage, String service, String errorMessage) { return QualityGateResult.builder() .project(project) .stage(stage) .service(service) .status(QualityGateStatus.FAIL) .score(0.0) .message("Quality gate failed: " + errorMessage) .sliResults(List.of()) .build(); } } @Data @Builder public class QualityGateReport { private Instant timestamp; private String project; private String stage; private String service; private QualityGateStatus overallStatus; private double overallScore; private long passedSLIs; private long warningSLIs; private long failedSLIs; private Map<String, SLIResult> sliResults; private String message; private Instant evaluationStart; private Instant evaluationEnd; }
4. Configuration Classes
@Configuration @ConfigurationProperties(prefix = "keptn") @Data public class KeptnConfig { private String apiUrl; private String apiToken; private String bridgeUrl; @PostConstruct public void validate() { if (apiUrl == null || apiToken == null) { throw new IllegalStateException("Keptn API URL and token must be configured"); } } } @Configuration @ConfigurationProperties(prefix = "quality-gate") @Data public class QualityGateConfig { private Duration evaluationTimeout = Duration.ofMinutes(10); private Duration evaluationWindow = Duration.ofMinutes(15); private boolean proceedOnWarning = true; private boolean proceedOnFailure = false; private double minimumScore = 80.0; private Map<String, StageConfig> stages = new HashMap<>(); @Data public static class StageConfig { private boolean enabled = true; private double minimumScore = 80.0; private boolean blockOnWarning = false; private List<String> requiredSLIs = List.of(); } public StageConfig getStageConfig(String stage) { return stages.getOrDefault(stage, new StageConfig()); } }
5. Metrics Service
@Service public class MetricsService { private final MeterRegistry meterRegistry; // Counters private final Counter qualityGateExecutions; private final Counter qualityGatePassed; private final Counter qualityGateFailed; // Gauges private final Map<String, Gauge> scoreGauges; public MetricsService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.scoreGauges = new ConcurrentHashMap<>(); this.qualityGateExecutions = Counter.builder("keptn.quality_gate.executions") .description("Total quality gate executions") .register(meterRegistry); this.qualityGatePassed = Counter.builder("keptn.quality_gate.passed") .description("Passed quality gates") .register(meterRegistry); this.qualityGateFailed = Counter.builder("keptn.quality_gate.failed") .description("Failed quality gates") .register(meterRegistry); } public void recordQualityGateResult(String project, String stage, String service, QualityGateStatus status, double score) { qualityGateExecutions.increment(); if (status == QualityGateStatus.PASS) { qualityGatePassed.increment(); } else { qualityGateFailed.increment(); } // Record score gauge String gaugeKey = project + "." + stage + "." + service; scoreGauges.put(gaugeKey, Gauge.builder("keptn.quality_gate.score") .tag("project", project) .tag("stage", stage) .tag("service", service) .register(meterRegistry)); // Update gauge value Timer.builder("keptn.quality_gate.evaluation.duration") .tag("project", project) .tag("stage", stage) .tag("service", service) .tag("status", status.name()) .register(meterRegistry); } public void recordSLIMetric(String project, String stage, String service, String sliName, double value, QualityGateStatus status) { Gauge.builder("keptn.sli.value") .tag("project", project) .tag("stage", stage) .tag("service", service) .tag("sli", sliName) .tag("status", status.name()) .register(meterRegistry) .set(value); Counter.builder("keptn.sli.status") .tag("project", project) .tag("stage", stage) .tag("service", service) .tag("sli", sliName) .tag("status", status.name()) .register(meterRegistry) .increment(); } }
6. REST Controller
@RestController @RequestMapping("/api/quality-gates") @Slf4j public class QualityGateController { private final QualityGateService qualityGateService; public QualityGateController(QualityGateService qualityGateService) { this.qualityGateService = qualityGateService; } @PostMapping("/evaluate") public ResponseEntity<QualityGateResponse> evaluateQualityGate( @RequestBody QualityGateRequest request) { log.info("Received quality gate evaluation request: {}", request); QualityGateResult result = qualityGateService.executeQualityGate( request.getProject(), request.getStage(), request.getService(), request.getDeploymentId()); boolean shouldProceed = qualityGateService.shouldProceedWithDeployment(result); QualityGateReport report = qualityGateService.generateReport(result); QualityGateResponse response = QualityGateResponse.builder() .success(true) .result(result) .report(report) .shouldProceed(shouldProceed) .timestamp(Instant.now()) .build(); return ResponseEntity.ok(response); } @PostMapping("/evaluate-with-time-range") public ResponseEntity<QualityGateResponse> evaluateWithTimeRange( @RequestBody TimeRangeQualityGateRequest request) { QualityGateResult result = qualityGateService.executeQualityGateWithTimeRange( request.getProject(), request.getStage(), request.getService(), request.getDeploymentId(), request.getStartTime(), request.getEndTime()); boolean shouldProceed = qualityGateService.shouldProceedWithDeployment(result); QualityGateReport report = qualityGateService.generateReport(result); QualityGateResponse response = QualityGateResponse.builder() .success(true) .result(result) .report(report) .shouldProceed(shouldProceed) .timestamp(Instant.now()) .build(); return ResponseEntity.ok(response); } @GetMapping("/projects/{project}/stages/{stage}/services/{service}/history") public ResponseEntity<List<QualityGateReport>> getQualityGateHistory( @PathVariable String project, @PathVariable String stage, @PathVariable String service, @RequestParam(defaultValue = "10") int limit) { // Implementation to fetch historical results from database List<QualityGateReport> history = List.of(); // Placeholder return ResponseEntity.ok(history); } } @Data @Builder class QualityGateRequest { @NotBlank private String project; @NotBlank private String stage; @NotBlank private String service; @NotBlank private String deploymentId; private Map<String, String> labels; } @Data @Builder class TimeRangeQualityGateRequest extends QualityGateRequest { @NotNull private Instant startTime; @NotNull private Instant endTime; } @Data @Builder class QualityGateResponse { private boolean success; private QualityGateResult result; private QualityGateReport report; private boolean shouldProceed; private Instant timestamp; private String message; }
7. CI/CD Integration
@Component @Slf4j public class CICDIntegrationService { private final QualityGateService qualityGateService; private final KeptnClient keptnClient; public CICDIntegrationService(QualityGateService qualityGateService, KeptnClient keptnClient) { this.qualityGateService = qualityGateService; this.keptnClient = keptnClient; } /** * Execute quality gate as part of deployment pipeline */ public boolean executeDeploymentQualityGate(String project, String stage, String service, String deploymentId) { log.info("Starting quality gate for deployment: {} to {}/{}", deploymentId, project, stage); try { QualityGateResult result = qualityGateService.executeQualityGate( project, stage, service, deploymentId); boolean shouldProceed = qualityGateService.shouldProceedWithDeployment(result); if (shouldProceed) { log.info("Quality gate PASSED - Deployment can proceed"); return true; } else { log.error("Quality gate FAILED - Deployment blocked"); return false; } } catch (Exception e) { log.error("Quality gate execution failed", e); return false; // Fail safe - block deployment on errors } } /** * Send deployment finished event to Keptn */ public void sendDeploymentFinishedEvent(String project, String stage, String service, String deploymentId, boolean success) { try { Map<String, Object> event = Map.of( "type", "sh.keptn.event.deployment.finished", "specversion", "1.0", "source", "java-cicd-integration", "id", UUID.randomUUID().toString(), "time", Instant.now().toString(), "contenttype", "application/json", "data", Map.of( "project", project, "stage", stage, "service", service, "deployment", Map.of( "deploymentNames", List.of(deploymentId), "deploymentURIsLocal", List.of("https://" + service + ".example.com"), "deploymentURIsPublic", List.of("https://" + service + ".example.com") ), "result", success ? "pass" : "fail", "message", success ? "Deployment completed successfully" : "Deployment failed" ) ); // Send event to Keptn String eventJson = new ObjectMapper().writeValueAsString(event); // Implementation to send event to Keptn API... log.info("Sent deployment finished event to Keptn: {}", success ? "SUCCESS" : "FAILED"); } catch (Exception e) { log.error("Failed to send deployment finished event to Keptn", e); } } }
8. Spring Boot Actuator Health Check
@Component public class KeptnHealthIndicator implements HealthIndicator { private final KeptnClient keptnClient; private final KeptnConfig keptnConfig; public KeptnHealthIndicator(KeptnClient keptnClient, KeptnConfig keptnConfig) { this.keptnClient = keptnClient; this.keptnConfig = keptnConfig; } @Override public Health health() { try { // Try to connect to Keptn API String url = keptnConfig.getApiUrl() + "/v1/metadata"; // Simple connectivity check return Health.up() .withDetail("apiUrl", keptnConfig.getApiUrl()) .withDetail("status", "connected") .build(); } catch (Exception e) { return Health.down() .withDetail("apiUrl", keptnConfig.getApiUrl()) .withDetail("error", e.getMessage()) .build(); } } }
9. Application Properties
# application.yml keptn: api-url: ${KEPTN_API_URL:http://localhost:8080/api} api-token: ${KEPTN_API_TOKEN:default-token} bridge-url: ${KEPTN_BRIDGE_URL:http://localhost:8080} quality-gate: evaluation-timeout: PT10M evaluation-window: PT15M proceed-on-warning: true proceed-on-failure: false minimum-score: 80.0 stages: staging: enabled: true minimum-score: 80.0 block-on-warning: false required-slis: - "response_time_p95" - "error_rate" production: enabled: true minimum-score: 90.0 block-on-warning: true required-slis: - "response_time_p95" - "error_rate" - "availability" management: endpoints: web: exposure: include: health,metrics,info endpoint: health: show-details: always

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class) class QualityGateServiceTest { @Mock private KeptnClient keptnClient; @Mock private MetricsService metricsService; @InjectMocks private QualityGateService qualityGateService; @Test void shouldExecuteQualityGateSuccessfully() throws Exception { // Given String keptnContext = "test-context-123"; QualityGateResult expectedResult = createPassingQualityGateResult(); when(keptnClient.triggerEvaluation(any())).thenReturn(keptnContext); when(keptnClient.waitForEvaluation(eq(keptnContext), any())).thenReturn(expectedResult); // When QualityGateResult result = qualityGateService.executeQualityGate( "my-project", "staging", "user-service", "deploy-123"); // Then assertThat(result.isPassed()).isTrue(); assertThat(result.getScore()).isGreaterThan(80.0); verify(metricsService).recordQualityGateResult(any(), any(), any(), any(), anyDouble()); } private QualityGateResult createPassingQualityGateResult() { return QualityGateResult.builder() .status(QualityGateStatus.PASS) .score(95.0) .sliResults(List.of( SLIResult.builder() .name("response_time_p95") .value(800.0) .status(QualityGateStatus.PASS) .build(), SLIResult.builder() .name("error_rate") .value(0.02) .status(QualityGateStatus.PASS) .build() )) .build(); } }

Best Practices

  1. Fail Safe: Block deployments when quality gates fail or encounter errors
  2. Meaningful SLIs: Choose SLIs that directly impact user experience
  3. Progressive Strictness: Use stricter criteria in production vs staging
  4. Monitoring: Monitor quality gate execution and success rates
  5. Feedback Loops: Use results to improve application quality continuously
// Example of progressive quality gates @Component public class ProgressiveQualityGate { public QualityGateConfig getStageConfig(String stage) { return switch (stage) { case "dev" -> createLenientConfig(); case "staging" -> createBalancedConfig(); case "production" -> createStrictConfig(); default -> createBalancedConfig(); }; } private QualityGateConfig createLenientConfig() { return QualityGateConfig.builder() .proceedOnWarning(true) .proceedOnFailure(true) // Allow failures in dev .minimumScore(60.0) .build(); } private QualityGateConfig createStrictConfig() { return QualityGateConfig.builder() .proceedOnWarning(false) .proceedOnFailure(false) // Block on any failure in production .minimumScore(90.0) .build(); } }

Conclusion

Keptn quality gates in Java provide:

  • Automated Quality Assurance: Continuous validation of deployment quality
  • SLO-based Decisions: Objective criteria for deployment approvals
  • Risk Reduction: Prevent problematic deployments from reaching production
  • Metrics-driven: Data-driven approach to quality management
  • Integration Friendly: Seamless integration with CI/CD pipelines

By implementing Keptn quality gates, you can ensure that only high-quality, performant changes are deployed to production, significantly reducing the risk of service degradation and improving overall system reliability.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper