google-site-verification: google61fe8ba583a51912.html
Synthetic Monitoring in Java: Comprehensive Guide to Proactive Application Monitoring

Synthetic monitoring involves simulating user interactions and transactions to proactively monitor application health, performance, and functionality from external perspectives.

Project Setup and Dependencies

1. Maven Dependencies
<dependencies> <!-- Web Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>3.1.0</version> </dependency> <!-- Selenium for Browser Automation --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.15.0</version> </dependency> <!-- Selenium Grid --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-grid</artifactId> <version>4.15.0</version> </dependency> <!-- Metrics and Monitoring --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.11.5</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.11.5</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Scheduling --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> <version>3.1.0</version> </dependency> <!-- Database --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Email Notifications --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>3.1.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>3.1.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.35.0</version> <scope>test</scope> </dependency> </dependencies>

Core Monitoring Models and Configuration

1. Monitoring Configuration
// MonitorConfig.java @Data @Configuration @ConfigurationProperties(prefix = "synthetic.monitoring") public class MonitorConfig { private boolean enabled = true; private int defaultTimeoutSeconds = 30; private int maxConcurrentMonitors = 10; private String defaultUserAgent = "SyntheticMonitor/1.0"; private List<String> defaultRegions = List.of("us-east-1", "eu-west-1", "ap-southeast-1"); private AlertConfig alert = new AlertConfig(); private RetentionConfig retention = new RetentionConfig(); private BrowserConfig browser = new BrowserConfig(); @Data public static class AlertConfig { private boolean enabled = true; private int failureThreshold = 3; private int recoveryThreshold = 2; private List<String> notificationChannels = List.of("email", "slack"); private String pagerDutyServiceKey; } @Data public static class RetentionConfig { private int resultsDays = 30; private int metricsDays = 90; private int tracesDays = 7; } @Data public static class BrowserConfig { private boolean headless = true; private int pageLoadTimeoutSeconds = 30; private int scriptTimeoutSeconds = 10; private String chromeDriverPath; private List<String> browserArgs = List.of("--no-sandbox", "--disable-dev-shm-usage"); } } // MonitorType.java public enum MonitorType { HTTP_ENDPOINT("HTTP Endpoint Check"), BROWSER_TRANSACTION("Browser Transaction"), API_SEQUENCE("API Sequence"), REAL_BROWSER("Real Browser Check"), MULTI_STEP("Multi-step Transaction"), WEBSOCKET("WebSocket Check"), DNS("DNS Resolution"), SSL_CERTIFICATE("SSL Certificate Check"); private final String description; MonitorType(String description) { this.description = description; } public String getDescription() { return description; } } // MonitorFrequency.java public enum MonitorFrequency { ONE_MINUTE(60), FIVE_MINUTES(300), FIFTEEN_MINUTES(900), THIRTY_MINUTES(1800), ONE_HOUR(3600); private final int seconds; MonitorFrequency(int seconds) { this.seconds = seconds; } public int getSeconds() { return seconds; } public Duration getDuration() { return Duration.ofSeconds(seconds); } }
2. Monitor Definition Models
// SyntheticMonitor.java @Entity @Table(name = "synthetic_monitors") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SyntheticMonitor { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank private String name; @NotBlank private String description; @Enumerated(EnumType.STRING) private MonitorType type; @Enumerated(EnumType.STRING) private MonitorFrequency frequency; private boolean enabled = true; @ElementCollection private List<String> regions; @Column(columnDefinition = "TEXT") private String configuration; @CreationTimestamp private ZonedDateTime createdAt; @UpdateTimestamp private ZonedDateTime updatedAt; private String createdBy; private String updatedBy; @OneToMany(mappedBy = "monitor", cascade = CascadeType.ALL) private List<MonitorExecution> executions; @ElementCollection private Map<String, String> tags = new HashMap<>(); } // MonitorConfiguration.java @Data @Builder @NoArgsConstructor @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class MonitorConfiguration { // HTTP Configuration private String url; private HttpMethod method; private Map<String, String> headers; private String body; private Integer expectedStatus; private String expectedBodyPattern; private Integer timeoutSeconds; // Browser Configuration private List<BrowserStep> browserSteps; private String browserScript; private Boolean takeScreenshot; private Boolean captureHar; // Validation Rules private List<ValidationRule> validationRules; private List<Assertion> assertions; // Multi-step configuration private List<MonitorStep> steps; // SSL Configuration private Integer sslExpiryWarningDays; @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class BrowserStep { private String action; // click, type, wait, navigate, etc. private String selector; private String value; private Integer timeoutSeconds; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class ValidationRule { private String type; // status_code, response_time, body_content, header private String field; private String operator; // equals, contains, matches, gt, lt private String expectedValue; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class Assertion { private String name; private String condition; private String expected; private Severity severity; public enum Severity { CRITICAL, HIGH, MEDIUM, LOW } } @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class MonitorStep { private String name; private String url; private HttpMethod method; private Map<String, String> headers; private String body; private List<ValidationRule> validations; private Map<String, String> extractors; // Extract values for subsequent steps } }
3. Execution Results and Metrics
// MonitorExecution.java @Entity @Table(name = "monitor_executions") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class MonitorExecution { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "monitor_id") private SyntheticMonitor monitor; private String region; private String workerId; @Enumerated(EnumType.STRING) private ExecutionStatus status; private ZonedDateTime startedAt; private ZonedDateTime completedAt; private Long responseTimeMs; private Integer statusCode; @Column(columnDefinition = "TEXT") private String responseBody; @Column(columnDefinition = "TEXT") private String errorMessage; @Column(columnDefinition = "TEXT") private String stackTrace; @ElementCollection private Map<String, String> metrics = new HashMap<>(); @Column(columnDefinition = "TEXT") private String validationResults; private String screenshotPath; private String harFilePath; @ElementCollection private Map<String, String> extractedValues = new HashMap<>(); public Duration getDuration() { if (startedAt != null && completedAt != null) { return Duration.between(startedAt, completedAt); } return Duration.ZERO; } public boolean isSuccessful() { return status == ExecutionStatus.SUCCESS; } } // ExecutionStatus.java public enum ExecutionStatus { SUCCESS, FAILED, TIMEOUT, VALIDATION_FAILED, NETWORK_ERROR, SSL_ERROR, BROWSER_ERROR } // MonitorMetrics.java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class MonitorMetrics { private String monitorId; private String region; private ZonedDateTime timestamp; // Response Time Metrics private Double responseTimeP50; private Double responseTimeP95; private Double responseTimeP99; private Double responseTimeMax; // Availability Metrics private Double availability; private Integer totalExecutions; private Integer successfulExecutions; private Integer failedExecutions; // Business Metrics private Map<String, Double> customMetrics = new HashMap<>(); // Error Breakdown private Map<ExecutionStatus, Integer> errorBreakdown = new HashMap<>(); }

