Eine fehlende Seite und ein blockierter Crawl brauchen unterschiedliche Reaktionen. In unserem Live-Lauf kam ein Request für eine Seite, die auf example.com nicht existiert, als HTTP 200 mit cb_status=200 und original_status=404 zurück. Der Crawl war erfolgreich und abrechenbar. Ein Request mit dem Normal token an Glassdoor kam als HTTP 520 mit cb_status=705 und original_status=403 zurück. Dieser Crawl schlug fehl und war kostenlos. Ein Monitor, der nur den äußeren HTTP-Status liest, kann die beiden nicht unterscheiden.

Dieser Beitrag macht aus diesen Signalen ein Monitoring-Dashboard für Web Scraping. Ein kleiner Client zeichnet jeden abgeschlossenen Versuch auf, Prometheus sammelt Zähler und Histogramme, und Grafana zeigt die Gesundheit pro Domain neben der kontoweiten Nutzung aus der Account API. Drei Alert-Regeln decken anhaltende Beschaffungsfehler, Antworten auf den Normal token, die eine Routing-Entscheidung brauchen, und ein Budget ab, das voraussichtlich ausgeht.

Das Begleit-Repository enthält den kompletten Stack, neun Walkthrough-Ordner, Unit-Tests und Live-Verifikationsskripte. Die Zahlen unten stammen aus dem Live-Lauf vom November 2026, der absichtlich einfache Ziele, eine geschützte Seite auf dem falschen Token und eine URL mit HTTP 500 mischte. Sie zeigen, was das Monitoring mit Fehlern macht. Sie sind kein Benchmark für die Erfolgsrate von Crawlbase.

Die Kurzfassung
  • Jede Antwort der Crawling API trägt vier Signale: den äußeren HTTP-Status, cb_status, original_status und die Komplexitätsstufe der Domain. Zeichnen Sie alle vier auf, nach Hostname, nie nach URL.
  • Abrechenbar und erfolgreich sind verschiedene Mengen. Ein gecrawlter 404 ist abrechenbar. Ein fehlgeschlagener 520 und ein Client-Timeout sind kostenlos. Halten Sie die beiden Definitionen in den Metriken und auf dem Dashboard auseinander.
  • Lokale Zähler beantworten „was passiert gerade“. Die Account API beantwortet „was hat das Konto diesen Monat verbraucht“, einmal alle fünf Minuten. Vergleichen Sie beide, aber erwarten Sie keine Übereinstimmung.

Crawl-Gesundheit von der Antwort der Zielseite trennen

Die Crawling API liefert Metadaten, die drei verschiedene Fragen beantworten: Hat Crawlbase den Request verarbeitet, was ist bei der Beschaffung passiert, und was hat die Zielseite geantwortet?

Signal Was es beschreibt Wie das Dashboard es nutzt
Äußerer HTTP-Status Die Antwort von Crawlbase an den Client Trennt erhaltene Antworten von Transportfehlern; fehlgeschlagene Crawls kommen als 520 an
cb_status Die Einschätzung des Crawls durch Crawlbase Definiert Beschaffungserfolg und gruppiert Fehlerergebnisse
original_status Der eigene Antwortstatus der Zielseite Trennt fehlende Seiten, Zugriffsverweigerungen und Upstream-Fehler
domain_complexity Die Preisstufe der Domain Gruppiert Latenz nach Stufe

In den aufgezeichneten HTML-Antworten kam die Stufe als Header x-domain-complexity an. Mit format=json erschien sie als domain_complexity im Envelope. Der Signal-Parser akzeptiert beide Schreibweisen, normalisiert Header-Namen und liest den JSON-Envelope, wenn der Request ihn angefordert hat. Er akzeptiert auch den alten Alias pc_status , während die Anwendung überall cb_status ausgibt.

Eine fehlgeschlagene Antwort, vier Signale. Der äußere Status sagt, dass der Crawl fehlschlug, cb_status sagt, warum Crawlbase aufgegeben hat, und original_status sagt, was die Seite geantwortet hat.

Der Raw-Request-Walkthrough gibt diese Signale für ein paar URLs aus. Diese beiden Beobachtungen stammen aus dem Lauf:

text
https://example.com/does-not-exist
  http_status       200
  cb_status         200
  original_status   404
  domain_complexity standard
  seconds           1.4

https://www.glassdoor.com/Reviews/index.htm
  http_status       520
  cb_status         705
  original_status   403
  domain_complexity standard
  seconds           20.3

Das Diagramm oben zeigt einen anderen Glassdoor-Request aus demselben Lauf, der mit cb_status=576 fehlschlug; das Listing ist der Request, der 705 zurückgab. Beide Codes gehören zur selben Fehlerfamilie. Die fehlende Seite war nach 1,4 Sekunden fertig. Glassdoor lieferte seine fehlgeschlagene Antwort nach 20,3 Sekunden, und ein zweiter Request an dieselbe URL brauchte 68 Sekunden, bevor er fehlschlug. Ein 404 kann also eine erfolgreiche Beschaffung sein, obwohl er nichts zum Extrahieren enthält. Die Prüfung von Pflichtfeldern gehört hinter die Beschaffung und liegt außerhalb der Erfolgsdefinition dieses Dashboards.

Der Lauf lieferte außerdem complex_ii als Stufe, daher akzeptiert der Parser jeden kurzen Slug in Kleinbuchstaben statt einer festen Liste aus drei Namen. Das passt zur größeren Bandbreite an Stufen auf der Preisseite. Beobachtete cb_status -Werte umfassten 0, 515, 521, 576, 611 und 702 neben den dokumentierten Ergebnissen. Die Status-Referenz erklärt die gängigen Codes und ihre Familien. Lassen Sie unbekannte Codes auf dem Dashboard sichtbar; eine Codenummer allein belegt keine Ursache.

HTTP 520 kennzeichnet einen fehlgeschlagenen Crawl, ist aber nicht die einzige Fehlerantwort. Authentifizierung, Ratenbegrenzung und Request-Validierung erzeugen andere HTTP-Status, und ein Transport-Timeout erzeugt gar keine HTTP-Antwort.

Jeden Versuch aufzeichnen, ohne für jede URL eine Zeitreihe anzulegen

Der instrumentierte Client stellt pro get() -Aufruf einen Request an die Crawling API. Er wirft bei HTTP 520 keine Exception; ein fehlgeschlagener Crawl ist ein normales, gelabeltes Ergebnis. Transportfehler werden gezählt und dann erneut geworfen, sodass die Retry-Strategie beim Aufrufer bleibt. Der Client wechselt nie selbst den Token und wiederholt keine URL von sich aus. So sieht der Antwortpfad im Repository aus:

python
def get(self, url: str, **params: Any) -> CrawlResult:
    domain = self._limit_domain(domain_label(url))
    token_type = "js" if str(params.get("javascript", "")).lower() == "true" else self.token_type
    success_codes = self.custom_success_codes | parse_success_codes(params.get("custom_success_codes"))

    self._ensure_series(domain, token_type)
    started = self._clock()
    try:
        response = self._http.get(
            f"{self.base_url}/", params={"token": self._token, "url": url, **params}
        )
    except httpx.HTTPError as exc:
        self._record_failure(domain, token_type, exc)
        raise
    elapsed = self._clock() - started

    json_body = None
    if params.get("format") == "json":
        try:
            json_body = response.json()
        except ValueError:
            json_body = None
    signals = parse_signals(response.status_code, response.headers, json_body)
    billable = is_billable(signals.cb_status, signals.original_status, success_codes)

    # Counted on completion, next to the outcome, so success <= attempts in any window.
    self.metrics.attempts.labels(domain=domain, token_type=token_type).inc()
    self.metrics.requests.labels(
        domain=domain,
        cb_status=status_label(signals.cb_status),
        original_status=status_label(signals.original_status),
        http_status=status_label(signals.http_status),
        token_type=token_type,
        billable=str(billable).lower(),
    ).inc()
    self.metrics.latency.labels(domain=domain, complexity=signals.complexity).observe(elapsed)
    self.metrics.response_bytes.labels(domain=domain).observe(len(response.content))

    return CrawlResult(url, domain, token_type, signals, billable, elapsed, response.content)

Die Metrikdefinitionen trennen abgeschlossene Versuche von einzelnen Ergebnissen. crawlbase_attempts_total zählt bei jeder erhaltenen Antwort und jedem Transportfehler hoch. crawlbase_requests_total trägt die Status-Labels; ein Transportfehler wird mit none für alle drei Status und billable="false" aufgezeichnet, sodass Timeouts im Nenner der Erfolgsrate bleiben:

python
def _record_failure(self, domain: str, token_type: str, exc: httpx.HTTPError) -> None:
    reason = "timeout" if isinstance(exc, httpx.TimeoutException) else "transport"
    self.metrics.attempts.labels(domain=domain, token_type=token_type).inc()
    self.metrics.client_errors.labels(domain=domain, token_type=token_type, reason=reason).inc()
    # Count it as a request too, so success-rate denominators include it.
    self.metrics.requests.labels(
        domain=domain, cb_status="none", original_status="none",
        http_status="none", token_type=token_type, billable="false",
    ).inc()

Labels entscheiden, wie viele Zeitreihen Prometheus speichern muss. Der Domain-Helfer schreibt den Hostnamen klein, IDNA-kodiert ihn, entfernt ein führendes www. und verwirft Pfad und Query. Subdomains bleiben getrennt. Die Demo teilt einen DomainLimiter zwischen beiden Token-Clients; er behält bis zu 200 Domainnamen und ordnet alles danach unter other.

Status-Labels akzeptieren Ganzzahlen von 0 bis 999, mit none für einen fehlenden Wert und other für alles außerhalb des Bereichs. Das Komplexitätslabel akzeptiert einen Slug in Kleinbuchstaben mit höchstens 24 Zeichen. Diese Obergrenzen begrenzen das Label-Wachstum, auch wenn sich die Kombinationen aus Status, Domain und Stufe weiterhin multiplizieren. Vollständige URLs und Request-IDs gehören in Diagnose-Logs, nicht in Metrik-Labels. Die Demo installiert außerdem einen Logging-Filter , der token= -Query-Parameter, die konfigurierten Token-Werte und /crawler/<token> -Pfade in jeder Logzeile maskiert.

Zähler-Basislinien anlegen, bevor der Traffic startet

Ein Zähler, den Prometheus zuerst bei 1 sieht, hat keinen beobachteten Übergang von null. rate() kann dieses erste Ereignis aus späteren Samples nicht rekonstruieren. Die Demo registriert daher ihre bekannten Domains vor und wartet 20 Sekunden vor dem ersten Request, damit der 15-Sekunden-Scrape die Nullwerte sieht:

python
def preregister(self, urls: Iterable[str], token_type: str | None = None) -> None:
    """Create zero-valued series before traffic starts.

    Prometheus only sees a counter once it exists. A series born at 1 between
    two scrapes shows no increase, so the first failure would be invisible.
    """
    for url in urls:
        self._ensure_series(self._limit_domain(domain_label(url)), token_type or self.token_type)

def _ensure_series(self, domain: str, token_type: str) -> None:
    if (domain, token_type) in self._known:
        return
    self._known.add((domain, token_type))
    self.metrics.attempts.labels(domain=domain, token_type=token_type)
    self.metrics.requests.labels(
        domain=domain, cb_status="200", original_status="200",
        http_status="200", token_type=token_type, billable="true",
    )

preregister() legt die Versuchs-Zeitreihe und die Standard-Erfolgsreihe 200/200 für jede Domain und jeden Token-Typ an. Es legt nicht jedes mögliche Ergebnis an. Eine Statuskombination, die zum ersten Mal auftritt, kann ihr erstes Ereignis in einer ratenbasierten Aufschlüsselung trotzdem verlieren. Die separate Versuchs-Basislinie macht den Erfolgsnenner zuverlässig, ohne jedes Fehlerlabel im Voraus zu verlangen.

Abrechenbare Antworten getrennt von erfolgreicher Extraktion klassifizieren

Der Abrechnungsklassifizierer folgt der dokumentierten Regel der Crawling API:

python
CHARGED_CB_STATUSES = frozenset({200, 207})
CHARGED_ORIGINAL_STATUSES = frozenset({200, 201, 301, 302, 303, 307, 308, 404, 410})


def is_billable(
    cb_status: int | None,
    original_status: int | None,
    custom_success_codes: Iterable[int] = (),
) -> bool:
    if cb_status not in CHARGED_CB_STATUSES or original_status is None:
        return False
    return original_status in CHARGED_ORIGINAL_STATUSES or original_status in set(custom_success_codes)

Eine Antwort braucht sowohl einen zulässigen cb_status als auch einen zulässigen Zielstatus, um berechnet zu werden. Ein erfolgreicher Crawl einer 404- oder 410-Seite ist abrechenbar. Übergeben Sie custom_success_codes=403, behandelt Crawlbase eine 403-Antwort als erfolgreichen Crawl, liefert cb_status=200 und berechnet sie. Der lokale Klassifizierer spiegelt diese Regel: Er akzeptiert einen eigenen Code nur zusammen mit einem cb_status von 200 oder 207, sodass er nie eine Antwort aufwertet, die Crawlbase selbst als fehlgeschlagen markiert hat.

Der Klassifizierer als Ablauf. Drei Ausgänge sind kostenlos, einer ist abrechenbar, und der Lauf teilte sich entlang dieses Ablaufs 46 zu 20 auf.

Der Abrechnungs-Walkthrough enthält eine Entscheidungstabelle und 41 Offline-Tests. Der Metrics-Endpoint-Walkthrough stellt die Zähler über start_http_server(8000) bereit. Zwei aufgezeichnete Zeilen zeigen die verwendeten Labels:

text
crawlbase_requests_total{billable="true",cb_status="200",domain="example.com",http_status="200",original_status="404",token_type="normal"} 9.0
crawlbase_requests_total{billable="false",cb_status="576",domain="glassdoor.com",http_status="520",original_status="403",token_type="normal"} 3.0

Die erste Zeitreihe verzeichnet neun abrechenbare Beschaffungen einer fehlenden Seite. Die zweite verzeichnet drei kostenlose Glassdoor-Fehler. Keine von beiden sagt, ob ein extrahierter Datensatz gültig war.

Das Dashboard definiert Beschaffungserfolg als cb_status=200. Die Kategorie abrechenbar umfasst auch qualifizierende 207-Antworten. Ein 207 ist eine leere Antwort, kann also abrechenbar sein und trotzdem außerhalb der Erfolgsrichtlinie liegen. Halten Sie diese Definitionen getrennt, wenn Sie Erfolgsrate und abrechenbaren Anteil nebeneinander lesen.

Lokale Aktivität mit der kontoweiten Nutzung vergleichen

Lokale Zähler beschreiben einen instrumentierten Prozess. Die Account API liefert monatliche Statistiken auf Kontoebene für ein Produkt, darunter Erfolge, Fehler, den fälligen Betrag und Zusammenfassungen pro Domain. Sie erlaubt einen Request alle fünf Minuten, was sie zu einer anderen Art von Quelle macht als einen /metrics -Endpoint, der alle 15 Sekunden gescrapt wird.

Der Account-Poller hält den letzten Snapshot und den Zeitpunkt des letzten Aufrufs auf der Platte. Er schreibt den Zeitstempel, bevor er den Request sendet, sodass auch ein fehlgeschlagener Aufruf oder ein abgebrochener Prozess das 300-Sekunden-Intervall verbraucht:

python
def poll(self, metrics: AccountMetrics | None = None) -> AccountSnapshot | None:
    """Return a fresh snapshot if allowed, otherwise the cached one."""
    if self.seconds_until_allowed() > 0:
        if metrics:
            metrics.cache_hits.inc()
        return self._cached

    self._last_call = self._clock()
    self._save_state()
    try:
        response = self._http.get(
            f"{self.base_url}/account", params={"token": self._token, "product": self.product}
        )
        response.raise_for_status()
        snapshot = parse_account(response.json())
    except (httpx.HTTPError, ValueError) as exc:
        result = "timeout" if isinstance(exc, httpx.TimeoutException) else "error"
        status = exc.response.status_code if isinstance(exc, httpx.HTTPStatusError) else None
        log.warning("Account API call failed (%s): %s status=%s", result, type(exc).__name__, status)
        if metrics:
            metrics.api_calls.labels(result=result).inc()
        return self._cached

    self._cached = snapshot
    self._save_state()
    if metrics:
        metrics.api_calls.labels(result="ok").inc()
    return snapshot

Beim Start lädt der Poller die gespeicherte Aufrufzeit und den Snapshot neu. Der Reconciler-Walkthrough zeigt einen zweiten Aufruf, der sofort aus dem Cache beantwortet wird, und im aufgezeichneten Test nutzte auch ein neuer Prozess die persistierte Antwort wieder.

Betreiben Sie einen Reconciler pro Token. Die Zustandsdatei unterstützt aufeinanderfolgende Neustarts, nicht die Koordination zwischen gleichzeitigen Pollern. Jede Instanz hält ihren eigenen Zeitstempel im Speicher und nimmt keinen prozessübergreifenden Lock. Im Test erzeugten zwei Poller mit getrennten Zustandsdateien eine Ablehnung wegen eines zu frühen Aufrufs, und eine gemeinsame Datei allein macht gleichzeitige Poller nicht sicher.

Die dokumentierte Feldtabelle beschreibt Summen und Domain-Aufschlüsselungen. Die aufgezeichnete Payload verschachtelte sie unter currentMonth und metrics:

json
{
  "currentMonth": {
    "totalSuccess": "<str>",
    "totalFailed": "<str>",
    "totalDue": "<float>",
    "domainStats": [
      {
        "domain": "<str>",
        "metrics": {
          "totalRequests": "<int>",
          "success": "<int>",
          "failure": "<int>",
          "successRate": "<str>"
        }
      }
    ]
  }
}

Der Parser akzeptiert diese beobachtete Struktur und eine flache. Er konvertiert String-Summen und Prozent-Strings wie "100.0%" und berechnet, wo Domain-Zähler vorhanden sind, die Quote aus Erfolgen und Gesamt-Requests. Die Live-Domainnamen waren bloße Labels wie glassdoor statt glassdoor.com. Die Daten hinkten dem Traffic um mehrere Minuten hinterher, und product=crawler lieferte dieselbe Monatsstruktur.

Schnelle lokale Zähler, langsame verbindliche Kontosummen. Die Zeitleiste ist illustrativ; die beiden Quoten sind die aufgezeichneten.

Prometheus kann die Gauges des Reconcilers alle 60 Sekunden scrapen, ohne bei jedem Scrape einen Aufruf der Account API auszulösen. Aufrufe, die ins Sperrintervall fallen, liefern Daten aus dem Cache, und der langlaufende Reconciler wartet einfach, bis der nächste Aufruf erlaubt ist.

Die Recording Rules leiten ein Label site ab, damit Hostnamen mit den bloßen Labels des Kontos verglichen werden können. Es ist ein einfacher Hostname-Regex, keine Auflösung mit Kenntnis der Public Suffix List. Das funktioniert für die .com - und .org -Ziele in der Demo; passen Sie die Zuordnung an, bevor Sie sie auf andere Endungen richten, etwa example.co.uk.

Den Budget-Fallback verstehen

remainingCredits fehlte in der aufgezeichneten Antwort der Crawling API. Die Dokumentation vermerkt, dass es nur für abonnementbasierte Produkte zurückgegeben wird, daher braucht der Reconciler einen Fallback. Ist CRAWLBASE_MONTHLY_BUDGET gesetzt, wird der Snapshot als max(0, budget - totalSuccess) unter source="budget" exportiert. Liefert die API tatsächlich verbleibende Credits, nutzt die Gauge source="account_api" stattdessen:

python
metrics.total_success.set(snapshot.total_success)
metrics.total_failed.set(snapshot.total_failed)
metrics.total_due.set(snapshot.total_due)
if snapshot.remaining_credits is not None:
    metrics.remaining_credits.labels(source="account_api").set(snapshot.remaining_credits)
elif monthly_budget:
    metrics.remaining_credits.labels(source="budget").set(
        max(0.0, monthly_budget - snapshot.total_success)
    )

Der Fallback ist eine Schätzung auf Basis der Request-Zahl. Die Tage-bis-null-Regel teilt die verbleibende Gauge durch die Rate abrechenbarer Requests der letzten Stunde mal 86.400, was einen Credit pro abrechenbarem Request annimmt. JavaScript-Rendering und höhere Komplexitätsstufen verbrauchen mehr Credits, und andere Produkte können auf dasselbe Guthaben zugreifen. Die Prognose reicht aus, um Budget-Alerting zu demonstrieren. Verwenden Sie credit-gewichteten Verbrauch, bevor Sie sie als finanzielle Grenze behandeln.

Exporter, Prometheus und Grafana verbinden

Beschaffung und Abgleich laufen als getrennte Prozesse. Jeder stellt einen Metrics-Endpoint bereit, Prometheus scrapt beide und wertet Recording Rules aus, und Grafana fragt diese Regeln für Panels und Alerts ab.

