Skip to content

Commit 077f300

Browse files
committed
fix: address PR review findings — double-counting, finally safety, JavaDoc, unit tests
- Remove duplicate recordError(INVALID_TOKEN) call from executeValidationRequest; outer catch (TurnstileValidationException) is the single correct recording site - Wrap metrics.recordResponseTime() in try/catch inside finally block so a metrics failure cannot suppress the original validation exception - Add log.debug to TurnstileValidationException catch for symmetry with other handlers - Improve broad catch to log exception class name for better diagnostics - Add complete JavaDoc to TurnstileMetrics interface (recordValidation, recordSuccess, recordResponseTime) including call-sequence contract and timing semantics - Add class and constructor JavaDoc to MicrometerTurnstileMetrics; add Objects.requireNonNull guard on registry parameter - Fix TurnstileValidationService class-level doc (no longer says "when metrics enabled") - Fix TurnstileHealthConfiguration comment to mention @ConditionalOnEnabledHealthIndicator - Fix noOpTurnstileMetrics comment to say "no TurnstileMetrics bean registered" rather than "Micrometer not on classpath" - Add MicrometerTurnstileMetricsTest (9 tests) covering all counter dispatch paths, timer recording, null-registry rejection, and aggregate/sub-counter consistency
1 parent 62a8855 commit 077f300

6 files changed

Lines changed: 173 additions & 11 deletions

File tree

