Log Sampling in Production in Java: Balancing Visibility and Performance

Log sampling is a critical technique for production systems where high-volume logging can impact performance, storage costs, and signal-to-noise ratio. Java provides sophisticated approaches to implement intelligent log sampling that captures meaningful data while controlling volume. This guide covers practical patterns for implementing effective log sampling strategies in production Java applications.

Understanding Log Sampling Strategies

Common Sampling Approaches:

  • Rate-based sampling (Nth occurrence)
  • Probabilistic sampling (random percentage)
  • Dynamic sampling (adaptive based on conditions)
  • Structured sampling (based on log content/context)
  • Head-based vs tail-based sampling

Core Implementation Patterns

1. Project Setup and Dependencies

Configure logging dependencies with sampling support.

Maven Configuration:

<dependencies>
<!-- Logback with Sampling Support -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<!-- Logstash Logback Encoder -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<!-- Micrometer for Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.11.5</version>
</dependency>
<!-- Configuration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
</dependencies>

2. Domain Models for Log Sampling

Create comprehensive models for sampling configuration and state.

Core Domain Models:

@Data
public class SamplingConfig {
private String strategy = "PROBABILISTIC";
private double rate = 0.1; // 10% for probabilistic
private int interval = 100; // For rate-based
private Map<String, Double> keyedRates = new HashMap<>();
private boolean enabled = true;
private int windowSize = 1000;
private double burstThreshold = 5.0;
private Map<String, Object> customRules = new HashMap<>();
public enum Strategy {
PROBABILISTIC,       // Random sampling
RATE_BASED,          // Every Nth occurrence
ADAPTIVE,            // Dynamic based on conditions
CONTENT_BASED,       // Based on log message content
BURST_DETECTION,     // Sample more during bursts
KEY_BASED           // Different rates for different log keys
}
}
@Data
public class LogEvent {
private String loggerName;
private String level;
private String message;
private String threadName;
private long timestamp;
private Map<String, String> mdc;
private Throwable throwable;
private String traceId;
private String spanId;
private Map<String, Object> customFields;
public String getLogKey() {
return loggerName + ":" + level;
}
public boolean isError() {
return "ERROR".equals(level) || "WARN".equals(level);
}
public boolean containsKeyword(String keyword) {
return message != null && message.toLowerCase().contains(keyword.toLowerCase());
}
}
@Data
public class SamplingDecision {
private boolean shouldLog;
private String reason;
private double sampleRate;
private String strategy;
private Map<String, Object> metadata;
public static SamplingDecision sample() {
return new SamplingDecision(true, "sampled", 1.0, "DEFAULT");
}
public static SamplingDecision skip() {
return new SamplingDecision(false, "sampled", 0.0, "DEFAULT");
}
public SamplingDecision(boolean shouldLog, String reason, double sampleRate, String strategy) {
this.shouldLog = shouldLog;
this.reason = reason;
this.sampleRate = sampleRate;
this.strategy = strategy;
this.metadata = new HashMap<>();
}
}
@Data
public class SamplingMetrics {
private String samplerName;
private long totalEvents;
private long sampledEvents;
private double effectiveRate;
private long windowStart;
private long windowEnd;
private Map<String, Long> decisionCounts;
private double currentLoad;
public SamplingMetrics(String samplerName) {
this.samplerName = samplerName;
this.decisionCounts = new HashMap<>();
this.windowStart = System.currentTimeMillis();
}
public void recordDecision(boolean sampled, String reason) {
totalEvents++;
if (sampled) {
sampledEvents++;
}
decisionCounts.merge(reason, 1L, Long::sum);
effectiveRate = totalEvents > 0 ? (double) sampledEvents / totalEvents : 0;
}
public void resetWindow() {
windowStart = System.currentTimeMillis();
totalEvents = 0;
sampledEvents = 0;
decisionCounts.clear();
}
}
@Data
public class BurstDetection {
private final int windowSize;
private final double threshold;
private final Deque<Long> eventTimestamps = new LinkedList<>();
private long lastBurstDetected = 0;
public BurstDetection(int windowSize, double threshold) {
this.windowSize = windowSize;
this.threshold = threshold;
}
public boolean isBurst() {
long now = System.currentTimeMillis();
long windowStart = now - (windowSize * 1000L);
// Remove old events
while (!eventTimestamps.isEmpty() && eventTimestamps.peekFirst() < windowStart) {
eventTimestamps.pollFirst();
}
double eventsPerSecond = (double) eventTimestamps.size() / windowSize;
return eventsPerSecond > threshold;
}
public void recordEvent() {
eventTimestamps.addLast(System.currentTimeMillis());
if (isBurst()) {
lastBurstDetected = System.currentTimeMillis();
}
}
public boolean isInBurstCoolDown() {
return System.currentTimeMillis() - lastBurstDetected < 30000; // 30 second cooldown
}
}

