Apache Curator is the standard Java client library for Apache ZooKeeper — it wraps raw ZooKeeper sessions with retry policies, leader election, distributed locks, service discovery, and other high-level coordination recipes. But when a Curator recipe stalls — a leader latch that never acquires leadership, a distributed lock stuck in a wait loop, a service discovery cache that stops refreshing — nothing throws an exception. The application just sits there, waiting indefinitely, while downstream callers time out or hang.
Vigilmon gives you external visibility into Curator-dependent application health through HTTP probe monitoring and heartbeat monitors tied directly to recipe lifecycle events. This tutorial covers both.
Why Curator Needs External Monitoring
Curator handles the hard parts of ZooKeeper correctly — retries, session recovery, watcher re-registration — but those same automatic recovery behaviors make failures invisible:
- Leader election stalls silently: a
LeaderLatchmay never callhasLeadership()if the ensemble is degraded, with no exception surfaced - Distributed lock timeouts accumulate:
InterProcessMutex.acquire()blocks until a timeout you may have set too high - Service discovery caches go stale when ZooKeeper sessions expire and the cache stops refreshing
- Connection state changes (SUSPENDED → LOST) are events, not exceptions — code that doesn't explicitly handle them silently operates on a stale view
External monitoring with Vigilmon adds:
- Proactive alerting when a Curator-based service stops completing its coordination cycles
- Heartbeat monitoring so you know immediately when a leader election or lock acquisition loop stops progressing
- HTTP probe monitoring for Curator client connection state exposed via a health endpoint
- Multi-region availability checking from outside your network perimeter
Step 1: Build a Curator Health Endpoint
Curator doesn't expose an HTTP health interface — you need to add one to your application that reflects client connection state and recipe liveness.
Java Spring Boot Health Endpoint (Actuator)
// CuratorHealthIndicator.java
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.state.ConnectionState;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CuratorHealthIndicator implements HealthIndicator {
private final CuratorFramework curator;
public CuratorHealthIndicator(CuratorFramework curator) {
this.curator = curator;
}
@Override
public Health health() {
CuratorFrameworkState state = curator.getState();
if (state != CuratorFrameworkState.STARTED) {
return Health.down()
.withDetail("reason", "curator_not_started")
.withDetail("state", state.name())
.build();
}
// Check ZooKeeper connection state via the underlying client
// CuratorFramework.getZookeeperClient().isConnected() checks the raw session
boolean connected = curator.getZookeeperClient().isConnected();
if (!connected) {
return Health.down()
.withDetail("reason", "zookeeper_not_connected")
.withDetail("state", state.name())
.build();
}
return Health.up()
.withDetail("state", state.name())
.withDetail("zookeeper_connected", true)
.build();
}
}
With Spring Boot Actuator, this exposes /actuator/health/curator automatically. Point Vigilmon directly at this endpoint.
Node.js / Express Health Sidecar (for non-Java services wrapping Curator output)
// health/curator.js
const express = require('express');
const { execSync } = require('child_process');
const app = express();
// This sidecar reads a shared state file written by your Curator-dependent Java app
// The Java app writes { "connected": true, "last_election_ms": 1234567890123 } periodically
const STATE_FILE = process.env.CURATOR_STATE_FILE || '/var/run/curator-health.json';
const STALE_THRESHOLD_MS = parseInt(process.env.STALE_THRESHOLD_MS || '120000'); // 2 minutes
app.get('/health/curator', (req, res) => {
try {
const raw = require('fs').readFileSync(STATE_FILE, 'utf8');
const state = JSON.parse(raw);
const age = Date.now() - (state.written_at_ms || 0);
if (!state.connected) {
return res.status(503).json({ status: 'down', reason: 'zookeeper_disconnected', state });
}
if (age > STALE_THRESHOLD_MS) {
return res.status(503).json({
status: 'degraded',
reason: 'stale_state_file',
age_ms: age,
threshold_ms: STALE_THRESHOLD_MS,
});
}
return res.status(200).json({ status: 'ok', age_ms: age, state });
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
app.listen(3007);
Java: Writing the State File
// CuratorStateWriter.java — run as a scheduled task every 30s
import org.apache.curator.framework.CuratorFramework;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.util.*;
@Component
public class CuratorStateWriter {
private final CuratorFramework curator;
private final ObjectMapper mapper = new ObjectMapper();
public CuratorStateWriter(CuratorFramework curator) {
this.curator = curator;
}
@Scheduled(fixedDelay = 30000)
public void writeState() throws Exception {
Map<String, Object> state = new HashMap<>();
state.put("connected", curator.getZookeeperClient().isConnected());
state.put("framework_state", curator.getState().name());
state.put("written_at_ms", System.currentTimeMillis());
File file = new File(System.getenv().getOrDefault("CURATOR_STATE_FILE", "/var/run/curator-health.json"));
mapper.writeValue(file, state);
}
}
Python Health Sidecar (kazoo-based, equivalent recipes)
# health_curator.py
import os, json, time
from flask import Flask, jsonify
app = Flask(__name__)
STATE_FILE = os.environ.get('CURATOR_STATE_FILE', '/var/run/curator-health.json')
STALE_THRESHOLD_MS = int(os.environ.get('STALE_THRESHOLD_MS', '120000'))
@app.route('/health/curator')
def health():
try:
with open(STATE_FILE) as f:
state = json.load(f)
age_ms = int(time.time() * 1000) - state.get('written_at_ms', 0)
if not state.get('connected'):
return jsonify({'status': 'down', 'reason': 'zookeeper_disconnected'}), 503
if age_ms > STALE_THRESHOLD_MS:
return jsonify({'status': 'degraded', 'reason': 'stale_state_file', 'age_ms': age_ms}), 503
return jsonify({'status': 'ok', 'age_ms': age_ms})
except Exception as e:
return jsonify({'status': 'down', 'error': str(e)}), 503
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3007)
Step 2: Configure Vigilmon HTTP Monitor for Curator
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your Curator health endpoint:
https://your-app.example.com/actuator/health/curator(orhttps://your-app.example.com/health/curatorfor the sidecar) - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"UP"(Actuator) or"status":"ok"(sidecar) - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- Save the monitor
Vigilmon's multi-region probe consensus means a single network blip won't page your team — only sustained failures trigger alerts.
Step 3: Heartbeat Monitoring for Curator Recipe Liveness
Connection state monitoring catches disconnects, but not recipe stalls. A Curator LeaderLatch can be "connected" while stuck waiting for an ensemble with too many failed nodes to grant leadership. A InterProcessMutex can be held by a dead process while your application queues indefinitely.
Vigilmon heartbeat monitors catch these: your application pings Vigilmon after each successful recipe cycle. If pings stop, Vigilmon alerts.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
my-service-curator-leader - 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 It Into a LeaderLatch
// LeaderElectionService.java
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.leader.LeaderLatch;
import org.apache.curator.framework.recipes.leader.LeaderLatchListener;
import java.net.http.*;
import java.net.URI;
@Service
public class LeaderElectionService implements LeaderLatchListener {
private final LeaderLatch leaderLatch;
private final HttpClient http = HttpClient.newHttpClient();
private final String heartbeatUrl;
public LeaderElectionService(CuratorFramework curator,
@Value("${vigilmon.heartbeat.url}") String heartbeatUrl) throws Exception {
this.heartbeatUrl = heartbeatUrl;
this.leaderLatch = new LeaderLatch(curator, "/my-service/leader", "node-1");
this.leaderLatch.addListener(this);
this.leaderLatch.start();
}
@Override
public void isLeader() {
// We just became leader — signal Vigilmon that election completed
try {
http.sendAsync(
HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} catch (Exception ignored) {}
// Perform leader work
performLeaderWork();
}
@Override
public void notLeader() {
// Leadership lost — do not ping heartbeat here
}
@Scheduled(fixedDelay = 60000)
public void heartbeatIfLeader() {
if (leaderLatch.hasLeadership()) {
// Confirm we're still active as leader
try {
http.sendAsync(
HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} catch (Exception ignored) {}
}
}
}
Wire It Into an InterProcessMutex Lock Loop
// DistributedLockService.java
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import java.util.concurrent.TimeUnit;
import java.net.http.*;
import java.net.URI;
@Service
public class DistributedLockService {
private final InterProcessMutex mutex;
private final HttpClient http = HttpClient.newHttpClient();
private final String heartbeatUrl;
public DistributedLockService(CuratorFramework curator,
@Value("${vigilmon.heartbeat.url}") String heartbeatUrl) {
this.mutex = new InterProcessMutex(curator, "/my-service/lock");
this.heartbeatUrl = heartbeatUrl;
}
@Scheduled(fixedDelay = 30000)
public void processWithLock() throws Exception {
boolean acquired = mutex.acquire(10, TimeUnit.SECONDS);
if (!acquired) {
log.warn("Could not acquire distributed lock within 10 seconds");
return;
}
try {
// Perform protected work
doProtectedWork();
// Signal Vigilmon that lock acquisition and work completed
http.sendAsync(
HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} finally {
mutex.release();
}
}
}
Step 4: Alert Routing for Curator Failures
Curator failures range from momentary session suspension (recoverable in seconds) to permanent quorum loss (requires operator action). Route alerts by severity:
| Monitor | Alert Channel | Priority |
|---|---|---|
| HTTP: /actuator/health/curator | Slack + PagerDuty | P1 |
| Heartbeat: leader election | Slack + PagerDuty | P1 |
| Heartbeat: distributed lock loop | Slack + email | P2 |
| HTTP: Curator state file staleness | Slack | P2 |
Set response time thresholds as early warning signals:
- Alert at
2000msfor health endpoint (elevated latency indicates ZooKeeper session pressure) - Alert at
4000msfor ZooKeeper admin endpoints that the sidecar queries
For production clusters, expose a status page in Vigilmon showing Curator-dependent service health alongside the underlying ZooKeeper ensemble — this makes it immediately clear whether an application recipe stall is caused by the application or by the ensemble.
Summary
Curator failures are coordination failures — they don't throw 500 errors, they cause distributed elections and locks to stall silently. External monitoring catches these before they become incidents:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /actuator/health/curator | Client connection state, session liveness |
| HTTP monitor on state file endpoint | State file freshness, background thread health |
| Heartbeat monitor on leader election | LeaderLatch acquisition and leader work completion |
| Heartbeat monitor on lock loop | InterProcessMutex acquisition and protected work completion |
Get started free at vigilmon.online — your first Curator monitor is running in under two minutes.