Subversion Repositories SmartDukaan

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
}