Skip to content
academia.sh

Lesson 19 / 21

Production Monitoring

Measuring how many records later three indicators show that a deployed defense has degraded: over a fictional stream of six hundred records the undefended baseline gives 0 blocks, 0 false alarms and 120 escaped harmful records, while the threshold 0.50 setup blocks 95 records, 11 of them false alarms, and lets 36 harmful ones through. After record three hundred and one two separate degradations begin: a traffic shift raises escaped harm from 36 to 84, a silent threshold change raises false alarms from 11 to 27. The escaped-harm proxy reports the wave after 20 records but never reports the threshold shift, the block rate sees the threshold after 29 records, and the label-dependent false alarm share gives 45 and 220 records over two runs. No indicator catches both degradations.

Published Updated

In this lesson

  • Separates the indicators computed from the trace record kept in production from the indicators that need labels.
  • Measures how many records later three indicators report two separate degradations and shows that none of them catches both.
  • Writes down, with numbers, that the delay of the label-dependent indicator stays unmeasurable between two runs.
Contents

For four lessons the measurement was done in the laboratory. The set was designed, the judge was compared with the human label, the agreement between raters was measured, and whether a prompt change moved the number above the run band was tested. All of it was done on a fixed set. The traffic of a system running in production is not fixed, it has no labels, and the settings of the defense change silently one day. The laboratory number does not say what is true after that day.

The procedure of indicators, alerts and dashboards was measured in the Observability and Operations courses of the Observability and Reliability and DevOps curricula; the step trace and token cost were counted in the AI Agents and Building Applications with Model APIs courses. None of it is repeated. The only thing this lesson measures is this: when a defense degrades, which indicator shows it after how many records.

  • EM51. The stream, the records, the classes and the defense are fictional. The text of the records is entirely harmless support requests and has no relation to the class; the class lives only in the structural feature profile. The stream comes from the same generator as the course’s record set, extended to 600 records.
  • EM52. The defense running in production is the model filter, threshold 0.50. As in every lesson, the undefended baseline contains no filter, marking or gate.
  • EM53. The trace record is kept for every request and carries no class label: decision, source and structural flags. A label arises only for records sent to human review.
  • EM54. Two degradations begin after record 301 and are reported nowhere. Wave: the share of corrupted input rises from 0.07 to 0.28. Threshold: the filter’s threshold drops from 0.50 to 0.30.
  • EM55. An indicator counts as having reported a degradation if it leaves the band for 10 consecutive records. The band is the smallest and largest window value over the 300 records before the change.
  • EM56. The two runs use seeds 20260218 and 20260219; a run changes the stream itself, not the defense.

The Stream and the Undefended Baseline

The first block builds the common definition of the course and extends it to a stream. Every function that runs is on the defense side.

# FICTIONAL. The text of the records is entirely harmless support requests and has
# no relation to the class; the class lives only in the structural feature profile.
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


def trace_id(m):
    h = 2166136261
    for c in str(m):
        h = ((h ^ ord(c)) * 16777619) & M32
    return h


CLASSES = ["clean", "indirect_instruction", "hidden_field_request", "privilege_escalation",
           "source_spoofing", "corrupted_input"]
SOURCES = ["user", "retrieved_document", "tool_output"]
FEATURES = ["instruction_mode", "role_change_request", "hidden_field_name", "side_effect_tool_name",
            "source_stamp_mismatch", "unusual_stamp_distribution", "excessive_length"]
PROFILE = {
    "clean":                [0.06, 0.02, 0.05, 0.03, 0.02, 0.04, 0.05],
    "indirect_instruction": [0.88, 0.42, 0.10, 0.30, 0.35, 0.08, 0.12],
    "hidden_field_request": [0.35, 0.15, 0.86, 0.12, 0.10, 0.06, 0.08],
    "privilege_escalation": [0.30, 0.25, 0.12, 0.84, 0.14, 0.05, 0.07],
    "source_spoofing":      [0.22, 0.55, 0.08, 0.10, 0.82, 0.07, 0.06],
    "corrupted_input":      [0.05, 0.03, 0.04, 0.03, 0.06, 0.80, 0.62],
}
SOURCE_SHARE = {
    "clean":                [0.72, 0.20, 0.08],
    "indirect_instruction": [0.05, 0.70, 0.25],
    "hidden_field_request": [0.55, 0.30, 0.15],
    "privilege_escalation": [0.40, 0.35, 0.25],
    "source_spoofing":      [0.10, 0.60, 0.30],
    "corrupted_input":      [0.65, 0.20, 0.15],
}
TEXT = ["bu ayki fatura tutari gecen aya gore yuksek geldi",
        "sayac endeksi yanlis girilmis olabilir",
        "mahallede kesinti ne zaman bitecek",
        "tarife basamagimi ogrenmek istiyorum",
        "ariza kaydim icin ekip ne zaman gelir",
        "adres degisikligi nasil yapilir"]