src/main/java/com/digitalsanctuary/cf/turnstile/TurnstileConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ static class TurnstileMetricsConfiguration {
4747

4848
/**
4949
* Health indicator configuration for Turnstile.
50-
* Only imported if Spring Actuator's HealthIndicator is available on the classpath.
50+
* Only imported if Spring Actuator's {@code HealthIndicator} class is on the classpath
51+
* and the turnstile health indicator has not been disabled via
52+
* {@code management.health.turnstile.enabled=false}.
5153
*/
5254
@Configuration
5355
@ConditionalOnEnabledHealthIndicator("turnstile")

src/main/java/com/digitalsanctuary/cf/turnstile/config/TurnstileServiceConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ public class TurnstileServiceConfig {
2727
private final TurnstileConfigProperties properties;
2828

2929
/**
30-
* Provides a no-op TurnstileMetrics bean when no other implementation is registered.
31-
* This is the fallback when Micrometer is not on the classpath.
30+
* Provides a no-op {@link TurnstileMetrics} bean when no other implementation is registered.
31+
* This fallback is active when Micrometer is absent from the classpath, when metrics are
32+
* disabled via {@code ds.cf.turnstile.metrics.enabled=false}, or when no custom
33+
* {@code TurnstileMetrics} bean has been supplied by the consuming application.
3234
*
3335
* @return a no-op TurnstileMetrics instance
3436
*/

src/main/java/com/digitalsanctuary/cf/turnstile/metrics/MicrometerTurnstileMetrics.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
44
import io.micrometer.core.instrument.Counter;
55
import io.micrometer.core.instrument.MeterRegistry;
66
import io.micrometer.core.instrument.Timer;
7+
import java.util.Objects;
78
import java.util.concurrent.TimeUnit;
89
import lombok.extern.slf4j.Slf4j;
910

1011
/**
11-
* Micrometer-backed implementation of TurnstileMetrics.
12-
* This class is only instantiated when Micrometer is present on the classpath.
12+
* Micrometer-backed implementation of {@link TurnstileMetrics}.
13+
* <p>
14+
* This class is only instantiated when Micrometer is present on the classpath, via
15+
* {@code TurnstileMetricsConfig} which is guarded by {@code @ConditionalOnClass(MeterRegistry.class)}.
16+
* </p>
17+
* <p>
18+
* All eight meters ({@code errorCounter} is the aggregate; the four type-specific counters are
19+
* sub-categories whose sum equals the aggregate) are eagerly registered at construction time so
20+
* they appear in monitoring dashboards before the first validation event occurs. A single instance
21+
* should be registered per application context to avoid duplicate meter registration errors.
22+
* </p>
23+
*
24+
* @see NoOpTurnstileMetrics
1325
*/
1426
@Slf4j
1527
public class MicrometerTurnstileMetrics implements TurnstileMetrics {
@@ -23,7 +35,24 @@ public class MicrometerTurnstileMetrics implements TurnstileMetrics {
2335
private final Counter inputErrorCounter;
2436
private final Timer responseTimer;
2537

38+
/**
39+
* Creates a {@code MicrometerTurnstileMetrics} instance and eagerly registers all Turnstile
40+
* meters with the provided registry. The registered meters are:
41+
* <ul>
42+
* <li>{@code turnstile.validation.requests} — total attempts</li>
43+
* <li>{@code turnstile.validation.success} — successful validations</li>
44+
* <li>{@code turnstile.validation.errors} — all errors (aggregate)</li>
45+
* <li>{@code turnstile.validation.errors.network} — network errors</li>
46+
* <li>{@code turnstile.validation.errors.config} — configuration errors</li>
47+
* <li>{@code turnstile.validation.errors.token} — invalid token errors</li>
48+
* <li>{@code turnstile.validation.errors.input} — input validation errors</li>
49+
* <li>{@code turnstile.validation.response.time} — response time timer</li>
50+
* </ul>
51+
*
52+
* @param registry the Micrometer {@link MeterRegistry} to register meters with; must not be null
53+
*/
2654
public MicrometerTurnstileMetrics(MeterRegistry registry) {
55+
Objects.requireNonNull(registry, "registry must not be null");
2756
log.info("Initializing Turnstile metrics with MeterRegistry");
2857
validationCounter = Counter.builder("turnstile.validation.requests")
2958
.description("Total number of Turnstile validation requests").register(registry);

src/main/java/com/digitalsanctuary/cf/turnstile/metrics/TurnstileMetrics.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,30 @@
44

55
/**
66
* Abstraction for recording Turnstile validation metrics.
7-
* Implementations may be no-op (when Micrometer is absent) or Micrometer-backed.
7+
* <p>
8+
* Implementations may be no-op (when Micrometer is absent from the classpath or metrics are
9+
* disabled) or Micrometer-backed. Consumers may also supply a custom implementation as a Spring
10+
* bean to integrate with their own metrics infrastructure.
11+
* </p>
12+
* <p>
13+
* Expected call sequence per validation attempt:
14+
* {@link #recordValidation()} is always called first, followed by exactly one of
15+
* {@link #recordSuccess()} or {@link #recordError(ValidationResultType)}, and then
16+
* {@link #recordResponseTime(long)} for any attempt that reached the network (input and
17+
* configuration errors do not record a response time).
18+
* </p>
819
*/
920
public interface TurnstileMetrics {
21+
22+
/**
23+
* Records a validation attempt. Called once per invocation of
24+
* {@code validateTurnstileResponseDetailed}, regardless of outcome.
25+
*/
1026
void recordValidation();
27+
28+
/**
29+
* Records a successful validation. Called only when Cloudflare returns a success response.
30+
*/
1131
void recordSuccess();
1232

1333
/**
@@ -22,5 +42,12 @@ public interface TurnstileMetrics {
2242
*/
2343
void recordError(ValidationResultType type);
2444

45+
/**
46+
* Records the elapsed wall-clock time for a validation attempt that reached the network.
47+
* Not called for input or configuration errors that short-circuit before the HTTP request.
48+
*
49+
* @param milliseconds elapsed time in milliseconds from the start of the validation call
50+
* to its completion (success or failure)
51+
*/
2552
void recordResponseTime(long milliseconds);
2653
}

src/main/java/com/digitalsanctuary/cf/turnstile/service/TurnstileValidationService.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
* Service for validating responses from Cloudflare's Turnstile API.
3030
* <p>
3131
* This service provides methods to validate Turnstile tokens with the Cloudflare API, handling various error scenarios with appropriate exceptions
32-
* and detailed validation results. It also collects metrics on validation attempts, success/failure rates, and response times when metrics are
33-
* enabled.
32+
* and detailed validation results. It maintains internal counters for validation attempts, success/failure rates, and response times, and delegates
33+
* to a {@link com.digitalsanctuary.cf.turnstile.metrics.TurnstileMetrics} implementation (Micrometer-backed or no-op) for external metric recording.
3434
* </p>
3535
*/
3636
@Slf4j
@@ -207,18 +207,23 @@ public ValidationResult validateTurnstileResponseDetailed(String token, String r
207207
recordError(ValidationResultType.NETWORK_ERROR);
208208
throw new TurnstileNetworkException("Network error: " + e.getMessage(), e);
209209
} catch (TurnstileValidationException e) {
210+
log.debug("Turnstile token rejected by Cloudflare: {}", e.getMessage());
210211
recordError(ValidationResultType.INVALID_TOKEN);
211212
throw e;
212213
} catch (Exception e) {
213-
log.error("Unexpected error during Turnstile validation: {}", e.getMessage(), e);
214+
log.error("Unexpected {} during Turnstile validation: {}", e.getClass().getSimpleName(), e.getMessage(), e);
214215
recordError(ValidationResultType.NETWORK_ERROR);
215216
throw new TurnstileNetworkException("Unexpected error: " + e.getMessage(), e);
216217
} finally {
217218
long elapsed = System.currentTimeMillis() - startTime;
218219
lastResponseTime.set(elapsed);
219220
totalResponseTime.addAndGet(elapsed);
220221
responseCount.incrementAndGet();
221-
metrics.recordResponseTime(elapsed);
222+
try {
223+
metrics.recordResponseTime(elapsed);
224+
} catch (Exception metricsEx) {
225+
log.warn("Failed to record response time metric; validation result is unaffected: {}", metricsEx.getMessage(), metricsEx);
226+
}
222227
}
223228
}
224229

@@ -241,7 +246,6 @@ private ValidationResult executeValidationRequest(Map<String, String> requestBod
241246
return ValidationResult.success();
242247
} else {
243248
log.warn("Turnstile validation failed with error codes: {}", response.getErrorCodes());
244-
recordError(ValidationResultType.INVALID_TOKEN);
245249
throw new TurnstileValidationException("Token validation failed", response.getErrorCodes());
246250
}
247251
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.digitalsanctuary.cf.test.turnstile;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import io.micrometer.core.instrument.Counter;
6+
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import com.digitalsanctuary.cf.turnstile.dto.ValidationResult.ValidationResultType;
10+
import com.digitalsanctuary.cf.turnstile.metrics.MicrometerTurnstileMetrics;
11+
12+
/**
13+
* Unit tests for MicrometerTurnstileMetrics using an in-memory SimpleMeterRegistry.
14+
* Verifies that each method routes to the correct named Micrometer meter.
15+
*/
16+
class MicrometerTurnstileMetricsTest {
17+
18+
private SimpleMeterRegistry registry;
19+
private MicrometerTurnstileMetrics metrics;
20+
21+
@BeforeEach
22+
void setUp() {
23+
registry = new SimpleMeterRegistry();
24+
metrics = new MicrometerTurnstileMetrics(registry);
25+
}
26+
27+
@Test
28+
void recordValidation_incrementsRequestsCounter() {
29+
metrics.recordValidation();
30+
metrics.recordValidation();
31+
assertEquals(2.0, registry.counter("turnstile.validation.requests").count());
32+
}
33+
34+
@Test
35+
void recordSuccess_incrementsSuccessCounter() {
36+
metrics.recordSuccess();
37+
assertEquals(1.0, registry.counter("turnstile.validation.success").count());
38+
}
39+
40+
@Test
41+
void recordError_networkError_incrementsAggregateAndSubcounter() {
42+
metrics.recordError(ValidationResultType.NETWORK_ERROR);
43+
assertEquals(1.0, registry.counter("turnstile.validation.errors").count());
44+
assertEquals(1.0, registry.counter("turnstile.validation.errors.network").count());
45+
assertEquals(0.0, registry.counter("turnstile.validation.errors.config").count());
46+
assertEquals(0.0, registry.counter("turnstile.validation.errors.token").count());
47+
assertEquals(0.0, registry.counter("turnstile.validation.errors.input").count());
48+
}
49+
50+
@Test
51+
void recordError_configurationError_incrementsAggregateAndSubcounter() {
52+
metrics.recordError(ValidationResultType.CONFIGURATION_ERROR);
53+
assertEquals(1.0, registry.counter("turnstile.validation.errors").count());
54+
assertEquals(0.0, registry.counter("turnstile.validation.errors.network").count());
55+
assertEquals(1.0, registry.counter("turnstile.validation.errors.config").count());
56+
}
57+
58+
@Test
59+
void recordError_invalidToken_incrementsAggregateAndSubcounter() {
60+
metrics.recordError(ValidationResultType.INVALID_TOKEN);
61+
assertEquals(1.0, registry.counter("turnstile.validation.errors").count());
62+
assertEquals(1.0, registry.counter("turnstile.validation.errors.token").count());
63+
}
64+
65+
@Test
66+
void recordError_inputError_incrementsAggregateAndSubcounter() {
67+
metrics.recordError(ValidationResultType.INPUT_ERROR);
68+
assertEquals(1.0, registry.counter("turnstile.validation.errors").count());
69+
assertEquals(1.0, registry.counter("turnstile.validation.errors.input").count());
70+
}
71+
72+
@Test
73+
void recordResponseTime_recordsToTimer() {
74+
metrics.recordResponseTime(150L);
75+
metrics.recordResponseTime(250L);
76+
assertEquals(2L, registry.timer("turnstile.validation.response.time").count());
77+
}
78+
79+
@Test
80+
void constructor_rejectsNullRegistry() {
81+
assertThrows(NullPointerException.class, () -> new MicrometerTurnstileMetrics(null));
82+
}
83+
84+
@Test
85+
void multipleErrors_aggregateCountEqualsSum() {
86+
metrics.recordError(ValidationResultType.NETWORK_ERROR);
87+
metrics.recordError(ValidationResultType.INVALID_TOKEN);
88+
metrics.recordError(ValidationResultType.INPUT_ERROR);
89+
90+
Counter aggregate = registry.counter("turnstile.validation.errors");
91+
double subTotal = registry.counter("turnstile.validation.errors.network").count()
92+
+ registry.counter("turnstile.validation.errors.config").count()
93+
+ registry.counter("turnstile.validation.errors.token").count()
94+
+ registry.counter("turnstile.validation.errors.input").count();
95+
96+
assertEquals(aggregate.count(), subTotal, "Aggregate error count must equal sum of sub-counters");
97+
}
98+
}

0 commit comments

Comments
 (0)