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.