Repository navigation
crawler: learn metrics from prebuilt exporters and OTel collector scrape jobs - #170
Merged
Merged
Conversation
…ape jobs - metrics-manifest.json / *.metrics-manifest.json: a new artefact kind, entries declared (origin_kind exporter-manifest), ranked first - .prom / .metrics exposition snapshots: # HELP / # TYPE and label names, never label values, never from test or fixture paths - an exporter's config read against a pluggable family catalogue (tools/lib/exporter-catalogue.mjs, built-in IBM mq-metric-samples entry; --exporter-catalogue / opts.exporterCatalogue): inferred candidates - an OTel collector's prometheus receiver scrape jobs, with or without service.pipelines; the job records receiver, target count and the exporters it leaves by (kafka topic / endpoint without secrets); the collector's metrics exporter replaces the prometheusremotewrite scaffold - crawler.discovered.exporters; an environment scope warns about the scrape-job and exporter-config files it leaves out - packc emit --exporter <url> (tools/emit-manifest.mjs) writes the manifest from a live /metrics endpoint; --from reads a saved one Goldens byte-identical. New suite tools/test-crawl-exporters.mjs; vendor manifest regenerated; CHANGELOG, DOWNSTREAM §18, README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
safeSourceUrl was a URL parse with a first-@ textual fallback: a password holding '@', '/' or '?' (user:p@ss@h, https://u:pa/ss@h/v1, //u:p@h, https://u:12/ss@h) reached crawler.discovered.scrape_job_origins and the manifest source. It is now textual: everything before the last '@', then from the first '?' or '#', is dropped. packc emit echoed an unparseable --exporter verbatim, and an unparseable redirect Location threw with the full request URL (query token included) on stderr. The echo is safeSourceUrl's form; the redirect refusal names the target with the query as keys only, so passing it with the values works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…fixed manifest or config A metrics-manifest.json that was not JSON put the parser's message, which quotes the file's first characters, into summary.warnings. A leading BOM hid an mq-metric-samples config from the key precheck (the file read as unknown and left the environment-scope warning) and failed a manifest's JSON.parse; one U+FEFF is now stripped, as mini-yaml does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…clared pipeline overrides A collector config split across --config files (components in one, service in another) took its metrics exporter from whichever file sorted first: the components file's first metrics-capable exporter (otlp/tempo) beat the service file's declared metrics pipeline (prometheusremotewrite), with a guess warning and a second pipeline counted. The guess is now applied after every file is walked, and only when no pipeline declares one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…ics families named as stored A .prom under __fixtures__/, __mocks__/, test-data/ or test-fixtures/ was read as an exporter snapshot: the test-path rule knew only tests/fixtures/ testdata. isTestOrFixturePath covers the usual spellings. An OpenMetrics counter 'jobs' and info 'build' were declared as 'jobs' and 'build', names Prometheus never stores (jobs_total, build_info), so a rule reading jobs_total matched nothing the snapshot declared. A family is now named by its sample's stored name, or with the suffix OpenMetrics implies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…hed files leave isCrawlerFile accepted every config.yaml, files/**/*.yaml and .prom, and the route keeps the first 200 matches in tree order: a monorepo with 210 apps/*/config.yaml fetched none of the alertmanager, prometheus, rules and collector files develop fetched. isCrawlerFile is develop's rule plus the metrics manifest; the broad names are isExporterCandidateFile's, and selectCrawlerFiles fetches them only after every established file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…ates and scopes are own names only detectArtefactKind used a catalogue it was given as already resolved: the built-in ibm-mq entry was gone (an mq config read as unknown), and the array form or an entry without match.keys threw, though the CHANGELOG says it merges like crawlFiles. asResolvedCatalogue resolves anything not already resolved. A family gated on 'toString' or scoped on 'constructor' passed validation through the prototype and was always emitted; gates, scopes and config paths are read as own properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…s and values with scrape jobs A collector config in a chart's values.yaml (top level, or under config: as the opentelemetry-collector chart writes it) was read as helm-values only: no scrape job, no kafka exporter, even with --env all. It is walked as a collector, before the values' images. The environment-scope warning named excluded scrape-config, collector and exporter-config files but not a Helm template or values file carrying the same jobs, which were dropped silently; collectionEvidenceOf now reads them as their walkers do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…g=value and echoes no URL-like argument safeSourceUrl's query cut stopped at a line break (no s flag), so 'http://mq/metrics?token=X\nx' kept its token in the manifest source and a collector endpoint; a URL parser drops the break and sends the query. Tabs and line breaks go first, and the cut runs to the end. packc emit printed '--exporter=<url with credentials>' whole as an unknown argument; it now reads --flag=value and never echoes a URL-like unknown argument. A body that ended early threw an uncaught stack; it is a refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…; umbrella values; null pipelines walkHelmValues walked the collector config before the images with no guard: the chart's own 'pipelines.metrics: null', a non-array list or a non-string name threw, the values file failed to parse, and backends develop found from it were lost. The walk is guarded (a warning, then the images), and a null pipeline is skipped in any collector config. An umbrella chart's subchart <key>.config is read and named in the scope warning. detectArtefactKind takes null options again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…, __snapshots__/ or *_spec.* The fixture-path rule listed single words: e2e/, integration-tests/, e2e-tests/, unit_tests/, testing/, __snapshots__/ and mq_spec.prom were still read as exporter snapshots (and fetched by the GitHub crawl). A folder is a test folder when its name is such a word, alone or joined to others by - or _; a longer word that only contains one (contest/, latest/) is not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…d pipeline that lists its receiver The guess was deferred for spec.pipelines but not for the scrape job: in a split config the job still named the guessed otlp exporter while the other file's metrics pipeline listed its receiver with prometheusremotewrite, the first-of-several warning was dropped, the receiver was pushed twice and the config counted as a second pipeline. settleCollectorGuesses now settles the jobs, receivers and the metrics exporter after the walk; the docs say a metrics backend image keeps precedence over a guess, as over a pipeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…; a malformed collector job costs only itself - a scrape receiver no metrics pipeline in its own file lists (traces only, metrics: null) got no exporters, though another file's metrics pipeline listed it; it is settled after the walk like a pipeline-less config's - an umbrella chart's disabled subchart (enabled: false, in the file or the environment's overlay) still had its collector config read - one malformed job in a collector config threw and lost the file's other jobs and documents (develop read the rest); it is warned about and skipped - the first-of-several warning fired for a guess no job carried - the CHANGELOG's precedence sentence: image and declared pipeline fill the metrics exporter in walk order, as before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
…t folders; packc emit never echoes a URL given to --from The folder rule dropped test[-_]?fixtures? when it was reworked (Gradle's src/testFixtures/ read as snapshots) and never had integrationTest/, unittests/ or e2etests/. packc emit echoed '--from <url with credentials>' in its cannot-read error and a schemeless host:port?token=… unknown argument whole; a URL-like value is never shown, and --from refuses one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
… subcharts; camelCase test source sets - safeSourceUrl dropped everything before the last @ before cutting the query, so 'https://h/push?user=ops@corp&api_key=X' kept 'corp&api_key=X' (endpoint, manifest source, emit's stderr). A '?' or '#' before the last '@' now records nothing; a value with nothing past its scheme is omitted. - a subchart disabled in values.yaml and re-enabled by values-prod.yaml kept its collector skipped; a crawl reads the merged toggles (overlay wins). - componentTest/, smokeTest/, intTest/, testUtils/, src/it/ are test folders. - packc emit echoed --generated-at and threw a stack for an unwritable --out; neither does now, and a --from file named with an @ is read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
- isExporterCandidateFile lowercased the path before the test-folder rule, so componentTest/, smokeTest/ and intTest/ snapshots were fetched (and could use up the 200-file budget) though the crawler then ignored them. - packc emit named a URL-like --from or --out in three messages (no metric family, cannot read, the success line), and a redirect's bare ?<token> as a query key; the query is named by its key=value keys only. - CHANGELOG: only a path '@' keeps what follows it (a query '@' records nothing since ff78d6d). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
MoebiusX
marked this pull request as ready for review
October 15, 2026 20:23
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.
The repo and folder crawler now learns metrics from prebuilt exporters and from an OTel collector's scrape jobs. The motivating case: a service runs IBM's mq-metric-samples exporter (its repo is a Dockerfile plus
config.yaml), and an OTel collector scrapes a fleet of those exporters and ships to Kafka. Before this change, crawling those repos yielded 0 metrics and 0 scrape jobs.What changes
Metrics from a prebuilt exporter (
tools/lib/exporter-metrics.mjs, new and listed), strongest first:origin_kindmetrics-manifest.json,*.metrics-manifest.jsonexporter-manifest*.prom,*.metrics(never under a test, fixture or mock folder)exporter-snapshotexporter-configmetricOriginRankputs the manifest first and the inferred config last. The relative order of the existing origin kinds is unchanged.jobscounter isjobs_total).tools/lib/exporter-catalogue.mjs, with a small built-in IBM MQ entry). A downstream passes a larger one withpackc x-ray --exporter-catalogue <file.json>orcrawlFiles(files, { exporterCatalogue }). Its entries replace or extend the built-in ones. The format is indocs/DOWNSTREAM.md§18.crawler.discovered.exporterslists each exporter and its sources.An OTel collector's scrape jobs:
config.scrape_configsare read, with or withoutservice.pipelines. They are found in a config file, a Helm ConfigMap, or a chart'svalues*.yaml(top level,config:, or an umbrella subchart's<key>.config:unless that subchart is disabled).crawler.discovered.scrape_job_originsentry addsvia,receiver,target_count, and theexportersits samples leave by: a kafka exporter's topic, or any other exporter's endpoint without userinfo, query or fragment.prometheusremotewritescaffold inspec.pipelines.exporters.metrics, which keeps its{ kind }shape.--configfile wins.Environment scope: a manifest and a snapshot are read in every environment. A file that the scope excludes and that carries scrape jobs or an exporter config is named in a warning, together with the way to read it (
--env all).packc emit --exporter <url>(tools/emit-manifest.mjs) scrapes a/metricsendpoint once and writesmetrics-manifest.json;--from <file.prom>reads a saved one instead.GitHub crawl: fetches manifests with the files it already fetched. Saved expositions,
config.yamlfiles and a chart'sfiles/*.yamlonly fill whatever the 200-file budget leaves over.Declared default changes
SCAN_EXTadds.promand.metrics.nullpipeline is skipped instead of failing the whole file (the opentelemetry-collector chart usesnullto drop a pipeline).Every input with no exporter or collector-scrape evidence produces byte-identical output: all goldens (crawl, compile, diff, snapshot, audit report, board) are unchanged.
Review
Four adversarial verification rounds ran against this branch, and every reproduced finding is fixed and pinned in
tools/test-crawl-exporters.mjs.safeSourceUrl, regressions in existing output, and a malformed values file losing its images.safeSourceUrlinputs, ran 97k crawls and 29kpackc emitruns, and found 0 leaks and 0 throws.Checks (local, at the head)
npm run lint: 0 errors, 153 warnings, no new file+rule+message key.npm test: passes on Node 22.22, on Node 22.16, and on the floor run (no Playwright).test:golden,:compile,:diff,:snapshot,:audit-report, and the golden board.vendor-manifest:check(in sync at every commit),sync-spec --check, test-doc-test-totals, test-platform.🤖 Generated with Claude Code
https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
Generated by Claude Code