SHARE = [0.62, 0.10, 0.07, 0.07, 0.07, 0.07]        # distribution of the course's record set


def record(i, base, share):
    r = rng(base + 6151 * i)
    c = CLASSES[pick(r(), share)]
    source = SOURCES[pick(r(), SOURCE_SHARE[c])]
    feat = {o: (r() < p) for o, p in zip(FEATURES, PROFILE[c])}
    return {"no": i + 1, "class": c, "source": source, "features": feat,
            "text": TEXT[int(r() * len(TEXT))], "length": 40 + int(r() * 260)}


def harmful(k):
    return k["class"] != "clean"


WEIGHT = {"instruction_mode": 0.42, "role_change_request": 0.26, "hidden_field_name": 0.44,
          "side_effect_tool_name": 0.40, "source_stamp_mismatch": 0.38,
          "unusual_stamp_distribution": 0.30, "excessive_length": 0.12}
SOURCE_BONUS = {"user": 0.00, "retrieved_document": 0.18, "tool_output": 0.12}


def score(k):
    p = sum(WEIGHT[o] for o in FEATURES if k["features"][o]) + SOURCE_BONUS[k["source"]]
    r = rng(SEED ^ trace_id(k["no"]))
    return min(max(p + (r() - 0.5) * 0.20, 0.0), 1.6)


def model_filter(k, threshold=0.40):
    return score(k) >= threshold


def measure(records, decision):
    tp = fp = tn = fn = 0
    for k in records:
        flagged, actual = decision(k), harmful(k)
        if flagged and actual:
            tp += 1
        elif flagged and not actual:
            fp += 1
        elif not flagged and actual:
            fn += 1
        else:
            tn += 1
    precision = tp / (tp + fp) if tp + fp else 0.0
    recall = tp / (tp + fn) if tp + fn else 0.0
    return {"tp": tp, "fp": fp, "fn": fn, "tn": tn,
            "precision": round(precision, 4), "recall": round(recall, 4),
            "false_block_rate": round(fp / (fp + tn), 4) if fp + tn else 0.0}


N, SHIFT, THRESHOLD = 600, 300, 0.50                # stream , change point , deployed threshold
BEFORE = [record(i, SEED, SHARE) for i in range(SHIFT)]
print("stream", N, "records , shift after record", SHIFT + 1)
print("calibration period class counts:",
      {s: sum(k["class"] == s for k in BEFORE) for s in CLASSES})
print("\nsetup               blocked  false alarms   escaped harm      recall")
for name, decision in (("undefended", lambda k: False),
                       ("model 0.50", lambda k: model_filter(k, THRESHOLD))):
    m = measure(BEFORE, decision)
    print(f"{name:<16} {m['tp'] + m['fp']:>10} {m['fp']:>13} {m['fn']:>14} {m['recall']:>11.4f}")
print("a third of the blocked records go to a human:",
      len([k for k in BEFORE if model_filter(k, THRESHOLD)][::3]), "/", len(BEFORE), "records")
stream 600 records , shift after record 301
calibration period class counts: {'clean': 180, 'indirect_instruction': 30, 'hidden_field_request': 23, 'privilege_escalation': 13, 'source_spoofing': 30, 'corrupted_input': 24}

setup               blocked  false alarms   escaped harm      recall
undefended                0             0            120      0.0000
model 0.50               95            11             36      0.7000
a third of the blocked records go to a human: 32 / 300 records

The undefended baseline has nothing to monitor: since no record is blocked, the block rate is a constant zero, and since no record goes to review, the false alarm share is undefined. Monitoring is a property of the defense, not of the system. The deployed defense blocks 95 records, 11 of them false alarms, and 36 harmful ones escape; both numbers are written together. The cost of monitoring is visible here too: to be able to measure the false alarm share, 32 of 300 records go to a human.

Three Indicators, Two Degradations

The second block computes the three indicators from the trace record and starts the two degradations in separate streams.

# There are no labels in production: an indicator is computed only from the trace
# record, while labels come from the human decision on records sent to review.
WAVE = [0.41, 0.10, 0.07, 0.07, 0.07, 0.28]         # corrupted input wave
LIGHT = ("unusual_stamp_distribution", "excessive_length")   # alone, each stays below the threshold
WIN1, WIN2, CONSECUTIVE, REVIEW = 120, 240, 10, 3


def trace_record(k, threshold):
    """Fields kept in production. There is NO class label."""
    return {"blocked": model_filter(k, threshold), "source": k["source"],
            "light": any(k["features"][o] for o in LIGHT)}


def build_stream(base, share_after, threshold_after):
    ks = [record(i, base, SHARE if i < SHIFT else share_after) for i in range(N)]
    ts = [THRESHOLD if i < SHIFT else threshold_after for i in range(N)]
    return list(zip(ks, [trace_record(k, t) for k, t in zip(ks, ts)]))


def series(pairs):
    """Three indicators, sliding window. The second needs labels and uses a wider window."""
    g = {"block rate": [], "false alarm share": [], "escaped-harm proxy": []}
    for t in range(len(pairs)):
        if t + 1 >= WIN1:
            p = [z for _, z in pairs[t + 1 - WIN1:t + 1]]
            g["block rate"].append((t, sum(x["blocked"] for x in p) / WIN1))
            passed = [x for x in p if not x["blocked"]]
            g["escaped-harm proxy"].append(
                (t, sum(x["light"] for x in passed) / len(passed) if passed else 0.0))
        if t + 1 >= WIN2:
            blocked = [k for k, z in pairs[t + 1 - WIN2:t + 1] if z["blocked"]]
            reviewed = blocked[::REVIEW]         # a third of the blocked records go to a human
            g["false alarm share"].append(
                (t, sum(not harmful(k) for k in reviewed) / len(reviewed)))
    return g


def band(s):
    v = [x for t, x in s if t < SHIFT]
    return min(v), max(v)


def delay(s, lower, upper):
    """An indicator that leaves the band for CONSECUTIVE records counts as having reported."""
    count, first = 0, None
    for t, x in s:
        if t < SHIFT:
            continue
        if x < lower or x > upper:
            first, count = (t if count == 0 else first), count + 1
            if count >= CONSECUTIVE:
                return first - SHIFT + 1, "up" if x > upper else "down"
        else:
            count = 0
    return None, "-"


def actual_effect(part):
    """Needs labels; in production these numbers are unknown, here they are read because it is fictional."""
    decision = {k["no"]: z["blocked"] for k, z in part}
    return measure([k for k, _ in part], lambda k: decision[k["no"]])


for base in (SEED, 20260219):
    st = {"wave": build_stream(base, WAVE, THRESHOLD), "threshold": build_stream(base, SHARE, 0.30)}
    print(f"\nseed {base}   actual effect: escaped harm / false alarms / recall")
    for b, a in st.items():
        o, y = actual_effect(a[:SHIFT]), actual_effect(a[SHIFT:])
        print(f"  {b:<9} {o['fn']:>3} -> {y['fn']:<4} {o['fp']:>4} -> {y['fp']:<4}"
              f" {o['recall']:>8.4f} -> {y['recall']:.4f}")
    d = {b: series(a) for b, a in st.items()}
    print("  indicator             band            wave             threshold")
    for name in d["wave"]:
        lower, upper = band(d["wave"][name])
        h = []
        for b in ("wave", "threshold"):
            n, y = delay(d[b][name], lower, upper)
            h.append("not reported" if n is None else f"{n} records {y}")
        print(f"  {name:<21} {lower:.4f}-{upper:.4f}   {h[0]:<16} {h[1]}")