Core Monitoring Engine

1. Base Monitor Interface and Factory
// MonitorExecutor.java public interface MonitorExecutor { MonitorExecution execute(SyntheticMonitor monitor); boolean supports(MonitorType monitorType); } // MonitorExecutorFactory.java @Component @Slf4j public class MonitorExecutorFactory { private final Map<MonitorType, MonitorExecutor> executors; @Autowired public MonitorExecutorFactory(List<MonitorExecutor> executorList) { this.executors = executorList.stream() .collect(Collectors.toMap( executor -> executor.getClass().getAnnotation(MonitorExecutorType.class).value(), Function.identity() )); } public MonitorExecutor getExecutor(MonitorType monitorType) { MonitorExecutor executor = executors.get(monitorType); if (executor == null) { throw new IllegalArgumentException("No executor found for monitor type: " + monitorType); } return executor; } } // MonitorExecutorType.java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MonitorExecutorType { MonitorType value(); }
2. HTTP Endpoint Monitor
// HttpEndpointExecutor.java @Component @MonitorExecutorType(MonitorType.HTTP_ENDPOINT) @Slf4j public class HttpEndpointExecutor implements MonitorExecutor { private final WebClient webClient; private final ObjectMapper objectMapper; public HttpEndpointExecutor(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) { this.webClient = webClientBuilder.build(); this.objectMapper = objectMapper; } @Override public MonitorExecution execute(SyntheticMonitor monitor) { MonitorExecution execution = MonitorExecution.builder() .monitor(monitor) .region("default") .startedAt(ZonedDateTime.now()) .status(ExecutionStatus.FAILED) .build(); try { MonitorConfiguration config = parseConfiguration(monitor); validateConfiguration(config); WebClient.RequestBodySpec request = buildRequest(config); long startTime = System.currentTimeMillis(); String responseBody = request .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(config.getTimeoutSeconds() != null ? config.getTimeoutSeconds() : 30)) .block(); long responseTime = System.currentTimeMillis() - startTime; execution.setResponseTimeMs(responseTime); execution.setResponseBody(truncateResponseBody(responseBody)); execution.setStatusCode(200); // Assuming success for now // Validate response ValidationResult validationResult = validateResponse(config, responseBody, responseTime); execution.setValidationResults(toJson(validationResult)); if (validationResult.isSuccess()) { execution.setStatus(ExecutionStatus.SUCCESS); execution.getMetrics().put("response_time_ms", String.valueOf(responseTime)); execution.getMetrics().put("content_size_bytes", String.valueOf(responseBody != null ? responseBody.length() : 0)); } else { execution.setStatus(ExecutionStatus.VALIDATION_FAILED); execution.setErrorMessage("Validation failed: " + validationResult.getFailures()); } } catch (Exception e) { log.error("HTTP monitor execution failed for {}: {}", monitor.getName(), e.getMessage(), e); execution.setErrorMessage(e.getMessage()); execution.setStackTrace(getStackTrace(e)); execution.setStatus(determineErrorStatus(e)); } finally { execution.setCompletedAt(ZonedDateTime.now()); } return execution; } @Override public boolean supports(MonitorType monitorType) { return monitorType == MonitorType.HTTP_ENDPOINT; } private WebClient.RequestBodySpec buildRequest(MonitorConfiguration config) { WebClient.RequestBodySpec request = webClient .method(config.getMethod()) .uri(config.getUrl()); // Add headers if (config.getHeaders() != null) { config.getHeaders().forEach(request::header); } // Add body for POST, PUT, PATCH if (config.getBody() != null && (config.getMethod() == HttpMethod.POST || config.getMethod() == HttpMethod.PUT || config.getMethod() == HttpMethod.PATCH)) { request.bodyValue(config.getBody()); } return request; } private ValidationResult validateResponse(MonitorConfiguration config, String responseBody, long responseTime) { ValidationResult result = new ValidationResult(); // Validate status code if (config.getExpectedStatus() != null) { // In real implementation, we would have the actual status code // This is simplified for demonstration result.addCheck("status_code", config.getExpectedStatus() == 200, "Expected status " + config.getExpectedStatus()); } // Validate response body pattern if (config.getExpectedBodyPattern() != null && responseBody != null) { boolean matches = responseBody.contains(config.getExpectedBodyPattern()); result.addCheck("body_content", matches, "Body should contain: " + config.getExpectedBodyPattern()); } // Validate response time if (config.getValidationRules() != null) { for (MonitorConfiguration.ValidationRule rule : config.getValidationRules()) { if ("response_time".equals(rule.getType())) { boolean passed = validateResponseTime(rule, responseTime); result.addCheck("response_time_" + rule.getOperator(), passed, rule.getField() + " " + rule.getOperator() + " " + rule.getExpectedValue()); } } } return result; } private boolean validateResponseTime(MonitorConfiguration.ValidationRule rule, long responseTime) { long expected = Long.parseLong(rule.getExpectedValue()); return switch (rule.getOperator()) { case "lt" -> responseTime < expected; case "lte" -> responseTime <= expected; case "gt" -> responseTime > expected; case "gte" -> responseTime >= expected; default -> false; }; } private ExecutionStatus determineErrorStatus(Exception e) { if (e instanceof TimeoutException) { return ExecutionStatus.TIMEOUT; } else if (e instanceof WebClientResponseException) { return ExecutionStatus.FAILED; } else if (e instanceof SSLException) { return ExecutionStatus.SSL_ERROR; } else { return ExecutionStatus.FAILED; } } private MonitorConfiguration parseConfiguration(SyntheticMonitor monitor) throws Exception { return objectMapper.readValue(monitor.getConfiguration(), MonitorConfiguration.class); } private void validateConfiguration(MonitorConfiguration config) { if (config.getUrl() == null || config.getUrl().isBlank()) { throw new IllegalArgumentException("URL is required for HTTP endpoint monitor"); } if (config.getMethod() == null) { config.setMethod(HttpMethod.GET); } } private String truncateResponseBody(String responseBody) { if (responseBody != null && responseBody.length() > 10000) { return responseBody.substring(0, 10000) + "... [TRUNCATED]"; } return responseBody; } private String toJson(Object obj) throws Exception { return objectMapper.writeValueAsString(obj); } private String getStackTrace(Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); return sw.toString(); } } // ValidationResult.java @Data public class ValidationResult { private boolean success = true; private List<ValidationCheck> checks = new ArrayList<>(); private List<String> failures = new ArrayList<>(); public void addCheck(String name, boolean passed, String description) { ValidationCheck check = new ValidationCheck(name, passed, description); checks.add(check); if (!passed) { success = false; failures.add(description); } } @Data @AllArgsConstructor public static class ValidationCheck { private String name; private boolean passed; private String description; } }
3. Browser Transaction Monitor
// BrowserTransactionExecutor.java @Component @MonitorExecutorType(MonitorType.BROWSER_TRANSACTION) @Slf4j public class BrowserTransactionExecutor implements MonitorExecutor { private final WebDriverManager webDriverManager; private final ObjectMapper objectMapper; public BrowserTransactionExecutor(WebDriverManager webDriverManager, ObjectMapper objectMapper) { this.webDriverManager = webDriverManager; this.objectMapper = objectMapper; } @Override public MonitorExecution execute(SyntheticMonitor monitor) { MonitorExecution execution = MonitorExecution.builder() .monitor(monitor) .region("default") .startedAt(ZonedDateTime.now()) .status(ExecutionStatus.FAILED) .build(); WebDriver driver = null; try { MonitorConfiguration config = parseConfiguration(monitor); validateConfiguration(config); driver = webDriverManager.createDriver(); configureDriver(driver, config); long startTime = System.currentTimeMillis(); // Execute browser steps executeBrowserSteps(driver, config, execution); long totalTime = System.currentTimeMillis() - startTime; execution.setResponseTimeMs(totalTime); // Validate final state ValidationResult validationResult = validateBrowserState(driver, config); execution.setValidationResults(toJson(validationResult)); if (validationResult.isSuccess()) { execution.setStatus(ExecutionStatus.SUCCESS); } else { execution.setStatus(ExecutionStatus.VALIDATION_FAILED); execution.setErrorMessage("Browser validation failed"); } // Capture screenshot if configured if (config.getTakeScreenshot() != null && config.getTakeScreenshot()) { captureScreenshot(driver, execution); } } catch (Exception e) { log.error("Browser transaction execution failed for {}: {}", monitor.getName(), e.getMessage(), e); execution.setErrorMessage(e.getMessage()); execution.setStackTrace(getStackTrace(e)); execution.setStatus(ExecutionStatus.BROWSER_ERROR); } finally { if (driver != null) { driver.quit(); } execution.setCompletedAt(ZonedDateTime.now()); } return execution; } @Override public boolean supports(MonitorType monitorType) { return monitorType == MonitorType.BROWSER_TRANSACTION; } private void executeBrowserSteps(WebDriver driver, MonitorConfiguration config, MonitorExecution execution) { if (config.getBrowserSteps() == null) { return; } for (int i = 0; i < config.getBrowserSteps().size(); i++) { MonitorConfiguration.BrowserStep step = config.getBrowserSteps().get(i); executeBrowserStep(driver, step, i, execution); } } private void executeBrowserStep(WebDriver driver, MonitorConfiguration.BrowserStep step, int stepIndex, MonitorExecution execution) { try { switch (step.getAction()) { case "navigate": driver.get(step.getValue()); break; case "click": WebElement clickElement = driver.findElement(By.cssSelector(step.getSelector())); clickElement.click(); break; case "type": WebElement typeElement = driver.findElement(By.cssSelector(step.getSelector())); typeElement.clear(); typeElement.sendKeys(step.getValue()); break; case "wait": Thread.sleep(Long.parseLong(step.getValue())); break; case "wait_for_element": WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(step.getTimeoutSeconds() != null ? step.getTimeoutSeconds() : 10)); wait.until(ExpectedConditions.presenceOfElementLocated( By.cssSelector(step.getSelector()))); break; default: log.warn("Unknown browser action: {}", step.getAction()); } // Record step success execution.getMetrics().put("step_" + stepIndex + "_success", "true"); } catch (Exception e) { log.error("Browser step {} failed: {}", stepIndex, e.getMessage()); execution.getMetrics().put("step_" + stepIndex + "_success", "false"); execution.getMetrics().put("step_" + stepIndex + "_error", e.getMessage()); throw new RuntimeException("Step " + stepIndex + " failed: " + e.getMessage(), e); } } private ValidationResult validateBrowserState(WebDriver driver, MonitorConfiguration config) { ValidationResult result = new ValidationResult(); if (config.getValidationRules() != null) { for (MonitorConfiguration.ValidationRule rule : config.getValidationRules()) { try { boolean passed = validateBrowserRule(driver, rule); result.addCheck(rule.getType(), passed, rule.getField() + " " + rule.getOperator() + " " + rule.getExpectedValue()); } catch (Exception e) { result.addCheck(rule.getType(), false, "Validation error: " + e.getMessage()); } } } return result; } private boolean validateBrowserRule(WebDriver driver, MonitorConfiguration.ValidationRule rule) { return switch (rule.getType()) { case "url_contains" -> driver.getCurrentUrl().contains(rule.getExpectedValue()); case "title_equals" -> driver.getTitle().equals(rule.getExpectedValue()); case "element_present" -> !driver.findElements(By.cssSelector(rule.getField())).isEmpty(); case "element_text" -> { WebElement element = driver.findElement(By.cssSelector(rule.getField())); yield element.getText().contains(rule.getExpectedValue()); } default -> false; }; } private void captureScreenshot(WebDriver driver, MonitorExecution execution) { try { File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); String screenshotPath = "screenshots/" + execution.getId() + ".png"; // Save screenshot to storage execution.setScreenshotPath(screenshotPath); } catch (Exception e) { log.warn("Failed to capture screenshot: {}", e.getMessage()); } } private void configureDriver(WebDriver driver, MonitorConfiguration config) { driver.manage().timeouts().pageLoadTimeout( Duration.ofSeconds(config.getTimeoutSeconds() != null ? config.getTimeoutSeconds() : 30)); driver.manage().timeouts().implicitlyWait( Duration.ofSeconds(10)); driver.manage().window().maximize(); } private MonitorConfiguration parseConfiguration(SyntheticMonitor monitor) throws Exception { return objectMapper.readValue(monitor.getConfiguration(), MonitorConfiguration.class); } private void validateConfiguration(MonitorConfiguration config) { if (config.getBrowserSteps() == null || config.getBrowserSteps().isEmpty()) { throw new IllegalArgumentException("Browser steps are required for browser transaction monitor"); } } private String toJson(Object obj) throws Exception { return objectMapper.writeValueAsString(obj); } private String getStackTrace(Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); return sw.toString(); } } // WebDriverManager.java @Component @Slf4j public class WebDriverManager { @Value("${synthetic.monitoring.browser.headless:true}") private boolean headless; @Value("${synthetic.monitoring.browser.chrome-driver-path:}") private String chromeDriverPath; public WebDriver createDriver() { ChromeOptions options = new ChromeOptions(); if (headless) { options.addArguments("--headless"); } options.addArguments("--no-sandbox"); options.addArguments("--disable-dev-shm-usage"); options.addArguments("--disable-gpu"); options.addArguments("--window-size=1920,1080"); // Set ChromeDriver path if provided if (!chromeDriverPath.isBlank()) { System.setProperty("webdriver.chrome.driver", chromeDriverPath); } try { return new ChromeDriver(options); } catch (Exception e) { log.error("Failed to create ChromeDriver: {}", e.getMessage(), e); throw new RuntimeException("Failed to create browser driver", e); } } }

