Une page manquante et un crawl bloqué appellent des réponses différentes. Dans notre exécution réelle, une requête vers une page qui n'existe pas sur example.com est revenue en HTTP 200 avec cb_status=200 et original_status=404. Le crawl a réussi et a été facturé. Une requête vers Glassdoor avec le Normal token est revenue en HTTP 520 avec cb_status=705 et original_status=403. Ce crawl a échoué et n'a rien coûté. Un moniteur qui ne lit que le statut HTTP externe ne peut pas distinguer ces deux cas.

Cet article transforme ces signaux en dashboard de monitoring du web scraping. Un petit client enregistre chaque tentative terminée, Prometheus collecte compteurs et histogrammes, et Grafana affiche la santé par domaine à côté de la consommation de tout le compte issue de l' Account API. Trois règles d'alerte couvrent les échecs d'acquisition prolongés, les réponses du Normal token qui demandent une décision de routage, et un budget dont l'épuisement est prévu.

Le dépôt compagnon contient la stack complète, neuf dossiers pas à pas, des tests unitaires et des scripts de vérification en conditions réelles. Les chiffres ci-dessous proviennent de l'exécution réelle de novembre 2026, qui mélangeait délibérément des cibles faciles, un site protégé sur le mauvais token et une URL qui renvoie HTTP 500. Ils montrent ce que le monitoring fait des échecs. Ils ne constituent pas un benchmark du taux de succès de Crawlbase.

En bref
  • Chaque réponse de la Crawling API porte quatre signaux : le statut HTTP externe, cb_status, original_status et le palier de complexité du domaine. Enregistrez les quatre, indexés par nom d'hôte, jamais par URL.
  • Facturable et réussi sont deux ensembles différents. Un 404 crawlé est facturable. Un 520 en échec et un timeout client sont gratuits. Gardez les deux définitions séparées dans les métriques et sur le dashboard.
  • Les compteurs locaux répondent à "que se passe-t-il maintenant". L'Account API répond à "qu'a consommé le compte ce mois-ci", une fois toutes les cinq minutes. Comparez-les, mais n'attendez pas qu'ils coïncident.

Séparer la santé du crawl de la réponse du site cible

La Crawling API renvoie des métadonnées qui répondent à trois questions différentes : Crawlbase a-t-il traité la requête, que s'est-il passé pendant l'acquisition, et qu'a répondu le site cible ?

Signal Ce qu'il décrit Comment le dashboard l'utilise
Statut HTTP externe La réponse de Crawlbase au client Sépare les réponses reçues des échecs de transport ; les crawls en échec arrivent en 520
cb_status L'évaluation du crawl par Crawlbase Définit le succès d'acquisition et regroupe les issues en échec
original_status Le statut de réponse propre au site cible Sépare pages manquantes, refus d'accès et erreurs en amont
domain_complexity Le palier tarifaire du domaine Regroupe la latence par palier

Dans les réponses HTML enregistrées, le palier arrivait dans l'en-tête x-domain-complexity . Avec format=json , il apparaissait sous le nom domain_complexity dans l'enveloppe. Le parseur de signaux accepte les deux graphies, normalise les noms d'en-tête et lit l'enveloppe JSON quand la requête l'a demandée. Il accepte aussi l'ancien alias pc_status , tandis que l'application expose cb_status partout.

Une réponse en échec, quatre signaux. Le statut externe dit que le crawl a échoué, cb_status dit pourquoi Crawlbase a abandonné, et original_status dit ce que le site a répondu.

Le pas à pas des requêtes brutes affiche ces signaux pour quelques URL. Ces deux observations proviennent de l'exécution :

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

Le diagramme ci-dessus montre une autre requête Glassdoor de la même exécution, qui a échoué avec cb_status=576 ; le listing est celle qui a renvoyé 705. Les deux codes appartiennent à la même famille d'échecs. La page manquante s'est terminée en 1,4 seconde. Glassdoor a renvoyé sa réponse en échec après 20,3 secondes, et une seconde requête vers la même URL a pris 68 secondes avant d'échouer. Un 404 peut donc être une acquisition réussie même s'il ne contient rien à extraire. La validation des champs obligatoires vient après l'acquisition et reste en dehors de la définition du succès de ce dashboard.

