Apache Griffin is an open-source data quality service platform for big data that runs accuracy, completeness, timeliness, validity, and uniqueness checks as Spark batch or streaming jobs. When a Griffin measure Spark job fails due to an executor OOM, the job silently exits with a non-zero status and Griffin marks the measure run as failed in its internal database — but no alert is sent to the data engineering team, so the next dashboard refresh shows a stale data quality score while analysts assume the pipeline is healthy; when Griffin's Livy job submission endpoint is unreachable due to a Yarn cluster resource shortage, new measure runs queue indefinitely in Griffin's scheduler without surfacing an actionable error; when Griffin's metric storage (Elasticsearch or InfluxDB) becomes unavailable, quality measurements complete but results are silently discarded — downstream consumers see a flatlined dashboard with no indication that ingestion failed. These are data quality SLA breaches disguised as normal system behavior.
Vigilmon gives you external visibility into Griffin's data quality service health through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.
Why Griffin Needs External Monitoring
Griffin's failure modes span the Spark execution, metric storage, and API scheduling layers:
- Spark measure job OOM failure: Griffin submits measure jobs to Spark via Livy; when an executor runs out of memory mid-execution, the job fails and Griffin logs the failure but does not alert consumers — data quality for that dimension is simply not measured for that window
- Livy session pool exhaustion: Griffin uses Livy to manage Spark sessions; when all Livy sessions are occupied by long-running streaming measure jobs, new batch measure jobs queue and never execute — Griffin's scheduler shows them as pending indefinitely
- Metric sink unavailability: Griffin writes measurement results to Elasticsearch, InfluxDB, or a JDBC sink; when the sink is unavailable during a measure run, Griffin completes the Spark job but discards the results silently — the measure is marked succeeded in Griffin's scheduler but no metric is ever written to the dashboard
- Hive metastore connectivity failure: Griffin's accuracy measures compare a source dataset against a target dataset via Hive tables; when the Hive metastore is unreachable, the measure job throws
MetaExceptionand fails — but Griffin's API reports the measure asPENDINGrather thanFAILED, causing indefinite queue growth - Checkpoint corruption in streaming measures: Griffin streaming measures use Spark checkpoints for fault tolerance; a corrupt checkpoint directory after a cluster restart causes the streaming job to fail at startup and never recover — the streaming data quality dashboard shows stale metrics that are weeks old
- Schedule drift in cron-based measures: Griffin uses a Quartz-based scheduler; after a Griffin service restart, Quartz can lose in-memory state for misfire-policy jobs and skip measure runs rather than catching up — data quality gaps accumulate across multiple windows before anyone notices
External monitoring with Vigilmon adds:
- Proactive alerting when Griffin's service endpoint or metric APIs stop responding
- Measure job liveness monitoring independent of Griffin's internal scheduler state
- Heartbeat monitoring so you know when batch quality scans stop completing within their expected windows
- Multi-region probe consensus that filters transient Livy session startup delays from genuine measure pipeline failures
Step 1: Build a Griffin Health Endpoint
Griffin 0.6+ ships a Spring Boot REST API. Use its built-in endpoints plus a custom health check that probes the Livy connection and metric sink.
Spring Boot Actuator (Griffin 0.6+)
Griffin's service module exposes /actuator/health by default. Enable component details in service/src/main/resources/application.properties:
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=health,info,metrics
The health endpoint includes Spring's default DB check (Griffin uses MySQL/PostgreSQL for measure metadata). Add a custom Livy connectivity check:
// health/LivyHealthIndicator.java
package com.yourorg.griffin.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class LivyHealthIndicator implements HealthIndicator {
@Value("${livy.uri:http://localhost:8998}")
private String livyUri;
private final RestTemplate restTemplate = new RestTemplate();
@Override
public Health health() {
try {
String url = livyUri + "/sessions";
restTemplate.getForObject(url, String.class);
return Health.up()
.withDetail("livy", livyUri)
.withDetail("sessions_endpoint", "reachable")
.build();
} catch (Exception e) {
return Health.down(e)
.withDetail("livy", livyUri)
.withDetail("sessions_endpoint", "unreachable")
.build();
}
}
}
Custom Griffin Measure Status Endpoint
Expose a dedicated endpoint that checks for stale or accumulating measure failures:
// api/GriffinHealthResource.java
package com.yourorg.griffin.api;
import org.apache.griffin.core.job.JobServiceImpl;
import org.apache.griffin.core.job.entity.GriffinJob;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import java.util.*;
@RestController
@RequestMapping("/api/v1/health")
public class GriffinHealthResource {
private final JobServiceImpl jobService;
public GriffinHealthResource(JobServiceImpl jobService) {
this.jobService = jobService;
}
@GetMapping("/measures")
public ResponseEntity<Map<String, Object>> measureHealth() {
Map<String, Object> result = new LinkedHashMap<>();
boolean healthy = true;
try {
List<GriffinJob> jobs = jobService.getAliveJobs();
long errorJobs = jobs.stream()
.filter(j -> "ERROR".equals(j.getJobStatus()))
.count();
result.put("total_jobs", jobs.size());
result.put("error_jobs", errorJobs);
if (errorJobs > 0) {
result.put("status", "degraded");
result.put("detail", errorJobs + " measure jobs in ERROR state");
healthy = false;
} else {
result.put("status", "ok");
}
} catch (Exception e) {
result.put("status", "down");
result.put("error", e.getMessage());
healthy = false;
}
return ResponseEntity.status(healthy ? 200 : 503).body(result);
}
}
Node.js Sidecar
// health/griffin.js
const express = require('express');
const http = require('http');
const app = express();
const GRIFFIN_HOST = process.env.GRIFFIN_HOST || 'localhost';
const GRIFFIN_PORT = parseInt(process.env.GRIFFIN_PORT || '9090');
const LIVY_HOST = process.env.LIVY_HOST || 'localhost';
const LIVY_PORT = parseInt(process.env.LIVY_PORT || '8998');
function probe(host, port, path) {
return new Promise((resolve, reject) => {
const req = http.get({ host, port, path, timeout: 5000 }, res => {
let body = '';
res.on('data', c => { body += c; });
res.on('end', () => resolve({ status: res.statusCode, body }));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
});
}
app.get('/health/griffin', async (req, res) => {
const checks = {};
let healthy = true;
try {
const griffin = await probe(GRIFFIN_HOST, GRIFFIN_PORT, '/actuator/health');
checks.griffin_api = griffin.status === 200 ? 'ok' : 'down';
if (griffin.status !== 200) healthy = false;
} catch (e) {
checks.griffin_api = 'down';
healthy = false;
}
try {
const livy = await probe(LIVY_HOST, LIVY_PORT, '/sessions');
checks.livy = livy.status === 200 ? 'ok' : 'down';
if (livy.status !== 200) healthy = false;
} catch (e) {
checks.livy = 'down';
healthy = false;
}
return res.status(healthy ? 200 : 503).json({ status: healthy ? 'ok' : 'down', checks });
});
app.listen(3020);
Python Sidecar
# health_griffin.py
import os, requests
from flask import Flask, jsonify
app = Flask(__name__)
GRIFFIN_BASE = f"http://{os.environ.get('GRIFFIN_HOST','localhost')}:{os.environ.get('GRIFFIN_PORT','9090')}"
LIVY_BASE = f"http://{os.environ.get('LIVY_HOST','localhost')}:{os.environ.get('LIVY_PORT','8998')}"
@app.route('/health/griffin')
def health():
checks = {}
healthy = True
try:
r = requests.get(f'{GRIFFIN_BASE}/actuator/health', timeout=5)
checks['griffin_api'] = 'ok' if r.status_code == 200 else 'down'
if r.status_code != 200:
healthy = False
except Exception as e:
checks['griffin_api'] = f'down: {e}'
healthy = False
try:
r = requests.get(f'{LIVY_BASE}/sessions', timeout=5)
checks['livy'] = 'ok' if r.status_code == 200 else 'down'
if r.status_code != 200:
healthy = False
except Exception as e:
checks['livy'] = f'down: {e}'
healthy = False
return jsonify({'status': 'ok' if healthy else 'down', 'checks': checks}), 200 if healthy else 503
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3020)
Step 2: Configure Vigilmon HTTP Monitor for Griffin
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to Griffin's health endpoint:
https://griffin.your-org.example.com/actuator/health - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"UP" - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- Save the monitor
Add monitors for Griffin's API and Livy:
| Endpoint | What it catches |
|---|---|
| /actuator/health | Griffin service, DB metadata store, disk space |
| /api/v1/health/measures | Measure job accumulation, ERROR state jobs |
| http://livy:8998/sessions | Livy session pool availability |
| /api/v1/griffin/metrics/values?metricName=accuracy | Metric sink write availability |
For the Livy monitor, point Vigilmon at an internal load-balanced address or use the sidecar health proxy if Livy is not publicly accessible.
Vigilmon's multi-region probing filters transient Livy session warmup delays from genuine Spark cluster resource exhaustion.
Step 3: Heartbeat Monitoring for Batch Measure Jobs
HTTP health checks verify Griffin service and Livy availability but not whether individual data quality measure runs are completing. Griffin can report healthy via Actuator while Quartz has drifted and skipped the last six measure windows due to a misfire policy misconfiguration.
Vigilmon heartbeat monitors catch measure pipeline gaps.
Set Up Heartbeat Monitors
For hourly accuracy measure liveness:
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
griffin-accuracy-measure - Set the expected interval: 70 minutes
- Set the grace period: 90 minutes
- Save — copy the heartbeat URL
For daily completeness measure liveness:
- Create another Heartbeat monitor named
griffin-completeness-daily - Set the expected interval: 25 hours
- Set the grace period: 27 hours
- Save — copy the heartbeat URL
Wire Heartbeats Into Griffin Measure Jobs
Add a post-execution heartbeat ping to Griffin's job completion hooks via a custom Quartz job listener:
// quartz/GriffinHeartbeatJobListener.java
package com.yourorg.griffin.quartz;
import org.quartz.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class GriffinHeartbeatJobListener implements JobListener {
private final Map<String, String> jobHeartbeatUrls;
private final HttpClient http = HttpClient.newHttpClient();
public GriffinHeartbeatJobListener(Map<String, String> jobHeartbeatUrls) {
this.jobHeartbeatUrls = jobHeartbeatUrls;
}
@Override
public String getName() { return "GriffinHeartbeatListener"; }
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException ex) {
if (ex != null) return; // do not ping on failure
String jobName = context.getJobDetail().getKey().getName();
String url = jobHeartbeatUrls.get(jobName);
if (url == null) return;
try {
http.sendAsync(
HttpRequest.newBuilder(URI.create(url)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} catch (Exception ignored) {}
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {}
}
For Griffin measure jobs submitted externally via the REST API, wrap the submission with a post-completion heartbeat in your pipeline orchestrator:
# pipeline/griffin_measure_runner.py
import os, requests, time
GRIFFIN_BASE = os.environ['GRIFFIN_BASE']
HEARTBEAT_URL = os.environ.get('VIGILMON_GRIFFIN_MEASURE_HEARTBEAT_URL')
def run_measure(job_name, cron=None):
# Trigger Griffin measure run via REST API
resp = requests.post(
f'{GRIFFIN_BASE}/api/v1/jobs/triggerJob',
json={'jobName': job_name},
timeout=30,
)
resp.raise_for_status()
# Poll until complete
job_id = resp.json()['jobId']
for _ in range(120):
status_resp = requests.get(
f'{GRIFFIN_BASE}/api/v1/jobs/{job_id}', timeout=10
)
status = status_resp.json().get('jobStatus')
if status == 'COMPLETE':
if HEARTBEAT_URL:
requests.get(HEARTBEAT_URL, timeout=5)
return
elif status in ('ERROR', 'FAILED'):
raise RuntimeError(f'Griffin measure {job_name} failed with status {status}')
time.sleep(30)
raise TimeoutError(f'Griffin measure {job_name} did not complete within 60 minutes')
if __name__ == '__main__':
run_measure('accuracy_measure_hourly')
Step 4: Alert Routing for Griffin Failures
Griffin failures range from a single measure job failing due to a Spark executor crash to the entire Livy session pool being exhausted. Route alerts by data quality SLA impact:
| Monitor | Alert Channel | Priority |
|---|---|---|
| HTTP: /actuator/health Griffin service | Slack + PagerDuty | P1 |
| HTTP: Livy sessions endpoint | Slack + PagerDuty | P1 |
| HTTP: measure error accumulation endpoint | Slack + PagerDuty | P1 |
| HTTP: metric sink write check | Slack | P2 |
| Heartbeat: hourly accuracy measure | Slack + PagerDuty | P1 |
| Heartbeat: daily completeness measure | Slack + email | P2 |
| Heartbeat: streaming data quality job | Slack + PagerDuty | P1 |
Set response time thresholds as early warning signals:
- Alert at
3000msfor the Griffin API health check (slow responses indicate metadata DB pressure) - Alert at
5000msfor the Livy sessions endpoint (slow responses indicate session pool pressure) - Alert at
90 minutesgap in hourly accuracy measure heartbeat (one full window missed) - Alert at
27 hoursgap in daily completeness heartbeat
For production Griffin deployments serving downstream data pipelines, set up a status page in Vigilmon showing data quality service availability and measure job liveness — data consumers and data stewards can check this independently rather than pinging the data engineering team every time a dashboard shows stale quality scores.
Summary
Griffin failures typically appear as stale data quality scores on dashboards rather than hard service outages — the API layer can be healthy while Spark measure jobs are silently failing or Quartz has drifted past the last scheduled window. External monitoring catches these before downstream teams discover a data quality SLA has been silently breached:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /actuator/health | Griffin service, DB metadata, Livy reachability |
| HTTP monitor on measure error endpoint | Accumulating ERROR-state measure jobs |
| HTTP monitor on Livy sessions | Spark session pool availability |
| Heartbeat monitor (batch measures) | End-to-end measure job completion within SLA window |
| Heartbeat monitor (streaming measures) | Continuous streaming data quality pipeline liveness |
Get started free at vigilmon.online — your first Griffin data quality monitor is running in under two minutes.