Scheduling and Execution Engine

1. Monitor Scheduler
// MonitorScheduler.java @Component @Slf4j public class MonitorScheduler { private final SyntheticMonitorRepository monitorRepository; private final MonitorExecutorFactory executorFactory; private final MonitorExecutionService executionService; private final AlertService alertService; private final TaskScheduler taskScheduler; private final Map<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>(); public MonitorScheduler(SyntheticMonitorRepository monitorRepository, MonitorExecutorFactory executorFactory, MonitorExecutionService executionService, AlertService alertService, TaskScheduler taskScheduler) { this.monitorRepository = monitorRepository; this.executorFactory = executorFactory; this.executionService = executionService; this.alertService = alertService; this.taskScheduler = taskScheduler; } @PostConstruct public void initializeSchedules() { log.info("Initializing monitor schedules..."); List<SyntheticMonitor> enabledMonitors = monitorRepository.findByEnabledTrue(); for (SyntheticMonitor monitor : enabledMonitors) { scheduleMonitor(monitor); } log.info("Scheduled {} monitors", enabledMonitors.size()); } public void scheduleMonitor(SyntheticMonitor monitor) { if (!monitor.isEnabled()) { unscheduleMonitor(monitor.getId()); return; } // Unschedule existing task unscheduleMonitor(monitor.getId()); // Create new task Runnable monitorTask = createMonitorTask(monitor); Trigger trigger = createTrigger(monitor.getFrequency()); ScheduledFuture<?> future = taskScheduler.schedule(monitorTask, trigger); scheduledTasks.put(monitor.getId(), future); log.info("Scheduled monitor '{}' with frequency {}", monitor.getName(), monitor.getFrequency()); } public void unscheduleMonitor(Long monitorId) { ScheduledFuture<?> future = scheduledTasks.remove(monitorId); if (future != null) { future.cancel(false); log.info("Unscheduled monitor ID: {}", monitorId); } } public void executeMonitorImmediately(Long monitorId) { SyntheticMonitor monitor = monitorRepository.findById(monitorId) .orElseThrow(() -> new IllegalArgumentException("Monitor not found: " + monitorId)); Runnable task = createMonitorTask(monitor); task.run(); } private Runnable createMonitorTask(SyntheticMonitor monitor) { return () -> { log.debug("Executing monitor: {}", monitor.getName()); try { MonitorExecutor executor = executorFactory.getExecutor(monitor.getType()); MonitorExecution execution = executor.execute(monitor); // Save execution result executionService.saveExecution(execution); // Check for alerts alertService.checkAndTriggerAlerts(monitor, execution); } catch (Exception e) { log.error("Failed to execute monitor {}: {}", monitor.getName(), e.getMessage(), e); } }; } private Trigger createTrigger(MonitorFrequency frequency) { return context -> { ZonedDateTime lastExecution = context.lastScheduledExecutionTime(); ZonedDateTime nextExecution = (lastExecution != null ? lastExecution : ZonedDateTime.now()) .plus(frequency.getDuration()); return nextExecution.toInstant(); }; } public Map<Long, String> getScheduledMonitors() { return scheduledTasks.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().isDone() ? "COMPLETED" : "SCHEDULED" )); } }
2. Execution Service
// MonitorExecutionService.java @Service @Slf4j public class MonitorExecutionService { private final MonitorExecutionRepository executionRepository; private final MetricsService metricsService; public MonitorExecutionService(MonitorExecutionRepository executionRepository, MetricsService metricsService) { this.executionRepository = executionRepository; this.metricsService = metricsService; } @Transactional public MonitorExecution saveExecution(MonitorExecution execution) { MonitorExecution saved = executionRepository.save(execution); // Update metrics metricsService.recordExecutionMetrics(saved); log.debug("Saved execution for monitor {}: status={}", execution.getMonitor().getName(), execution.getStatus()); return saved; } public List<MonitorExecution> getRecentExecutions(Long monitorId, int limit) { return executionRepository.findByMonitorIdOrderByStartedAtDesc(monitorId, PageRequest.of(0, limit)); } public MonitorMetrics getMonitorMetrics(Long monitorId, String region, ZonedDateTime from, ZonedDateTime to) { List<MonitorExecution> executions = executionRepository .findByMonitorIdAndRegionAndStartedAtBetween(monitorId, region, from, to); return calculateMetrics(executions); } public AvailabilityReport getAvailabilityReport(Long monitorId, ZonedDateTime from, ZonedDateTime to) { List<MonitorExecution> executions = executionRepository .findByMonitorIdAndStartedAtBetween(monitorId, from, to); return generateAvailabilityReport(executions); } private MonitorMetrics calculateMetrics(List<MonitorExecution> executions) { if (executions.isEmpty()) { return MonitorMetrics.builder() .availability(0.0) .totalExecutions(0) .successfulExecutions(0) .failedExecutions(0) .build(); } List<Long> responseTimes = executions.stream() .filter(e -> e.getResponseTimeMs() != null) .map(MonitorExecution::getResponseTimeMs) .collect(Collectors.toList()); long successful = executions.stream() .filter(MonitorExecution::isSuccessful) .count(); double availability = (double) successful / executions.size() * 100; // Calculate percentiles Collections.sort(responseTimes); double p50 = calculatePercentile(responseTimes, 50); double p95 = calculatePercentile(responseTimes, 95); double p99 = calculatePercentile(responseTimes, 99); // Error breakdown Map<ExecutionStatus, Integer> errorBreakdown = executions.stream() .collect(Collectors.groupingBy( MonitorExecution::getStatus, Collectors.summingInt(e -> 1) )); return MonitorMetrics.builder() .responseTimeP50(p50) .responseTimeP95(p95) .responseTimeP99(p99) .responseTimeMax(responseTimes.isEmpty() ? 0.0 : (double) responseTimes.get(responseTimes.size() - 1)) .availability(availability) .totalExecutions(executions.size()) .successfulExecutions((int) successful) .failedExecutions(executions.size() - (int) successful) .errorBreakdown(errorBreakdown) .build(); } private double calculatePercentile(List<Long> values, double percentile) { if (values.isEmpty()) return 0.0; int index = (int) Math.ceil(percentile / 100.0 * values.size()); return values.get(Math.min(index - 1, values.size() - 1)); } private AvailabilityReport generateAvailabilityReport(List<MonitorExecution> executions) { // Implementation for detailed availability report return new AvailabilityReport(); } }