Zwei Exporter, ein Prometheus, ein Grafana. Die Benachrichtigungsziele fügen Sie selbst hinzu.

Der Compose-Walkthrough führt vier Dienste ein: den Scraper auf Port 8000, den Reconciler auf 8001, Prometheus auf 9090 und Grafana auf 3000. Der finale Stack pinnt Prometheus 3.1.0 und Grafana 11.4.0, und seine Python-Requirements pinnen HTTPX 0.28.1, prometheus-client 0.21.1 und python-dotenv 1.0.1. Die Scrape-Konfiguration ist kurz:

yaml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - /etc/prometheus/rules/*.yml

scrape_configs:
  - job_name: crawlbase-scraper
    static_configs:
      - targets: ["scraper:8000"]

  # The gauges only change every 5 minutes, a slower scrape is plenty.
  - job_name: crawlbase-account
    scrape_interval: 60s
    static_configs:
      - targets: ["reconciler:8001"]

Regeln werden global alle 15 Sekunden ausgewertet, die Crawlbase-Recording-Gruppe ist auf 30 Sekunden gesetzt. Grafana aktualisiert das Dashboard alle 30 Sekunden und wertet seine Alert-Gruppe jede Minute aus. Keiner dieser Zeitpläne ist die Fünf-Minuten-Sperre der Account API; es sind getrennte Uhren.

Das benannte Volume cbwatch-account-state wird von den Walkthrough-Stacks und dem finalen Stack gemeinsam genutzt. Ein einfaches docker compose down behält es, sodass Sie zwischen Stacks wechseln können, ohne die Sperre zu verwerfen. Wer das Volume entfernt, entfernt die gespeicherte Aufrufzeit mit.

Der Dashboard-Walkthrough führt Recording Rules ein, damit Panels und Alerts eine Definition teilen. Die zentrale Quote pro Domain ist:

yaml
# Success = the crawl worked (cb_status 200). The denominator is
# crawlbase_attempts_total, which also counts timeouts and is created at 0.
- record: crawlbase:domain_success_ratio:rate5m
  expr: |
    clamp_max(
      sum by (domain) (rate(crawlbase_requests_total{cb_status="200"}[5m]))
      /
      sum by (domain) (rate(crawlbase_attempts_total[5m])),
      1
    )

Im Zähler steht cb_status=200. Im Nenner steht jeder abgeschlossene Versuch, Timeouts eingeschlossen, mit beiden Token-Typen pro Domain zusammengefasst. clamp_max begrenzt die Quote auf 1, wenn zwei getrennt gesampelte Zähler kurzzeitig darüber hinausschießen. Die Ein-Stunden-Zusammenfassung verwendet dieselbe Definition über ein längeres Fenster.

Die Prometheus-Regeltests speisen synthetische Zählerfolgen durch die Regeln. Ihre Walmart-Folge ergibt 90% Erfolg, und eine zweite Folge bestätigt, dass Timeouts die Quote senken. Das sind Fixtures, nicht die Live-Beobachtungen.

Jedes Dashboard-Panel anhand seiner eigenen Definition lesen

Grafana lädt das Dashboard aus provisioniertem JSON. Der Dashboard-Generator baut dieses JSON aus Python, und ein Unit-Test prüft, dass die eingecheckte Datei zum Generator passt. Das Erfolgsraten-Panel zeigt auf die gemeinsame Regel:

python
timeseries("Success rate per domain", [target("crawlbase:domain_success_ratio:rate5m", "{{domain}}")],
           0, 6, unit="percentunit", minv=0, maxv=1,
           desc="cb_status 200 share per domain, 5 minute window. Alert fires under 90% for 10m."),
Panel-Layout mit den aufgezeichneten Werten. Jedes Panel hat sein eigenes Fenster, die Zahlen sollen sich also nicht gegenseitig aufaddieren.

Das Schema ordnet die aufgezeichneten Beobachtungen nach operativer Frage. Lesen Sie jeden Wert mit seinem Abfragefenster; eine Ein-Stunden-Rate, eine Fünf-Minuten-Rate und eine rohe Zählersumme beantworten verschiedene Fragen und sollten nicht zu einem einzigen Batch-Erfolgsanteil verrechnet werden.

Panel oder Diagnose Aufgezeichnete Beobachtung Interpretation
Erfolgsrate, eine Stunde 65.1% Beschaffungserfolg in einer Demo, die absichtlich Fehler enthält
Abrechenbarer Anteil, eine Stunde 65.1% Gefensterter abrechenbarer Anteil, getrennt von rohen Zählersummen
Verbleibendes Budget und Prognose 890 von einem Budget von 1.000; 0.7 Tage Die Schätzung mit einem Credit pro Request
Domain-Erfolg example.com 100%; glassdoor.com 15%; httpbin.org 0% Beschaffungsgesundheit pro Domain; das httpbin-Ziel liefert absichtlich 500
Latenz p95, Stufe standard 87.8 Sekunden Histogramm-Schätzung nur über erhaltene Antworten
Domains mit den meisten Fehlern glassdoor.com und httpbin.org Die 20 Nicht-200-Ergebnisse, Timeouts eingeschlossen
Blockierter Anteil Glassdoor mit Normal token 41% Die separate Blockiert-Quoten-Diagnose; das Dashboard-Panel zeigt Requests pro Minute
Zähler abrechenbar und kostenlos 46 abrechenbar; 20 kostenlos Dieselben 66 Versuche wie in der Status-Aufschlüsselung
Erfolg lokal versus Konto 65.1% versus 70.1% Lokale Ein-Stunden-Quote gegen die Quote des Kontos seit Monatsbeginn
Glassdoor lokal versus Konto 15% versus 36.4% Derselbe Fenster- und Umfangsunterschied auf Site-Ebene

Die Status-Aufschlüsselung enthielt 46 Antworten mit 200, sechs Client-Timeouts, vier mit 576, vier mit 0, zwei mit 704, zwei mit 705 und je eine mit 521 und 611. Alle 46 Antworten mit 200 waren abrechenbar und die anderen 20 kostenlos, das Label abrechenbar und die Status-Aufschlüsselung beschreiben also dieselben 66 Versuche. Die 20 kostenlosen Ergebnisse lagen alle auf glassdoor.com und httpbin.org. Jeder cb_status ungleich 200 kam als HTTP 520 an; die Timeouts hatten keine Antwort und wurden als none aufgezeichnet. Gefensterte Raten und rohe Zähler unterscheiden sich trotzdem: Die Ein-Stunden-Erfolgsrate ist nicht die rohe Quote 46 von 66.

Der p95 von 87.8 Sekunden braucht einen präzisen Nenner. Der Client beobachtet Latenz erst, wenn eine Antwort ankommt, HTTP 520 eingeschlossen. Client-Timeouts gelangen nie ins Histogramm. Der Wert ist eine Bucket-Interpolation, kein exaktes Perzentil über einzelne Dauern, und die obere Bucket-Grenze ist das 90-Sekunden-Timeout. Gewöhnliche Seiten lagen bei etwa zwei Sekunden. Das 90-Sekunden-Timeout von HTTPX gilt für jede einzelne Netzwerkoperation, einschließlich des Wartens auf den nächsten Chunk, und ist daher keine strikte Wanduhr-Frist, wie die HTTPX-Timeout-Dokumentation erklärt.

Die Lücke zwischen lokalen und Kontozahlen hat mehrere erwartbare Ursachen. Lokale Metriken decken die Versuche dieses Prozesses ab und enthalten clientseitige Fehler. Die Account API deckt den ganzen Monat für jeden Aufrufer des gewählten Produkts ab und kann dem aktuellen Traffic hinterherhinken. Diese Umfänge liefern keinen Eins-zu-eins-Abgleich mit der Rechnung und keine identische Timeout-Zählung.

Das Datenalter-Panel bedeutet weniger, als sein Name nahelegt. Der Reconciler aktualisiert seinen Zeitstempel immer, wenn er einen Snapshot anwendet, auch Cache-Daten nach einem fehlgeschlagenen Poll oder einem Neustart. Lesen Sie es neben den API-Fehlerzählern; allein beweist es nicht, dass die Upstream-Daten frisch geholt wurden.

Alerting auf die Scraping-Erfolgsrate handlungsfähig machen

Der Alert-Walkthrough provisioniert drei von Grafana verwaltete Regeln. Sie trennen einen anhaltenden Beschaffungsverlust von einer Token-Routing-Untersuchung und einer Budgetwarnung:

Alert Bedingung Vorgesehene Behandlung
Domain-Erfolg unter 90% Fünf-Minuten-Erfolgsquote unter 0.9 für 10 Minuten, auf Domains mit einer Versuchsrate über 0.008/s Page
Normal token blockiert Ausgewählte Ergebnisse des Normal token über 20% der Versuche für fünf Minuten Ticket
Credits gehen voraussichtlich aus Geschätzte Tage bis null unter drei für 15 Minuten Page

Die Ratenschwelle des Domain-Alerts entspricht grob einem Request alle 125 Sekunden oder 2,4 Requests pro Fünf-Minuten-Fenster. Sie filtert nahezu untätige Domains, ist aber keine Garantie für große Stichproben. Setzen Sie Traffic-Untergrenze und Schwellenwert nach dem Volumen und der Zuverlässigkeit, die Ihr Job tatsächlich braucht.

yaml
- uid: cb-domain-success-low
  title: Crawlbase domain success rate below 90%
  condition: C
  for: 10m
  noDataState: OK
  execErrState: Error
  labels:
    severity: page
  annotations:
    summary: "Success rate for {{ $labels.domain }} is {{ humanizePercentage $values.B.Value }}"
    description: >-
      cb_status 200 share has been under 90% for 10 minutes. Open the cb_status
      breakdown panel: 5xx/6xx codes are the crawl layer, 704/705/706 mean the
      target site said no (check original_status).
    runbook_url: https://crawlbase.com/docs/status-codes/
  data:
    - refId: A
      relativeTimeRange: { from: 600, to: 0 }
      datasourceUid: prometheus
      model:
        refId: A
        instant: true
        # Ignore domains with almost no traffic (under ~1 request per 2 minutes).
        expr: crawlbase:domain_success_ratio:rate5m and on (domain) crawlbase:domain_requests:rate5m > 0.008
    - refId: B
      datasourceUid: __expr__
      model: { refId: B, type: reduce, expression: A, reducer: last }
    - refId: C
      datasourceUid: __expr__
      model:
        refId: C
        type: threshold
        expression: B
        conditions:
          - evaluator: { type: lt, params: [0.9] }

Die Blockiert-Regel matcht 503|207|525|576|705 auf dem Normal token. Die Dokumentation empfiehlt für 207 und 503 den JavaScript token; das Projekt behält 525 für ältere Antworten und ergänzt die beobachteten Ergebnisse 576 und 705. Diese Ergänzungen sind eine Anwendungsrichtlinie, keine Produktregel. Ein anhaltender 403 verlangt weiterhin einen Blick auf die Antwort des Ziels und Ihre Request-Rate statt der Annahme, dass ein Token-Wechsel jeden Fall löst.

Page, Ticket oder Beobachten. Severity-Labels routen die ersten beiden; das dritte bleibt auf dem Dashboard.

Im Live-Lauf feuerte zuerst der Alert für den Normal token, für glassdoor.com. Der Domain-Erfolgs-Alert feuerte drei Minuten später, und der Budget-Alert ging auf Pending, sobald sich seine Ratenschätzung eingependelt hatte.

Das Repository provisioniert Regeln und Severity-Labels, nicht Slack, E-Mail, PagerDuty oder eine Ticket-Integration. Konfigurieren Sie Grafana-Kontaktpunkte und Benachrichtigungsrichtlinien , um severity=page und severity=ticket dorthin zu routen, wo sie hingehören. Ein Label allein alarmiert niemanden.

Ein vereinzelter 520, eine langsame Antwort oder ein bekannter 404 gehört auf das Dashboard, nicht in einen Page. Eine anhaltende Veränderung von Fehlerrate, Abschlusszeit oder Pflichtfeld-Abdeckung verdient einen Menschen. Die provisionierten Regeln behandeln fehlende Daten als OK, überwachen Sie daher die Verfügbarkeit der Exporter und gestoppte Jobs separat.

Grafana-Annotationen verwenden ein einzelnes $ in Referenzen wie $labels.domain und $values.B.Value. Das aufgezeichnete Verhalten von Grafana 11 ließ doppelte Dollarzeichen wörtlich stehen, daher testet das Repository die Template-Schreibweise zusammen mit den Schwellenwerten.

Dieselben Metriken auf Crawler-Queues und Token-Routing ausweiten

Für den Crawler liest der optionale Stats-Poller /crawler/<TOKEN>/stats und exportiert Kontosummen plus Gauges für Wartende, Parallelität, Latenz und Pausiert pro Crawler. Mit CRAWLER_STATS_ENABLED=true versucht der Reconciler diese Polls in einem nominellen 60-Sekunden-Takt, den synchrone API-Aufrufe verzögern können.

Der Crawler- und Multi-Token-Walkthrough hält ebenfalls einen Client pro Token und routet Glassdoor über ein konfiguriertes Set JS_DOMAINS . Im aufgezeichneten Lauf lieferte der Request mit dem JavaScript token einen sauberen 200. Der Operator aktualisiert dieses Set nach der Untersuchung eines Alerts; der Alert schreibt das Routing nie selbst um:

python
# Filled from the alert: Normal token gets cb_status 503/576/705 on these.
JS_DOMAINS = {"glassdoor.com"}

URLS = [
    "https://example.com/",
    "https://www.glassdoor.com/Reviews/index.htm",
]


def main() -> None:
    load_dotenv(find_dotenv(usecwd=True))
    clients = {
        "normal": InstrumentedCrawlingAPI(os.environ["CRAWLBASE_NORMAL_TOKEN"], "normal"),
        "js": InstrumentedCrawlingAPI(os.environ["CRAWLBASE_JS_TOKEN"], "js"),
    }
    for url in URLS:
        token_type = "js" if domain_label(url) in JS_DOMAINS else "normal"
        r = clients[token_type].get(url)
        print(f"{token_type:<6} http={r.status_code} cb_status={r.headers.get('cb_status')} {url}")

Beide Clients schreiben in dieselben Metrikfamilien mit unterschiedlichen token_type -Werten, sodass Sie tokenspezifische Fehler untersuchen und trotzdem eine Sicht pro Domain behalten können. Crawler-Namen sollten auf dieselbe Weise stabile Queues bezeichnen, nie einzelne Jobs oder URLs.

Den Stack starten und die Integration verifizieren

Starten Sie den finalen Stack aus dem Repository-Root mit:

bash
cd final
cp .env.example .env          # add CRAWLBASE_NORMAL_TOKEN and CRAWLBASE_JS_TOKEN
docker compose up -d --build
# Grafana: http://localhost:3000/d/crawlbase-observability (admin / admin)

Die finale Umgebungsvorlage enthält das Budget und die optionalen Crawler-Einstellungen. Die Root-Vorlage liefert Tokens für die Walkthrough-Ordner, und erzeugte .env -Dateien ignoriert git. Der finale Stack sendet standardmäßig 40 Requests und liefert danach weiter Metriken. Erfolgreiche Beschaffungen verbrauchen Credits. Setzen Sie ein Grafana-Passwort, bevor Sie den Stack über Ihren Rechner hinaus freigeben.

Die README beschreibt jede Stufe und Prüfung. Im veröffentlichten Layout liegen final/ und steps/ direkt unter dem Root; README-Befehle mit einem Präfix code/ beziehen sich auf das frühere übergeordnete Layout.

Das Projekt liefert 319 Offline-Tests mit pytest und respx sowie sechs Live-Tests, die separat über einen Marker ausgewählt werden. Die Offline-Verifikation bestand alle 319 und die 41 Abrechnungstests des Walkthroughs. Die gemockten HTTP-Tests decken Parser, Statusklassifikation, Metrik-Updates, Cache-Verhalten und Neustartbehandlung ab.

Der Konfigurationsprüfer führt Prometheus-Config- und Regelprüfungen, die synthetischen Regeltests und die Dashboard-Validierung aus. Der Smoke-Test prüft an einem laufenden Stack Targets, exportierte Zeitreihen, Recording Rules, Dashboard, Datasource und Alert-Regel-Auswertung. Die Live-Sonde, Sample-Erfassung und das Panel-Query-Skript legen die Antwortformen und Abfragen hinter den Zahlen in diesem Beitrag offen.

Fazit

Nützliche Scraping-Observability beginnt damit, drei Dinge auseinanderzuhalten: das Crawl-Ergebnis, die Antwort des Ziels und Ihre eigene Akzeptanzrichtlinie. Crawlbase liefert die Beschaffungssignale, der Wrapper zeichnet sie auf, Prometheus macht daraus gefensterte Metriken, und Grafana macht anhaltende Veränderung sichtbar.

Der Live-Lauf hat gezeigt, warum die Unterscheidungen wichtig sind. Gültige 404-Beschaffungen waren abrechenbar, ein geschütztes Ziel erzeugte mehrere Fehlercodes, Client-Timeouts brauchten ihre eigene Zählung, und kontoweite Statistiken unterschieden sich von der lokalen Ein-Stunden-Sicht. Das Projekt macht all das inspizierbar, ohne jede Seite zu labeln oder eine Monitoring-Plattform in den Scraper einzubetten.

Verwenden Sie die komplette Implementierung mit einer kleinen Menge repräsentativer Ziele, bestätigen Sie Ihre Erfolgsdefinition und Credit-Annahmen und verbinden Sie die Alerts mit einer echten Bereitschaftsreaktion. Messen Sie dann die Ziele, von denen Ihre Anwendung tatsächlich abhängt.

Crawlbase Crawling API

Jede Antwort trägt cb_status, original_status und die Domain-Stufe, damit Ihr Monitoring einen blockierten Crawl von einer fehlenden Seite unterscheiden kann. Fehlgeschlagene Requests werden nie berechnet, und die Account API liefert Ihnen die Monatssummen pro Domain. Starten Sie mit bis zu 5.000 kostenlosen Requests, ohne Karte.

Häufig gestellte Fragen (FAQs)

Was sollte als erfolgreicher Scraping-Request zählen?

Dieses Projekt zählt cb_status=200 als Beschaffungserfolg und nimmt jeden abgeschlossenen Versuch in den Nenner. Ein 404 des Ziels erfüllt diese Definition; ein leerer 207 nicht. Für eine Produkt- oder Artikel-Pipeline ergänzen Sie separate Metriken für Datensätze, die Ihre Pflichtfeld- und Inhaltsprüfungen bestehen. Erfolgreiche Beschaffung und nutzbare Geschäftsdaten sind verschiedene Fragen.

Warum unterscheidet sich die Erfolgsrate der Account API von der lokalen Rate in Grafana?

Die lokale Zusammenfassung ist ein gleitendes Ein-Stunden-Fenster über die Versuche dieses Prozesses. Die Account API meldet die Aktivität seit Monatsbeginn für das gewählte Produkt, kann andere Aufrufer einschließen und hinkte im aufgezeichneten Lauf dem Traffic um mehrere Minuten hinterher. Gleichen Sie die Umfänge ab, bevor Sie eine Lücke als fehlenden Traffic oder falsche Abrechnung lesen.

Zeigt das Dashboard die exakte Zahl der verbleibenden Credits?

Wenn die Account API remainingCredits zurückgibt, was sie nur für abonnementbasierte Produkte tut, exportiert der Reconciler dieses Guthaben. Andernfalls zieht er die erfolgreichen Requests des Monats von einem konfigurierten Budget ab. Dieser Fallback und die Tage-bis-null-Regel nehmen einen Credit pro Request an. Berücksichtigen Sie Rendering, Domain-Multiplikatoren und geteilte Produktnutzung, bevor Sie sich auf die Schätzung als echtes Guthaben verlassen.

Wiederholt der Alert für blockierte Tokens den Request automatisch mit dem JavaScript token?

Nein. Der Client zeichnet das Ergebnis auf und überlässt Wiederholungen dem Aufrufer. Das Multi-Token-Beispiel routet Domains über ein konfiguriertes Set, und der Alert sagt einem Operator, wo er hinsehen soll. Dieses Routing zu aktualisieren und Benachrichtigungsziele anzubinden sind explizite Anwendungs- und Betriebsentscheidungen.

Jetzt loslegen

Crawlen Sie jede Website im großen Maßstab, ohne gegen die Infrastruktur zu kämpfen.

Crawlbase übernimmt Proxys, Fingerprints und CAPTCHAs, damit Ihr Team Daten-Pipelines liefert, statt Crawl-Infrastruktur zu warten. Bis zu 5.000 Requests kostenlos, keine Karte erforderlich.

Self-Service · Kein Verkaufsgespräch erforderlich · Enterprise-Crawl-Volumen verfügbar