3. Core Sampling Strategies

Implement various sampling strategies.

Sampling Strategy Interface:

public interface SamplingStrategy {
SamplingDecision shouldSample(LogEvent event);
String getName();
void configure(SamplingConfig config);
SamplingMetrics getMetrics();
}
@Component
@Slf4j
public class ProbabilisticSampler implements SamplingStrategy {
private final Random random = new Random();
private double sampleRate;
private final SamplingMetrics metrics;
public ProbabilisticSampler() {
this.metrics = new SamplingMetrics("probabilistic");
this.sampleRate = 0.1; // Default 10%
}
@Override
public SamplingDecision shouldSample(LogEvent event) {
metrics.recordDecision(false, "evaluated");
// Always sample errors
if (event.isError()) {
metrics.recordDecision(true, "error");
return new SamplingDecision(true, "error", 1.0, getName());
}
// Sample based on probability
boolean shouldSample = random.nextDouble() < sampleRate;
String reason = shouldSample ? "probabilistic" : "sampled_out";
metrics.recordDecision(shouldSample, reason);
return new SamplingDecision(shouldSample, reason, sampleRate, getName());
}
@Override
public String getName() {
return "PROBABILISTIC";
}
@Override
public void configure(SamplingConfig config) {
this.sampleRate = config.getRate();
log.info("Configured probabilistic sampler with rate: {}", sampleRate);
}
@Override
public SamplingMetrics getMetrics() {
return metrics;
}
}
@Component
@Slf4j
public class RateBasedSampler implements SamplingStrategy {
private final AtomicLong counter = new AtomicLong(0);
private int sampleInterval;
private final SamplingMetrics metrics;
public RateBasedSampler() {
this.metrics = new SamplingMetrics("rate_based");
this.sampleInterval = 100; // Default: sample every 100th event
}
@Override
public SamplingDecision shouldSample(LogEvent event) {
metrics.recordDecision(false, "evaluated");
// Always sample errors
if (event.isError()) {
metrics.recordDecision(true, "error");
return new SamplingDecision(true, "error", 1.0, getName());
}
long count = counter.incrementAndGet();
boolean shouldSample = count % sampleInterval == 0;
String reason = shouldSample ? "rate_based" : "sampled_out";
metrics.recordDecision(shouldSample, reason);
return new SamplingDecision(shouldSample, reason, 1.0 / sampleInterval, getName());
}
@Override
public String getName() {
return "RATE_BASED";
}
@Override
public void configure(SamplingConfig config) {
this.sampleInterval = config.getInterval();
log.info("Configured rate-based sampler with interval: {}", sampleInterval);
}
@Override
public SamplingMetrics getMetrics() {
return metrics;
}
}
@Component
@Slf4j
public class AdaptiveSampler implements SamplingStrategy {
private double baseRate;
private double currentRate;
private final BurstDetection burstDetection;
private final SamplingMetrics metrics;
private long lastAdjustment = System.currentTimeMillis();
public AdaptiveSampler() {
this.metrics = new SamplingMetrics("adaptive");
this.baseRate = 0.1;
this.currentRate = 0.1;
this.burstDetection = new BurstDetection(10, 5.0); // 10s window, 5 events/sec threshold
}
@Override
public SamplingDecision shouldSample(LogEvent event) {
metrics.recordDecision(false, "evaluated");
burstDetection.recordEvent();
// Adjust rate based on conditions
adjustSamplingRate();
// Always sample errors and important keywords
if (shouldAlwaysSample(event)) {
metrics.recordDecision(true, "important");
return new SamplingDecision(true, "important", 1.0, getName());
}
// Sample based on current adaptive rate
boolean shouldSample = Math.random() < currentRate;
String reason = shouldSample ? "adaptive" : "sampled_out";
metrics.recordDecision(shouldSample, reason);
SamplingDecision decision = new SamplingDecision(shouldSample, reason, currentRate, getName());
decision.getMetadata().put("current_rate", currentRate);
decision.getMetadata().put("base_rate", baseRate);
return decision;
}
private boolean shouldAlwaysSample(LogEvent event) {
if (event.isError()) return true;
// Sample important business events
String message = event.getMessage();
if (message != null) {
return message.contains("transaction") ||
message.contains("payment") ||
message.contains("audit") ||
message.contains("security");
}
return false;
}
private void adjustSamplingRate() {
long now = System.currentTimeMillis();
if (now - lastAdjustment < 5000) { // Adjust every 5 seconds
return;
}
double currentLoad = calculateCurrentLoad();
if (burstDetection.isBurst()) {
// Reduce sampling during bursts to control volume
currentRate = Math.max(baseRate * 0.1, 0.01);
log.debug("Burst detected, reducing sampling rate to: {}", currentRate);
} else if (currentLoad > 0.8) {
// High load, reduce sampling
currentRate = Math.max(baseRate * 0.5, 0.05);
} else if (currentLoad < 0.2) {
// Low load, increase sampling
currentRate = Math.min(baseRate * 2.0, 1.0);
} else {
// Normal load, use base rate
currentRate = baseRate;
}
lastAdjustment = now;
}
private double calculateCurrentLoad() {
// Simplified load calculation based on recent sampling rate
double targetEventsPerSecond = 1000; // Target volume
double actualEventsPerSecond = metrics.getTotalEvents() / 10.0; // Last 10 seconds
return Math.min(actualEventsPerSecond / targetEventsPerSecond, 1.0);
}
@Override
public String getName() {
return "ADAPTIVE";
}
@Override
public void configure(SamplingConfig config) {
this.baseRate = config.getRate();
this.currentRate = config.getRate();
log.info("Configured adaptive sampler with base rate: {}", baseRate);
}
@Override
public SamplingMetrics getMetrics() {
return metrics;
}
}
@Component
@Slf4j
public class KeyBasedSampler implements SamplingStrategy {
private final Map<String, Double> keyRates;
private final Double defaultRate;
private final SamplingMetrics metrics;
public KeyBasedSampler() {
this.metrics = new SamplingMetrics("key_based");
this.keyRates = new HashMap<>();
this.defaultRate = 0.1;
// Default rates for common loggers
keyRates.put("com.example.service.PaymentService:INFO", 1.0); // Always log payments
keyRates.put("com.example.controller.AuthController:DEBUG", 0.01); // Rarely log auth debug
}
@Override
public SamplingDecision shouldSample(LogEvent event) {
metrics.recordDecision(false, "evaluated");
String logKey = event.getLogKey();
double rate = keyRates.getOrDefault(logKey, defaultRate);
boolean shouldSample = Math.random() < rate;
String reason = shouldSample ? "key_based" : "sampled_out";
metrics.recordDecision(shouldSample, reason);
SamplingDecision decision = new SamplingDecision(shouldSample, reason, rate, getName());
decision.getMetadata().put("log_key", logKey);
decision.getMetadata().put("key_rate", rate);
return decision;
}
@Override
public String getName() {
return "KEY_BASED";
}
@Override
public void configure(SamplingConfig config) {
keyRates.clear();
if (config.getKeyedRates() != null) {
keyRates.putAll(config.getKeyedRates());
}
log.info("Configured key-based sampler with {} key rates", keyRates.size());
}
@Override
public SamplingMetrics getMetrics() {
return metrics;
}
}

4. Sampling Manager and Orchestration

Coordinate multiple sampling strategies.

Sampling Manager:

@Service
@Slf4j
public class SamplingManager {
private final Map<String, SamplingStrategy> strategies;
private SamplingStrategy activeStrategy;
private SamplingConfig currentConfig;
private final MeterRegistry meterRegistry;
private final Map<String, SamplingMetrics> historicalMetrics;
public SamplingManager(List<SamplingStrategy> strategyList, MeterRegistry meterRegistry) {
this.strategies = strategyList.stream()
.collect(Collectors.toMap(SamplingStrategy::getName, Function.identity()));
this.meterRegistry = meterRegistry;
this.historicalMetrics = new ConcurrentHashMap<>();
this.activeStrategy = strategies.get("PROBABILISTIC");
this.currentConfig = new SamplingConfig();
// Initialize metrics collection
startMetricsCollection();
}
public SamplingDecision shouldSample(LogEvent event) {
if (!currentConfig.isEnabled()) {
return SamplingDecision.sample();
}
SamplingDecision decision = activeStrategy.shouldSample(event);
// Record metrics
recordSamplingMetrics(decision);
return decision;
}
public void configureSampling(SamplingConfig config) {
this.currentConfig = config;
SamplingStrategy strategy = strategies.get(config.getStrategy());
if (strategy != null) {
strategy.configure(config);
this.activeStrategy = strategy;
log.info("Switched to sampling strategy: {}", config.getStrategy());
} else {
log.warn("Unknown sampling strategy: {}, keeping current: {}", 
config.getStrategy(), activeStrategy.getName());
}
}
public Map<String, Object> getSamplingStatus() {
Map<String, Object> status = new HashMap<>();
status.put("active_strategy", activeStrategy.getName());
status.put("config", currentConfig);
status.put("enabled", currentConfig.isEnabled());
// Add metrics from all strategies
Map<String, Object> strategyMetrics = new HashMap<>();
strategies.forEach((name, strategy) -> {
strategyMetrics.put(name, strategy.getMetrics());
});
status.put("strategy_metrics", strategyMetrics);
return status;
}
public void setSamplingRate(double rate) {
currentConfig.setRate(rate);
activeStrategy.configure(currentConfig);
log.info("Updated sampling rate to: {}", rate);
}
public void setKeyedRate(String key, double rate) {
currentConfig.getKeyedRates().put(key, rate);
if (activeStrategy instanceof KeyBasedSampler) {
activeStrategy.configure(currentConfig);
}
log.info("Updated keyed rate for {} to: {}", key, rate);
}
private void recordSamplingMetrics(SamplingDecision decision) {
// Record to Micrometer metrics
Counter.builder("log.sampling.decisions")
.tag("strategy", decision.getStrategy())
.tag("decision", decision.shouldLog() ? "sampled" : "dropped")
.tag("reason", decision.getReason())
.register(meterRegistry)
.increment();
// Record sampling rate
Gauge.builder("log.sampling.rate", 
() -> activeStrategy.getMetrics().getEffectiveRate())
.tag("strategy", activeStrategy.getName())
.register(meterRegistry);
}
private void startMetricsCollection() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
collectHistoricalMetrics();
} catch (Exception e) {
log.warn("Failed to collect sampling metrics", e);
}
}, 0, 60, TimeUnit.SECONDS); // Collect every minute
}
private void collectHistoricalMetrics() {
String timestamp = Instant.now().toString();
strategies.forEach((name, strategy) -> {
String key = name + "_" + timestamp;
// Create a copy of current metrics
SamplingMetrics current = strategy.getMetrics();
SamplingMetrics historical = new SamplingMetrics(current.getSamplerName());
historical.setTotalEvents(current.getTotalEvents());
historical.setSampledEvents(current.getSampledEvents());
historical.setEffectiveRate(current.getEffectiveRate());
historical.setDecisionCounts(new HashMap<>(current.getDecisionCounts()));
historicalMetrics.put(key, historical);
});
// Keep only last hour of metrics
long oneHourAgo = System.currentTimeMillis() - 3600000;
historicalMetrics.keySet().removeIf(key -> {
String[] parts = key.split("_");
if (parts.length > 1) {
try {
Instant metricTime = Instant.parse(parts[1]);
return metricTime.toEpochMilli() < oneHourAgo;
} catch (Exception e) {
return true;
}
}
return true;
});
}
public Map<String, SamplingMetrics> getHistoricalMetrics() {
return new HashMap<>(historicalMetrics);
}
}

5. Logback Sampling Appender

Implement custom Logback appender with sampling.

Custom Sampling Appender:

public class SamplingAppender extends AppenderBase<ILoggingEvent> {
private SamplingManager samplingManager;
private LogEventConverter logEventConverter;
private boolean initialized = false;
@Override
public void start() {
if (samplingManager == null) {
// Try to get from Spring context or create default
initializeFromContext();
}
if (samplingManager != null) {
initialized = true;
super.start();
} else {
addError("SamplingManager not configured for SamplingAppender");
}
}
@Override
protected void append(ILoggingEvent loggingEvent) {
if (!initialized || samplingManager == null) {
// Fallback to always log if not initialized
fallbackAppend(loggingEvent);
return;
}
try {
LogEvent logEvent = logEventConverter.convert(loggingEvent);
SamplingDecision decision = samplingManager.shouldSample(logEvent);
if (decision.shouldLog()) {
logWithSamplingMetadata(loggingEvent, decision);
}
// Else: skip logging based on sampling decision
} catch (Exception e) {
// On error, log without sampling
addError("Sampling failed, falling back to regular logging", e);
fallbackAppend(loggingEvent);
}
}
private void logWithSamplingMetadata(ILoggingEvent originalEvent, SamplingDecision decision) {
// Create a new event with sampling metadata
Map<String, String> mdc = originalEvent.getMDCPropertyMap() != null ?
new HashMap<>(originalEvent.getMDCPropertyMap()) : new HashMap<>();
// Add sampling metadata to MDC
mdc.put("sampling_strategy", decision.getStrategy());
mdc.put("sampling_rate", String.valueOf(decision.getSampleRate()));
mdc.put("sampling_reason", decision.getReason());
LoggingEventWithMdc sampledEvent = new LoggingEventWithMdc(
originalEvent, mdc);
// Delegate to actual appenders
getContext().getLogger(originalEvent.getLoggerName())
.callAppenders(sampledEvent);
}
private void fallbackAppend(ILoggingEvent loggingEvent) {
// Delegate to actual appenders without sampling
getContext().getLogger(loggingEvent.getLoggerName())
.callAppenders(loggingEvent);
}
private void initializeFromContext() {
try {
// Try to get from Spring context
ApplicationContext context = getSpringApplicationContext();
if (context != null) {
this.samplingManager = context.getBean(SamplingManager.class);
this.logEventConverter = context.getBean(LogEventConverter.class);
}
} catch (Exception e) {
addWarn("Could not initialize from Spring context", e);
}
// Fallback to default initialization
if (samplingManager == null) {
this.samplingManager = createDefaultSamplingManager();
this.logEventConverter = new LogEventConverter();
}
}
private ApplicationContext getSpringApplicationContext() {
try {
// This would need to be adapted based on your application setup
return ApplicationContextProvider.getApplicationContext();
} catch (Exception e) {
return null;
}
}
private SamplingManager createDefaultSamplingManager() {
MeterRegistry meterRegistry = new SimpleMeterRegistry();
List<SamplingStrategy> strategies = Arrays.asList(
new ProbabilisticSampler(),
new RateBasedSampler(),
new AdaptiveSampler(),
new KeyBasedSampler()
);
return new SamplingManager(strategies, meterRegistry);
}
// Setter for Spring configuration
public void setSamplingManager(SamplingManager samplingManager) {
this.samplingManager = samplingManager;
}
public void setLogEventConverter(LogEventConverter logEventConverter) {
this.logEventConverter = logEventConverter;
}
}
@Component
public class LogEventConverter {
public LogEvent convert(ILoggingEvent loggingEvent) {
LogEvent event = new LogEvent();
event.setLoggerName(loggingEvent.getLoggerName());
event.setLevel(loggingEvent.getLevel().toString());
event.setMessage(loggingEvent.getFormattedMessage());
event.setThreadName(loggingEvent.getThreadName());
event.setTimestamp(loggingEvent.getTimeStamp());
event.setMdc(copyMdc(loggingEvent));
event.setThrowable(loggingEvent.getThrowableProxy() != null ? 
deserializeThrowable(loggingEvent.getThrowableProxy()) : null);
// Extract tracing information
if (loggingEvent.getMDCPropertyMap() != null) {
event.setTraceId(loggingEvent.getMDCPropertyMap().get("traceId"));
event.setSpanId(loggingEvent.getMDCPropertyMap().get("spanId"));
}
return event;
}
private Map<String, String> copyMdc(ILoggingEvent event) {
if (event.getMDCPropertyMap() == null) {
return new HashMap<>();
}
return new HashMap<>(event.getMDCPropertyMap());
}
private Throwable deserializeThrowable(IThrowableProxy throwableProxy) {
// Simplified - in practice you'd want to reconstruct the throwable
return new RuntimeException(throwableProxy.getClassName() + ": " + 
throwableProxy.getMessage());
}
}
// Custom logging event that allows MDC modification
class LoggingEventWithMdc implements ILoggingEvent {
private final ILoggingEvent delegate;
private final Map<String, String> mdc;
public LoggingEventWithMdc(ILoggingEvent delegate, Map<String, String> mdc) {
this.delegate = delegate;
this.mdc = mdc;
}
@Override
public String getThreadName() {
return delegate.getThreadName();
}
@Override
public Level getLevel() {
return delegate.getLevel();
}
@Override
public String getMessage() {
return delegate.getMessage();
}
@Override
public Object[] getArgumentArray() {
return delegate.getArgumentArray();
}
@Override
public String getFormattedMessage() {
return delegate.getFormattedMessage();
}
@Override
public String getLoggerName() {
return delegate.getLoggerName();
}
@Override
public LoggerContextVO getLoggerContextVO() {
return delegate.getLoggerContextVO();
}
@Override
public IThrowableProxy getThrowableProxy() {
return delegate.getThrowableProxy();
}
@Override
public StackTraceElement[] getCallerData() {
return delegate.getCallerData();
}
@Override
public boolean hasCallerData() {
return delegate.hasCallerData();
}
@Override
public long getTimeStamp() {
return delegate.getTimeStamp();
}
@Override
public long getNanoseconds() {
return delegate.getNanoseconds();
}
@Override
public Map<String, String> getMDCPropertyMap() {
return mdc;
}
@Override
public String getMDCProperty(String key) {
return mdc.get(key);
}
@Override
public void prepareForDeferredProcessing() {
delegate.prepareForDeferredProcessing();
}
}

6. Dynamic Configuration Management

Manage sampling configuration at runtime.

Configuration Service:

@Service
@Slf4j
public class SamplingConfigurationService {
private final SamplingManager samplingManager;
private final Map<String, SamplingConfig> configTemplates;
private final String configFilePath;
public SamplingConfigurationService(SamplingManager samplingManager,
@Value("${app.sampling.config-file:sampling-config.json}") 
String configFilePath) {
this.samplingManager = samplingManager;
this.configFilePath = configFilePath;
this.configTemplates = loadConfigTemplates();
// Load initial configuration
loadInitialConfiguration();
}
public void updateConfiguration(SamplingConfig newConfig) {
try {
samplingManager.configureSampling(newConfig);
saveConfiguration(newConfig);
log.info("Updated sampling configuration: strategy={}, rate={}", 
newConfig.getStrategy(), newConfig.getRate());
} catch (Exception e) {
log.error("Failed to update sampling configuration", e);
throw new RuntimeException("Configuration update failed", e);
}
}
public void applyTemplate(String templateName) {
SamplingConfig template = configTemplates.get(templateName);
if (template != null) {
updateConfiguration(template);
} else {
throw new IllegalArgumentException("Unknown template: " + templateName);
}
}
public void setSamplingRate(double rate) {
SamplingConfig current = getCurrentConfig();
current.setRate(rate);
updateConfiguration(current);
}
public void enableSampling() {
SamplingConfig config = getCurrentConfig();
config.setEnabled(true);
updateConfiguration(config);
}
public void disableSampling() {
SamplingConfig config = getCurrentConfig();
config.setEnabled(false);
updateConfiguration(config);
}
public SamplingConfig getCurrentConfig() {
Map<String, Object> status = samplingManager.getSamplingStatus();
return (SamplingConfig) status.get("config");
}
public Map<String, Object> getSamplingStatus() {
return samplingManager.getSamplingStatus();
}
public void reloadConfiguration() {
loadInitialConfiguration();
}
private void loadInitialConfiguration() {
try {
Path path = Paths.get(configFilePath);
if (Files.exists(path)) {
ObjectMapper mapper = new ObjectMapper();
SamplingConfig config = mapper.readValue(path.toFile(), SamplingConfig.class);
samplingManager.configureSampling(config);
log.info("Loaded sampling configuration from: {}", configFilePath);
} else {
// Use default configuration
SamplingConfig defaultConfig = new SamplingConfig();
samplingManager.configureSampling(defaultConfig);
log.info("Using default sampling configuration");
}
} catch (Exception e) {
log.error("Failed to load sampling configuration, using defaults", e);
SamplingConfig defaultConfig = new SamplingConfig();
samplingManager.configureSampling(defaultConfig);
}
}
private void saveConfiguration(SamplingConfig config) {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter()
.writeValue(Paths.get(configFilePath).toFile(), config);
} catch (Exception e) {
log.error("Failed to save sampling configuration", e);
}
}
private Map<String, SamplingConfig> loadConfigTemplates() {
Map<String, SamplingConfig> templates = new HashMap<>();
// Development template - sample everything
SamplingConfig devConfig = new SamplingConfig();
devConfig.setStrategy("PROBABILISTIC");
devConfig.setRate(1.0);
devConfig.setEnabled(true);
templates.put("development", devConfig);
// Production template - sample 10%
SamplingConfig prodConfig = new SamplingConfig();
prodConfig.setStrategy("ADAPTIVE");
prodConfig.setRate(0.1);
prodConfig.setEnabled(true);
templates.put("production", prodConfig);
// Debug template - sample more during debugging
SamplingConfig debugConfig = new SamplingConfig();
debugConfig.setStrategy("KEY_BASED");
debugConfig.setEnabled(true);
debugConfig.setKeyedRates(Map.of(
"com.example:DEBUG", 0.5,
"com.example:INFO", 0.2,
"com.example:ERROR", 1.0
));
templates.put("debug", debugConfig);
return templates;
}
@Scheduled(fixedRate = 300000) // Every 5 minutes
public void adaptiveConfigurationAdjustment() {
if (!isAdaptiveStrategyActive()) {
return;
}
Map<String, Object> status = samplingManager.getSamplingStatus();
Map<String, Object> metrics = (Map<String, Object>) status.get("strategy_metrics");
Map<String, Object> adaptiveMetrics = (Map<String, Object>) metrics.get("ADAPTIVE");
if (adaptiveMetrics != null) {
double effectiveRate = (Double) adaptiveMetrics.get("effectiveRate");
SamplingConfig currentConfig = getCurrentConfig();
// Adjust if we're significantly off target
double targetRate = currentConfig.getRate();
if (Math.abs(effectiveRate - targetRate) > 0.2) {
log.info("Adjusting sampling configuration. Effective rate: {}, Target: {}", 
effectiveRate, targetRate);
// Could implement more sophisticated adjustment logic here
}
}
}
private boolean isAdaptiveStrategyActive() {
SamplingConfig config = getCurrentConfig();
return "ADAPTIVE".equals(config.getStrategy());
}
}

7. REST API for Sampling Management

Expose sampling configuration as web services.

Sampling Controller:

@RestController
@RequestMapping("/api/sampling")
@Slf4j
public class SamplingController {
private final SamplingConfigurationService configService;
private final SamplingManager samplingManager;
public SamplingController(SamplingConfigurationService configService,
SamplingManager samplingManager) {
this.configService = configService;
this.samplingManager = samplingManager;
}
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getSamplingStatus() {
return ResponseEntity.ok(configService.getSamplingStatus());
}
@PostMapping("/configure")
public ResponseEntity<String> configureSampling(@RequestBody SamplingConfig config) {
try {
configService.updateConfiguration(config);
return ResponseEntity.ok("Sampling configuration updated successfully");
} catch (Exception e) {
return ResponseEntity.badRequest()
.body("Failed to update configuration: " + e.getMessage());
}
}
@PostMapping("/template/{templateName}")
public ResponseEntity<String> applyTemplate(@PathVariable String templateName) {
try {
configService.applyTemplate(templateName);
return ResponseEntity.ok("Applied template: " + templateName);
} catch (Exception e) {
return ResponseEntity.badRequest()
.body("Failed to apply template: " + e.getMessage());
}
}
@PostMapping("/rate")
public ResponseEntity<String> setSamplingRate(@RequestParam double rate) {
try {
configService.setSamplingRate(rate);
return ResponseEntity.ok("Sampling rate set to: " + rate);
} catch (Exception e) {
return ResponseEntity.badRequest()
.body("Failed to set sampling rate: " + e.getMessage());
}
}
@PostMapping("/enable")
public ResponseEntity<String> enableSampling() {
configService.enableSampling();
return ResponseEntity.ok("Sampling enabled");
}
@PostMapping("/disable")
public ResponseEntity<String> disableSampling() {
configService.disableSampling();
return ResponseEntity.ok("Sampling disabled");
}
@GetMapping("/metrics")
public ResponseEntity<Map<String, SamplingMetrics>> getMetrics() {
return ResponseEntity.ok(samplingManager.getHistoricalMetrics());
}
@PostMapping("/keyed-rate")
public ResponseEntity<String> setKeyedRate(
@RequestParam String key, 
@RequestParam double rate) {
try {
samplingManager.setKeyedRate(key, rate);
return ResponseEntity.ok("Set rate for " + key + " to: " + rate);
} catch (Exception e) {
return ResponseEntity.badRequest()
.body("Failed to set keyed rate: " + e.getMessage());
}
}
@PostMapping("/reload")
public ResponseEntity<String> reloadConfiguration() {
configService.reloadConfiguration();
return ResponseEntity.ok("Configuration reloaded");
}
}

8. Logback Configuration

Configure Logback with sampling appender.

logback-spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Spring Profile-based configuration -->
<springProfile name="dev">
<include resource="console-appender.xml" />
</springProfile>
<springProfile name="prod">
<include resource="file-appender.xml" />
</springProfile>
<!-- Sampling Appender Configuration -->
<appender name="SAMPLING" class="com.example.logging.SamplingAppender">
<!-- These will be injected by Spring -->
</appender>
<!-- Async Appender for Better Performance -->
<appender name="ASYNC_SAMPLING" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="SAMPLING" />
<queueSize>10000</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>false</includeCallerData>
<neverBlock>true</neverBlock>
</appender>
<!-- Root Logger with Sampling -->
<root level="INFO">
<appender-ref ref="ASYNC_SAMPLING" />
</root>
<!-- Specific logger configurations with different sampling rates -->
<logger name="com.example.service.PaymentService" level="DEBUG" additivity="false">
<appender-ref ref="ASYNC_SAMPLING" />
</logger>
<logger name="com.example.security" level="WARN" additivity="false">
<appender-ref ref="ASYNC_SAMPLING" />
</logger>
<!-- Always log errors regardless of sampling -->
<turboFilter class="ch.qos.logback.classic.turbo.DuplicateMessageFilter">
<AllowedRepetitions>0</AllowedRepetitions>
</turboFilter>
</configuration>

Best Practices for Production Log Sampling

  1. Start Conservative: Begin with higher sampling rates and adjust based on volume
  2. Monitor Effectiveness: Track sampling rates and adjust strategies based on actual usage
  3. Preserve Errors: Always sample error-level logs and important business events
  4. Consider Costs: Balance storage costs against debugging needs
  5. Dynamic Configuration: Enable runtime configuration changes without redeployment
  6. Test Thoroughly: Verify sampling behavior doesn't break log-based monitoring
  7. Document Strategy: Clearly document sampling strategy for troubleshooting

Conclusion: Intelligent Log Volume Management

Log sampling in Java production environments is essential for maintaining system performance while preserving observability. By implementing sophisticated sampling strategies that adapt to runtime conditions, prioritize important events, and provide operational control, organizations can achieve the optimal balance between log volume and debugging capability.

This implementation demonstrates that effective log sampling isn't about simply reducing volume—it's about making intelligent decisions about which logs provide the most value, ensuring that when issues occur, the right information is available while maintaining system performance and controlling costs.

Leave a Reply

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


Macro Nepal Helper