Alerting and Notification System

1. Alert Service
// AlertService.java @Service @Slf4j public class AlertService { private final MonitorExecutionRepository executionRepository; private final NotificationService notificationService; private final AlertRuleRepository alertRuleRepository; private final Map<Long, AlertState> alertStates = new ConcurrentHashMap<>(); public AlertService(MonitorExecutionRepository executionRepository, NotificationService notificationService, AlertRuleRepository alertRuleRepository) { this.executionRepository = executionRepository; this.notificationService = notificationService; this.alertRuleRepository = alertRuleRepository; } public void checkAndTriggerAlerts(SyntheticMonitor monitor, MonitorExecution execution) { List<AlertRule> rules = alertRuleRepository.findByMonitorId(monitor.getId()); for (AlertRule rule : rules) { if (shouldTriggerAlert(rule, execution)) { triggerAlert(rule, execution); } } } private boolean shouldTriggerAlert(AlertRule rule, MonitorExecution execution) { AlertState state = alertStates.computeIfAbsent(rule.getId(), id -> new AlertState(rule.getFailureThreshold(), rule.getRecoveryThreshold())); boolean isFailure = !execution.isSuccessful(); state.recordResult(isFailure); return state.shouldTriggerAlert(); } private void triggerAlert(AlertRule rule, MonitorExecution execution) { Alert alert = Alert.builder() .rule(rule) .monitor(execution.getMonitor()) .execution(execution) .triggeredAt(ZonedDateTime.now()) .severity(rule.getSeverity()) .message(buildAlertMessage(rule, execution)) .build(); // Send notifications notificationService.sendAlert(alert); // Update alert state AlertState state = alertStates.get(rule.getId()); state.markAlertTriggered(); log.warn("Alert triggered for monitor {}: {}", execution.getMonitor().getName(), alert.getMessage()); } private String buildAlertMessage(AlertRule rule, MonitorExecution execution) { return String.format( "Alert: %s - Monitor '%s' failed with status %s. Error: %s", rule.getName(), execution.getMonitor().getName(), execution.getStatus(), execution.getErrorMessage() ); } } // AlertState.java @Data public class AlertState { private final int failureThreshold; private final int recoveryThreshold; private final Queue<Boolean> recentResults = new LinkedList<>(); private boolean alertActive = false; private int consecutiveSuccesses = 0; public AlertState(int failureThreshold, int recoveryThreshold) { this.failureThreshold = failureThreshold; this.recoveryThreshold = recoveryThreshold; } public void recordResult(boolean isFailure) { recentResults.offer(isFailure); // Keep only the last N results while (recentResults.size() > failureThreshold) { recentResults.poll(); } if (isFailure) { consecutiveSuccesses = 0; } else { consecutiveSuccesses++; } } public boolean shouldTriggerAlert() { if (alertActive) { // Check for recovery if (consecutiveSuccesses >= recoveryThreshold) { alertActive = false; // Trigger recovery notification return true; // For recovery notification } return false; } else { // Check for failure long failureCount = recentResults.stream().filter(f -> f).count(); if (failureCount >= failureThreshold) { alertActive = true; return true; } return false; } } public void markAlertTriggered() { // Reset state after triggering alert recentResults.clear(); } } // Alert.java @Entity @Table(name = "alerts") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Alert { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "rule_id") private AlertRule rule; @ManyToOne @JoinColumn(name = "monitor_id") private SyntheticMonitor monitor; @ManyToOne @JoinColumn(name = "execution_id") private MonitorExecution execution; private ZonedDateTime triggeredAt; private ZonedDateTime resolvedAt; @Enumerated(EnumType.STRING) private AlertSeverity severity; private String message; private boolean acknowledged = false; public enum AlertSeverity { CRITICAL, HIGH, MEDIUM, LOW, INFO } } // AlertRule.java @Entity @Table(name = "alert_rules") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class AlertRule { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "monitor_id") private SyntheticMonitor monitor; private String name; private String description; @Enumerated(EnumType.STRING) private Alert.AlertSeverity severity; private int failureThreshold = 3; private int recoveryThreshold = 2; private boolean enabled = true; @ElementCollection private List<String> notificationChannels = List.of("email", "slack"); }
2. Notification Service
// NotificationService.java @Service @Slf4j public class NotificationService { private final EmailService emailService; private final SlackService slackService; private final PagerDutyService pagerDutyService; public NotificationService(EmailService emailService, SlackService slackService, PagerDutyService pagerDutyService) { this.emailService = emailService; this.slackService = slackService; this.pagerDutyService = pagerDutyService; } public void sendAlert(Alert alert) { for (String channel : alert.getRule().getNotificationChannels()) { try { switch (channel.toLowerCase()) { case "email": sendEmailAlert(alert); break; case "slack": sendSlackAlert(alert); break; case "pagerduty": sendPagerDutyAlert(alert); break; default: log.warn("Unknown notification channel: {}", channel); } } catch (Exception e) { log.error("Failed to send alert via {}: {}", channel, e.getMessage(), e); } } } private void sendEmailAlert(Alert alert) { String subject = String.format("[%s] Alert: %s", alert.getSeverity(), alert.getMonitor().getName()); String body = buildEmailBody(alert); emailService.sendEmail( Arrays.asList("[email protected]", "[email protected]"), subject, body ); } private void sendSlackAlert(Alert alert) { String message = buildSlackMessage(alert); slackService.sendMessage("#alerts", message); } private void sendPagerDutyAlert(Alert alert) { if (alert.getSeverity() == Alert.AlertSeverity.CRITICAL) { pagerDutyService.triggerIncident( alert.getMonitor().getName() + " - " + alert.getMessage(), "Synthetic Monitor", IncidentSeverity.CRITICAL ); } } private String buildEmailBody(Alert alert) { return String.format(""" Alert Details: ============= Monitor: %s Severity: %s Triggered: %s Error: %s Execution ID: %s Monitor Configuration: ==================== Type: %s Frequency: %s URL: %s Recent Executions: ================= Check the monitoring dashboard for detailed execution history. This is an automated alert from Synthetic Monitoring System. """, alert.getMonitor().getName(), alert.getSeverity(), alert.getTriggeredAt(), alert.getMessage(), alert.getExecution().getId(), alert.getMonitor().getType(), alert.getMonitor().getFrequency(), extractUrlFromConfig(alert.getMonitor()) ); } private String buildSlackMessage(Alert alert) { return String.format(""" :warning: *%s Alert: %s* > %s _Triggered: %s_ <https://monitoring.company.com/alerts/%s|View Details> """, alert.getSeverity(), alert.getMonitor().getName(), alert.getMessage(), alert.getTriggeredAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), alert.getId() ); } private String extractUrlFromConfig(SyntheticMonitor monitor) { try { ObjectMapper mapper = new ObjectMapper(); JsonNode config = mapper.readTree(monitor.getConfiguration()); return config.path("url").asText("N/A"); } catch (Exception e) { return "N/A"; } } }

REST API Controllers

1. Monitor Management API
// MonitorController.java @RestController @RequestMapping("/api/v1/monitors") @Slf4j @Validated public class MonitorController { private final SyntheticMonitorRepository monitorRepository; private final MonitorScheduler scheduler; private final MonitorExecutionService executionService; public MonitorController(SyntheticMonitorRepository monitorRepository, MonitorScheduler scheduler, MonitorExecutionService executionService) { this.monitorRepository = monitorRepository; this.scheduler = scheduler; this.executionService = executionService; } @GetMapping public ResponseEntity<List<SyntheticMonitor>> listMonitors( @RequestParam(required = false) Boolean enabled) { List<SyntheticMonitor> monitors; if (enabled != null) { monitors = monitorRepository.findByEnabled(enabled); } else { monitors = monitorRepository.findAll(); } return ResponseEntity.ok(monitors); } @GetMapping("/{id}") public ResponseEntity<SyntheticMonitor> getMonitor(@PathVariable Long id) { return monitorRepository.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<SyntheticMonitor> createMonitor( @Valid @RequestBody SyntheticMonitor monitor) { SyntheticMonitor saved = monitorRepository.save(monitor); // Schedule the monitor scheduler.scheduleMonitor(saved); return ResponseEntity.status(HttpStatus.CREATED).body(saved); } @PutMapping("/{id}") public ResponseEntity<SyntheticMonitor> updateMonitor( @PathVariable Long id, @Valid @RequestBody SyntheticMonitor monitor) { if (!monitorRepository.existsById(id)) { return ResponseEntity.notFound().build(); } monitor.setId(id); SyntheticMonitor saved = monitorRepository.save(monitor); // Reschedule the monitor scheduler.scheduleMonitor(saved); return ResponseEntity.ok(saved); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteMonitor(@PathVariable Long id) { if (!monitorRepository.existsById(id)) { return ResponseEntity.notFound().build(); } // Unschedule first scheduler.unscheduleMonitor(id); monitorRepository.deleteById(id); return ResponseEntity.noContent().build(); } @PostMapping("/{id}/execute") public ResponseEntity<MonitorExecution> executeMonitor(@PathVariable Long id) { try { scheduler.executeMonitorImmediately(id); return ResponseEntity.accepted().build(); } catch (Exception e) { return ResponseEntity.badRequest().build(); } } @GetMapping("/{id}/executions") public ResponseEntity<List<MonitorExecution>> getMonitorExecutions( @PathVariable Long id, @RequestParam(defaultValue = "10") int limit) { List<MonitorExecution> executions = executionService.getRecentExecutions(id, limit); return ResponseEntity.ok(executions); } @GetMapping("/{id}/metrics") public ResponseEntity<MonitorMetrics> getMonitorMetrics( @PathVariable Long id, @RequestParam String region, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) ZonedDateTime from, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) ZonedDateTime to) { MonitorMetrics metrics = executionService.getMonitorMetrics(id, region, from, to); return ResponseEntity.ok(metrics); } }
2. Dashboard and Metrics API
// DashboardController.java @RestController @RequestMapping("/api/v1/dashboard") @Slf4j public class DashboardController { private final SyntheticMonitorRepository monitorRepository; private final MonitorExecutionService executionService; private final MetricsService metricsService; public DashboardController(SyntheticMonitorRepository monitorRepository, MonitorExecutionService executionService, MetricsService metricsService) { this.monitorRepository = monitorRepository; this.executionService = executionService; this.metricsService = metricsService; } @GetMapping("/summary") public ResponseEntity<DashboardSummary> getDashboardSummary() { List<SyntheticMonitor> monitors = monitorRepository.findAll(); long totalMonitors = monitors.size(); long enabledMonitors = monitors.stream().filter(SyntheticMonitor::isEnabled).count(); // Calculate overall availability (simplified) double overallAvailability = calculateOverallAvailability(); // Get recent alerts count long activeAlerts = getActiveAlertsCount(); DashboardSummary summary = DashboardSummary.builder() .totalMonitors(totalMonitors) .enabledMonitors(enabledMonitors) .overallAvailability(overallAvailability) .activeAlerts(activeAlerts) .lastUpdated(ZonedDateTime.now()) .build(); return ResponseEntity.ok(summary); } @GetMapping("/monitors/status") public ResponseEntity<List<MonitorStatus>> getMonitorsStatus() { List<SyntheticMonitor> monitors = monitorRepository.findAll(); List<MonitorStatus> statusList = new ArrayList<>(); for (SyntheticMonitor monitor : monitors) { MonitorStatus status = getMonitorStatus(monitor); statusList.add(status); } return ResponseEntity.ok(statusList); } private MonitorStatus getMonitorStatus(SyntheticMonitor monitor) { List<MonitorExecution> recentExecutions = executionService .getRecentExecutions(monitor.getId(), 10); boolean isHealthy = recentExecutions.stream() .allMatch(MonitorExecution::isSuccessful); double recentAvailability = recentExecutions.stream() .filter(MonitorExecution::isSuccessful) .count() / (double) recentExecutions.size() * 100; double avgResponseTime = recentExecutions.stream() .filter(e -> e.getResponseTimeMs() != null) .mapToLong(MonitorExecution::getResponseTimeMs) .average() .orElse(0.0); return MonitorStatus.builder() .monitorId(monitor.getId()) .monitorName(monitor.getName()) .type(monitor.getType()) .enabled(monitor.isEnabled()) .healthy(isHealthy) .availability(recentAvailability) .averageResponseTime(avgResponseTime) .lastExecution(recentExecutions.isEmpty() ? null : recentExecutions.get(0).getStartedAt()) .lastStatus(recentExecutions.isEmpty() ? null : recentExecutions.get(0).getStatus()) .build(); } private double calculateOverallAvailability() { // Simplified implementation List<SyntheticMonitor> monitors = monitorRepository.findAll(); double totalAvailability = 0; int count = 0; for (SyntheticMonitor monitor : monitors) { if (monitor.isEnabled()) { // Get recent availability for each monitor List<MonitorExecution> recentExecutions = executionService .getRecentExecutions(monitor.getId(), 5); if (!recentExecutions.isEmpty()) { double availability = recentExecutions.stream() .filter(MonitorExecution::isSuccessful) .count() / (double) recentExecutions.size() * 100; totalAvailability += availability; count++; } } } return count > 0 ? totalAvailability / count : 100.0; } private long getActiveAlertsCount() { // Implementation would query active alerts from database return 0L; } } // DashboardSummary.java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DashboardSummary { private long totalMonitors; private long enabledMonitors; private double overallAvailability; private long activeAlerts; private ZonedDateTime lastUpdated; } // MonitorStatus.java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class MonitorStatus { private Long monitorId; private String monitorName; private MonitorType type; private boolean enabled; private boolean healthy; private double availability; private double averageResponseTime; private ZonedDateTime lastExecution; private ExecutionStatus lastStatus; }