L'exécution a aussi renvoyé complex_ii comme palier, donc le parseur accepte n'importe quel slug court en minuscules au lieu d'une liste fixe de trois noms. Cela correspond à la gamme de paliers plus large de la page des tarifs. Les valeurs de cb_status observées comprenaient 0, 515, 521, 576, 611 et 702 à côté des issues documentées. La référence des statuts explique les codes courants et leurs familles. Gardez les codes inconnus visibles sur le dashboard ; un numéro de code seul n'établit pas une cause racine.

HTTP 520 identifie un crawl en échec, mais ce n'est pas la seule réponse d'erreur. L'authentification, la limitation de débit et la validation des requêtes produisent d'autres statuts HTTP, et un timeout de transport ne produit aucune réponse HTTP.

Enregistrer chaque tentative sans créer une série par URL

Le client instrumenté effectue une requête Crawling API par appel à get() . Il ne lève pas d'exception sur HTTP 520 ; un crawl en échec est une issue normale et étiquetée. Les erreurs de transport sont comptées puis relancées, donc la politique de retry reste chez l'appelant. Le client ne change jamais de token et ne réessaie jamais une URL de lui-même. Voici le chemin de réponse tel qu'il apparaît dans le dépôt :

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)

Les définitions de métriques séparent les tentatives terminées des issues individuelles. crawlbase_attempts_total s'incrémente à chaque réponse reçue et à chaque erreur de transport. crawlbase_requests_total porte les labels de statut ; une erreur de transport est enregistrée avec none pour les trois statuts et billable="false", de sorte que les timeouts restent dans le dénominateur du taux de succès :

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()

Les labels déterminent combien de séries Prometheus doit stocker. Le helper de domaine met le nom d'hôte en minuscules, l'encode en IDNA, retire un préfixe www. et supprime le chemin et la query string. Les sous-domaines restent distincts. La démo partage un seul DomainLimiter entre les deux clients de token ; il conserve jusqu'à 200 noms de domaine et classe tout le reste sous other.

Les labels de statut acceptent des entiers de 0 à 999, avec none pour une valeur absente et other pour tout ce qui sort de la plage. Le label de complexité accepte un slug en minuscules de 24 caractères au plus. Ces plafonds bornent la croissance des labels, même si les combinaisons de statut, domaine et palier se multiplient toujours. Les URL complètes et les identifiants de requête ont leur place dans les logs de diagnostic, pas dans les labels de métriques. La démo installe aussi un filtre de logging qui masque les paramètres de requête token= , les valeurs de token configurées et les chemins /crawler/<token> dans chaque ligne de log.

Établir la base des compteurs avant le début du trafic

Un compteur que Prometheus voit pour la première fois à 1 n'a aucune transition observée depuis zéro. rate() ne peut pas retrouver ce premier événement à partir des échantillons suivants. La démo préenregistre donc ses domaines connus et attend 20 secondes avant la première requête, pour que le scrape de 15 secondes voie les valeurs à zéro :

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() crée la série des tentatives et la série de succès par défaut 200/200 pour chaque domaine et type de token. Cet appel ne crée pas toutes les issues possibles. Une combinaison de statuts qui apparaît pour la première fois peut encore perdre son premier événement dans une ventilation basée sur rate. La base distincte des tentatives rend le dénominateur du succès fiable sans exiger chaque label d'échec à l'avance.

Classer les réponses facturables séparément de l'extraction réussie

Le classifieur de facturation suit la règle documentée de la 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)

Une réponse a besoin à la fois d'un cb_status éligible et d'un statut cible éligible pour être facturée. Un crawl réussi d'une page 404 ou 410 est facturable. Si vous passez custom_success_codes=403, Crawlbase traite une réponse 403 comme un crawl réussi, renvoie cb_status=200 et la facture. Le classifieur local reflète cette règle : il n'accepte un code personnalisé qu'avec un cb_status 200 ou 207, donc il ne promeut jamais une réponse que Crawlbase lui-même a marquée comme échouée.

