Repository navigation
Auto-promote high-volume crawlers and API partners out of the other bucket - #13673
Merged
Merged
Conversation
Both the crawler and partner API monitors classified traffic against hardcoded allowlists, collapsing everything else into a single `other` series and discarding the agent name -- which the crawler path already had in hand. `other` reached 7.89K against labelled crawlers of 338/280, and 2.27K against labelled partners of 74/25/11. Promotion is now decided on volume over a rolling 1h window (60 one-minute ticks) while emission stays per-tick and unchanged, so no existing series changes meaning or scale. A crawler sustaining 75 requests an hour, or a partner sustaining 50, gets its own label with no code change. max_labels caps how many are promoted at once, so the metric tree stays bounded even if the floor is mistuned. - promotion.py: RollingPromoter, plus the shared parsing and label sanitizing. Labels are length-bounded and `other` is reserved, since User-Agent is attacker-controlled. - utils.sh: `other` is no longer computed here. list_unknown_bot_counts prints per-agent counts for bots outside obfi_grep_bots' list, and monitor.py promotes from those and sums the remainder into `other`. - monitor.py: the committed UA snapshot becomes a pin list rather than a gate, so quiet-but-known partners keep their series while new ones are promoted on volume. Also fixes duplicate first-token partner UAs clobbering each other rather than summing (Pinakes-POC, ReRoll, WonderclubBot).
The bash test for log_recent_bot_traffic asserted the exact graphite line `<bucket>.other 0 <ts>`. When the promotion decision moved to python that emission moved with it, and the assertion was dropped without being replaced: test_promotion only checked that split() returns an `other` key, which is the promoter in isolation. Nothing covered the metric path actually being submitted. Split event construction out of submission as promoted_events() so the paths can be asserted without a socket, and cover three cases that the deleted line used to: nothing promoted, something promoted alongside it, and a completely quiet tick (which must still report zero rather than let the series go stale). Also walks the full bash -> python path end to end, so the metric cannot silently stop being submitted if either half changes.
Five issues, all of which meant this either did not work or regressed
something. Kept the fixes minimal rather than adding machinery.
1. Labels were minted from the whole log line, not the User-Agent. A request
for /robots.txt became a crawler called "robot", /works/OL1W/I-Robot became
"i_robot", and `?q=a@b.com` became a partner called "GET" -- so any visitor
could name a metric by requesting a URL. Both pipelines now read the UA
field before matching.
2. Pinned names competed in the ranking despite already having a guaranteed
series, so they ate the slots and starved the new agents this exists to
find. Replaying the committed snapshot plus 10 new partners: 5 of 10 were
promoted before, 10 of 10 now.
3. Promoted and pinned labels emitted nothing on a quiet minute. An agent near
the floor sends nothing in most minutes, so their series rendered as broken
sawtooths. Every kept label now reports 0.
4. Only `other` was reserved, but utils.sh emits `<bucket>.non_bot` into the
same bucket from bash, and monitor.py submits second. A UA of `non-bot`
overwrote the real human-traffic count. Reserve both.
5. parse_uniq_c used splitlines(), which also breaks on \v \f \x1c-\x1e -- one
crafted UA could forge a row with a count of its choosing against a pinned
partner. Use split("\n").
Also: a socket timeout on graphite submission, so a blocked port cannot wedge
a job permanently and take every other metric it submits down with it.
Adds test_monitor.py. The jobs themselves had no coverage at all, so the
metric paths could stop being submitted with the suite green -- which is
exactly what the previous commit claimed to have fixed and did not.
Corrects two comments that overstated what the code guarantees.
…fects Second adversarial review round. 1. `grep -o` emitted every match in a UA, so an agent that names itself twice got two series and double its volume. "OAI-SearchBot/1.0; +https://openai.com/searchbot" produced both `oai_searchbot` and a phantom `searchbot`, each at the full count, and burned two of the 25 slots. Take only the first match per UA. (obfi_top_bots has the same bug -- see the FIXME in test_utils_sh.py -- so this stops copying it into new series.) 2. `non_bot` still excluded on the whole line while the bot path matched only the UA, so a browser request for /robots.txt was counted in neither and the two stopped adding up to the total. Scope `non_bot` to the UA field too. 3. obfi_grep_bots' patterns contain literal hyphens, but we sed separators to underscores, so a UA of "Aranet_SearchBot" slipped the filter and then minted the label obfi_top_bots uses for the real Aranet-SearchBot -- overwriting it, since monitor.py submits second. Normalise separators to "-", re-filter, then convert to "_". 4. split() re-consumed `pinned` after promoted() had already exhausted it, so a generator would have silently dropped every pin. Materialise it once. Also reverts zero-filling for pinned names. Zero-filling promoted labels stops new series rendering as sawtooths, but doing it for the 89 pinned partners would change what every mean/average panel over those existing series reads, with no traffic change behind it. Promoted labels are zero-filled; pinned stay sparse, exactly as before. Corrects three comments that overstated what the code guarantees: the splitlines() vector is unreachable given nginx escaping, a blocked port costs two ticks rather than wedging a job forever, and this no longer counts occurrences the way obfi_top_bots does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #13672.
otherhad become the dominant series in both the crawler and partner API panels, so neither panel told you who was actually hitting the site. Both paths classified against a hardcoded allowlist that ages; anything off the list collapsed intootherand the agent's name was discarded.Read the caveat below before the design section — two adversarial review rounds found that the numbers justifying this issue were substantially a measurement artifact, and that changes how much of this change is load-bearing.
Caveat: the issue's numbers were inflated, and the two-line fix may be most of the win
The issue justifies the work with
otherat 7.89K against labelled crawlers of 338/280, and 2.27K against labelled partners of 74/25/11. Those came from matching the whole nginx log line rather than the User-Agent field. So:othercounted/robots.txtfetched by ordinary browsers —\b[a-z_-]+botmatches "robot" inside "robots" — and book titles, so/works/OL1W/I-Robotregistered as a crawler namedi_robotothercounted anything with@anywhere in the line, so its largest entries were "partners" literally namedGET,POSTandhttps— request lines and referrers with an email in a query stringA reviewer measured 622 req/min of ordinary human traffic landing in partner
otheras a partner calledhttps.Scoping both pipelines to the UA field (
awk -F'"' '{print $6}') removes that. That two-line change is the load-bearing fix;promotion.pyhandles whatever is left. Whether the remainder justifies a rolling-window promoter can only be settled by watching realotherfor a day after this deploys. Ifpartners.othercomes back near-empty, the partner half of the promoter can be deleted — the 89-name pin list already covers the known world, and new partners introduce themselves by email before they do by volume.The threshold calibration is unaffected: I checked that the committed snapshot is 92 rows of genuine partner UAs with zero request-lines, so
>=50selecting 47 agents covering 91.3% of volume was computed over clean data. What was inflated is the size of the bucket those thresholds drain, not where they sit on the distribution.No existing series disappears
A PR titled "auto-promote out of
other" invites the assumption that the allowlist was deleted and quiet partners like Gleeph (11) and Bookscovery (25) are about to vanish. They are not. The snapshot stops being a gate and becomes a pin list: pinned partners keep their series however quiet, bypassing both the threshold and the cap, while new partners are promoted on volume.Verified rather than assumed — I reimplemented the old
extract_agent_countsand diffed its labelling against the new path: 89 labels before, 89 after, none lost, none gained. Two reviewers independently confirmed it.Three series will step at deploy. That is bugs being fixed, not traffic changing.
Partner undercounts. The old code did
agent_counts[name] = count— assignment, not accumulation — so UA variants sharing a first token overwrote each other:partners.otherdrops sharply, because it was largelyGET/POST/referrer junk.bot_traffic.otherchanges definition, from whole-line lines to UA-field first-match occurrences. Direction is mixed: browser noise disappears, self-naming crawlers no longer double-count. Expect a step; it needs a dashboard annotation.How promotion works
Both jobs tick once a minute, but the volume justifying a label accrues over hours — so the promotion decision and the metric emission are separated. Emission is unchanged: each tick reports that tick's counts. Only which names get their own label is decided over a rolling 60-tick window.
Applying the thresholds literally per-minute would promote nobody. Simulated at the issue's own volumes:
max_labels(25 crawlers / 50 partners) caps concurrent labels so the legend cannot fill with drive-by bots, and promotion lapses when an agent ages out of the window, so a one-off burst holds a slot for at most an hour.What two adversarial review rounds found and fixed
robot,i_robot,GET,https. Fixed: UA field only.grep -oemitted every match, and polite crawlers name themselves twice:OAI-SearchBot/1.0; +https://openai.com/searchbotproducedoai_searchbotand a phantomsearchbot, each at full count, burning two slots. Fixed: first match only.non_botandotherstopped summing to the total once the bot path was UA-scoped andnon_botwas not — a browser fetching/robots.txtwas counted in neither (~11.6% of lines on one reviewer's fixture). Both UA-scoped now.otherwas reserved, but bash emits<bucket>.non_botinto the same bucket and Python submits second, sonon-botoverwrote the real human-traffic count.Aranet_SearchBotlikewise slipped the known-bot filter (literal hyphens) and minted the real bot's label. Both closed.Plus a socket timeout on graphite submission, and
split("\n")oversplitlines().Limitations, stated plainly
max_labelsbounds concurrent series, not whisper files; a promoted name that later drops out leaves its file behind.MAX_CREATES_PER_MINUTE = infin olsystem'scarbon.conf, so there is no create throttle — the cost is disk and inodes, not starvation of other services. (That config is in olsystem; I could not confirm from here that it is live on the graphite host.)max_labelscaps concurrency — but someone willing to sustain volume can still churn new metric paths or evict a legitimate agent from the ranking. Deliberately not hardened further; this is observability tooling.other, notnon_bot. The regex needs a letter immediately before bot/spider/crawler, soScrapy,python-requests,facebookexternalhit,Go-http-client,Screaming Frog SEO SpiderandBarkrowlerremain invisible. If those are a large share of traffic, that is a separate change.obfi.sh/USER_AGENT_BOTSduplication is untouched; this adds no new copy of the bot list, and theFIXMEstays. ($is_sus_user_agentin web_nginx.conf is a 403 blocklist, not a third copy.)Before merge
nc -zv graphite.us.archive.org 2004from ol-covers0. That host only ever used plaintext 2003; the promotion path uses pickle 2004. The server binds it on0.0.0.0exactly as 2003, and ol-www0/ol-home0/ol-solr0-2 already submit there, so this is expected to work — but covers0 has not done it before..other. Moving spikes out ofotheris the point, which defeats any "unknown traffic spike" alert pointed at it. Re-point at an aggregate such assumSeries(stats.ol.partners.*).Testing
49 tests, pre-commit clean.
test_monitor.pycovers the jobs themselves — metric paths and submission — because the helpers being green did not stop the submission from being deletable without a failure. Beyond the assertions, the fixes above were each verified by running the real bash pipeline against the reviewers' own repro inputs.