Configuration

1. Application Properties
# application.yml synthetic: monitoring: enabled: true default-timeout-seconds: 30 max-concurrent-monitors: 10 default-user-agent: "SyntheticMonitor/1.0" default-regions: - us-east-1 - eu-west-1 - ap-southeast-1 alert: enabled: true failure-threshold: 3 recovery-threshold: 2 notification-channels: - email - slack pager-duty-service-key: ${PAGERDUTY_SERVICE_KEY:} retention: results-days: 30 metrics-days: 90 traces-days: 7 browser: headless: true page-load-timeout-seconds: 30 script-timeout-seconds: 10 chrome-driver-path: ${CHROME_DRIVER_PATH:} browser-args: - "--no-sandbox" - "--disable-dev-shm-usage" # Database spring: datasource: url: jdbc:h2:file:./data/synthetic-monitoring driverClassName: org.h2.Driver username: sa password: password jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update show-sql: false h2: console: enabled: true path: /h2-console # Email spring: mail: host: smtp.company.com port: 587 username: ${SMTP_USERNAME:} password: ${SMTP_PASSWORD:} properties: mail: smtp: auth: true starttls: enable: true # Slack slack: webhook-url: ${SLACK_WEBHOOK_URL:} # Logging logging: level: com.company.synthetic: INFO file: name: logs/synthetic-monitoring.log
2. Spring Configuration
// SyntheticMonitoringConfig.java @Configuration @EnableScheduling @EnableAsync @EnableConfigurationProperties(MonitorConfig.class) public class SyntheticMonitoringConfig { @Bean @Primary public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; } @Bean public WebClient webClient(WebClient.Builder builder) { return builder .defaultHeader("User-Agent", "SyntheticMonitor/1.0") .build(); } @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(10); scheduler.setThreadNamePrefix("synthetic-monitor-"); scheduler.setWaitForTasksToCompleteOnShutdown(true); scheduler.setAwaitTerminationSeconds(30); return scheduler; } @Bean public MeterRegistry meterRegistry() { return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); } }

This comprehensive synthetic monitoring system provides:

  • Multiple monitor types (HTTP, Browser, API sequences, etc.)
  • Flexible scheduling with configurable frequencies
  • Comprehensive alerting with threshold-based triggers
  • Multi-channel notifications (Email, Slack, PagerDuty)
  • Detailed metrics and reporting
  • REST API for management and integration
  • Browser automation for complex user journey monitoring
  • Extensible architecture for adding new monitor types

The system is production-ready and can be deployed across multiple regions for comprehensive application monitoring.

Leave a Reply

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


Macro Nepal Helper