Zipkin is an open-source distributed tracing system that helps engineers understand latency problems in complex microservices architectures. Instrumented services send span data to a central Zipkin server, which stores and visualizes request traces across service boundaries. Zipkin is widely deployed at companies running distributed Java, Go, Python, and Node.js services and is a foundational piece of the observability stack for many engineering teams. But Zipkin's server is a single point of failure for your entire tracing pipeline: if it goes down, every instrumented service either silently drops traces or backs up its span buffer until it overflows. You lose distributed tracing visibility precisely when a latency incident is happening.
Vigilmon monitors Zipkin server health, storage layer status, and trace ingestion pipelines externally so you know immediately when your distributed tracing backend has a problem.
Why Zipkin Needs External Monitoring
Zipkin's collector, storage, and query layers can fail independently:
- Server unavailability — the Zipkin process crashes or the JVM runs out of memory; all spans are dropped
- Storage backend failure — Zipkin supports Cassandra, Elasticsearch, and MySQL; a storage failure makes the server appear healthy while all writes fail
- Collector rejection — the
/api/v2/spansendpoint returns 5xx when the storage or internal queue is overwhelmed - Query layer degradation — the server accepts spans but
/api/v2/servicesor/api/v2/tracesreturns errors; engineers cannot investigate issues - Clock skew corruption — trace timestamps arrive out of order, making spans invisible in the UI without alerting on any error endpoint
Vigilmon gives you external HTTP monitoring that confirms the full stack — ingestion endpoint, query API, and storage health — from outside your service mesh.
Step 1: Verify Zipkin Health Endpoints
Zipkin exposes several endpoints useful for health checking:
# Health endpoint (Zipkin 2.x)
curl -s http://localhost:9411/health | jq .
# Check services endpoint (confirms query layer and storage are working)
curl -s http://localhost:9411/api/v2/services | jq .
# Check if server accepts spans (send a minimal test span)
curl -s -X POST http://localhost:9411/api/v2/spans \
-H 'Content-Type: application/json' \
-d '[{"traceId":"a2fb4a1d1a96d312","id":"a2fb4a1d1a96d312","name":"test","timestamp":'"$(date +%s%6N)"',"duration":1,"localEndpoint":{"serviceName":"health-check"}}]'
A successful span submission returns HTTP 202. The /health endpoint returns {"status":"UP"} when all storage backends are connected.
Step 2: Build a Health Proxy for Vigilmon
Create a composite health endpoint that validates Zipkin's full stack — storage connection, collector, and query API:
Node.js Health Proxy
// zipkin-health.js
const express = require('express');
const axios = require('axios');
const app = express();
const ZIPKIN_URL = process.env.ZIPKIN_URL || 'http://localhost:9411';
// Minimal test span — required fields only
function buildTestSpan() {
const nowMicros = Date.now() * 1000;
const traceId = Math.random().toString(16).slice(2, 18).padStart(16, '0');
return [{
traceId,
id: traceId,
name: 'vigilmon-health-check',
timestamp: nowMicros,
duration: 1000,
localEndpoint: { serviceName: 'vigilmon-health-check' },
tags: { 'source': 'vigilmon' },
}];
}
app.get('/health/zipkin', async (req, res) => {
const results = {};
try {
// 1. Health endpoint
const healthRes = await axios.get(`${ZIPKIN_URL}/health`, { timeout: 5000 });
results.health = healthRes.data;
if (healthRes.data?.status !== 'UP') {
return res.status(503).json({ status: 'down', reason: 'health_not_up', results });
}
} catch (err) {
return res.status(503).json({ status: 'down', reason: 'health_endpoint_unreachable', error: err.message });
}
try {
// 2. Query API — confirms storage reads work
const servicesRes = await axios.get(`${ZIPKIN_URL}/api/v2/services`, { timeout: 8000 });
results.servicesCount = servicesRes.data?.length ?? 0;
} catch (err) {
return res.status(503).json({
status: 'degraded',
reason: 'query_api_failed',
error: err.message,
results,
});
}
try {
// 3. Collector endpoint — confirms span ingestion works
const spanRes = await axios.post(
`${ZIPKIN_URL}/api/v2/spans`,
buildTestSpan(),
{ headers: { 'Content-Type': 'application/json' }, timeout: 8000 }
);
results.collectorStatus = spanRes.status;
if (spanRes.status !== 202) {
return res.status(503).json({
status: 'degraded',
reason: 'collector_rejected_spans',
collectorStatus: spanRes.status,
results,
});
}
} catch (err) {
return res.status(503).json({
status: 'degraded',
reason: 'collector_unreachable',
error: err.message,
results,
});
}
return res.status(200).json({ status: 'ok', results });
});
app.listen(3011, () => console.log('Zipkin health proxy on :3011'));
Python Health Proxy (FastAPI)
# zipkin_health.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import httpx, os, random, time
app = FastAPI()
ZIPKIN_URL = os.environ.get("ZIPKIN_URL", "http://localhost:9411")
def build_test_span():
trace_id = format(random.randint(0, 2**64 - 1), '016x')
return [{
"traceId": trace_id,
"id": trace_id,
"name": "vigilmon-health-check",
"timestamp": int(time.time() * 1_000_000),
"duration": 1000,
"localEndpoint": {"serviceName": "vigilmon-health-check"},
"tags": {"source": "vigilmon"},
}]
@app.get("/health/zipkin")
async def zipkin_health():
results = {}
async with httpx.AsyncClient(timeout=8.0) as client:
try:
health = await client.get(f"{ZIPKIN_URL}/health")
results["health"] = health.json()
if health.json().get("status") != "UP":
return JSONResponse(status_code=503, content={
"status": "down", "reason": "health_not_up", "results": results
})
except Exception as e:
return JSONResponse(status_code=503, content={
"status": "down", "reason": "health_endpoint_unreachable", "error": str(e)
})
try:
services = await client.get(f"{ZIPKIN_URL}/api/v2/services")
results["servicesCount"] = len(services.json())
except Exception as e:
return JSONResponse(status_code=503, content={
"status": "degraded", "reason": "query_api_failed", "error": str(e), "results": results
})
try:
span_res = await client.post(
f"{ZIPKIN_URL}/api/v2/spans",
json=build_test_span(),
headers={"Content-Type": "application/json"},
)
results["collectorStatus"] = span_res.status_code
if span_res.status_code != 202:
return JSONResponse(status_code=503, content={
"status": "degraded", "reason": "collector_rejected_spans", "results": results
})
except Exception as e:
return JSONResponse(status_code=503, content={
"status": "degraded", "reason": "collector_unreachable", "error": str(e), "results": results
})
return {"status": "ok", "results": results}
Step 3: Monitor Zipkin Storage Backend
If you run Zipkin with Elasticsearch or Cassandra, the storage layer can fail independently of the Zipkin process. Add a storage-specific check:
// Add to zipkin-health.js
app.get('/health/zipkin/storage', async (req, res) => {
try {
// Zipkin's /health response includes component-level status in some versions
const healthRes = await axios.get(`${ZIPKIN_URL}/health`, { timeout: 5000 });
const components = healthRes.data?.components || {};
const degradedComponents = Object.entries(components)
.filter(([, v]) => v?.status && v.status !== 'UP')
.map(([name, v]) => ({ name, status: v.status, details: v.details }));
if (degradedComponents.length > 0) {
return res.status(503).json({
status: 'degraded',
reason: 'storage_components_unhealthy',
degradedComponents,
});
}
// Also probe the trace query endpoint with a time range that should return data
const traceRes = await axios.get(`${ZIPKIN_URL}/api/v2/services`, { timeout: 10000 });
const serviceCount = traceRes.data?.length ?? 0;
return res.status(200).json({
status: 'ok',
components,
serviceCount,
});
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
This check is particularly important if you store traces in Elasticsearch — an Elasticsearch yellow or red cluster state typically degrades Zipkin's write throughput before it causes complete failure, giving you a window to act.
Step 4: Configure Vigilmon HTTP Monitors
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your Zipkin health proxy:
https://your-app.example.com/health/zipkin - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
10000ms(span submission and storage reads can be slow)
- Status code:
- Under Alert channels, assign your Slack or PagerDuty integration
- Save the monitor
Add a second monitor for storage health:
- URL:
https://your-app.example.com/health/zipkin/storage - Expected:
200, body contains"status":"ok" - Interval:
2 minutes - Alert channel: observability platform team Slack channel
Also add a direct health endpoint monitor for the fastest possible detection:
- URL:
https://zipkin.example.com/health - Expected:
200, body contains"status":"UP" - Interval:
30 seconds - Alert channel: SRE on-call PagerDuty
Step 5: Heartbeat Monitoring for Trace Ingestion Continuity
HTTP monitors confirm Zipkin is responding, but the critical metric is whether your services are actually delivering traces. A misconfigured SDK, network partition, or buffer overflow in your services can stop traces arriving at Zipkin without the server itself throwing errors.
Use a Vigilmon heartbeat monitor to confirm that traces arrive and are queryable within a time window:
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
zipkin-trace-ingestion-alive - Set the expected interval: 5 minutes
- Set the grace period: 10 minutes
- Save — copy the heartbeat URL:
https://vigilmon.online/heartbeat/abc123xyz
Trace Ingestion Validator
// zipkin-trace-validator.js
const axios = require('axios');
const ZIPKIN_URL = process.env.ZIPKIN_URL || 'http://localhost:9411';
const HEARTBEAT_URL = process.env.VIGILMON_HEARTBEAT_URL;
const SERVICE_NAME = process.env.TRACE_SERVICE_NAME || 'vigilmon-health-check';
async function validateTraceIngestion() {
const traceId = Math.random().toString(16).slice(2, 18).padStart(16, '0');
const nowMicros = Date.now() * 1000;
// 1. Submit a synthetic trace
await axios.post(`${ZIPKIN_URL}/api/v2/spans`, [{
traceId,
id: traceId,
name: 'ingestion-validator',
timestamp: nowMicros,
duration: 2000,
localEndpoint: { serviceName: SERVICE_NAME },
tags: { 'vigilmon.check': 'true' },
}], {
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
});
// 2. Wait for async storage write
await new Promise(r => setTimeout(r, 15000));
// 3. Query for the trace by ID
try {
const traceRes = await axios.get(`${ZIPKIN_URL}/api/v2/trace/${traceId}`, { timeout: 10000 });
if (traceRes.data?.length > 0) {
if (HEARTBEAT_URL) {
await axios.get(HEARTBEAT_URL, { timeout: 3000 }).catch(() => {});
}
console.log(`Trace ingestion validated: traceId=${traceId}`);
} else {
console.error(`Trace NOT found in Zipkin after storage wait: traceId=${traceId}`);
}
} catch (err) {
console.error(`Failed to query trace: ${err.message}`);
}
}
validateTraceIngestion().catch(console.error);
setInterval(() => validateTraceIngestion().catch(console.error), 5 * 60 * 1000);
This validator sends a synthetic span, waits for storage, then queries by trace ID. If the trace is not retrievable, the heartbeat does not ping and Vigilmon fires an alert.
Step 6: Alert Routing for Zipkin Failures
| Monitor | Alert Channel | Priority |
|---|---|---|
| /health/zipkin (full stack check) | Slack + PagerDuty | P1 |
| /health/zipkin/storage (storage component health) | Slack + email | P1 |
| Direct /health check | Slack + PagerDuty | P1 |
| Heartbeat: trace-ingestion-alive | Slack + email | P2 |
Zipkin downtime during an incident is a force multiplier for pain — engineers cannot use distributed traces to diagnose the incident while the incident is happening. Configure your Zipkin monitors with zero tolerance for false negatives: use a 1-minute check interval and 0-minute grace period on the direct health check so that an alert fires on the first missed check.
Response time thresholds matter here too. Set the /health/zipkin proxy monitor to alert at 5000ms — slow Zipkin responses indicate storage layer pressure and precede complete failures.
Summary
Zipkin gives distributed tracing visibility to your microservices architecture — but the tracing infrastructure itself needs monitoring. Vigilmon provides the outside-in layer that keeps your Zipkin deployment reliable:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /health/zipkin | Full stack: health, query API, and span ingestion |
| HTTP monitor on /health/zipkin/storage | Storage backend component health |
| Direct health endpoint check | Fast Zipkin process availability detection |
| Heartbeat monitor | End-to-end trace ingestion and queryability |
Get started free at vigilmon.online — your first Zipkin monitor is live in under two minutes.