Overview
The RateLimitMetricsRecorder interface records rate limit metrics for successful evaluations and errors. Implementations typically integrate with monitoring systems like Micrometer.
Package: io.github.v4runsharma.ratelimiter.metrics
Source: RateLimitMetricsRecorder.java:10
Methods
recordDecision
void recordDecision(
String name,
RateLimitPolicy policy,
RateLimitDecision decision,
Duration latency
)
Records a rate limit decision (allowed or denied).
The logical name of the rate limit (from annotation or key).
The rate limit policy that was evaluated.
decision
RateLimitDecision
required
The evaluation decision indicating if the request was allowed or denied.
The time taken to evaluate the rate limit.
Source: RateLimitMetricsRecorder.java:12
recordError
void recordError(
String name,
RateLimitPolicy policy,
Duration latency,
Throwable error
)
Records a rate limit evaluation error (e.g., backend failure).
The logical name of the rate limit (from annotation or key).
The rate limit policy being evaluated when the error occurred.
The time taken before the error occurred.
The error that occurred during evaluation.
Source: RateLimitMetricsRecorder.java:14
Usage examples
Micrometer implementation
import io.github.v4runsharma.ratelimiter.metrics.RateLimitMetricsRecorder;
import io.github.v4runsharma.ratelimiter.model.RateLimitDecision;
import io.github.v4runsharma.ratelimiter.model.RateLimitPolicy;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import java.time.Duration;
public class MicrometerMetricsRecorder implements RateLimitMetricsRecorder {
private final MeterRegistry registry;
public MicrometerMetricsRecorder(MeterRegistry registry) {
this.registry = registry;
}
@Override
public void recordDecision(
String name,
RateLimitPolicy policy,
RateLimitDecision decision,
Duration latency
) {
// Record counter
Counter.builder("rate_limit.decisions")
.tag("name", name)
.tag("scope", policy.getScope())
.tag("allowed", String.valueOf(decision.isAllowed()))
.register(registry)
.increment();
// Record latency
Timer.builder("rate_limit.evaluation.time")
.tag("name", name)
.tag("scope", policy.getScope())
.register(registry)
.record(latency);
// Record specific denied counter
if (!decision.isAllowed()) {
Counter.builder("rate_limit.exceeded")
.tag("name", name)
.tag("scope", policy.getScope())
.register(registry)
.increment();
}
}
@Override
public void recordError(
String name,
RateLimitPolicy policy,
Duration latency,
Throwable error
) {
Counter.builder("rate_limit.errors")
.tag("name", name)
.tag("scope", policy.getScope())
.tag("error_type", error.getClass().getSimpleName())
.register(registry)
.increment();
Timer.builder("rate_limit.error.time")
.tag("name", name)
.register(registry)
.record(latency);
}
}
No-op implementation
import io.github.v4runsharma.ratelimiter.metrics.RateLimitMetricsRecorder;
import io.github.v4runsharma.ratelimiter.model.RateLimitDecision;
import io.github.v4runsharma.ratelimiter.model.RateLimitPolicy;
import java.time.Duration;
public class NoOpMetricsRecorder implements RateLimitMetricsRecorder {
@Override
public void recordDecision(
String name,
RateLimitPolicy policy,
RateLimitDecision decision,
Duration latency
) {
// No-op - do nothing
}
@Override
public void recordError(
String name,
RateLimitPolicy policy,
Duration latency,
Throwable error
) {
// No-op - do nothing
}
}
Logging implementation
import io.github.v4runsharma.ratelimiter.metrics.RateLimitMetricsRecorder;
import io.github.v4runsharma.ratelimiter.model.RateLimitDecision;
import io.github.v4runsharma.ratelimiter.model.RateLimitPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
public class LoggingMetricsRecorder implements RateLimitMetricsRecorder {
private static final Logger log = LoggerFactory.getLogger(LoggingMetricsRecorder.class);
@Override
public void recordDecision(
String name,
RateLimitPolicy policy,
RateLimitDecision decision,
Duration latency
) {
if (decision.isAllowed()) {
log.debug(
"Rate limit check passed - name: {}, scope: {}, latency: {}ms",
name, policy.getScope(), latency.toMillis()
);
} else {
log.warn(
"Rate limit exceeded - name: {}, scope: {}, limit: {}, window: {}, latency: {}ms",
name, policy.getScope(), policy.getLimit(),
policy.getWindow(), latency.toMillis()
);
}
}
@Override
public void recordError(
String name,
RateLimitPolicy policy,
Duration latency,
Throwable error
) {
log.error(
"Rate limit evaluation error - name: {}, scope: {}, latency: {}ms",
name, policy.getScope(), latency.toMillis(), error
);
}
}
Using with enforcer
import io.github.v4runsharma.ratelimiter.core.*;
import io.github.v4runsharma.ratelimiter.metrics.RateLimitMetricsRecorder;
import io.github.v4runsharma.ratelimiter.model.*;
import io.github.v4runsharma.ratelimiter.exception.RateLimitExceededException;
import java.time.Duration;
import java.time.Instant;
public class MetricsAwareEnforcer implements RateLimitEnforcer {
private final RateLimiter rateLimiter;
private final RateLimitKeyResolver keyResolver;
private final RateLimitPolicyProvider policyProvider;
private final RateLimitMetricsRecorder metricsRecorder;
public MetricsAwareEnforcer(
RateLimiter rateLimiter,
RateLimitKeyResolver keyResolver,
RateLimitPolicyProvider policyProvider,
RateLimitMetricsRecorder metricsRecorder
) {
this.rateLimiter = rateLimiter;
this.keyResolver = keyResolver;
this.policyProvider = policyProvider;
this.metricsRecorder = metricsRecorder;
}
@Override
public RateLimitDecision evaluate(RateLimitContext context) {
String key = keyResolver.resolveKey(context);
RateLimitPolicy policy = policyProvider.resolvePolicy(context);
String name = context.getAnnotation().name();
Instant start = Instant.now();
try {
RateLimitDecision decision = rateLimiter.evaluate(key, policy);
Duration latency = Duration.between(start, Instant.now());
metricsRecorder.recordDecision(name, policy, decision, latency);
return decision;
} catch (Exception error) {
Duration latency = Duration.between(start, Instant.now());
metricsRecorder.recordError(name, policy, latency, error);
throw error;
}
}
@Override
public void enforce(RateLimitContext context) throws RateLimitExceededException {
RateLimitDecision decision = evaluate(context);
if (!decision.isAllowed()) {
String name = context.getAnnotation().name();
String key = keyResolver.resolveKey(context);
RateLimitPolicy policy = policyProvider.resolvePolicy(context);
throw new RateLimitExceededException(name, key, policy, decision);
}
}
}
Custom metrics implementation
import io.github.v4runsharma.ratelimiter.metrics.RateLimitMetricsRecorder;
import io.github.v4runsharma.ratelimiter.model.RateLimitDecision;
import io.github.v4runsharma.ratelimiter.model.RateLimitPolicy;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class CustomMetricsRecorder implements RateLimitMetricsRecorder {
private final ConcurrentHashMap<String, Metrics> metricsMap = new ConcurrentHashMap<>();
@Override
public void recordDecision(
String name,
RateLimitPolicy policy,
RateLimitDecision decision,
Duration latency
) {
Metrics metrics = metricsMap.computeIfAbsent(
name,
k -> new Metrics()
);
metrics.totalRequests.incrementAndGet();
if (decision.isAllowed()) {
metrics.allowedRequests.incrementAndGet();
} else {
metrics.deniedRequests.incrementAndGet();
}
metrics.totalLatencyMs.addAndGet(latency.toMillis());
}
@Override
public void recordError(
String name,
RateLimitPolicy policy,
Duration latency,
Throwable error
) {
Metrics metrics = metricsMap.computeIfAbsent(
name,
k -> new Metrics()
);
metrics.errors.incrementAndGet();
}
public void printStats() {
metricsMap.forEach((name, metrics) -> {
long total = metrics.totalRequests.get();
long allowed = metrics.allowedRequests.get();
long denied = metrics.deniedRequests.get();
long errors = metrics.errors.get();
long avgLatency = total > 0 ? metrics.totalLatencyMs.get() / total : 0;
System.out.println("Metrics for: " + name);
System.out.println(" Total: " + total);
System.out.println(" Allowed: " + allowed);
System.out.println(" Denied: " + denied);
System.out.println(" Errors: " + errors);
System.out.println(" Avg Latency: " + avgLatency + "ms");
});
}
private static class Metrics {
final AtomicLong totalRequests = new AtomicLong();
final AtomicLong allowedRequests = new AtomicLong();
final AtomicLong deniedRequests = new AtomicLong();
final AtomicLong errors = new AtomicLong();
final AtomicLong totalLatencyMs = new AtomicLong();
}
}
Configuration
import io.github.v4runsharma.ratelimiter.metrics.RateLimitMetricsRecorder;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
@ConditionalOnProperty(
name = "rate-limiter.metrics.enabled",
havingValue = "true",
matchIfMissing = true
)
public RateLimitMetricsRecorder rateLimitMetricsRecorder(
MeterRegistry registry
) {
return new MicrometerMetricsRecorder(registry);
}
@Bean
@ConditionalOnProperty(
name = "rate-limiter.metrics.enabled",
havingValue = "false"
)
public RateLimitMetricsRecorder noOpMetricsRecorder() {
return new NoOpMetricsRecorder();
}
}
Available implementations
The library provides:
MicrometerRateLimitMetricsRecorder
Production implementation that integrates with Micrometer for metrics recording.
NoOpRateLimitMetricsRecorder
No-operation implementation that does nothing. Useful for disabling metrics in tests or when metrics are not needed.