1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package de.kaiserpfalzedv.services.eansearch.filter;
19
20 import java.time.OffsetDateTime;
21 import java.time.ZoneOffset;
22 import java.time.temporal.ChronoUnit;
23
24 import org.springframework.beans.factory.annotation.Autowired;
25
26 import de.kaiserpfalzedv.services.eansearch.mapper.EanSearchException;
27 import de.kaiserpfalzedv.services.eansearch.mapper.EanSearchTooManyRequestsException;
28 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
29 import feign.InvocationContext;
30 import feign.RequestInterceptor;
31 import feign.RequestTemplate;
32 import feign.ResponseInterceptor;
33 import io.micrometer.core.instrument.Counter;
34 import io.micrometer.core.instrument.MeterRegistry;
35 import io.micrometer.core.instrument.Tags;
36 import jakarta.annotation.PostConstruct;
37 import jakarta.annotation.PreDestroy;
38 import jakarta.inject.Singleton;
39 import lombok.RequiredArgsConstructor;
40 import lombok.extern.slf4j.Slf4j;
41
42
43
44
45
46
47
48 @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "lombok created constructor used.")
49 @Singleton
50 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
51 @Slf4j
52 public class RequestLimitFilter implements RequestInterceptor, ResponseInterceptor {
53 private static final String API_REMAINING_REQUEST_HEADER = "X-Credits-Remaining";
54 private static final String API_REMAINING_METRICS_NAME = "ean-search.credits.remaining";
55 private static final String API_REQUESTS_HANDLED_SINCE_START = "ean-search.credits.used-since-start";
56
57 private final MeterRegistry registry;
58 private Counter requestCounter;
59
60
61 private int remaining = -1;
62 private OffsetDateTime lastRequest = OffsetDateTime.now(ZoneOffset.UTC).minus(1, ChronoUnit.DAYS);
63
64 @PostConstruct
65 public void registerMetric() {
66 this.requestCounter = this.registry.counter(API_REQUESTS_HANDLED_SINCE_START, Tags.empty());
67 }
68
69 @PreDestroy
70 public void unregisterMetrics() {
71 this.requestCounter.close();
72 }
73
74 public synchronized int getRemaining() {
75 return this.remaining;
76 }
77
78
79
80
81 public void reset() {
82 synchronized(this) {
83 this.remaining = -1;
84 log.info("EAN Search credit reporter reset. Requests should now result in requests to the API.");
85 }
86 }
87
88
89
90
91
92
93
94
95
96 @Override
97 public Object intercept(final InvocationContext context, final Chain chain) throws Exception {
98 final String remainingRequestCount = context.response().headers().get(API_REMAINING_REQUEST_HEADER).stream().findFirst().orElse(null);
99
100 synchronized (this) {
101 if (remainingRequestCount != null) {
102 remaining = Integer.valueOf(remainingRequestCount, 10);
103 registry.gauge(API_REMAINING_METRICS_NAME, Tags.empty(), remaining);
104 }
105 }
106
107 log.debug("EAN-Search remaining requests. remaining={}, used={}", remaining, requestCounter.count());
108 return chain.next(context);
109 }
110
111
112
113
114
115
116
117
118 @Override
119 public void apply(final RequestTemplate request) throws EanSearchException {
120 if (
121 this.remaining == 0
122 && this.lastRequestWasToday()
123 ) {
124 log.error("Can't search for EAN any more. There is no remaining credit left. lastRequest='{}',", this.lastRequest);
125
126 throw new EanSearchTooManyRequestsException();
127 }
128
129 synchronized (this) {
130 this.lastRequest = OffsetDateTime.now(ZoneOffset.UTC);
131 }
132 }
133
134 private boolean lastRequestWasToday() {
135 return this.lastRequest.isAfter(
136 OffsetDateTime.now(ZoneOffset.UTC).toLocalDate()
137 .atStartOfDay(ZoneOffset.UTC)
138 .toOffsetDateTime()
139 );
140 }
141 }