Le classifieur sous forme de flux. Trois sorties sont gratuites, une est facturable, et l'exécution s'est répartie 46 contre 20 le long de ce flux.

Le pas à pas de la facturation comprend une table de décision et 41 tests hors ligne. Le pas à pas de l'endpoint de métriques expose les compteurs via start_http_server(8000). Deux lignes enregistrées montrent les labels en usage :

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

La première série enregistre neuf acquisitions facturables d'une page manquante. La seconde enregistre trois échecs Glassdoor gratuits. Aucune ne dit si un enregistrement extrait était valide.

Le dashboard définit le succès d'acquisition comme cb_status=200. La catégorie facturable inclut aussi les réponses 207 éligibles. Un 207 est une réponse vide, donc il peut être facturable tout en restant hors de la politique de succès. Gardez ces définitions séparées quand vous lisez le taux de succès et la part facturable côte à côte.

Comparer l'activité locale à la consommation du compte

Les compteurs locaux décrivent un seul processus instrumenté. L' Account API renvoie des statistiques mensuelles au niveau du compte pour un produit, dont les succès, les échecs, le montant dû et des résumés par domaine. Elle autorise une requête toutes les cinq minutes, ce qui en fait une source d'un autre genre qu'un endpoint /metrics scrapé toutes les 15 secondes.

Le poller du compte conserve sur disque le dernier snapshot et l'heure du dernier appel. Il écrit l'horodatage avant d'envoyer la requête, donc un appel en échec ou un processus interrompu consomme quand même l'intervalle de 300 secondes :

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

Au démarrage, le poller recharge l'heure d'appel et le snapshot sauvegardés. Le pas à pas du réconciliateur montre un second appel servi immédiatement depuis le cache, et dans le test enregistré, un nouveau processus a aussi réutilisé la réponse persistée.

Exécutez un réconciliateur par token. Le fichier d'état prend en charge les redémarrages séquentiels, pas la coordination entre pollers concurrents. Chaque instance garde son propre horodatage en mémoire et ne prend aucun verrou interprocessus. Pendant les tests, deux pollers avec des fichiers d'état séparés ont provoqué un rejet pour appel prématuré, et un fichier partagé ne suffit pas à lui seul à rendre des pollers concurrents sûrs.

La table de champs documentée décrit des totaux et des ventilations par domaine. La charge utile enregistrée les imbriquait sous currentMonth et metrics:

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

Le parseur accepte cette structure observée et une structure plate. Il convertit les totaux sous forme de chaîne et les pourcentages sous forme de chaîne tels que "100.0%", et quand les comptes par domaine sont présents, il calcule le ratio à partir des succès et du total des requêtes. Les noms de domaine réels étaient des labels nus tels que glassdoor plutôt que glassdoor.com. Les données accusaient plusieurs minutes de retard sur le trafic, et product=crawler renvoyait la même structure mensuelle.

Compteurs locaux rapides, totaux de compte lents mais de référence. La chronologie est illustrative ; les deux ratios sont ceux enregistrés.

Prometheus peut scraper les jauges du réconciliateur toutes les 60 secondes sans déclencher un appel à l'Account API à chaque scrape. Les appels qui tombent dans l'intervalle de garde renvoient des données en cache, et le réconciliateur de longue durée attend simplement que le prochain appel soit autorisé.

Les recording rules dérivent un label site pour que les noms d'hôte puissent être comparés aux labels nus du compte. C'est une simple regex sur le nom d'hôte, pas une résolution tenant compte des suffixes publics. Elle fonctionne pour les cibles en .com et .org de la démo ; adaptez le mapping avant de la pointer vers example.co.uk.

Comprendre le repli sur le budget

remainingCredits était absent de la réponse Crawling API enregistrée. La documentation précise qu'il n'est renvoyé que pour les produits par abonnement, donc le réconciliateur a besoin d'un repli. Avec CRAWLBASE_MONTHLY_BUDGET défini, le snapshot est exporté comme max(0, budget - totalSuccess) sous source="budget". Quand l'API fournit bien les crédits restants, la jauge utilise source="account_api" à la place :

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)
    )

Le repli est une estimation par nombre de requêtes. La règle des jours avant zéro divise la jauge restante par le taux de requêtes facturables de la dernière heure multiplié par 86 400, ce qui suppose un crédit par requête facturable. Le rendu JavaScript et les paliers de complexité supérieurs consomment plus de crédits, et d'autres produits peuvent puiser dans le même solde. La prévision suffit pour démontrer l'alerte budgétaire. Utilisez une consommation pondérée en crédits avant de la traiter comme une limite financière.

Relier les exporters, Prometheus et Grafana

L'acquisition et la réconciliation tournent dans des processus séparés. Chacun expose un endpoint de métriques, Prometheus scrape les deux et évalue les recording rules, et Grafana interroge ces règles pour les panneaux et les alertes.

Deux exporters, un Prometheus, un Grafana. Les destinations de notification sont à vous d'ajouter.

Le pas à pas Compose introduit quatre services : le scraper sur le port 8000, le réconciliateur sur 8001, Prometheus sur 9090 et Grafana sur 3000. La stack finale épingle Prometheus 3.1.0 et Grafana 11.4.0, et ses requirements Python épinglent HTTPX 0.28.1, prometheus-client 0.21.1 et python-dotenv 1.0.1. La configuration de scrape est courte :

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"]

Les règles sont évaluées globalement toutes les 15 secondes, le groupe de recording Crawlbase étant réglé sur 30 secondes. Grafana rafraîchit le dashboard toutes les 30 secondes et évalue son groupe d'alertes chaque minute. Aucune de ces cadences n'est la garde de cinq minutes de l'Account API ; ce sont des horloges distinctes.

Le volume nommé cbwatch-account-state est partagé par les stacks des pas à pas et la stack finale. Un simple docker compose down le conserve, donc vous pouvez passer d'une stack à l'autre sans perdre la garde. Supprimer le volume supprime avec lui l'heure d'appel sauvegardée.

Le pas à pas du dashboard introduit des recording rules pour que panneaux et alertes partagent une seule définition. Le ratio central par domaine est :

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
    )

Le numérateur compte les cb_status=200. Le dénominateur est chaque tentative terminée, timeouts compris, les deux types de token étant combinés par domaine. clamp_max plafonne le ratio à 1 quand deux compteurs échantillonnés séparément produisent un dépassement transitoire. Le résumé sur une heure utilise la même définition sur une fenêtre plus longue.

Les tests de règles Prometheus font passer des séquences de compteurs synthétiques dans les règles. Leur séquence Walmart donne 90% de succès, et une seconde séquence confirme que les timeouts abaissent le ratio. Ce sont des fixtures, pas les observations réelles.

Lire chaque panneau du dashboard selon sa propre définition

Grafana charge le dashboard depuis un JSON provisionné. Le générateur de dashboard construit ce JSON depuis Python, et un test unitaire vérifie que le fichier commité correspond au générateur. Le panneau du taux de succès pointe vers la règle partagée :

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."),
Disposition des panneaux avec les valeurs enregistrées. Chaque panneau a sa propre fenêtre, donc les nombres ne sont pas censés s'additionner entre eux.

Le schéma organise les observations enregistrées par question opérationnelle. Lisez chaque valeur avec sa fenêtre de requête ; un taux sur une heure, un taux sur cinq minutes et un total brut de compteur répondent à des questions différentes et ne doivent pas être combinés en une seule fraction de succès du lot.

