Skip to content

crawler: learn metrics from prebuilt exporters and OTel collector scrape jobs - #170

Merged
MoebiusX merged 17 commits into
developfrom
codex/exporter-metrics
Oct 16, 2026
Merged

MoebiusX merged 17 commits into
developfrom
codex/exporter-metrics

Conversation

@MoebiusX

Copy link
Copy Markdown
Owner

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:

Source Files origin_kind Confidence
metrics manifest metrics-manifest.json, *.metrics-manifest.json exporter-manifest declared
saved exposition *.prom, *.metrics (never under a test, fixture or mock folder) exporter-snapshot declared
exporter config × catalogue YAML whose top-level keys match a catalogue entry exporter-config inferred (candidate)
  • Ranking: metricOriginRank puts the manifest first and the inferred config last. The relative order of the existing origin kinds is unchanged.
  • Snapshots: label names are kept, label values never. A family is named as Prometheus stores it (an OpenMetrics jobs counter is jobs_total).
  • Catalogue: the family catalogue is data, not engine code (tools/lib/exporter-catalogue.mjs, with a small built-in IBM MQ entry). A downstream passes a larger one with packc x-ray --exporter-catalogue <file.json> or crawlFiles(files, { exporterCatalogue }). Its entries replace or extend the built-in ones. The format is in docs/DOWNSTREAM.md §18.
  • Annotation: crawler.discovered.exporters lists each exporter and its sources.

An OTel collector's scrape jobs:

  • The prometheus receivers' config.scrape_configs are read, with or without service.pipelines. They are found in a config file, a Helm ConfigMap, or a chart's values*.yaml (top level, config:, or an umbrella subchart's <key>.config: unless that subchart is disabled).
  • Each job's crawler.discovered.scrape_job_origins entry adds via, receiver, target_count, and the exporters its samples leave by: a kafka exporter's topic, or any other exporter's endpoint without userinfo, query or fragment.
  • The collector's metrics exporter (e.g. kafka) replaces the prometheusremotewrite scaffold in spec.pipelines.exporters.metrics, which keeps its { kind } shape.
  • A pipeline-less config's exporter is only a guess. It is settled after the walk, so a pipeline declared in another --config file 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 /metrics endpoint once and writes metrics-manifest.json; --from <file.prom> reads a saved one instead.

  • A redirect, a URL with credentials, a non-200 status, a body that is cut off, and an empty response are each refused with a way out that works when followed literally.
  • No credential or query value is ever echoed.

GitHub crawl: fetches manifests with the files it already fetched. Saved expositions, config.yaml files and a chart's files/*.yaml only fill whatever the 200-file budget leaves over.

Declared default changes

  • SCAN_EXT adds .prom and .metrics.
  • Collector scrape jobs are read.
  • A collector config with no service block is classified as a collector.
  • A collector config inside a values file is walked.
  • A null pipeline is skipped instead of failing the whole file (the opentelemetry-collector chart uses null to drop a pipeline).
  • The environment-scope warning is new.

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.

  • Findings included credential leaks through safeSourceUrl, regressions in existing output, and a malformed values file losing its images.
  • The last round generated 81M safeSourceUrl inputs, ran 97k crawls and 29k packc emit runs, and found 0 leaks and 0 throws.
  • A byte-identity diff against develop over 220 captured inputs differs only where there is exporter or collector-scrape evidence.

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).
  • Goldens: 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.
  • Prestore strict and backend-live strict.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ


Generated by Claude Code

claude added 17 commits October 15, 2026 18:12
…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
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
MoebiusX marked this pull request as ready for review October 15, 2026 20:23
@MoebiusX
MoebiusX merged commit 862a750 into develop Oct 16, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants