tutorial

How to Monitor Grafana Faro with Vigilmon

Grafana Faro collects real user monitoring data from browsers and mobile apps, but the Faro collector itself can fail silently. Learn how to monitor Faro collector availability, ingestion health, and session data pipelines with Vigilmon.

Grafana Faro is an open-source frontend observability SDK and collector that captures real user monitoring (RUM) data — JavaScript errors, performance traces, web vitals, user sessions, and custom events — from browsers and mobile apps and streams them into Grafana's observability stack. Faro bridges the gap between backend APM and the end-user experience by giving you visibility into what users actually encounter in their browsers. But Faro's collector service is a network-facing ingestion pipeline, and when it goes down, your frontend telemetry stops silently: no JavaScript errors get captured, Web Vitals data disappears, and you lose the user-experience signal exactly when an incident is happening.

Vigilmon provides external monitoring for the Grafana Faro collector and your RUM data pipeline so you know immediately when frontend observability breaks down — not hours later when someone notices empty dashboards.


Why Grafana Faro Needs External Monitoring

Faro is typically deployed as a standalone collector that accepts push traffic from SDK-instrumented frontends. That collector has its own availability, capacity, and health characteristics separate from Grafana itself:

  • Collector downtime means zero RUM data for the outage window — errors happening in production go undetected
  • Ingestion backlog can build up if the downstream Grafana Loki or Tempo cannot keep pace with incoming payloads
  • CORS or TLS misconfiguration blocks browser SDKs silently — browsers drop the request, no data arrives, and no server-side error is logged
  • Memory or connection exhaustion in the collector causes 5xx responses, which the SDK typically ignores after retries
  • SDK delivery health — the browser sends data but the collector may reject it with a 4xx; without server-side monitoring this is invisible

Vigilmon adds the outside-in perspective: it checks the collector endpoint from outside your network, validates that it accepts payloads in the correct format, and fires alerts before your on-call team notices missing panels in Grafana.


Step 1: Verify the Faro Collector Health Endpoint

The Grafana Faro collector exposes a health path that returns a 200 when ready. Confirm your collector is accessible:

# Default Faro collector health check
curl -s -o /dev/null -w "%{http_code}" http://localhost:12347/ready

# Send a test RUM payload to verify ingestion
curl -s -X POST http://localhost:12347/collect \
  -H "Content-Type: application/json" \
  -d '{"meta":{"sdk":{"name":"@grafana/faro-web-sdk","version":"1.0.0"},"app":{"name":"test","version":"0.0.1"}},"logs":[{"timestamp":"2026-07-02T00:00:00Z","level":"info","message":"health-check","context":{}}]}'

If your Faro collector sits behind a reverse proxy (Nginx, Caddy, or Traefik), also verify the proxy layer:

# Through a proxy
curl -s -o /dev/null -w "%{http_code}" https://telemetry.example.com/collect \
  -X POST -H "Content-Type: application/json" -d '{}'

A 200 or 202 confirms the collector is accepting traffic.


Step 2: Build a Health Proxy for Vigilmon

Vigilmon's HTTP monitors perform GET requests. Since Faro's /collect endpoint expects POST with a JSON body, expose a thin health wrapper that tests the full ingestion path:

Node.js Health Proxy

// faro-health.js
const express = require('express');
const axios = require('axios');

const app = express();
const FARO_URL = process.env.FARO_URL || 'http://localhost:12347';

const TEST_PAYLOAD = {
  meta: {
    sdk: { name: '@grafana/faro-web-sdk', version: '1.0.0' },
    app: { name: 'vigilmon-health-check', version: '0.0.1', environment: 'monitoring' },
    user: {},
    page: { url: 'https://example.com/health' },
  },
  logs: [{
    timestamp: new Date().toISOString(),
    level: 'info',
    message: 'vigilmon-health-check',
    context: { source: 'vigilmon' },
  }],
};

app.get('/health/faro', async (req, res) => {
  try {
    // Check readiness endpoint
    const readyRes = await axios.get(`${FARO_URL}/ready`, { timeout: 5000 });
    if (readyRes.status !== 200) {
      return res.status(503).json({ status: 'down', reason: 'readiness_failed', code: readyRes.status });
    }

    // Send a minimal payload to verify ingestion
    const collectRes = await axios.post(`${FARO_URL}/collect`, TEST_PAYLOAD, {
      headers: { 'Content-Type': 'application/json' },
      timeout: 8000,
    });

    if (collectRes.status >= 200 && collectRes.status < 300) {
      return res.status(200).json({ status: 'ok', collectorStatus: collectRes.status });
    }

    return res.status(503).json({
      status: 'degraded',
      reason: 'collect_endpoint_rejected',
      collectorStatus: collectRes.status,
    });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

app.listen(3010, () => console.log('Faro health proxy on :3010'));

Python Health Proxy (FastAPI)

# faro_health.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import httpx, os
from datetime import datetime, timezone

app = FastAPI()

FARO_URL = os.environ.get("FARO_URL", "http://localhost:12347")

TEST_PAYLOAD = {
    "meta": {
        "sdk": {"name": "@grafana/faro-web-sdk", "version": "1.0.0"},
        "app": {"name": "vigilmon-health-check", "version": "0.0.1", "environment": "monitoring"},
        "user": {},
        "page": {"url": "https://example.com/health"},
    },
    "logs": [{
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "level": "info",
        "message": "vigilmon-health-check",
        "context": {"source": "vigilmon"},
    }],
}

@app.get("/health/faro")
async def faro_health():
    async with httpx.AsyncClient(timeout=8.0) as client:
        try:
            ready = await client.get(f"{FARO_URL}/ready")
            if ready.status_code != 200:
                return JSONResponse(status_code=503, content={
                    "status": "down", "reason": "readiness_failed", "code": ready.status_code
                })

            collect = await client.post(
                f"{FARO_URL}/collect",
                json=TEST_PAYLOAD,
                headers={"Content-Type": "application/json"},
            )

            if 200 <= collect.status_code < 300:
                return {"status": "ok", "collectorStatus": collect.status_code}

            return JSONResponse(status_code=503, content={
                "status": "degraded",
                "reason": "collect_endpoint_rejected",
                "collectorStatus": collect.status_code,
            })

        except Exception as e:
            return JSONResponse(status_code=503, content={"status": "down", "error": str(e)})

Step 3: Monitor CORS Headers for Browser SDK Delivery

Browser-based Faro SDKs require correct CORS headers on the collector. A missing Access-Control-Allow-Origin header causes all browsers to silently drop payloads. Test CORS from your monitoring proxy:

// Add to faro-health.js
app.get('/health/faro/cors', async (req, res) => {
  try {
    // Simulate a browser preflight OPTIONS request
    const preflightRes = await axios.options(`${FARO_URL}/collect`, {
      headers: {
        Origin: 'https://your-app.example.com',
        'Access-Control-Request-Method': 'POST',
        'Access-Control-Request-Headers': 'content-type',
      },
      timeout: 5000,
    });

    const acao = preflightRes.headers['access-control-allow-origin'];
    const acam = preflightRes.headers['access-control-allow-methods'];

    if (!acao) {
      return res.status(503).json({
        status: 'degraded',
        reason: 'missing_cors_allow_origin_header',
        headers: preflightRes.headers,
      });
    }

    return res.status(200).json({
      status: 'ok',
      'access-control-allow-origin': acao,
      'access-control-allow-methods': acam,
    });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

This check is critical if your Faro collector and your frontend are on different origins. A broken CORS configuration causes 100% data loss with no server-side error.


Step 4: Configure Vigilmon HTTP Monitors

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your Faro health proxy: https://your-app.example.com/health/faro
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 5000ms
  6. Under Alert channels, assign your Slack or PagerDuty integration
  7. Save the monitor

Add a second monitor for CORS health:

  • URL: https://your-app.example.com/health/faro/cors
  • Expected: 200, body contains "status":"ok"
  • Interval: 5 minutes
  • Alert channel: frontend platform Slack channel

If your Faro collector exposes its readiness endpoint publicly, also add a direct check:

  • URL: https://telemetry.example.com/ready
  • Expected: 200
  • Interval: 30 seconds
  • Alert channel: SRE on-call

Step 5: Heartbeat Monitoring for RUM Data Pipeline Continuity

HTTP monitors confirm the collector is accepting requests, but they cannot verify that data flows all the way through to Grafana dashboards. Use a Vigilmon heartbeat monitor to validate the end-to-end pipeline:

Set Up the Heartbeat Monitor

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: faro-rum-pipeline-alive
  3. Set the expected interval: 10 minutes
  4. Set the grace period: 20 minutes
  5. Save — copy the heartbeat URL: https://vigilmon.online/heartbeat/abc123xyz

Pipeline Validator Sidecar

Run a small process that sends a synthetic RUM event, then checks that Grafana Loki received it:

// faro-pipeline-validator.js
const axios = require('axios');

const FARO_URL = process.env.FARO_URL || 'http://localhost:12347';
const LOKI_URL = process.env.LOKI_URL || 'http://localhost:3100';
const HEARTBEAT_URL = process.env.VIGILMON_HEARTBEAT_URL;
const CHECK_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes

async function validatePipeline() {
  const traceId = `vigilmon-${Date.now()}`;

  // 1. Send synthetic event to Faro
  await axios.post(`${FARO_URL}/collect`, {
    meta: {
      sdk: { name: '@grafana/faro-web-sdk', version: '1.0.0' },
      app: { name: 'pipeline-validator', version: '0.0.1', environment: 'monitoring' },
      user: {},
      page: { url: 'https://example.com/validate' },
    },
    logs: [{
      timestamp: new Date().toISOString(),
      level: 'info',
      message: `pipeline-check:${traceId}`,
      context: { traceId, source: 'vigilmon-validator' },
    }],
  }, { headers: { 'Content-Type': 'application/json' }, timeout: 10000 });

  // 2. Wait for Loki ingestion (Faro buffers briefly before shipping)
  await new Promise(r => setTimeout(r, 30000));

  // 3. Query Loki for the synthetic event
  const query = `{app="pipeline-validator"} |= "${traceId}"`;
  const lokiRes = await axios.get(`${LOKI_URL}/loki/api/v1/query_range`, {
    params: {
      query,
      start: Date.now() * 1e6 - 5 * 60 * 1e9, // 5 min ago in ns
      end: Date.now() * 1e6,
      limit: 1,
    },
    timeout: 10000,
  });

  const streams = lokiRes.data?.data?.result || [];
  const found = streams.some(s => s.values?.length > 0);

  if (found && HEARTBEAT_URL) {
    await axios.get(HEARTBEAT_URL, { timeout: 3000 }).catch(() => {});
    console.log(`Pipeline validated for traceId=${traceId}`);
  } else {
    console.error(`Pipeline validation FAILED for traceId=${traceId} — event not found in Loki`);
  }
}

// Run immediately, then on interval
validatePipeline().catch(console.error);
setInterval(() => validatePipeline().catch(console.error), CHECK_INTERVAL_MS);

This sidecar validates the full path: Faro SDK → Collector → Loki → queryable. If the event doesn't appear in Loki within the window, the pipeline has a gap and the heartbeat stops — Vigilmon fires an alert.


Step 6: Alert Routing for Faro Failures

| Monitor | Alert Channel | Priority | |---|---|---| | /health/faro (collector down or rejecting payloads) | Slack + PagerDuty | P1 | | /health/faro/cors (CORS misconfiguration) | Slack + email | P2 | | Direct /ready check | Slack + PagerDuty | P1 | | Heartbeat: faro-rum-pipeline-alive | Slack + email | P2 |

CORS misconfiguration is P2 at first but escalates to P1 if your application has high traffic — a broken CORS header means 100% of browser-originated RUM data is silently dropped. Configure Vigilmon to escalate the CORS alert from Slack to PagerDuty after 15 minutes without resolution.

Set the response time threshold on the collector health check to 3000ms — a slow collector is often the first symptom of memory pressure before it starts dropping payloads.


Summary

Grafana Faro closes the frontend observability gap — but only when the collector is healthy and the pipeline flows. Vigilmon gives you the outside-in monitoring that keeps your RUM data stream reliable:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on /health/faro | Collector availability, ingestion path end-to-end | | HTTP monitor on /health/faro/cors | CORS header validity for browser SDK delivery | | Direct readiness check | Collector process health | | Heartbeat monitor | Full pipeline continuity — Faro → Loki → queryable |

Get started free at vigilmon.online — your first Faro collector monitor is live in under two minutes.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →