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