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 remaining = context.response().headers().get(API_REMAINING_REQUEST_HEADER).stream().findFirst().orElse(null);
99
100 synchronized (this) {
101 if (remaining != null) {
102 this.remaining = Integer.valueOf(remaining, 10);
103 this.registry.gauge(API_REMAINING_METRICS_NAME, Tags.empty(), this.remaining);
104 }
105
106 log.debug("EAN-Search remaining requests. remaining={}, used={}", this.remaining, this.requestCounter.count());
107 }
108
109 return chain.next(context);
110 }
111
112
113
114
115
116
117
118
119 @Override
120 public void apply(final RequestTemplate request) throws EanSearchException {
121 if (
122 this.remaining == 0
123 && this.lastRequestWasToday()
124 ) {
125 log.error("Can't search for EAN any more. There is no remaining credit left. lastRequest='{}',", this.lastRequest);
126
127 throw new EanSearchTooManyRequestsException();
128 }
129
130 synchronized (this) {
131 this.lastRequest = OffsetDateTime.now(ZoneOffset.UTC);
132 }
133 }
134
135 private boolean lastRequestWasToday() {
136 return this.lastRequest.isAfter(
137 OffsetDateTime.now(ZoneOffset.UTC).toLocalDate()
138 .atStartOfDay(ZoneOffset.UTC)
139 .toOffsetDateTime()
140 );
141 }
142 }