Panneau ou diagnostic Observation enregistrée Interprétation
Taux de succès, une heure 65.1% Succès d'acquisition dans une démo qui contient délibérément des échecs
Part facturable, une heure 65.1% Part facturable fenêtrée, distincte des totaux bruts des compteurs
Budget restant et prévision 890 sur un budget de 1 000 ; 0,7 jour L'estimation à un crédit par requête
Succès par domaine example.com 100% ; glassdoor.com 15% ; httpbin.org 0% Santé d'acquisition par domaine ; la cible httpbin renvoie 500 volontairement
Latence p95, palier standard 87,8 secondes Estimation par histogramme sur les seules réponses reçues
Domaines les plus en échec glassdoor.com et httpbin.org Les 20 issues non-200, timeouts compris
Part bloquée de Glassdoor sur le Normal token 41% Le diagnostic distinct du ratio de blocage ; le panneau du dashboard trace les requêtes par minute
Compteurs facturable et gratuit 46 facturables ; 20 gratuites Les mêmes 66 tentatives que la ventilation par statut
Succès local contre succès du compte 65.1% contre 70.1% Ratio local sur une heure face au ratio du compte depuis le début du mois
Glassdoor, local contre compte 15% contre 36.4% La même différence de fenêtre et de périmètre au niveau du site

La ventilation par statut comptait 46 réponses en 200, six timeouts client, quatre en 576, quatre en 0, deux en 704, deux en 705, et une pour chacun des codes 521 et 611. Les 46 réponses en 200 étaient toutes facturables et les 20 autres gratuites, donc le label facturable et la ventilation par statut décrivent les mêmes 66 tentatives. Les 20 issues gratuites étaient toutes sur glassdoor.com et httpbin.org. Chaque cb_status non-200 est arrivé en HTTP 520 ; les timeouts n'avaient pas de réponse et ont été enregistrés avec none. Les taux fenêtrés et les compteurs bruts diffèrent toujours : le taux de succès sur une heure n'est pas le ratio brut de 46 sur 66.

Le p95 de 87,8 secondes a besoin d'un dénominateur précis. Le client n'observe la latence qu'une fois qu'une réponse arrive, HTTP 520 compris. Les timeouts client n'entrent jamais dans l'histogramme. La valeur est une interpolation entre buckets, pas un percentile exact sur les durées individuelles, et la borne du bucket supérieur est le timeout de 90 secondes. Les pages ordinaires ont été observées autour de deux secondes. Le timeout de 90 secondes de HTTPX s'applique à chaque opération réseau, y compris l'attente du prochain chunk, donc ce n'est pas une échéance stricte en temps horloge, comme l'explique la documentation des timeouts HTTPX .

L'écart entre les chiffres locaux et ceux du compte a plusieurs causes attendues. Les métriques locales couvrent les tentatives de ce processus et incluent les erreurs côté client. L'Account API couvre le mois entier pour chaque appelant utilisant le produit sélectionné et peut être en retard sur le trafic récent. Ces périmètres ne vous donnent ni une réconciliation de facture un pour un ni une comptabilisation identique des timeouts.

Le panneau d'âge des données signifie moins que son libellé ne le suggère. Le réconciliateur met à jour son horodatage chaque fois qu'il applique un snapshot, y compris des données en cache après un poll en échec ou un redémarrage. Lisez-le à côté des compteurs d'erreurs API ; seul, il ne prouve pas que les données en amont viennent d'être récupérées.

Rendre les alertes sur le taux de succès du scraping actionnables

Le pas à pas des alertes provisionne trois règles gérées par Grafana. Elles séparent une perte d'acquisition prolongée d'une enquête sur le routage des tokens et d'un avertissement budgétaire :

Alerte Condition Traitement prévu
Succès du domaine sous 90% Ratio de succès sur cinq minutes sous 0.9 pendant 10 minutes, sur les domaines dont le taux de tentatives dépasse 0.008/s Page
Normal token bloqué Issues sélectionnées du Normal token au-dessus de 20% des tentatives pendant cinq minutes Ticket
Épuisement prévu des crédits Jours estimés avant zéro sous trois pendant 15 minutes Page

Le seuil de débit de l'alerte par domaine correspond à peu près à une requête toutes les 125 secondes, soit 2,4 requêtes par fenêtre de cinq minutes. Il filtre les domaines presque inactifs, mais ce n'est pas une garantie de grand échantillon. Réglez le plancher de trafic et le seuil selon le volume et la fiabilité dont votre job a réellement besoin.

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] }

