google-site-verification: google61fe8ba583a51912.html
Synthetic Monitoring in Java: Complete Implementation Guide

Introduction

Synthetic monitoring involves simulating user interactions and transactions to proactively monitor application health, performance, and functionality. Unlike real-user monitoring, synthetic tests run on a schedule from various locations to detect issues before users are affected.

This guide covers building a comprehensive synthetic monitoring framework in Java for web applications, APIs, and business transactions.


Architecture Overview

[Synthetic Tests] → [Test Executors] → [Applications] → [Metrics Collector] → [Dashboard/Alerts] ↓ ↓ ↓ ↓ ↓ HTTP Checks Browser Microservices Metrics Storage PagerDuty API Validation Automation Databases Log Aggregator Slack/Email Business Flows Performance External APIs Time Series DB Kibana

Step 1: Project Dependencies

<properties> <spring-boot.version>3.2.0</spring-boot.version> <selenium.version>4.15.0</selenium.version> <micrometer.version>1.12.0</micrometer.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-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Scheduling --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Selenium for Browser Automation --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>${selenium.version}</version> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-chrome-driver</artifactId> <version>${selenium.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> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <!-- Database --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies>

Step 2: Configuration

application.yml

synthetic: monitoring: enabled: true execution: thread-pool-size: 10 timeout-seconds: 300 retry-attempts: 2 retry-delay-ms: 5000 # Browser automation browser: headless: true window-width: 1920 window-height: 1080 implicit-wait-seconds: 10 page-load-timeout-seconds: 30 # Notifications alerts: slack-webhook-url: ${SLACK_WEBHOOK_URL:} pagerduty-routing-key: ${PAGERDUTY_ROUTING_KEY:} email-recipients: ${ALERT_EMAILS:} # Storage storage: retain-days: 30 cleanup-enabled: true # Metrics management: endpoints: web: exposure: include: health,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true # Logging logging: level: com.example.synthetic: DEBUG pattern: level: "%5p [%X{traceId:-},%X{testId:-}]" # Scheduled tasks spring: quartz: job-store-type: jdbc jdbc: initialize-schema: always datasource: url: jdbc:h2:file:./data/synthetic-monitoring driverClassName: org.h2.Driver username: sa password: "" jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update show-sql: false

Step 3: Core Data Models

Test Definition Models

MonitorTest.java

package com.example.synthetic.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.Map; @Entity @Table(name = "monitor_tests") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "test_type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = HttpTest.class, name = "HTTP"), @JsonSubTypes.Type(value = BrowserTest.class, name = "BROWSER"), @JsonSubTypes.Type(value = ApiValidationTest.class, name = "API_VALIDATION"), @JsonSubTypes.Type(value = BusinessFlowTest.class, name = "BUSINESS_FLOW") }) public abstract class MonitorTest { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @Column(nullable = false) private String name; private String description; @NotBlank @Column(nullable = false) private String targetUrl; @NotNull @Enumerated(EnumType.STRING) @Column(nullable = false) private TestFrequency frequency; @NotNull @Enumerated(EnumType.STRING) @Column(nullable = false) private TestPriority priority; private boolean enabled = true; private int timeoutSeconds = 300; private int retryAttempts = 2; @ElementCollection @CollectionTable(name = "test_labels", joinColumns = @JoinColumn(name = "test_id")) @MapKeyColumn(name = "label_key") @Column(name = "label_value") private Map<String, String> labels; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createdAt; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updatedAt; // Pre-persist and pre-update methods @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getTargetUrl() { return targetUrl; } public void setTargetUrl(String targetUrl) { this.targetUrl = targetUrl; } public TestFrequency getFrequency() { return frequency; } public void setFrequency(TestFrequency frequency) { this.frequency = frequency; } public TestPriority getPriority() { return priority; } public void setPriority(TestPriority priority) { this.priority = priority; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getTimeoutSeconds() { return timeoutSeconds; } public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } public int getRetryAttempts() { return retryAttempts; } public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; } public Map<String, String> getLabels() { return labels; } public void setLabels(Map<String, String> labels) { this.labels = labels; } public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } } @Entity @DiscriminatorValue("HTTP") class HttpTest extends MonitorTest { private HttpMethod method = HttpMethod.GET; @Column(length = 2000) private String requestBody; @ElementCollection @CollectionTable(name = "http_headers", joinColumns = @JoinColumn(name = "test_id")) @MapKeyColumn(name = "header_name") @Column(name = "header_value") private Map<String, String> headers; private Integer expectedStatusCode = 200; @Column(length = 1000) private String expectedResponsePattern; private Long maxResponseTimeMs = 5000L; // Getters and Setters public HttpMethod getMethod() { return method; } public void setMethod(HttpMethod method) { this.method = method; } public String getRequestBody() { return requestBody; } public void setRequestBody(String requestBody) { this.requestBody = requestBody; } public Map<String, String> getHeaders() { return headers; } public void setHeaders(Map<String, String> headers) { this.headers = headers; } public Integer getExpectedStatusCode() { return expectedStatusCode; } public void setExpectedStatusCode(Integer expectedStatusCode) { this.expectedStatusCode = expectedStatusCode; } public String getExpectedResponsePattern() { return expectedResponsePattern; } public void setExpectedResponsePattern(String expectedResponsePattern) { this.expectedResponsePattern = expectedResponsePattern; } public Long getMaxResponseTimeMs() { return maxResponseTimeMs; } public void setMaxResponseTimeMs(Long maxResponseTimeMs) { this.maxResponseTimeMs = maxResponseTimeMs; } } @Entity @DiscriminatorValue("BROWSER") class BrowserTest extends MonitorTest { @Column(length = 2000) private String script; private String browserType = "CHROME"; private boolean takeScreenshotOnFailure = true; private Long maxLoadTimeMs = 10000L; // Getters and Setters public String getScript() { return script; } public void setScript(String script) { this.script = script; } public String getBrowserType() { return browserType; } public void setBrowserType(String browserType) { this.browserType = browserType; } public boolean isTakeScreenshotOnFailure() { return takeScreenshotOnFailure; } public void setTakeScreenshotOnFailure(boolean takeScreenshotOnFailure) { this.takeScreenshotOnFailure = takeScreenshotOnFailure; } public Long getMaxLoadTimeMs() { return maxLoadTimeMs; } public void setMaxLoadTimeMs(Long maxLoadTimeMs) { this.maxLoadTimeMs = maxLoadTimeMs; } }

Enums and Supporting Classes

TestEnums.java

package com.example.synthetic.model; public enum TestFrequency { EVERY_MINUTE(60), EVERY_5_MINUTES(300), EVERY_15_MINUTES(900), EVERY_30_MINUTES(1800), HOURLY(3600), EVERY_6_HOURS(21600), DAILY(86400); private final int seconds; TestFrequency(int seconds) { this.seconds = seconds; } public int getSeconds() { return seconds; } public String getCronExpression() { switch (this) { case EVERY_MINUTE: return "0 * * * * ?"; case EVERY_5_MINUTES: return "0 */5 * * * ?"; case EVERY_15_MINUTES: return "0 */15 * * * ?"; case EVERY_30_MINUTES: return "0 */30 * * * ?"; case HOURLY: return "0 0 * * * ?"; case EVERY_6_HOURS: return "0 0 */6 * * ?"; case DAILY: return "0 0 0 * * ?"; default: return "0 0 * * * ?"; } } } public enum TestPriority { LOW, MEDIUM, HIGH, CRITICAL } public enum HttpMethod { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } public enum TestStatus { SUCCESS, FAILURE, TIMEOUT, ERROR, SKIPPED }

Test Execution Results

TestExecutionResult.java

package com.example.synthetic.model; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.Map; @Entity @Table(name = "test_execution_results") public class TestExecutionResult { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "test_id", nullable = false) private MonitorTest test; @Enumerated(EnumType.STRING) @Column(nullable = false) private TestStatus status; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Column(nullable = false) private LocalDateTime executionTime; private Long responseTimeMs; private Integer statusCode; @Column(length = 500) private String errorMessage; @Column(length = 4000) private String responseBody; @ElementCollection @CollectionTable(name = "execution_metrics", joinColumns = @JoinColumn(name = "execution_id")) @MapKeyColumn(name = "metric_name") @Column(name = "metric_value") private Map<String, Double> metrics; private String screenshotPath; private String location; // Where test was executed from @Column(length = 1000) private String additionalInfo; // Constructors public TestExecutionResult() {} public TestExecutionResult(MonitorTest test, TestStatus status) { this.test = test; this.status = status; this.executionTime = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public MonitorTest getTest() { return test; } public void setTest(MonitorTest test) { this.test = test; } public TestStatus getStatus() { return status; } public void setStatus(TestStatus status) { this.status = status; } public LocalDateTime getExecutionTime() { return executionTime; } public void setExecutionTime(LocalDateTime executionTime) { this.executionTime = executionTime; } public Long getResponseTimeMs() { return responseTimeMs; } public void setResponseTimeMs(Long responseTimeMs) { this.responseTimeMs = responseTimeMs; } public Integer getStatusCode() { return statusCode; } public void setStatusCode(Integer statusCode) { this.statusCode = statusCode; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } public String getResponseBody() { return responseBody; } public void setResponseBody(String responseBody) { this.responseBody = responseBody; } public Map<String, Double> getMetrics() { return metrics; } public void setMetrics(Map<String, Double> metrics) { this.metrics = metrics; } public String getScreenshotPath() { return screenshotPath; } public void setScreenshotPath(String screenshotPath) { this.screenshotPath = screenshotPath; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } public String getAdditionalInfo() { return additionalInfo; } public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } }

Step 4: Test Executors

Base Test Executor

TestExecutor.java

package com.example.synthetic.executor; import com.example.synthetic.model.MonitorTest; import com.example.synthetic.model.TestExecutionResult; import com.example.synthetic.model.TestStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.time.Duration; import java.time.Instant; import java.util.concurrent.Callable; public abstract class TestExecutor { protected final Logger logger = LoggerFactory.getLogger(getClass()); public abstract boolean supports(MonitorTest test); public TestExecutionResult execute(MonitorTest test) { Instant start = Instant.now(); TestExecutionResult result = new TestExecutionResult(test, TestStatus.SUCCESS); try { logger.info("Executing test: {} - {}", test.getId(), test.getName()); result = doExecute(test); } catch (Exception e) { logger.error("Test execution failed for test: {}", test.getName(), e); result.setStatus(TestStatus.ERROR); result.setErrorMessage(e.getMessage()); } finally { long duration = Duration.between(start, Instant.now()).toMillis(); result.setResponseTimeMs(duration); logger.info("Test completed: {} - Status: {} - Duration: {}ms", test.getName(), result.getStatus(), duration); } return result; } protected abstract TestExecutionResult doExecute(MonitorTest test) throws Exception; protected <T> T executeWithTimeout(Callable<T> callable, int timeoutSeconds) throws Exception { // Implementation with timeout handling // Using CompletableFuture with timeout return callable.call(); // Simplified - implement proper timeout } }

HTTP Test Executor

HttpTestExecutor.java

package com.example.synthetic.executor; import com.example.synthetic.model.HttpTest; import com.example.synthetic.model.MonitorTest; import com.example.synthetic.model.TestExecutionResult; import com.example.synthetic.model.TestStatus; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Map; import java.util.regex.Pattern; @Component public class HttpTestExecutor extends TestExecutor { private final WebClient webClient; public HttpTestExecutor(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder.build(); } @Override public boolean supports(MonitorTest test) { return test instanceof HttpTest; } @Override protected TestExecutionResult doExecute(MonitorTest test) throws Exception { HttpTest httpTest = (HttpTest) test; TestExecutionResult result = new TestExecutionResult(test, TestStatus.SUCCESS); try { WebClient.RequestBodySpec requestSpec = webClient .method(HttpMethod.valueOf(httpTest.getMethod().name())) .uri(httpTest.getTargetUrl()); // Set headers if (httpTest.getHeaders() != null) { httpTest.getHeaders().forEach(requestSpec::header); } // Set body for POST, PUT, PATCH if (httpTest.getRequestBody() != null && (httpTest.getMethod() == HttpMethod.POST || httpTest.getMethod() == HttpMethod.PUT || httpTest.getMethod() == HttpMethod.PATCH)) { requestSpec.contentType(MediaType.APPLICATION_JSON) .bodyValue(httpTest.getRequestBody()); } String responseBody = requestSpec .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(httpTest.getTimeoutSeconds())) .block(); result.setStatusCode(200); // Assuming success since no exception result.setResponseBody(truncateResponse(responseBody, 4000)); // Validate response validateResponse(httpTest, result, responseBody); } catch (WebClientResponseException e) { result.setStatusCode(e.getStatusCode().value()); result.setResponseBody(truncateResponse(e.getResponseBodyAsString(), 4000)); if (httpTest.getExpectedStatusCode() != null && httpTest.getExpectedStatusCode() != e.getStatusCode().value()) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Expected status " + httpTest.getExpectedStatusCode() + " but got " + e.getStatusCode().value()); } else if (httpTest.getExpectedStatusCode() == null && e.getStatusCode().isError()) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("HTTP error: " + e.getStatusCode()); } } catch (Exception e) { result.setStatus(TestStatus.ERROR); result.setErrorMessage(e.getMessage()); } return result; } private void validateResponse(HttpTest test, TestExecutionResult result, String responseBody) { // Check expected status code if (test.getExpectedStatusCode() != null && result.getStatusCode() != test.getExpectedStatusCode()) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Expected status " + test.getExpectedStatusCode() + " but got " + result.getStatusCode()); return; } // Check response pattern if (test.getExpectedResponsePattern() != null && responseBody != null) { Pattern pattern = Pattern.compile(test.getExpectedResponsePattern()); if (!pattern.matcher(responseBody).find()) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Response pattern not found"); return; } } // Check response time if (test.getMaxResponseTimeMs() != null && result.getResponseTimeMs() > test.getMaxResponseTimeMs()) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Response time " + result.getResponseTimeMs() + "ms exceeds maximum " + test.getMaxResponseTimeMs() + "ms"); } } private String truncateResponse(String response, int maxLength) { if (response == null || response.length() <= maxLength) { return response; } return response.substring(0, maxLength - 3) + "..."; } }

Browser Test Executor

BrowserTestExecutor.java

package com.example.synthetic.executor; import com.example.synthetic.model.BrowserTest; import com.example.synthetic.model.MonitorTest; import com.example.synthetic.model.TestExecutionResult; import com.example.synthetic.model.TestStatus; import org.openqa.selenium.*; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.support.ui.WebDriverWait; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.Base64; import java.util.HashMap; import java.util.Map; @Component public class BrowserTestExecutor extends TestExecutor { @Value("${synthetic.monitoring.browser.headless:true}") private boolean headless; @Value("${synthetic.monitoring.screenshot.directory:./screenshots}") private String screenshotDirectory; @Override public boolean supports(MonitorTest test) { return test instanceof BrowserTest; } @Override protected TestExecutionResult doExecute(MonitorTest test) throws Exception { BrowserTest browserTest = (BrowserTest) test; WebDriver driver = null; TestExecutionResult result = new TestExecutionResult(test, TestStatus.SUCCESS); try { // Setup WebDriver driver = createWebDriver(); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); // Navigate to target URL long startTime = System.currentTimeMillis(); driver.get(browserTest.getTargetUrl()); long loadTime = System.currentTimeMillis() - startTime; result.setResponseTimeMs(loadTime); // Execute custom script if provided if (browserTest.getScript() != null && !browserTest.getScript().trim().isEmpty()) { executeBrowserScript(driver, browserTest.getScript(), result); } // Check page load performance if (browserTest.getMaxLoadTimeMs() != null && loadTime > browserTest.getMaxLoadTimeMs()) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Page load time " + loadTime + "ms exceeds maximum " + browserTest.getMaxLoadTimeMs() + "ms"); } // Capture performance metrics capturePerformanceMetrics(driver, result); } catch (TimeoutException e) { result.setStatus(TestStatus.TIMEOUT); result.setErrorMessage("Browser operation timed out: " + e.getMessage()); } catch (Exception e) { result.setStatus(TestStatus.ERROR); result.setErrorMessage("Browser test failed: " + e.getMessage()); } finally { // Take screenshot on failure if (driver != null) { if (result.getStatus() != TestStatus.SUCCESS && browserTest.isTakeScreenshotOnFailure()) { String screenshotPath = takeScreenshot(driver, test.getName()); result.setScreenshotPath(screenshotPath); } driver.quit(); } } return result; } private WebDriver createWebDriver() { ChromeOptions options = new ChromeOptions(); if (headless) { options.addArguments("--headless"); } options.addArguments("--no-sandbox"); options.addArguments("--disable-dev-shm-usage"); options.addArguments("--window-size=1920,1080"); options.addArguments("--disable-gpu"); options.addArguments("--disable-extensions"); // Performance logging options.setCapability("goog:loggingPrefs", Map.of("performance", "ALL", "browser", "ALL")); return new ChromeDriver(options); } private void executeBrowserScript(WebDriver driver, String script, TestExecutionResult result) { try { // Simple script execution - extend for more complex scenarios if (script.contains("waitForElement")) { // Example: waitForElement("#submit-button") String selector = script.split("\\(")[1].replace(")", "").replace("\"", "").trim(); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(webDriver -> webDriver.findElement(By.cssSelector(selector)).isDisplayed()); } if (script.contains("click")) { String selector = script.split("\\(")[1].replace(")", "").replace("\"", "").trim(); driver.findElement(By.cssSelector(selector)).click(); } if (script.contains("assertText")) { // Example: assertText(".status", "Success") String[] parts = script.split(","); String selector = parts[0].split("\\(")[1].replace("\"", "").trim(); String expectedText = parts[1].replace(")", "").replace("\"", "").trim(); String actualText = driver.findElement(By.cssSelector(selector)).getText(); if (!actualText.contains(expectedText)) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Text assertion failed. Expected: " + expectedText + ", Actual: " + actualText); } } } catch (NoSuchElementException e) { result.setStatus(TestStatus.FAILURE); result.setErrorMessage("Element not found: " + e.getMessage()); } catch (TimeoutException e) { result.setStatus(TestStatus.TIMEOUT); result.setErrorMessage("Element wait timeout: " + e.getMessage()); } } private void capturePerformanceMetrics(WebDriver driver, TestExecutionResult result) { try { JavascriptExecutor js = (JavascriptExecutor) driver; Map<String, Double> metrics = new HashMap<>(); // Navigation timing API Long loadEventEnd = (Long) js.executeScript( "return window.performance.timing.loadEventEnd"); Long navigationStart = (Long) js.executeScript( "return window.performance.timing.navigationStart"); if (loadEventEnd != null && navigationStart != null) { metrics.put("page_load_time", (double)(loadEventEnd - navigationStart)); } // Resource timing Long totalResourceSize = (Long) js.executeScript( "return window.performance.getEntriesByType('resource')" + ".reduce((acc, entry) => acc + entry.transferSize, 0)"); if (totalResourceSize != null) { metrics.put("total_resource_size_kb", totalResourceSize / 1024.0); } result.setMetrics(metrics); } catch (Exception e) { logger.warn("Failed to capture performance metrics", e); } } private String takeScreenshot(WebDriver driver, String testName) { try { Files.createDirectories(Paths.get(screenshotDirectory)); String timestamp = String.valueOf(System.currentTimeMillis()); String filename = testName.replaceAll("[^a-zA-Z0-9]", "_") + "_" + timestamp + ".png"; Path screenshotPath = Paths.get(screenshotDirectory, filename); File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); Files.copy(screenshot.toPath(), screenshotPath); return screenshotPath.toString(); } catch (Exception e) { logger.error("Failed to take screenshot", e); return null; } } }

Step 5: Test Orchestration Service

TestOrchestrationService.java

package com.example.synthetic.service; import com.example.synthetic.executor.TestExecutor; import com.example.synthetic.model.MonitorTest; import com.example.synthetic.model.TestExecutionResult; import com.example.synthetic.model.TestStatus; import com.example.synthetic.repository.MonitorTestRepository; import com.example.synthetic.repository.TestExecutionResultRepository; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Service public class TestOrchestrationService { private static final Logger logger = LoggerFactory.getLogger(TestOrchestrationService.class); private final List<TestExecutor> testExecutors; private final MonitorTestRepository testRepository; private final TestExecutionResultRepository resultRepository; private final AlertService alertService; private final MeterRegistry meterRegistry; private final Map<Long, CompletableFuture<TestExecutionResult>> runningTests = new ConcurrentHashMap<>(); public TestOrchestrationService(List<TestExecutor> testExecutors, MonitorTestRepository testRepository, TestExecutionResultRepository resultRepository, AlertService alertService, MeterRegistry meterRegistry) { this.testExecutors = testExecutors; this.testRepository = testRepository; this.resultRepository = resultRepository; this.alertService = alertService; this.meterRegistry = meterRegistry; } @Async("testExecutionTaskExecutor") public CompletableFuture<TestExecutionResult> executeTest(Long testId) { return testRepository.findById(testId) .map(this::executeTest) .orElse(CompletableFuture.completedFuture( createErrorResult("Test not found with id: " + testId))); } @Async("testExecutionTaskExecutor") public CompletableFuture<TestExecutionResult> executeTest(MonitorTest test) { // Check if test is already running if (runningTests.containsKey(test.getId())) { logger.warn("Test {} is already running", test.getId()); return runningTests.get(test.getId()); } CompletableFuture<TestExecutionResult> future = CompletableFuture.supplyAsync(() -> { Timer.Sample sample = Timer.start(meterRegistry); TestExecutionResult result = null; try { TestExecutor executor = findExecutor(test); if (executor == null) { return createErrorResult("No executor found for test type: " + test.getClass().getSimpleName()); } result = executor.execute(test); } catch (Exception e) { logger.error("Test execution failed for test: {}", test.getName(), e); result = createErrorResult("Execution error: " + e.getMessage()); } finally { sample.stop(Timer.builder("synthetic.test.execution.time") .tag("testName", test.getName()) .tag("status", result != null ? result.getStatus().name() : "ERROR") .register(meterRegistry)); runningTests.remove(test.getId()); } return result != null ? result : createErrorResult("Unknown execution error"); }); runningTests.put(test.getId(), future); // Process result when complete future.thenAccept(this::processExecutionResult); return future; } @Transactional public void processExecutionResult(TestExecutionResult result) { try { // Save result to database resultRepository.save(result); // Update metrics updateMetrics(result); // Send alert if test failed if (result.getStatus() != TestStatus.SUCCESS) { alertService.sendTestFailureAlert(result); } logger.info("Processed test result for {}: {}", result.getTest().getName(), result.getStatus()); } catch (Exception e) { logger.error("Failed to process test execution result", e); } } public void executeScheduledTests() { List<MonitorTest> scheduledTests = testRepository.findEnabledTestsForExecution( LocalDateTime.now()); logger.info("Executing {} scheduled tests", scheduledTests.size()); scheduledTests.forEach(test -> { try { executeTest(test); } catch (Exception e) { logger.error("Failed to execute scheduled test: {}", test.getName(), e); } }); } private TestExecutor findExecutor(MonitorTest test) { return testExecutors.stream() .filter(executor -> executor.supports(test)) .findFirst() .orElse(null); } private TestExecutionResult createErrorResult(String errorMessage) { TestExecutionResult result = new TestExecutionResult(); result.setStatus(TestStatus.ERROR); result.setErrorMessage(errorMessage); result.setExecutionTime(LocalDateTime.now()); return result; } private void updateMetrics(TestExecutionResult result) { String testName = result.getTest().getName(); String status = result.getStatus().name().toLowerCase(); // Counter for test executions meterRegistry.counter("synthetic.test.executions", "testName", testName, "status", status ).increment(); // Response time histogram if (result.getResponseTimeMs() != null) { meterRegistry.timer("synthetic.test.response.time", "testName", testName ).record(java.time.Duration.ofMillis(result.getResponseTimeMs())); } // Gauge for success rate (you might want to calculate this differently) meterRegistry.gauge("synthetic.test.success.rate", Map.of("testName", testName), this, service -> calculateSuccessRate(testName) ); } private double calculateSuccessRate(String testName) { // Implement success rate calculation based on recent results return 95.0; // Simplified } public Map<Long, String> getRunningTests() { return runningTests.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().isDone() ? "COMPLETED" : "RUNNING" )); } }

Step 6: Scheduling Configuration

SchedulingConfig.java

package com.example.synthetic.config; import org.springframework.boot.autoconfigure.quartz.QuartzDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import javax.sql.DataSource; import java.util.concurrent.Executor; @Configuration @EnableAsync @EnableScheduling public class SchedulingConfig { @Bean("testExecutionTaskExecutor") public Executor testExecutionTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("TestExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolTaskExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Bean @QuartzDataSource @ConfigurationProperties(prefix = "spring.datasource") public DataSource quartzDataSource() { return DataSourceBuilder.create().build(); } }

TestScheduler.java

package com.example.synthetic.scheduler; import com.example.synthetic.service.TestOrchestrationService; import org.quartz.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class TestScheduler { private static final Logger logger = LoggerFactory.getLogger(TestScheduler.class); private final TestOrchestrationService orchestrationService; public TestScheduler(TestOrchestrationService orchestrationService) { this.orchestrationService = orchestrationService; } // Run every minute to check for scheduled tests @Scheduled(cron = "0 * * * * ?") public void executeScheduledTests() { logger.debug("Checking for scheduled tests..."); orchestrationService.executeScheduledTests(); } // Health check - run every 5 minutes @Scheduled(cron = "0 */5 * * * ?") public void healthCheck() { logger.info("Synthetic monitoring health check - Running tests: {}", orchestrationService.getRunningTests().size()); } }

Step 7: REST API for Test Management

MonitorTestController.java

package com.example.synthetic.controller; import com.example.synthetic.model.MonitorTest; import com.example.synthetic.model.TestExecutionResult; import com.example.synthetic.service.MonitorTestService; import com.example.synthetic.service.TestOrchestrationService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/api/v1/monitor-tests") public class MonitorTestController { private final MonitorTestService testService; private final TestOrchestrationService orchestrationService; public MonitorTestController(MonitorTestService testService, TestOrchestrationService orchestrationService) { this.testService = testService; this.orchestrationService = orchestrationService; } @GetMapping public ResponseEntity<Page<MonitorTest>> getTests( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { return ResponseEntity.ok(testService.findAll(PageRequest.of(page, size))); } @GetMapping("/{id}") public ResponseEntity<MonitorTest> getTest(@PathVariable Long id) { return testService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<MonitorTest> createTest(@RequestBody MonitorTest test) { return ResponseEntity.ok(testService.save(test)); } @PutMapping("/{id}") public ResponseEntity<MonitorTest> updateTest(@PathVariable Long id, @RequestBody MonitorTest test) { test.setId(id); return ResponseEntity.ok(testService.save(test)); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteTest(@PathVariable Long id) { testService.deleteById(id); return ResponseEntity.ok().build(); } @PostMapping("/{id}/execute") public CompletableFuture<ResponseEntity<TestExecutionResult>> executeTest(@PathVariable Long id) { return orchestrationService.executeTest(id) .thenApply(ResponseEntity::ok); } @GetMapping("/{id}/results") public ResponseEntity<Page<TestExecutionResult>> getTestResults( @PathVariable Long id, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { return ResponseEntity.ok(testService.findResultsByTestId(id, PageRequest.of(page, size))); } }

Step 8: Alerting Service

AlertService.java

package com.example.synthetic.service; import com.example.synthetic.model.TestExecutionResult; 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; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; @Service public class AlertService { private static final Logger logger = LoggerFactory.getLogger(AlertService.class); @Value("${synthetic.monitoring.alerts.slack-webhook-url:}") private String slackWebhookUrl; @Value("${synthetic.monitoring.alerts.pagerduty-routing-key:}") private String pagerdutyRoutingKey; private final RestTemplate restTemplate; public AlertService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public void sendTestFailureAlert(TestExecutionResult result) { // Send to Slack if (slackWebhookUrl != null && !slackWebhookUrl.isEmpty()) { sendSlackAlert(result); } // Send to PagerDuty for critical failures if (pagerdutyRoutingKey != null && !pagerdutyRoutingKey.isEmpty() && result.getTest().getPriority().ordinal() >= 2) { // HIGH or CRITICAL sendPagerDutyAlert(result); } logger.warn("Test failure alert sent for: {} - {}", result.getTest().getName(), result.getErrorMessage()); } private void sendSlackAlert(TestExecutionResult result) { try { Map<String, Object> slackMessage = new HashMap<>(); slackMessage.put("text", "❌ Synthetic Test Failure"); Map<String, Object> attachment = new HashMap<>(); attachment.put("color", "danger"); attachment.put("title", result.getTest().getName()); attachment.put("text", result.getErrorMessage()); Map<String, Object> fields = new HashMap<>(); fields.put("title", "Details"); fields.put("value", String.format( "Test: %s\nStatus: %s\nResponse Time: %dms\nURL: %s", result.getTest().getName(), result.getStatus(), result.getResponseTimeMs(), result.getTest().getTargetUrl() )); fields.put("short", false); attachment.put("fields", new Object[]{fields}); slackMessage.put("attachments", new Object[]{attachment}); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Map<String, Object>> request = new HttpEntity<>(slackMessage, headers); restTemplate.postForEntity(slackWebhookUrl, request, String.class); } catch (Exception e) { logger.error("Failed to send Slack alert", e); } } private void sendPagerDutyAlert(TestExecutionResult result) { try { Map<String, Object> pagerDutyEvent = new HashMap<>(); pagerDutyEvent.put("routing_key", pagerdutyRoutingKey); pagerDutyEvent.put("event_action", "trigger"); Map<String, Object> payload = new HashMap<>(); payload.put("summary", String.format("Synthetic test failure: %s", result.getTest().getName())); payload.put("source", "synthetic-monitoring"); payload.put("severity", "error"); payload.put("custom_details", Map.of( "test_name", result.getTest().getName(), "error_message", result.getErrorMessage(), "response_time_ms", result.getResponseTimeMs(), "target_url", result.getTest().getTargetUrl(), "test_priority", result.getTest().getPriority().name() )); pagerDutyEvent.put("payload", payload); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Map<String, Object>> request = new HttpEntity<>(pagerDutyEvent, headers); restTemplate.postForEntity("https://events.pagerduty.com/v2/enqueue", request, String.class); } catch (Exception e) { logger.error("Failed to send PagerDuty alert", e); } } }

Step 9: Sample Test Definitions

HTTP Health Check Test

{ "type": "HTTP", "name": "API Health Check", "description": "Check if main API is responding", "targetUrl": "https://api.example.com/health", "frequency": "EVERY_5_MINUTES", "priority": "HIGH", "method": "GET", "expectedStatusCode": 200, "expectedResponsePattern": "\"status\":\"UP\"", "maxResponseTimeMs": 1000, "headers": { "User-Agent": "Synthetic-Monitoring/1.0" } }

Browser User Journey Test

{ "type": "BROWSER", "name": "User Login Flow", "description": "Test complete user login journey", "targetUrl": "https://app.example.com/login", "frequency": "HOURLY", "priority": "HIGH", "script": "waitForElement('#username'); waitForElement('#password'); click('#login-btn'); waitForElement('.dashboard'); assertText('.welcome-message', 'Welcome');", "maxLoadTimeMs": 10000, "takeScreenshotOnFailure": true }

Benefits and Use Cases

Key Benefits:

  1. Proactive Monitoring - Detect issues before users are affected
  2. Performance Baselines - Establish and monitor performance benchmarks
  3. Business Transaction Monitoring - Ensure critical flows work end-to-end
  4. Geographic Monitoring - Test from different locations (extendable)
  5. Trend Analysis - Track performance degradation over time

Common Use Cases:

  • API Health Checks - Verify microservices are responsive
  • User Journey Validation - Ensure critical business flows work
  • Third-party Integration Checks - Monitor external dependencies
  • Performance Regression Detection - Catch performance degradation early
  • SLA Monitoring - Ensure compliance with service level agreements

Conclusion

This comprehensive synthetic monitoring solution provides:

  • Flexible test definitions for HTTP, browser, and custom validations
  • Scalable execution with async processing and thread pooling
  • Comprehensive alerting with Slack and PagerDuty integration
  • Performance metrics with Micrometer and Prometheus
  • Persistent storage for historical analysis and trending
  • REST API for test management and execution

By implementing this synthetic monitoring framework, you can proactively detect issues, ensure application reliability, and maintain optimal user experience across your digital services.

Leave a Reply

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


Macro Nepal Helper