Overview
The Four Golden Signals of observability are key metrics for monitoring any service: Latency, Traffic, Errors, and Saturation. These signals provide a comprehensive view of system health and performance.
Core Implementation
1. Golden Signals Framework
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.time.Instant;
import java.time.Duration;
public class GoldenSignals {
private final String serviceName;
private final AtomicLong totalRequests = new AtomicLong(0);
private final AtomicLong errorRequests = new AtomicLong(0);
private final AtomicLong totalLatency = new AtomicLong(0);
private final AtomicLong inProgressRequests = new AtomicLong(0);
private final LongAdder saturationMetric = new LongAdder();
// For latency distribution
private final ConcurrentHashMap<String, AtomicLong> latencyBuckets = new ConcurrentHashMap<>();
private final int[] latencyThresholds = {10, 50, 100, 500, 1000, 5000}; // milliseconds
// For error tracking by type
private final ConcurrentHashMap<String, AtomicLong> errorCounts = new ConcurrentHashMap<>();
// For resource saturation
private final ResourceMonitor resourceMonitor;
public GoldenSignals(String serviceName) {
this.serviceName = serviceName;
this.resourceMonitor = new ResourceMonitor();
initializeLatencyBuckets();
}
private void initializeLatencyBuckets() {
for (int threshold : latencyThresholds) {
latencyBuckets.put("under_" + threshold + "ms", new AtomicLong(0));
}
latencyBuckets.put("over_" + latencyThresholds[latencyThresholds.length - 1] + "ms",
new AtomicLong(0));
}
// Main method to track request execution
public <T> T trackRequest(String operationName, Callable<T> operation) throws Exception {
long startTime = System.currentTimeMillis();
inProgressRequests.incrementAndGet();
try {
T result = operation.call();
recordSuccess(operationName, System.currentTimeMillis() - startTime);
return result;
} catch (Exception e) {
recordError(operationName, System.currentTimeMillis() - startTime, e);
throw e;
} finally {
inProgressRequests.decrementAndGet();
}
}
// Async version
public <T> CompletableFuture<T> trackRequestAsync(String operationName,
Supplier<CompletableFuture<T>> operation) {
long startTime = System.currentTimeMillis();
inProgressRequests.incrementAndGet();
return operation.get()
.whenComplete((result, throwable) -> {
long latency = System.currentTimeMillis() - startTime;
inProgressRequests.decrementAndGet();
if (throwable != null) {
recordError(operationName, latency, throwable);
} else {
recordSuccess(operationName, latency);
}
});
}
private void recordSuccess(String operationName, long latency) {
totalRequests.incrementAndGet();
totalLatency.addAndGet(latency);
recordLatencyBucket(latency);
}
private void recordError(String operationName, long latency, Throwable error) {
totalRequests.incrementAndGet();
errorRequests.incrementAndGet();
totalLatency.addAndGet(latency);
recordLatencyBucket(latency);
// Track error by type
String errorType = error.getClass().getSimpleName();
errorCounts.computeIfAbsent(errorType, k -> new AtomicLong(0))
.incrementAndGet();
}
private void recordLatencyBucket(long latency) {
boolean bucketFound = false;
for (int threshold : latencyThresholds) {
if (latency <= threshold) {
latencyBuckets.get("under_" + threshold + "ms").incrementAndGet();
bucketFound = true;
break;
}
}
if (!bucketFound) {
latencyBuckets.get("over_" + latencyThresholds[latencyThresholds.length - 1] + "ms")
.incrementAndGet();
}
}
// Get current signals snapshot
public SignalMetrics getCurrentMetrics() {
long total = totalRequests.get();
long errors = errorRequests.get();
long totalLat = totalLatency.get();
double errorRate = total > 0 ? (double) errors / total * 100 : 0.0;
double averageLatency = total > 0 ? (double) totalLat / total : 0.0;
return new SignalMetrics(
serviceName,
Instant.now(),
total,
errors,
errorRate,
averageLatency,
inProgressRequests.get(),
resourceMonitor.getCurrentSaturation(),
getLatencyDistribution(),
getErrorDistribution(),
resourceMonitor.getResourceMetrics()
);
}
public Map<String, Long> getLatencyDistribution() {
Map<String, Long> distribution = new LinkedHashMap<>();
latencyBuckets.forEach((bucket, count) -> {
distribution.put(bucket, count.get());
});
return distribution;
}
public Map<String, Long> getErrorDistribution() {
Map<String, Long> distribution = new HashMap<>();
errorCounts.forEach((errorType, count) -> {
distribution.put(errorType, count.get());
});
return distribution;
}
// Update saturation metrics
public void updateSaturation(double saturation) {
saturationMetric.add((long) (saturation * 100)); // Store as percentage * 100
}
// Signal Metrics DTO
public static class SignalMetrics {
private final String serviceName;
private final Instant timestamp;
private final long traffic;
private final long errors;
private final double errorRate;
private final double averageLatency;
private final long inProgressRequests;
private final double saturation;
private final Map<String, Long> latencyDistribution;
private final Map<String, Long> errorDistribution;
private final Map<String, Double> resourceMetrics;
public SignalMetrics(String serviceName, Instant timestamp, long traffic, long errors,
double errorRate, double averageLatency, long inProgressRequests,
double saturation, Map<String, Long> latencyDistribution,
Map<String, Long> errorDistribution, Map<String, Double> resourceMetrics) {
this.serviceName = serviceName;
this.timestamp = timestamp;
this.traffic = traffic;
this.errors = errors;
this.errorRate = errorRate;
this.averageLatency = averageLatency;
this.inProgressRequests = inProgressRequests;
this.saturation = saturation;
this.latencyDistribution = latencyDistribution;
this.errorDistribution = errorDistribution;
this.resourceMetrics = resourceMetrics;
}
// Getters
public String getServiceName() { return serviceName; }
public Instant getTimestamp() { return timestamp; }
public long getTraffic() { return traffic; }
public long getErrors() { return errors; }
public double getErrorRate() { return errorRate; }
public double getAverageLatency() { return averageLatency; }
public long getInProgressRequests() { return inProgressRequests; }
public double getSaturation() { return saturation; }
public Map<String, Long> getLatencyDistribution() { return latencyDistribution; }
public Map<String, Long> getErrorDistribution() { return errorDistribution; }
public Map<String, Double> getResourceMetrics() { return resourceMetrics; }
}
}
// Resource Monitor for Saturation
class ResourceMonitor {
private final Runtime runtime = Runtime.getRuntime();
private final OperatingSystemMXBean osBean =
ManagementFactory.getOperatingSystemMXBean();
private final com.sun.management.OperatingSystemMXBean sunOsBean =
(com.sun.management.OperatingSystemMXBean) osBean;
public double getCurrentSaturation() {
// Calculate overall saturation as weighted average of key resources
double cpuSaturation = getCpuSaturation();
double memorySaturation = getMemorySaturation();
double threadSaturation = getThreadSaturation();
// Weighted average (adjust weights based on your application)
return (cpuSaturation * 0.4 + memorySaturation * 0.4 + threadSaturation * 0.2);
}
public Map<String, Double> getResourceMetrics() {
Map<String, Double> metrics = new HashMap<>();
metrics.put("cpu_usage", getCpuSaturation());
metrics.put("memory_usage", getMemorySaturation());
metrics.put("thread_usage", getThreadSaturation());
metrics.put("disk_usage", getDiskSaturation());
return metrics;
}
private double getCpuSaturation() {
return sunOsBean.getSystemCpuLoad();
}
private double getMemorySaturation() {
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
return (double) usedMemory / totalMemory;
}
private double getThreadSaturation() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
int threadCount = threadBean.getThreadCount();
int peakThreadCount = threadBean.getPeakThreadCount();
return peakThreadCount > 0 ? (double) threadCount / peakThreadCount : 0.0;
}
private double getDiskSaturation() {
// This would need to be implemented based on your specific disk usage patterns
return 0.0; // Placeholder
}
}
2. Spring Boot Integration with AOP
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
public class GoldenSignalsAspect {
private final GoldenSignals goldenSignals;
public GoldenSignalsAspect(GoldenSignals goldenSignals) {
this.goldenSignals = goldenSignals;
}
// Monitor REST controllers
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public Object monitorRestController(ProceedingJoinPoint joinPoint) throws Throwable {
String operationName = getOperationName(joinPoint);
return goldenSignals.trackRequest(operationName, () -> {
try {
return joinPoint.proceed();
} catch (Throwable t) {
if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new RuntimeException(t);
}
}
});
}
// Monitor service methods
@Around("@annotation(MonitorService)")
public Object monitorServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String operationName = getOperationName(joinPoint);
return goldenSignals.trackRequest(operationName, () -> {
try {
return joinPoint.proceed();
} catch (Throwable t) {
if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new RuntimeException(t);
}
}
});
}
// Monitor database operations
@Around("execution(* org.springframework.data.repository.Repository+.*(..))")
public Object monitorRepositoryMethods(ProceedingJoinPoint joinPoint) throws Throwable {
String operationName = "db." + joinPoint.getSignature().getName();
return goldenSignals.trackRequest(operationName, () -> {
try {
return joinPoint.proceed();
} catch (Throwable t) {
if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new RuntimeException(t);
}
}
});
}
private String getOperationName(ProceedingJoinPoint joinPoint) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// For HTTP requests
String method = attributes.getRequest().getMethod();
String path = attributes.getRequest().getRequestURI();
return "http." + method + "." + path;
} else {
// For service methods
return joinPoint.getSignature().getDeclaringType().getSimpleName() +
"." + joinPoint.getSignature().getName();
}
}
}
// Custom annotation for service monitoring
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitorService {
String value() default "";
}
3. Metrics Collector and Exporter
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import java.util.concurrent.TimeUnit;
@Component
public class MetricsExporter {
private final MeterRegistry meterRegistry;
private final GoldenSignals goldenSignals;
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public MetricsExporter(MeterRegistry meterRegistry, GoldenSignals goldenSignals) {
this.meterRegistry = meterRegistry;
this.goldenSignals = goldenSignals;
startMetricsExport();
}
private void startMetricsExport() {
// Export metrics every 30 seconds
scheduler.scheduleAtFixedRate(this::exportMetrics, 0, 30, TimeUnit.SECONDS);
}
private void exportMetrics() {
GoldenSignals.SignalMetrics metrics = goldenSignals.getCurrentMetrics();
// Traffic - requests per second
meterRegistry.counter("golden_signals.traffic",
Tags.of("service", metrics.getServiceName()))
.increment(metrics.getTraffic());
// Errors - error rate
meterRegistry.gauge("golden_signals.error_rate",
Tags.of("service", metrics.getServiceName()),
metrics.getErrorRate());
// Latency - average and percentiles
meterRegistry.timer("golden_signals.latency",
Tags.of("service", metrics.getServiceName()))
.record((long) metrics.getAverageLatency(), TimeUnit.MILLISECONDS);
// Saturation
meterRegistry.gauge("golden_signals.saturation",
Tags.of("service", metrics.getServiceName()),
metrics.getSaturation());
// In-progress requests
meterRegistry.gauge("golden_signals.in_progress_requests",
Tags.of("service", metrics.getServiceName()),
metrics.getInProgressRequests());
// Export latency distribution
metrics.getLatencyDistribution().forEach((bucket, count) -> {
meterRegistry.gauge("golden_signals.latency_distribution",
Tags.of("service", metrics.getServiceName(), "bucket", bucket),
count.doubleValue());
});
// Export error distribution
metrics.getErrorDistribution().forEach((errorType, count) -> {
meterRegistry.gauge("golden_signals.error_distribution",
Tags.of("service", metrics.getServiceName(), "error_type", errorType),
count.doubleValue());
});
// Export resource metrics
metrics.getResourceMetrics().forEach((resource, usage) -> {
meterRegistry.gauge("golden_signals.resource_usage",
Tags.of("service", metrics.getServiceName(), "resource", resource),
usage);
});
}
@PreDestroy
public void cleanup() {
scheduler.shutdown();
}
}
4. REST API for Golden Signals
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.util.Map;
@RestController
@RequestMapping("/api/observability")
public class ObservabilityController {
private final GoldenSignals goldenSignals;
private final AlertService alertService;
public ObservabilityController(GoldenSignals goldenSignals, AlertService alertService) {
this.goldenSignals = goldenSignals;
this.alertService = alertService;
}
@GetMapping("/golden-signals")
public ResponseEntity<GoldenSignals.SignalMetrics> getGoldenSignals() {
return ResponseEntity.ok(goldenSignals.getCurrentMetrics());
}
@GetMapping("/golden-signals/history")
public ResponseEntity<Map<String, Object>> getHistoricalSignals(
@RequestParam(defaultValue = "1h") String duration) {
// Implementation would fetch from time-series database
Map<String, Object> historicalData = Map.of(
"service", goldenSignals.getCurrentMetrics().getServiceName(),
"duration", duration,
"data", "Historical data implementation needed"
);
return ResponseEntity.ok(historicalData);
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> getHealthStatus() {
GoldenSignals.SignalMetrics metrics = goldenSignals.getCurrentMetrics();
Map<String, Object> health = Map.of(
"status", calculateHealthStatus(metrics),
"signals", metrics,
"timestamp", System.currentTimeMillis()
);
return ResponseEntity.ok(health);
}
@GetMapping("/alerts")
public ResponseEntity<List<Alert>> getActiveAlerts() {
return ResponseEntity.ok(alertService.getActiveAlerts());
}
private String calculateHealthStatus(GoldenSignals.SignalMetrics metrics) {
if (metrics.getErrorRate() > 10.0) {
return "UNHEALTHY";
} else if (metrics.getErrorRate() > 5.0) {
return "DEGRADED";
} else if (metrics.getSaturation() > 80.0) {
return "STRESSED";
} else {
return "HEALTHY";
}
}
}
// Alert Service
@Component
class AlertService {
private final List<Alert> activeAlerts = new CopyOnWriteArrayList<>();
private final GoldenSignals goldenSignals;
private final ScheduledExecutorService alertChecker =
Executors.newScheduledThreadPool(1);
public AlertService(GoldenSignals goldenSignals) {
this.goldenSignals = goldenSignals;
startAlertMonitoring();
}
private void startAlertMonitoring() {
alertChecker.scheduleAtFixedRate(this::checkAlerts, 0, 30, TimeUnit.SECONDS);
}
private void checkAlerts() {
GoldenSignals.SignalMetrics metrics = goldenSignals.getCurrentMetrics();
// Check for high error rate
if (metrics.getErrorRate() > 5.0) {
createAlert("HIGH_ERROR_RATE",
String.format("Error rate is %.2f%%", metrics.getErrorRate()),
"WARNING");
}
// Check for high latency
if (metrics.getAverageLatency() > 1000) { // 1 second
createAlert("HIGH_LATENCY",
String.format("Average latency is %.2fms", metrics.getAverageLatency()),
"WARNING");
}
// Check for saturation
if (metrics.getSaturation() > 80.0) {
createAlert("HIGH_SATURATION",
String.format("Saturation is %.2f%%", metrics.getSaturation()),
"CRITICAL");
}
}
private void createAlert(String type, String message, String severity) {
// Check if similar alert already exists
boolean exists = activeAlerts.stream()
.anyMatch(alert -> alert.getType().equals(type) && !alert.isResolved());
if (!exists) {
Alert alert = new Alert(type, message, severity, System.currentTimeMillis());
activeAlerts.add(alert);
// TODO: Send notification
System.out.println("ALERT: " + message);
}
}
public List<Alert> getActiveAlerts() {
return activeAlerts.stream()
.filter(alert -> !alert.isResolved())
.collect(Collectors.toList());
}
public void resolveAlert(String alertId) {
activeAlerts.stream()
.filter(alert -> alert.getId().equals(alertId))
.findFirst()
.ifPresent(alert -> alert.setResolved(true));
}
}
class Alert {
private final String id;
private final String type;
private final String message;
private final String severity;
private final long timestamp;
private boolean resolved;
public Alert(String type, String message, String severity, long timestamp) {
this.id = UUID.randomUUID().toString();
this.type = type;
this.message = message;
this.severity = severity;
this.timestamp = timestamp;
this.resolved = false;
}
// Getters and setters
public String getId() { return id; }
public String getType() { return type; }
public String getMessage() { return message; }
public String getSeverity() { return severity; }
public long getTimestamp() { return timestamp; }
public boolean isResolved() { return resolved; }
public void setResolved(boolean resolved) { this.resolved = resolved; }
}
5. Advanced Signal Analysis
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@Component
public class SignalAnalyzer {
private final GoldenSignals goldenSignals;
private final Map<String, Deque<GoldenSignals.SignalMetrics>> metricsHistory =
new ConcurrentHashMap<>();
private final int historySize = 100; // Keep last 100 data points
public SignalAnalyzer(GoldenSignals goldenSignals) {
this.goldenSignals = goldenSignals;
startMetricsCollection();
}
private void startMetricsCollection() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
GoldenSignals.SignalMetrics metrics = goldenSignals.getCurrentMetrics();
String serviceName = metrics.getServiceName();
metricsHistory.computeIfAbsent(serviceName, k -> new ConcurrentLinkedDeque<>())
.add(metrics);
// Trim history to maintain size
Deque<GoldenSignals.SignalMetrics> history = metricsHistory.get(serviceName);
while (history.size() > historySize) {
history.removeFirst();
}
}, 0, 10, TimeUnit.SECONDS); // Collect every 10 seconds
}
public TrendAnalysis analyzeTrends(String serviceName, String signalType) {
Deque<GoldenSignals.SignalMetrics> history = metricsHistory.get(serviceName);
if (history == null || history.isEmpty()) {
return new TrendAnalysis("NO_DATA", 0.0, Collections.emptyList());
}
List<Double> signalValues = extractSignalValues(history, signalType);
return calculateTrend(signalValues);
}
public AnomalyDetection detectAnomalies(String serviceName) {
Deque<GoldenSignals.SignalMetrics> history = metricsHistory.get(serviceName);
if (history == null || history.size() < 10) {
return new AnomalyDetection(Collections.emptyList());
}
List<Anomaly> anomalies = new ArrayList<>();
// Check for error rate anomalies
List<Double> errorRates = extractSignalValues(history, "error_rate");
detectSpikes(errorRates, "ERROR_RATE").forEach(anomaly ->
anomalies.add(new Anomaly("ERROR_RATE_SPIKE", anomaly, "High error rate detected")));
// Check for latency anomalies
List<Double> latencies = extractSignalValues(history, "latency");
detectSpikes(latencies, "LATENCY").forEach(anomaly ->
anomalies.add(new Anomaly("LATENCY_SPIKE", anomaly, "High latency detected")));
// Check for traffic anomalies
List<Double> traffic = extractSignalValues(history, "traffic");
detectSpikes(traffic, "TRAFFIC").forEach(anomaly ->
anomalies.add(new Anomaly("TRAFFIC_SPIKE", anomaly, "Unusual traffic pattern")));
return new AnomalyDetection(anomalies);
}
private List<Double> extractSignalValues(Deque<GoldenSignals.SignalMetrics> history,
String signalType) {
return history.stream()
.map(metrics -> {
switch (signalType) {
case "error_rate": return metrics.getErrorRate();
case "latency": return metrics.getAverageLatency();
case "traffic": return (double) metrics.getTraffic();
case "saturation": return metrics.getSaturation();
default: return 0.0;
}
})
.collect(Collectors.toList());
}
private TrendAnalysis calculateTrend(List<Double> values) {
if (values.size() < 2) {
return new TrendAnalysis("STABLE", 0.0, values);
}
// Simple linear regression for trend
double sumX = 0.0, sumY = 0.0, sumXY = 0.0, sumX2 = 0.0;
int n = values.size();
for (int i = 0; i < n; i++) {
sumX += i;
sumY += values.get(i);
sumXY += i * values.get(i);
sumX2 += i * i;
}
double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
String trend;
if (Math.abs(slope) < 0.1) {
trend = "STABLE";
} else if (slope > 0) {
trend = "INCREASING";
} else {
trend = "DECREASING";
}
return new TrendAnalysis(trend, slope, values);
}
private List<Double> detectSpikes(List<Double> values, String signalType) {
List<Double> spikes = new ArrayList<>();
if (values.size() < 3) return spikes;
double mean = values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
double stdDev = calculateStandardDeviation(values, mean);
double threshold = mean + (2 * stdDev); // 2 standard deviations
for (int i = 0; i < values.size(); i++) {
if (values.get(i) > threshold) {
spikes.add(values.get(i));
}
}
return spikes;
}
private double calculateStandardDeviation(List<Double> values, double mean) {
double variance = values.stream()
.mapToDouble(v -> Math.pow(v - mean, 2))
.average()
.orElse(0.0);
return Math.sqrt(variance);
}
// Analysis result classes
public static class TrendAnalysis {
private final String trend;
private final double slope;
private final List<Double> values;
public TrendAnalysis(String trend, double slope, List<Double> values) {
this.trend = trend;
this.slope = slope;
this.values = values;
}
// Getters
public String getTrend() { return trend; }
public double getSlope() { return slope; }
public List<Double> getValues() { return values; }
}
public static class AnomalyDetection {
private final List<Anomaly> anomalies;
public AnomalyDetection(List<Anomaly> anomalies) {
this.anomalies = anomalies;
}
public List<Anomaly> getAnomalies() { return anomalies; }
public boolean hasAnomalies() { return !anomalies.isEmpty(); }
}
public static class Anomaly {
private final String type;
private final double value;
private final String description;
public Anomaly(String type, double value, String description) {
this.type = type;
this.value = value;
this.description = description;
}
// Getters
public String getType() { return type; }
public double getValue() { return value; }
public String getDescription() { return description; }
}
}
6. Configuration and Setup
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
@EnableAspectJAutoProxy
public class ObservabilityConfig {
@Bean
public GoldenSignals goldenSignals() {
return new GoldenSignals("my-application");
}
@Bean
public MetricsExporter metricsExporter(MeterRegistry meterRegistry,
GoldenSignals goldenSignals) {
return new MetricsExporter(meterRegistry, goldenSignals);
}
@Bean
public SignalAnalyzer signalAnalyzer(GoldenSignals goldenSignals) {
return new SignalAnalyzer(goldenSignals);
}
@Bean
public AlertService alertService(GoldenSignals goldenSignals) {
return new AlertService(goldenSignals);
}
}
// Micrometer configuration in application.properties
/*
management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.export.prometheus.enabled=true
management.metrics.distribution.percentiles-histogram.http.server.requests=true
management.metrics.distribution.percentiles=http.server.requests:0.5,0.95,0.99
*/
7. Usage in Service Classes
@Service
public class UserService {
private final GoldenSignals goldenSignals;
private final UserRepository userRepository;
public UserService(GoldenSignals goldenSignals, UserRepository userRepository) {
this.goldenSignals = goldenSignals;
this.userRepository = userRepository;
}
@MonitorService
public User getUserById(Long userId) {
return goldenSignals.trackRequest("UserService.getUserById", () -> {
// Business logic
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
});
}
@MonitorService
public CompletableFuture<User> getUserByIdAsync(Long userId) {
return goldenSignals.trackRequestAsync("UserService.getUserByIdAsync", () -> {
return CompletableFuture.supplyAsync(() ->
userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId))
);
});
}
public void updateUserSaturation() {
// Example: Update saturation based on some business metric
double saturation = calculateUserServiceSaturation();
goldenSignals.updateSaturation(saturation);
}
private double calculateUserServiceSaturation() {
// Implement saturation calculation based on your business logic
// This could be based on: active users, request queue size, cache hit rate, etc.
return 0.0; // Placeholder
}
}
Best Practices
- Comprehensive Coverage: Instrument all critical paths including external calls, database operations, and business logic
- Meaningful Metrics: Ensure metrics provide actionable insights
- Low Overhead: Keep observability overhead minimal
- Context Propagation: Include correlation IDs for distributed tracing
- Alerting Strategy: Set up meaningful alerts with appropriate thresholds
- Historical Analysis: Store metrics for trend analysis and capacity planning
- Dashboarding: Create comprehensive dashboards for different stakeholders
- Continuous Improvement: Regularly review and refine observability practices
This implementation provides a complete observability framework for monitoring the Four Golden Signals in Java applications, enabling comprehensive system monitoring, alerting, and analysis.