La règle de blocage correspond à 503|207|525|576|705 sur le Normal token. La documentation suggère le JavaScript token pour 207 et 503 ; le projet garde 525 pour les réponses plus anciennes et ajoute les issues 576 et 705 observées. Ces ajouts sont une politique applicative, pas une règle du produit. Un 403 persistant appelle toujours un examen de la réponse de la cible et de votre débit de requêtes plutôt que l'hypothèse qu'un changement de token règle tous les cas.

Page, ticket ou veille. Les labels de sévérité routent les deux premiers ; le troisième reste sur le dashboard.

Dans l'exécution réelle, l'alerte du Normal token s'est déclenchée la première, pour glassdoor.com. L'alerte de succès du domaine s'est déclenchée trois minutes plus tard, et l'alerte budgétaire est passée en attente une fois son estimation de débit stabilisée.

Le dépôt provisionne des règles et des labels de sévérité, pas Slack, l'e-mail, PagerDuty ni une intégration de ticketing. Configurez les points de contact et politiques de notification de Grafana pour router severity=page et severity=ticket là où ils doivent aller. Un label seul ne déclenche aucune astreinte.

Un 520 isolé, une réponse lente ou un 404 connu ont leur place sur le dashboard, pas dans une page d'astreinte. Un changement prolongé du taux d'échec, du temps d'exécution ou de la couverture des champs obligatoires mérite une personne. Les règles provisionnées traitent les données manquantes comme OK, donc surveillez séparément la disponibilité des exporters et les jobs arrêtés.

Les annotations Grafana utilisent un seul $ dans des références telles que $labels.domain et $values.B.Value. Le comportement enregistré de Grafana 11 laissait les doubles signes dollar tels quels, donc le dépôt teste l'écriture du template en même temps que les seuils.

Étendre les mêmes métriques aux files du Crawler et au routage des tokens

Pour le Crawler, le poller de statistiques optionnel lit /crawler/<TOKEN>/stats et exporte les totaux du compte ainsi que des jauges par crawler : en attente, concurrence, latence et en pause. Avec CRAWLER_STATS_ENABLED=true , le réconciliateur tente ces polls selon une cadence nominale de 60 secondes, que des appels API synchrones peuvent retarder.

Le pas à pas Crawler et multi-token garde aussi un client par token et route Glassdoor via un ensemble JS_DOMAINS configuré. Dans l'exécution enregistrée, la requête avec le JavaScript token a renvoyé un 200 propre. L'opérateur met à jour cet ensemble après avoir enquêté sur une alerte ; l'alerte ne réécrit jamais le routage d'elle-même :

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}")

Les deux clients écrivent dans les mêmes familles de métriques avec des valeurs de token_type différentes, donc vous pouvez inspecter les échecs propres à un token tout en gardant une seule vue par domaine. Les noms de Crawler devraient identifier des files stables de la même manière, jamais des jobs ou des URL individuels.

Lancer la stack et vérifier l'intégration

Depuis la racine du dépôt, démarrez la stack finale avec :

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)

Le modèle d'environnement final inclut le budget et les réglages optionnels du Crawler. Le modèle racine fournit les tokens pour les dossiers pas à pas, et les fichiers .env générés sont ignorés par git. La stack finale envoie 40 requêtes par défaut puis continue de servir les métriques. Les acquisitions réussies consomment des crédits. Définissez un mot de passe Grafana avant d'exposer la stack au-delà de votre machine.

Le README décrit chaque étape et chaque vérification. Dans l'arborescence publiée, final/ et steps/ se trouvent directement sous la racine ; les commandes du README qui gardent un préfixe code/ renvoient à l'ancienne arborescence parente.

Le projet livre 319 tests hors ligne avec pytest et respx, plus six tests en conditions réelles sélectionnés séparément par un marqueur. La vérification hors ligne a passé les 319 ainsi que les 41 tests de facturation du pas à pas. Les tests HTTP mockés couvrent les parseurs, la classification des statuts, les mises à jour de métriques, le comportement du cache et la gestion des redémarrages.

Le vérificateur de configuration exécute les contrôles de configuration et de règles Prometheus, les tests de règles synthétiques et la validation du dashboard. Le smoke test vérifie les cibles d'une stack en cours d'exécution, les séries exportées, les recording rules, le dashboard, la datasource et l'évaluation des règles d'alerte. La sonde en conditions réelles, la capture d'échantillons et le script des requêtes de panneaux exposent les formes de réponse et les requêtes derrière les chiffres de cet article.

Conclusion

Une observabilité du scraping utile commence par garder trois choses séparées : l'issue du crawl, la réponse de la cible et votre propre politique d'acceptation. Crawlbase fournit les signaux d'acquisition, le wrapper les enregistre, Prometheus les transforme en métriques fenêtrées, et Grafana rend visible le changement prolongé.

L'exécution réelle a montré pourquoi ces distinctions comptent. Les acquisitions 404 valides étaient facturables, une cible protégée a produit plusieurs codes d'échec, les timeouts client ont eu besoin de leur propre comptabilisation, et les statistiques de tout le compte différaient de la vue locale sur une heure. Le projet rend tout cela inspectable sans étiqueter chaque page ni embarquer une plateforme de monitoring dans le scraper.

Utilisez l' implémentation complète avec un petit ensemble de cibles représentatives, confirmez votre définition du succès et vos hypothèses de crédits, et reliez les alertes à une vraie réponse d'astreinte. Mesurez ensuite les cibles dont votre application dépend réellement.

Crawlbase Crawling API

Chaque réponse porte cb_status, original_status et le palier du domaine, pour que votre monitoring distingue un crawl bloqué d'une page manquante. Les requêtes en échec ne sont jamais facturées, et l'Account API vous donne les totaux du mois par domaine. Commencez avec jusqu'à 5 000 requêtes gratuites, sans carte.

Foire aux questions (FAQ)

Que faut-il compter comme requête de scraping réussie ?

Ce projet compte cb_status=200 comme succès d'acquisition et met chaque tentative terminée au dénominateur. Un 404 de la cible satisfait cette définition ; un 207 vide non. Pour un pipeline de produits ou d'articles, ajoutez des métriques séparées pour les enregistrements qui passent vos contrôles de champs obligatoires et de contenu. Acquisition réussie et données métier exploitables sont deux questions différentes.

Pourquoi le taux de succès de l'Account API diffère-t-il du taux local de Grafana ?

Le résumé local est une fenêtre glissante d'une heure sur les tentatives de ce processus. L'Account API rapporte l'activité depuis le début du mois pour le produit sélectionné, peut inclure d'autres appelants, et accusait plusieurs minutes de retard sur le trafic dans l'exécution enregistrée. Alignez les périmètres avant de lire un écart comme du trafic manquant ou une facturation incorrecte.

Le dashboard affiche-t-il le nombre exact de crédits restants ?

Quand l'Account API renvoie remainingCredits, ce qu'elle ne fait que pour les produits par abonnement, le réconciliateur exporte ce solde. Sinon, il soustrait les requêtes réussies du mois d'un budget configuré. Ce repli et la règle des jours avant zéro supposent un crédit par requête. Tenez compte du rendu, des multiplicateurs de domaine et de l'usage partagé entre produits avant de vous fier à l'estimation comme à un solde réel.

L'alerte de token bloqué réessaie-t-elle automatiquement avec le JavaScript token ?

Non. Le client enregistre le résultat et laisse les retries à l'appelant. L'exemple multi-token route les domaines via un ensemble configuré, et l'alerte indique à un opérateur où regarder. Mettre à jour ce routage et câbler les destinations de notification sont des décisions applicatives et opérationnelles explicites.

Commencer à construire

Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.

Crawlbase gère les proxies, les empreintes et les CAPTCHA pour que votre équipe livre des pipelines de données au lieu de maintenir l'infrastructure de crawl. Jusqu'à 5 000 requêtes gratuites, sans carte bancaire requise.

En libre-service · Sans appel commercial requis · Volumes de crawl entreprise disponibles