fix(license): explicit @Autowired ctor + tolerate audit failures
Two follow-ups to LicenseEnforcer review: - Add @Autowired to the 3-arg ctor so Spring picks it unambiguously (the 2-arg test ctor is otherwise an equally-greedy candidate). - Wrap audit.log() in try/catch + log.warn so a degraded audit DB cannot mask a cap rejection: callers still see HTTP 403 even when audit storage is unhealthy. - Extract counter name to private static final. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@ import com.cameleer.server.core.license.LicenseLimits;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -30,11 +33,15 @@ import java.util.concurrent.ConcurrentMap;
|
||||
@Component
|
||||
public class LicenseEnforcer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LicenseEnforcer.class);
|
||||
private static final String COUNTER_NAME = "cameleer_license_cap_rejections_total";
|
||||
|
||||
private final LicenseGate gate;
|
||||
private final MeterRegistry meters;
|
||||
private final AuditService audit;
|
||||
private final ConcurrentMap<String, Counter> rejectionCounters = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
public LicenseEnforcer(LicenseGate gate, MeterRegistry meters, AuditService audit) {
|
||||
this.gate = gate;
|
||||
this.meters = meters;
|
||||
@@ -51,16 +58,21 @@ public class LicenseEnforcer {
|
||||
int cap = effective.get(limitKey); // throws IllegalArgumentException if unknown key
|
||||
long projected = currentUsage + requestedDelta;
|
||||
if (projected > cap) {
|
||||
rejectionCounters.computeIfAbsent(limitKey, k -> Counter.builder("cameleer_license_cap_rejections_total")
|
||||
rejectionCounters.computeIfAbsent(limitKey, k -> Counter.builder(COUNTER_NAME)
|
||||
.tag("limit", k).register(meters)).increment();
|
||||
if (audit != null) {
|
||||
Map<String, Object> detail = new LinkedHashMap<>();
|
||||
detail.put("limit", limitKey);
|
||||
detail.put("current", currentUsage);
|
||||
detail.put("requested", requestedDelta);
|
||||
detail.put("cap", cap);
|
||||
detail.put("state", gate.getState().name());
|
||||
audit.log("system", "cap_exceeded", AuditCategory.LICENSE, limitKey, detail, AuditResult.FAILURE, null);
|
||||
try {
|
||||
Map<String, Object> detail = new LinkedHashMap<>();
|
||||
detail.put("limit", limitKey);
|
||||
detail.put("current", currentUsage);
|
||||
detail.put("requested", requestedDelta);
|
||||
detail.put("cap", cap);
|
||||
detail.put("state", gate.getState().name());
|
||||
audit.log("system", "cap_exceeded", AuditCategory.LICENSE, limitKey, detail, AuditResult.FAILURE, null);
|
||||
} catch (RuntimeException e) {
|
||||
// Audit storage degraded; log and continue so the cap rejection still surfaces as 403.
|
||||
log.warn("Failed to write cap_exceeded audit row for limit={}: {}", limitKey, e.toString());
|
||||
}
|
||||
}
|
||||
throw new LicenseCapExceededException(limitKey, projected, cap);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user