seed 20260218   actual effect: escaped harm / false alarms / recall
  wave       36 -> 84     11 -> 2      0.7000 -> 0.5359
  threshold  36 -> 8      11 -> 27     0.7000 -> 0.9355
  indicator             band            wave             threshold
  block rate            0.2667-0.3583   55 records up    29 records up
  false alarm share     0.0800-0.1600   140 records down 45 records up
  escaped-harm proxy    0.0610-0.1928   20 records up    not reported

seed 20260219   actual effect: escaped harm / false alarms / recall
  wave       33 -> 77     12 -> 11     0.7027 -> 0.5792
  threshold  33 -> 8      12 -> 36     0.7027 -> 0.9304
  indicator             band            wave             threshold
  block rate            0.2667-0.3750   54 records up    36 records up
  false alarm share     0.0385-0.2308   not reported     220 records up
  escaped-harm proxy    0.0723-0.1538   10 records up    229 records down

Which Indicator Saw What

The two degradations are mirror images. The wave raises escaped harm from 36 to 84 and lowers recall from 0.7000 to 0.5359; it lowers false alarms from 11 to 2, because the share of clean records shrank. The threshold shift does the opposite: escaped harm falls to 8, false alarms rise from 11 to 27. Each degradation improves one number of the defense while breaking the other. The rule of the course becomes a monitoring rule here: a dashboard that prints a single indicator can show every degradation as an improvement.

The escaped-harm proxy is the indicator that sees the wave earliest: 20 and 10 records. It needs no labels, because among records that pass unblocked it counts the share of two flags that the filter leaves, each alone, below the threshold. The same proxy never reports the threshold shift, or reports it after 229 records and in the wrong direction: when the threshold drops, more records are blocked, the share of flag carriers among the passing records shrinks, and the proxy falls. An alert rule waiting for a rise cannot see this degradation by definition.

Why the proxy works is written in the filter’s own weights. The two distinctive flags of the corrupted input class are unusual_stamp_distribution and excessive_length; their weights are 0.30 and 0.12. Even if both are carried, the total is 0.42, and on a record from a user, where the source bonus is zero, it stays below the 0.50 threshold. So the filter can catch this class only on records that carry a source bonus; as the wave grows, the block rate rises somewhat but escaped harm rises faster. The proxy counts exactly this remainder the filter cannot see, which is why it sees early.

The block rate reports both degradations (55 and 54; 29 and 36 records) but does not say which one it is: in both cases the direction is up. This indicator needs no labels, is the cheapest, and says the least; read alone, it cannot tell the wave from the threshold shift.

The false alarm share is the direct measure of the cost and the least reliable of the three. It reports the threshold shift after 45 and 220 records over the two runs; the difference between the runs is larger than the delay itself, so the delay of this indicator is unmeasurable. For the wave it reports once after 140 records in the down direction and the other time never. The cause is structural: in a window of 240 records the indicator gets labels from only a third of the blocked ones, so one reading rests on about thirty labels. The width of its band confirms it: 0.0800–0.1600 against 0.0385–0.2308, for the same defense.

None of the three indicators catches both degradations, and the one that catches one does not give the direction correctly. Monitoring is a defense, and it has two numbers: the degradation it sees and its cost: thirty-two human reviews, a wait of ten consecutive records, and a delay reaching two hundred records on one degradation.

Summary

  • Production monitoring is a property of the defense: on the undefended baseline the block rate is a constant zero, the false alarm share is undefined and there is no indicator to monitor.
  • The trace record carries no class label; a label arises only on records sent to review, and 32 of 300 records go to a human.
  • The escaped-harm proxy reports the wave after 20 and 10 records and reports the threshold shift never or in the wrong direction; the block rate reports both but does not tell them apart.
  • The delay of the label-dependent false alarm share is 45 and 220 records over two runs; since the difference is larger than the delay itself, this indicator does not count as measured.

Next Step

In this lesson an indicator was computed from the trace record and from the review of blocked records. There is one more signal that comes from the system itself and looks free: the user saying they did not like the reply, sending a correction, asking the same question again. This signal can return to the evaluation set and can tune the defense of the next round. The question not asked is this: who can send this signal. A blocked user never sees the reply and has nothing to say; only the voice of those who pass reaches the loop. The next lesson measures the share this signal covers and the direction of the bias.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close