| 37472 |
amit |
1 |
package com.smartdukaan.cron.monitored;
|
|
|
2 |
|
|
|
3 |
import java.util.Map;
|
|
|
4 |
import java.util.concurrent.ConcurrentHashMap;
|
|
|
5 |
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
6 |
import java.util.concurrent.atomic.AtomicLong;
|
|
|
7 |
|
|
|
8 |
import io.micrometer.core.instrument.Gauge;
|
|
|
9 |
import io.micrometer.core.instrument.MeterRegistry;
|
|
|
10 |
import io.micrometer.core.instrument.Tags;
|
|
|
11 |
import org.apache.logging.log4j.LogManager;
|
|
|
12 |
import org.apache.logging.log4j.Logger;
|
|
|
13 |
import org.springframework.stereotype.Component;
|
|
|
14 |
|
|
|
15 |
/**
|
|
|
16 |
* The per-brand IMEI activation funnel, on /actuator/prometheus.
|
|
|
17 |
*
|
|
|
18 |
* Until now the only way to know whether a brand was working was to grep free text out of
|
|
|
19 |
* a 1.2GB cron.log and reconstruct the funnel with awk. That is how two outages stayed
|
|
|
20 |
* invisible: oppo returned an empty map for a week (same 10 imeis recycling every 5
|
|
|
21 |
* minutes, nothing written), and the vivo captcha solver was dead for 46 days. Both had
|
|
|
22 |
* the same shape and neither raised anything.
|
|
|
23 |
*
|
|
|
24 |
* The funnel is deliberately staged, because WHICH stage stalls says what broke:
|
|
|
25 |
*
|
|
|
26 |
* due -> pool the pass snapshotted. 0 with a large backlog = the query is wrong.
|
|
|
27 |
* churned -> imeis the pass actually reached. Short of `due` = the pass ran out of
|
|
|
28 |
* day. This is the capacity signal.
|
|
|
29 |
* captcha_shown -> got as far as a rendered captcha. Collapses when the far end stops
|
|
|
30 |
* serving the widget -- realme's rate-limit signature, which looks like
|
|
|
31 |
* nothing else in the funnel.
|
|
|
32 |
* captcha_solved -> we broke it. shown/solved is the solver's real accuracy.
|
|
|
33 |
* answered -> the portal gave a verdict. THIS is what bumps createTimestamp and lets
|
|
|
34 |
* an imei leave the queue, so answered==0 while churned>0 is the precise
|
|
|
35 |
* signature of both historical outages.
|
|
|
36 |
* dates_found-> an activation date came back. Always a fraction of `answered`: most
|
|
|
37 |
* handsets legitimately are not activated yet, so this is a business
|
|
|
38 |
* number, not a health one. Do not alert on it alone.
|
|
|
39 |
*
|
|
|
40 |
* Rates are deliberately NOT published. solved/shown and dates/answered are one division
|
|
|
41 |
* in PromQL and storing them would freeze the numerator and denominator apart.
|
|
|
42 |
*
|
|
|
43 |
* -1 means "no pass has finished since this JVM started", so a scrape before the first
|
|
|
44 |
* run is distinguishable from a genuine zero -- same convention as {@link BalanceGauges}.
|
|
|
45 |
*/
|
|
|
46 |
@Component
|
|
|
47 |
public class ImeiActivationGauges {
|
|
|
48 |
|
|
|
49 |
private static final Logger LOGGER = LogManager.getLogger(ImeiActivationGauges.class);
|
|
|
50 |
|
|
|
51 |
private static final int NOT_RUN_YET = -1;
|
|
|
52 |
|
|
|
53 |
private final MeterRegistry meterRegistry;
|
|
|
54 |
private final Map<String, BrandFunnel> byBrand = new ConcurrentHashMap<>();
|
|
|
55 |
|
|
|
56 |
public ImeiActivationGauges(MeterRegistry meterRegistry) {
|
|
|
57 |
this.meterRegistry = meterRegistry;
|
|
|
58 |
}
|
|
|
59 |
|
|
|
60 |
/** Called once at the start of a brand's daily pass, with the snapshotted pool size. */
|
|
|
61 |
public void beginPass(String brand, int due) {
|
|
|
62 |
BrandFunnel f = funnel(brand);
|
|
|
63 |
f.reset(due);
|
|
|
64 |
LOGGER.info("[{}] pass begin: {} imeis due", brand, due);
|
|
|
65 |
}
|
|
|
66 |
|
|
|
67 |
/** A captcha widget rendered and is ready to be attacked. */
|
|
|
68 |
public void captchaShown(String brand) {
|
|
|
69 |
funnel(brand).captchaShown.incrementAndGet();
|
|
|
70 |
}
|
|
|
71 |
|
|
|
72 |
/** A captcha was broken. */
|
|
|
73 |
public void captchaSolved(String brand) {
|
|
|
74 |
funnel(brand).captchaSolved.incrementAndGet();
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
/**
|
|
|
78 |
* The portal returned a verdict for one imei. {@code dateFound} is false for a
|
|
|
79 |
* legitimate "not activated yet" -- that is still an answer, and still lets the row
|
|
|
80 |
* leave the queue.
|
|
|
81 |
*/
|
|
|
82 |
public void answered(String brand, boolean dateFound) {
|
|
|
83 |
BrandFunnel f = funnel(brand);
|
|
|
84 |
f.answered.incrementAndGet();
|
|
|
85 |
if (dateFound) {
|
|
|
86 |
f.datesFound.incrementAndGet();
|
|
|
87 |
}
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
/** One imei was consumed from the pass, whatever the outcome. */
|
|
|
91 |
public void churned(String brand, int count) {
|
|
|
92 |
funnel(brand).churned.addAndGet(count);
|
|
|
93 |
}
|
|
|
94 |
|
|
|
95 |
/** An exception escaped somewhere in this brand's pass. */
|
|
|
96 |
public void error(String brand) {
|
|
|
97 |
funnel(brand).errors.incrementAndGet();
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
/**
|
|
|
101 |
* Called once when a brand's pass ends. Stamps the duration and the completion time,
|
|
|
102 |
* and logs the one line worth grepping for.
|
|
|
103 |
*/
|
|
|
104 |
public void endPass(String brand) {
|
|
|
105 |
BrandFunnel f = funnel(brand);
|
|
|
106 |
long seconds = (System.currentTimeMillis() - f.startedAtMillis) / 1000;
|
|
|
107 |
f.runSeconds.set(seconds);
|
|
|
108 |
f.lastFinishEpoch.set(System.currentTimeMillis() / 1000);
|
|
|
109 |
|
|
|
110 |
int shown = f.captchaShown.get();
|
|
|
111 |
int solved = f.captchaSolved.get();
|
|
|
112 |
int answered = f.answered.get();
|
|
|
113 |
LOGGER.info("[{}] pass end: due={} churned={} captcha {}/{} ({}%) answered={} dates={} errors={} in {}s",
|
|
|
114 |
brand, f.due.get(), f.churned.get(), solved, shown, percent(solved, shown),
|
|
|
115 |
answered, f.datesFound.get(), f.errors.get(), seconds);
|
|
|
116 |
|
|
|
117 |
if (f.churned.get() > 0 && answered == 0) {
|
|
|
118 |
LOGGER.error("[{}] pass answered NOTHING across {} imeis -- the brand is down, not merely unlucky",
|
|
|
119 |
brand, f.churned.get());
|
|
|
120 |
}
|
|
|
121 |
if (f.churned.get() < f.due.get()) {
|
|
|
122 |
LOGGER.warn("[{}] pass did not finish: {} of {} imeis churned, the tail rolls into tomorrow",
|
|
|
123 |
brand, f.churned.get(), f.due.get());
|
|
|
124 |
}
|
|
|
125 |
}
|
|
|
126 |
|
|
|
127 |
private static int percent(int part, int whole) {
|
|
|
128 |
return whole == 0 ? 0 : (int) Math.round(100.0 * part / whole);
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
private BrandFunnel funnel(String brand) {
|
|
|
132 |
return byBrand.computeIfAbsent(brand, b -> new BrandFunnel(meterRegistry, b));
|
|
|
133 |
}
|
|
|
134 |
|
|
|
135 |
/**
|
|
|
136 |
* One brand's counters, registered against the meter registry the first time that
|
|
|
137 |
* brand is seen. Gauges hold last-pass values rather than lifetime totals: these
|
|
|
138 |
* passes run once a day, so "what did the last run do" is the question being asked,
|
|
|
139 |
* and a counter that only moves once a day is awkward to alert on.
|
|
|
140 |
*/
|
|
|
141 |
private static final class BrandFunnel {
|
|
|
142 |
|
|
|
143 |
private final AtomicInteger due = new AtomicInteger(NOT_RUN_YET);
|
|
|
144 |
private final AtomicInteger churned = new AtomicInteger(NOT_RUN_YET);
|
|
|
145 |
private final AtomicInteger captchaShown = new AtomicInteger(NOT_RUN_YET);
|
|
|
146 |
private final AtomicInteger captchaSolved = new AtomicInteger(NOT_RUN_YET);
|
|
|
147 |
private final AtomicInteger answered = new AtomicInteger(NOT_RUN_YET);
|
|
|
148 |
private final AtomicInteger datesFound = new AtomicInteger(NOT_RUN_YET);
|
|
|
149 |
private final AtomicInteger errors = new AtomicInteger(NOT_RUN_YET);
|
|
|
150 |
private final AtomicLong runSeconds = new AtomicLong(NOT_RUN_YET);
|
|
|
151 |
private final AtomicLong lastFinishEpoch = new AtomicLong(NOT_RUN_YET);
|
|
|
152 |
|
|
|
153 |
private volatile long startedAtMillis = System.currentTimeMillis();
|
|
|
154 |
|
|
|
155 |
BrandFunnel(MeterRegistry registry, String brand) {
|
|
|
156 |
Tags tags = Tags.of("brand", brand.toLowerCase());
|
|
|
157 |
gauge(registry, "imei_activation_due", tags, due,
|
|
|
158 |
"Imeis the last pass snapshotted for this brand; -1 = no pass yet");
|
|
|
159 |
gauge(registry, "imei_activation_churned", tags, churned,
|
|
|
160 |
"Imeis the last pass actually reached; short of due means it ran out of day");
|
|
|
161 |
gauge(registry, "imei_activation_captcha_shown", tags, captchaShown,
|
|
|
162 |
"Captchas that rendered; collapses when the far end stops serving the widget");
|
|
|
163 |
gauge(registry, "imei_activation_captcha_solved", tags, captchaSolved,
|
|
|
164 |
"Captchas broken; divide by captcha_shown for the solver's accuracy");
|
|
|
165 |
gauge(registry, "imei_activation_answered", tags, answered,
|
|
|
166 |
"Imeis the portal gave a verdict for; this is what lets a row leave the queue");
|
|
|
167 |
gauge(registry, "imei_activation_dates_found", tags, datesFound,
|
|
|
168 |
"Activation dates obtained; a fraction of answered by nature, not a health signal");
|
|
|
169 |
gauge(registry, "imei_activation_errors", tags, errors,
|
|
|
170 |
"Exceptions during the last pass for this brand");
|
|
|
171 |
Gauge.builder("imei_activation_run_seconds", runSeconds, AtomicLong::get)
|
|
|
172 |
.tags(tags)
|
|
|
173 |
.description("Wall-clock seconds of the last pass; -1 = no pass yet")
|
|
|
174 |
.register(registry);
|
|
|
175 |
Gauge.builder("imei_activation_last_finish_epoch_seconds", lastFinishEpoch, AtomicLong::get)
|
|
|
176 |
.tags(tags)
|
|
|
177 |
.description("Unix time the last pass finished; staleness alerts key off this")
|
|
|
178 |
.register(registry);
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
private static void gauge(MeterRegistry registry, String name, Tags tags,
|
|
|
182 |
AtomicInteger value, String description) {
|
|
|
183 |
Gauge.builder(name, value, AtomicInteger::get)
|
|
|
184 |
.tags(tags)
|
|
|
185 |
.description(description)
|
|
|
186 |
.register(registry);
|
|
|
187 |
}
|
|
|
188 |
|
|
|
189 |
void reset(int dueCount) {
|
|
|
190 |
startedAtMillis = System.currentTimeMillis();
|
|
|
191 |
due.set(dueCount);
|
|
|
192 |
churned.set(0);
|
|
|
193 |
captchaShown.set(0);
|
|
|
194 |
captchaSolved.set(0);
|
|
|
195 |
answered.set(0);
|
|
|
196 |
datesFound.set(0);
|
|
|
197 |
errors.set(0);
|
|
|
198 |
}
|
|
|
199 |
}
|
|
|
200 |
}
|