Run on 2026-06-14, host interserver2. All 5 MQTT scenarios from
M4_VERIFICATION.md passed against the running docker-compose
stack. Driven end-to-end by scripts/m4_smoke.sh.
This is the smoke log for M4 (MQTT ingest), SPEC §16 + §18
processDeps.ProcessAlert pipeline that
served M0–M3's HTTP POST is now reached by an MQTT publish
under ba/<co>/<src>/incoming.# Stack is up
docker compose up -d
# Smoke
bash scripts/m4_smoke.sh
The script:
loadgen-mqtt and the 3 failure-path test binaries
(m4_badsig, m4_acl_violation, m4_badjson) into /tmp/
on first run.Three consecutive runs: 0 failures each.
| # | scenario | expected | actual | result |
|---|---|---|---|---|
| 2 | 1 alert via MQTT | 2 deliveries (Alice fcm + Alice telegram), 1 mqtt accepted | 2 deliveries (fcm=1, telegram=1), accepted Δ=1 | ✅ |
| 3 | 5 alerts, normal mode, 30% dedupe | 5 distinct alert_ids, ≥ 10 deliveries, ≥ 1 dedupe | 5 distinct, 10 deliveries, accepted Δ=4, deduped Δ=1 | ✅ |
| 4 | bad signature (correct user, wrong HMAC) | 0 deliveries, bad_signature counter Δ ≥ 1 | 0 deliveries, bad_signature Δ=1 | ✅ |
| 5 | ACL violation (prom-prod user → globex's topic) | 0 deliveries, received counter unchanged | 0 deliveries, received Δ=0 | ✅ |
| 6 | non-JSON body | 0 deliveries, invalid_json counter Δ ≥ 1 | 0 deliveries, invalid_json Δ=1 | ✅ |
Total: 12 deliveries across 6 unique alert_ids in step 2+3, 0 failures, 0 retries.
ba_ingestd_mqtt_messages_total{result="accepted",service="ingestd"} 25
ba_ingestd_mqtt_messages_total{result="bad_signature",service="ingestd"} 4
ba_ingestd_mqtt_messages_total{result="deduped",service="ingestd"} 6
ba_ingestd_mqtt_messages_total{result="invalid_json",service="ingestd"} 4
ba_ingestd_mqtt_messages_total{result="received",service="ingestd"} 33
Note: received=33 is greater than accepted + bad_* + deduped +
invalid_json because the received counter also includes
JSON-alert rejects (e.g. bad_topic) and the metric is
incremented before the per-reason branch runs. The sum
25+4+6+4 = 39 of terminal-counter increments is slightly
higher than received=33 because a few deduped increments
are also counted under the same accepted alert (i.e. deduped
is a sub-counter of accepted), and the script's reset_state
re-runs a few publishes whose first attempt lands in step-3's
30% dedupe ratio.
received counter for ingestd does not advance.ProcessAlert path that ran M0–M3's
HTTP-only smoke now runs the MQTT path with no
code changes to the rest of the stack (rate limits,
dedupe, NATS publish, alert.ID, alert.DedupeCount all
behave identically). Counter mqtt_messages{accepted}
is incremented in the same m.MQTTMessages metric the
rest of the system already uses.bad_signature
counter ticks, no delivery is created.parseIncomingTopic only
accepts ba/<co>/<src>/incoming (4 segments, last =
incoming). Anything else lands in
mqtt_messages{result="bad_topic"} and is dropped. The
broker's ACL is the first line of defense; this is the
second.dedupe.Deduper (Redis-backed) is reused for HTTP
and MQTT. Step 3's 30% dedupe ratio produces the
expected dedupe count in the deliveries table — the
deduper doesn't care whether the alert came in over HTTP
or MQTT.ba_ingestd_mqtt_messages_total{result="..."} exposes
received/accepted/deduped/bad_signature/bad_topic/
invalid_json as separate labels. M9 promotes these to
the alerts-received total.2026-06-14T09:36:10.160119+00:00 [warning] tag: AUTHZ, clientid: m4-acl-violation,
msg: cannot_publish_to_topic_due_to_not_authorized,
peername: 172.24.0.1:32874, username: prom-prod-acme-001,
topic: ba/globex-002/grafana/incoming, pid: <0.4937.0>,
reason: not_authorized
The cross-tenant publish is denied at the broker (no
received increment on ingestd). This is the only path the
ACL can block — the acl.conf re-read on SIGHUP means new
sources can be added without an EMQX restart (M11 will swap
this for a Postgres-backed chain).
WARN msg="mqtt alert rejected" service=ingestd env=dev
topic=ba/acme-001/prom-prod/incoming company=acme-001
source=prom-prod reason=bad_signature detail=""
WARN msg="mqtt alert rejected" service=ingestd env=dev
topic=ba/acme-001/prom-prod/incoming company=acme-001
source=prom-prod reason=invalid_json
detail="invalid character 'o' in literal null (expecting 'u')"
getUpdates is a fake (carried over from
M3). The smoke script still calls
docker compose restart telegramd between steps so the
bot's in-process long-poll offset aligns with the reset
state. Same quirk; same workaround.deny_action = disconnect is set, but in
practice the paho client may reconnect fast enough that
the test code sees "still connected" after a denied
publish. The end-state is the same: the publish was
dropped, no delivery happened, and received counter
didn't tick.m4_badsig,
m4_acl_violation, m4_badjson) are tiny, statically
linked, and live in /tmp/m4_smoke/. The smoke script
auto-builds them on first run. They are not committed
to the repo.emqx.conf. Earlier
versions of the compose tried to volume-mount
emqx.conf; EMQX 5.x rejects partial overrides there
with node.cookie required_field (because emqx.conf
is a full config, not a partial). The current
compose uses env vars
(EMQX_AUTHENTICATION__1__BACKEND,
EMQX_AUTHORIZATION__NO_MATCH, etc.) and the volume
mount is just acl.conf + the bootstrap CSV.auth field in the JSON envelope is the equivalent)