Apache Flink powers low-latency streaming pipelines — but when the JobManager goes down, all running jobs pause silently. Checkpoints stop completing. Backpressure builds on TaskManagers. Events pile up in Kafka topics. And unless you have external monitoring, your downstream consumers keep reading from stale state, completely unaware the pipeline is frozen.
Vigilmon gives you external visibility into Flink health through HTTP probe monitoring of the Flink REST API and heartbeat monitoring for streaming pipeline SLAs. This tutorial covers both.
Why Flink Needs External Monitoring
Flink's built-in web UI and metrics reporters (Prometheus, StatsD) provide deep internal visibility — but only when they're accessible and someone is watching. External monitoring with Vigilmon adds:
- JobManager availability probing from outside your cluster network
- Job failure detection via the Flink REST API job overview endpoint
- Checkpoint lag alerting before checkpoint failures trigger job restarts
- Backpressure detection to catch task bottlenecks before they cascade upstream
- Heartbeat SLA monitoring so you know when a streaming job stops producing output events
These layers work together: Flink can show all jobs as "RUNNING" while checkpoint lag climbs toward the timeout threshold, and a job can checkpoint successfully while a downstream sink is silently dropping events.
Step 1: Build a Flink Health Endpoint
Flink exposes a REST API on the JobManager at http://jobmanager-host:8081. You need a sidecar that wraps critical checks into a single HTTP health response for Vigilmon to probe.
Node.js Health Sidecar
// health/flink.js
const express = require('express');
const axios = require('axios');
const app = express();
const FLINK_API = process.env.FLINK_API_URL || 'http://localhost:8081';
// JobManager overview — are all expected jobs running?
app.get('/health/flink', async (req, res) => {
try {
const overview = await axios.get(`${FLINK_API}/overview`, { timeout: 5000 });
const data = overview.data;
const failing = data['jobs-failed'] || 0;
const cancelled = data['jobs-cancelled'] || 0;
if (data.taskmanagers === 0) {
return res.status(503).json({
status: 'down',
reason: 'no_taskmanagers',
detail: data,
});
}
if (failing > 0) {
return res.status(503).json({
status: 'degraded',
reason: 'failed_jobs',
jobs_failed: failing,
});
}
return res.status(200).json({
status: 'ok',
jobs_running: data['jobs-running'],
taskmanagers: data.taskmanagers,
slots_available: data['slots-available'],
slots_total: data['slots-total'],
});
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
// Checkpoint health — check recent checkpoint completion lag
app.get('/health/flink/checkpoints', async (req, res) => {
try {
// List all running jobs
const jobsRes = await axios.get(`${FLINK_API}/jobs`, { timeout: 5000 });
const runningJobs = jobsRes.data.jobs.filter(j => j.status === 'RUNNING');
const checkpointIssues = [];
for (const job of runningJobs) {
const cpRes = await axios.get(`${FLINK_API}/jobs/${job.id}/checkpoints`, { timeout: 5000 });
const summary = cpRes.data.summary;
const latest = cpRes.data.latest;
if (!latest || !latest.completed) {
checkpointIssues.push({ job: job.id, reason: 'no_completed_checkpoint' });
continue;
}
// Alert if last checkpoint completed more than 10 minutes ago
const lagMs = Date.now() - latest.completed['trigger-timestamp'];
const lagThresholdMs = parseInt(process.env.CHECKPOINT_LAG_THRESHOLD_MS || '600000');
if (lagMs > lagThresholdMs) {
checkpointIssues.push({
job: job.id,
reason: 'checkpoint_lag',
lag_ms: lagMs,
});
}
}
if (checkpointIssues.length > 0) {
return res.status(503).json({
status: 'degraded',
reason: 'checkpoint_issues',
issues: checkpointIssues,
});
}
return res.status(200).json({ status: 'ok', jobs_checked: runningJobs.length });
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
// Backpressure — detect tasks with high backpressure ratio
app.get('/health/flink/backpressure', async (req, res) => {
try {
const jobsRes = await axios.get(`${FLINK_API}/jobs`, { timeout: 5000 });
const runningJobs = jobsRes.data.jobs.filter(j => j.status === 'RUNNING');
const backpressured = [];
for (const job of runningJobs) {
const verticesRes = await axios.get(`${FLINK_API}/jobs/${job.id}`, { timeout: 5000 });
for (const vertex of verticesRes.data.vertices) {
const bpRes = await axios.get(
`${FLINK_API}/jobs/${job.id}/vertices/${vertex.id}/backpressure`,
{ timeout: 5000 }
);
const ratio = bpRes.data['backpressure-level'];
if (ratio === 'high') {
backpressured.push({ job: job.id, vertex: vertex.name, level: ratio });
}
}
}
if (backpressured.length > 0) {
return res.status(503).json({
status: 'degraded',
reason: 'backpressure_detected',
tasks: backpressured,
});
}
return res.status(200).json({ status: 'ok' });
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
app.listen(3009);
Python Health Sidecar
# health/flink_health.py
import os, time, requests
from flask import Flask, jsonify
app = Flask(__name__)
FLINK_API = os.environ.get("FLINK_API_URL", "http://localhost:8081")
CHECKPOINT_LAG_THRESHOLD = int(os.environ.get("CHECKPOINT_LAG_THRESHOLD_MS", 600000))
@app.route("/health/flink")
def flink_health():
try:
r = requests.get(f"{FLINK_API}/overview", timeout=5)
data = r.json()
if data.get("taskmanagers", 0) == 0:
return jsonify({"status": "down", "reason": "no_taskmanagers"}), 503
if data.get("jobs-failed", 0) > 0:
return jsonify({"status": "degraded", "jobs_failed": data["jobs-failed"]}), 503
return jsonify({"status": "ok", "jobs_running": data.get("jobs-running", 0)}), 200
except Exception as e:
return jsonify({"status": "down", "error": str(e)}), 503
@app.route("/health/flink/checkpoints")
def checkpoint_health():
try:
jobs = [j for j in requests.get(f"{FLINK_API}/jobs", timeout=5).json()["jobs"]
if j["status"] == "RUNNING"]
issues = []
for job in jobs:
cp = requests.get(f"{FLINK_API}/jobs/{job['id']}/checkpoints", timeout=5).json()
latest = (cp.get("latest") or {}).get("completed")
if not latest:
issues.append({"job": job["id"], "reason": "no_completed_checkpoint"})
continue
lag = int(time.time() * 1000) - latest["trigger-timestamp"]
if lag > CHECKPOINT_LAG_THRESHOLD:
issues.append({"job": job["id"], "reason": "checkpoint_lag", "lag_ms": lag})
if issues:
return jsonify({"status": "degraded", "issues": issues}), 503
return jsonify({"status": "ok", "jobs_checked": len(jobs)}), 200
except Exception as e:
return jsonify({"status": "down", "error": str(e)}), 503
if __name__ == "__main__":
app.run(port=3009)
Step 2: Configure Vigilmon HTTP Monitors for Flink
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your Flink health endpoint:
https://your-host.example.com/health/flink - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
8000ms
- Status code:
- Under Alert channels, assign your on-call Slack channel or PagerDuty integration
- Save the monitor
Add monitors for checkpoints and backpressure:
| Monitor | URL | Interval | Priority |
|---|---|---|---|
| JobManager | /health/flink | 1 minute | P1 |
| Checkpoints | /health/flink/checkpoints | 2 minutes | P1 |
| Backpressure | /health/flink/backpressure | 5 minutes | P2 |
Vigilmon's multi-region consensus prevents transient checkpoint delays from triggering false positives.
Step 3: Heartbeat Monitoring for Streaming Pipeline SLAs
For streaming pipelines with output SLAs (e.g., "enriched events must reach the sink within 5 minutes of source ingestion"), Vigilmon heartbeat monitors detect when output stops flowing — even when the Flink job shows as RUNNING.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
flink-enrichment-pipeline-output - Set the expected interval: 5 minutes
- Set the grace period: 10 minutes
- Save — copy the heartbeat URL, e.g.
https://vigilmon.online/heartbeat/abc123xyz
Wire Heartbeats Into Your Flink Job (Java)
// VigilmonHeartbeatSink.java
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicLong;
public class VigilmonHeartbeatSink<T> extends RichSinkFunction<T> {
private final String heartbeatUrl;
private final long intervalMs;
private transient AtomicLong lastPing;
private transient HttpClient httpClient;
public VigilmonHeartbeatSink(String heartbeatUrl, long intervalMs) {
this.heartbeatUrl = heartbeatUrl;
this.intervalMs = intervalMs;
}
@Override
public void open(org.apache.flink.configuration.Configuration parameters) {
this.lastPing = new AtomicLong(0);
this.httpClient = HttpClient.newHttpClient();
}
@Override
public void invoke(T value, Context context) throws Exception {
long now = Instant.now().toEpochMilli();
if (now - lastPing.get() >= intervalMs) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(heartbeatUrl))
.GET()
.build();
httpClient.sendAsync(req, HttpResponse.BodyHandlers.discarding());
lastPing.set(now);
}
}
}
Wire it into your Flink pipeline:
DataStream<EnrichedEvent> enriched = sourceStream
.keyBy(Event::getCustomerId)
.process(new EnrichmentFunction())
.name("enrich");
// Real sink
enriched.sinkTo(kafkaSink).name("kafka-output");
// Vigilmon heartbeat tap — sends ping every 2 minutes of output
enriched.addSink(new VigilmonHeartbeatSink<>(
System.getenv("VIGILMON_HEARTBEAT_URL"),
120_000L
)).name("vigilmon-heartbeat").setParallelism(1);
Python (PyFlink)
# vigilmon_sink.py
import os, time, urllib.request
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.functions import SinkFunction
class VigilmonHeartbeatSink(SinkFunction):
def __init__(self, heartbeat_url: str, interval_seconds: int = 120):
self._url = heartbeat_url
self._interval = interval_seconds
self._last_ping = 0
def invoke(self, value, context):
now = time.time()
if now - self._last_ping >= self._interval:
try:
urllib.request.urlopen(self._url, timeout=5)
except Exception:
pass
self._last_ping = now
Step 4: TaskManager Slot Utilization Alerting
When Flink jobs request more slots than are available, new jobs queue or fail to start. Monitor slot pressure:
# Check slot availability via Flink REST API
AVAILABLE=$(curl -s http://localhost:8081/overview | jq '.["slots-available"]')
TOTAL=$(curl -s http://localhost:8081/overview | jq '.["slots-total"]')
if [ "${AVAILABLE}" -lt 2 ]; then
echo "WARNING: only ${AVAILABLE}/${TOTAL} slots available"
# Do NOT ping heartbeat — let the slot-pressure monitor alert
else
curl -fsS "${VIGILMON_SLOT_HEARTBEAT_URL}"
fi
Summary
Flink failures are subtle: jobs stay in RUNNING state while checkpoints lag, backpressure builds, and output stalls. External monitoring with Vigilmon catches all three failure modes before downstream consumers notice:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /health/flink | JobManager availability, failed job count, TaskManager count |
| HTTP monitor on /health/flink/checkpoints | Checkpoint completion lag, missing checkpoints |
| HTTP monitor on /health/flink/backpressure | High-backpressure task detection |
| Heartbeat monitor | Streaming pipeline output SLA — events flowing to sink on schedule |
Get started free at vigilmon.online — your first Flink monitor is running in under two minutes.