Apache Heron is Twitter's battle-tested distributed real-time analytics system — the successor to Apache Storm — designed for higher throughput, lower latency, and simpler debugging at scale. But when the Topology Master (tmaster) crashes, a scheduler loses contact with its workers, or a topology's spouts fall behind, your event processing pipeline stalls silently. Heron's topology state transitions don't surface as obvious errors; they appear as missing downstream records, growing consumer lag, or stale aggregates that look valid but are hours behind.
Vigilmon gives you external visibility into Heron cluster health through HTTP probe monitoring (via Heron's built-in tracker REST API or a custom health sidecar) and heartbeat monitoring for your Heron topology client applications. This tutorial covers both.
Why Heron Needs External Monitoring
Heron's built-in tooling (Heron UI on port 8889, Tracker REST API on port 8888, metrics API) provides rich internal metrics — but only if someone is watching. External monitoring with Vigilmon adds:
- Proactive alerting when the Heron Tracker API becomes unreachable (early sign of tmaster failure or cluster partition)
- Topology liveness detection via the Tracker REST API that checks active topology status and component health
- Heartbeat monitoring so you know immediately when a client application or spout stops successfully processing events
- Multi-region availability checking from outside your Heron cluster network
These layers work together: the Tracker API can report a topology as RUNNING while the tmaster is restarting, and a topology can appear active while individual bolt threads are deadlocked on downstream I/O.
Step 1: Build a Heron Health Endpoint
Heron exposes a Tracker REST API (default port 8888) and a UI on port 8889. Use them directly or wrap in a custom health sidecar for richer checks.
Using the Heron Tracker REST API Directly
# Check all topologies in the default cluster
curl -i http://heron-tracker.example.com:8888/topologies
# Check a specific topology's runtime state
curl -i "http://heron-tracker.example.com:8888/topologies/runtime?cluster=default&environ=prod&topology=my-topology"
# Check tmaster location for a topology
curl -i "http://heron-tracker.example.com:8888/topologies/logicalplan?cluster=default&environ=prod&topology=my-topology"
Point a Vigilmon monitor directly at http://heron-tracker.example.com:8888/topologies for a quick reachability check.
Custom Python Health Sidecar
For deeper health checks — topology status, stmgr connectivity, tmaster liveness — build a lightweight sidecar that aggregates Heron Tracker responses:
# heron_health.py
from flask import Flask, jsonify
import requests
import os
app = Flask(__name__)
TRACKER_URL = os.environ.get('HERON_TRACKER_URL', 'http://localhost:8888')
CLUSTER = os.environ.get('HERON_CLUSTER', 'default')
ENVIRON = os.environ.get('HERON_ENVIRON', 'prod')
REQUIRED_TOPOLOGY = os.environ.get('HERON_TOPOLOGY_NAME', 'production-pipeline')
@app.route('/health/heron')
def heron_health():
try:
# Check tracker is reachable and list topologies
resp = requests.get(
f'{TRACKER_URL}/topologies',
params={'cluster': CLUSTER, 'environ': ENVIRON},
timeout=5
)
resp.raise_for_status()
data = resp.json()
topologies = [
t.get('name') for t in data.get('result', [])
]
if REQUIRED_TOPOLOGY not in topologies:
return jsonify({
'status': 'down',
'reason': f'topology_{REQUIRED_TOPOLOGY}_not_found',
}), 503
# Check topology runtime state
runtime_resp = requests.get(
f'{TRACKER_URL}/topologies/runtime',
params={
'cluster': CLUSTER,
'environ': ENVIRON,
'topology': REQUIRED_TOPOLOGY,
},
timeout=5
)
runtime_resp.raise_for_status()
runtime = runtime_resp.json().get('result', {})
stmgrs = runtime.get('stmgrs', {})
total = len(stmgrs)
alive = sum(1 for s in stmgrs.values() if s.get('is_alive', False))
if alive == 0:
return jsonify({
'status': 'down',
'reason': 'no_alive_stmgrs',
'stmgrs_total': total,
'stmgrs_alive': alive,
}), 503
if alive < total:
return jsonify({
'status': 'degraded',
'stmgrs_total': total,
'stmgrs_alive': alive,
}), 503
return jsonify({
'status': 'ok',
'topology': REQUIRED_TOPOLOGY,
'stmgrs_total': total,
'stmgrs_alive': alive,
}), 200
except requests.RequestException as e:
return jsonify({'status': 'down', 'error': str(e)}), 503
if __name__ == '__main__':
app.run(port=8092)
Java Health Sidecar (Heron Client)
// HeronHealthController.java
@RestController
public class HeronHealthController {
@Value("${heron.tracker.url:http://localhost:8888}")
private String trackerUrl;
@Value("${heron.cluster:default}")
private String cluster;
@Value("${heron.environ:prod}")
private String environ;
@Value("${heron.topology.name}")
private String topologyName;
private final RestTemplate restTemplate = new RestTemplate();
@GetMapping("/health/heron")
public ResponseEntity<Map<String, Object>> checkHealth() {
Map<String, Object> status = new LinkedHashMap<>();
try {
String listUrl = trackerUrl + "/topologies?cluster=" + cluster + "&environ=" + environ;
ResponseEntity<Map> listResp = restTemplate.getForEntity(listUrl, Map.class);
List<Map<String, Object>> result =
(List<Map<String, Object>>) listResp.getBody().get("result");
boolean found = result.stream()
.anyMatch(t -> topologyName.equals(t.get("name")));
if (!found) {
status.put("status", "down");
status.put("reason", "topology_not_found");
return ResponseEntity.status(503).body(status);
}
String runtimeUrl = trackerUrl + "/topologies/runtime?cluster=" + cluster
+ "&environ=" + environ + "&topology=" + topologyName;
ResponseEntity<Map> runtimeResp = restTemplate.getForEntity(runtimeUrl, Map.class);
Map<String, Object> runtimeResult =
(Map<String, Object>) runtimeResp.getBody().get("result");
Map<String, Object> stmgrs =
(Map<String, Object>) runtimeResult.get("stmgrs");
long alive = stmgrs.values().stream()
.filter(s -> Boolean.TRUE.equals(((Map<?, ?>) s).get("is_alive")))
.count();
if (alive == 0) {
status.put("status", "down");
status.put("reason", "no_alive_stmgrs");
return ResponseEntity.status(503).body(status);
}
status.put("status", "ok");
status.put("topology", topologyName);
status.put("stmgrs_alive", alive);
status.put("stmgrs_total", stmgrs.size());
return ResponseEntity.ok(status);
} catch (Exception e) {
status.put("status", "down");
status.put("error", e.getMessage());
return ResponseEntity.status(503).body(status);
}
}
}
Step 2: Configure Vigilmon HTTP Monitor for Heron
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your Heron health endpoint:
https://your-app.example.com/health/heron - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
5000ms(Tracker API calls aggregate tmaster and stmgr lookups)
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- Save the monitor
Add a second monitor for the Heron Tracker API directly:
- URL:
http://heron-tracker.example.com:8888/topologies - Expected:
200 - Interval:
2 minutes - Alert channel: on-call channel
Vigilmon's multi-region probe consensus prevents false positives from transient network blips between probe nodes and your Heron cluster.
Step 3: Heartbeat Monitoring for Heron Topology Clients
Tracker API and topology health checks are necessary — but not sufficient. A Heron spout can be stuck in retry loops for failed tuples, a bolt can be processing tuples and acking them without producing correct output, or a topology can appear healthy while processing at a fraction of normal throughput. Vigilmon heartbeat monitors detect these silent processing stalls: your topology pings Vigilmon after successfully processing each batch of tuples. If pings stop, Vigilmon fires an alert.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
heron-topology-spout - Set the expected interval: 5 minutes (adjust to your tuple throughput)
- Set the grace period: 10 minutes
- Save — copy the heartbeat URL, e.g.
https://vigilmon.online/heartbeat/abc123xyz
Wire It Into Your Heron Spout (Java)
// MonitoredHeronSpout.java
import org.apache.heron.api.spout.BaseRichSpout;
import org.apache.heron.api.spout.SpoutOutputCollector;
import org.apache.heron.api.topology.TopologyContext;
import org.apache.heron.api.tuple.Values;
import java.net.http.*;
import java.net.URI;
import java.util.Map;
public class MonitoredHeronSpout extends BaseRichSpout {
private SpoutOutputCollector collector;
private final String heartbeatUrl = System.getenv("VIGILMON_HEARTBEAT_URL");
private final HttpClient httpClient = HttpClient.newHttpClient();
private long tuplesEmitted = 0;
private static final int HEARTBEAT_EVERY = 1000;
@Override
public void open(Map<String, Object> conf, TopologyContext context,
SpoutOutputCollector collector) {
this.collector = collector;
}
@Override
public void nextTuple() {
String event = fetchNextEvent();
if (event != null) {
collector.emit(new Values(event));
tuplesEmitted++;
if (tuplesEmitted % HEARTBEAT_EVERY == 0) {
pingVigilmon();
}
}
}
private void pingVigilmon() {
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(heartbeatUrl))
.GET().build();
httpClient.sendAsync(req, HttpResponse.BodyHandlers.discarding());
} catch (Exception e) {
// Non-fatal: don't interrupt tuple emission
}
}
private String fetchNextEvent() {
// Your event source logic here
return null;
}
@Override
public void declareOutputFields(
org.apache.heron.api.topology.OutputFieldsDeclarer declarer) {
declarer.declare(new org.apache.heron.api.tuple.Fields("event"));
}
}
Python Heron Bolt with Heartbeat (heronpy)
# monitored_bolt.py
import heronpy.api.bolt.bolt as bolt
import requests
import os
import time
class MonitoredProcessingBolt(bolt.Bolt):
HEARTBEAT_INTERVAL = 60 # seconds
def initialize(self, config, context):
self.heartbeat_url = os.environ['VIGILMON_HEARTBEAT_URL']
self.last_heartbeat = time.time()
def process(self, tup):
result = process_event(tup.values[0])
self.emit([result])
self.ack(tup)
if time.time() - self.last_heartbeat > self.HEARTBEAT_INTERVAL:
try:
requests.get(self.heartbeat_url, timeout=5)
except Exception:
pass
self.last_heartbeat = time.time()
Step 4: Alert Routing for Heron Failures
Heron failures cascade: a tmaster crash causes stmgrs to lose coordination, which causes containers to restart, which causes bolt and spout instances to become unavailable. Alert routing should reflect this cascade order.
Configure alert routing in Vigilmon:
| Monitor | Alert Channel | Priority |
|---|---|---|
| Heron health /health/heron | Slack + PagerDuty | P1 |
| Tracker API /topologies | Slack | P2 |
| Heartbeat: production topology spout | Slack + email | P2 |
| Heartbeat: analytics bolt output sink | Email | P3 |
Set response time thresholds for early warning:
- Alert at
3000msfor the health endpoint (slow tracker responses signal tmaster pressure) - Alert at
8000msfor the tracker endpoint (slow aggregation signals stmgr communication delay)
For event-critical topologies, enable two consecutive failures before alerting — Heron's container restarts during autoscaling events can briefly cause the tracker to report degraded stmgr status without representing a full outage.
Summary
Heron failures are silent at the data layer. External monitoring catches tmaster crashes, topology stalls, and spout processing halts before they become event pipeline outages:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /health/heron | Stmgr liveness, topology presence, alive container count |
| HTTP monitor on Tracker API | Tracker availability, topology list reachability |
| Heartbeat monitor | Spout/bolt liveness, tuple processing throughput |
Get started free at vigilmon.online — your first Heron monitor is running in under two minutes.