Skip to content

Monitoring

Ratchet provides two monitoring channels: Micrometer metrics for quantitative dashboards and alerts, and the event system for real-time programmatic observation.

Micrometer integration

Setup

Add the Micrometer module:

xml
<dependency>
  <groupId>run.ratchet</groupId>
  <artifactId>ratchet-micrometer</artifactId>
  <version>0.4.0</version>
</dependency>

ratchet-micrometer ships a default SimpleMeterRegistry, so a registry is always present. To send metrics to a real backend such as Prometheus, override the default with an @Alternative producer:

java
@Produces
@Alternative
@Priority(2000)
@Singleton // @Singleton avoids a Weld proxy on the abstract MeterRegistry (WELD-001435)
public MeterRegistry meterRegistry() {
    return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}

The MicrometerMetricsCollector is annotated @Alternative @Priority(1000), so it automatically overrides the default NoOpMetricsCollector when the module is on the classpath.

You can install ratchet-otel beside ratchet-micrometer. In that combination, OpenTelemetry's priority-1100 adapter handles tracing while Micrometer continues to publish metrics. If both modules are present, Ratchet deliberately chooses direct OpenTelemetry tracing instead of the Micrometer tracing bridge. A custom tracing collector with a priority above 1100 overrides both.

Published metrics

MetricTypeTagsDescription
ratchet.jobs.startedCountertype, priorityJob execution attempts started
ratchet.jobs.completedCountertypeJob execution attempts completed successfully
ratchet.jobs.failedCountertype, familyFailed attempts, grouped by bounded exception family
ratchet.jobs.durationTimertypeWall-clock duration of successful attempts
ratchet.store.finalization.retriesCountertypeTransient conflicts while persisting a successful result
ratchet.store.finalization.minimal_successCountertypeFull-result persistence was exhausted and Ratchet used the minimal success write
ratchet.store.finalization.stuckCountertypeBoth finalization paths were exhausted; the job remains RUNNING for recovery
ratchet.store.claim.transient_failuresCounterexecution_typeTransient store conflicts on the claim path
ratchet.poller.claimed.jobsCounterexecution_typeNumber of jobs claimed
ratchet.submission.gate.rejectionsCounterexecution_type, gate_statusClaimed jobs blocked by a local submission gate
ratchet.wakeup.localCountersourceDirect local poller wakeups after new work
ratchet.execution.target.fallbackCounterrequested_target, effective_targetRequested executor target was unavailable and Ratchet used a fallback pool
ratchet.wakeup.cluster.publishCountertransport, outcomeCluster wakeup publish attempts
ratchet.wakeup.cluster.receiveCountertransport, outcomeCluster wakeup messages observed by a receiver
ratchet.callbacks.failedCountertype, familyonSuccess or onFailure callback failures; these do not fail the parent job
ratchet.signal.waitingCountertype, signal_keyJobs created in WAITING for a signal
ratchet.signal.deliveredCountertype, signal_key, outcomeSignal deliveries that moved a job to PENDING
ratchet.signal.timed_outCountertype, signal_keySignal waits that expired
ratchet.signal.cancelledCountertype, signal_keySignal waits cancelled before delivery
ratchet.store.operationTimerstore, operation, outcomeTimed store operations on claim and execution hot paths
ratchet.poller.breaker.stateGaugebreakerPoller claim-breaker state: 0 closed/unknown, 1 half-open, 2 open
ratchet.circuit.breaker.stateGaugeservice, profileApplication circuit-breaker state: 0 closed/unknown, 1 half-open, 2 open
ratchet.encryption.integrity.violationsCountersurfaceA row marked as encrypted contained plaintext. The read succeeds, but the writer or rollout needs investigation.
ratchet.encryption.envelope.version_skewCounterversion_gapA newer envelope reached this node. next, multiple_versions_ahead, and not_newer keep the diagnostic bounded; Ratchet releases valid newer jobs for an upgraded peer.

The type tag corresponds to JobType (SINGLE, RECURRING, BATCH, CHAIN, WORKFLOW, SYSTEM). The priority tag corresponds to JobPriority (LOWEST, LOW, NORMAL, HIGH, CRITICAL). The family tag corresponds to ExceptionFamily, a coarse classification of the failure cause rather than the raw exception class name.

MicrometerMetricTagPolicy keeps string-valued tags bounded. Framework values pass through, blanks become UNKNOWN, and unrecognized values become OTHER. Signal keys are application strings and collapse to OTHER unless you explicitly allowlist selected keys. See Metrics Collection for the tag-policy details.

Grafana dashboard queries

Job throughput (Prometheus):

text
rate(ratchet_jobs_completed_total[5m])

Failure rate:

text
rate(ratchet_jobs_failed_total[5m])
  / (rate(ratchet_jobs_completed_total[5m]) + rate(ratchet_jobs_failed_total[5m]))

P95 execution time:

text
histogram_quantile(0.95, rate(ratchet_jobs_duration_seconds_bucket[5m]))

Failures by exception family:

text
topk(5, sum by (family) (rate(ratchet_jobs_failed_total[5m])))

Custom MetricsCollector

If you need different metric names, additional tags, or a non-Micrometer backend, implement the MetricsCollector SPI. Its callbacks cover job outcomes, success finalization, claims and gates, wakeups and executor routing, callback and signal events, store timing, poller and application circuit breakers, and encryption integrity/version signals.

The example below is intentionally partial: it records two basic job outcomes and drops every other signal. Extending NoOpMetricsCollector is convenient when that is what you want. For a production replacement, review every callback in Metrics Collection and either export it or delegate it to a complete collector.

java
public class MyMetricsCollector extends NoOpMetricsCollector {

    @Override
    public void jobStarted(UUID jobId, JobType type, JobPriority priority) {
        // record your metric
    }

    @Override
    public void jobFailed(UUID jobId, JobType type, Throwable cause, int attempt) {
        // record your metric
    }
}

Register your implementation as a CDI alternative with a higher priority than 1000 to override the Micrometer collector.

Event-based monitoring

The event system provides fine-grained lifecycle notifications. Unlike metrics (which are aggregated counters/timers), events carry full context about individual job executions.

CDI observers

java
@ApplicationScoped
public class JobMonitor {

    public void onStarted(@Observes JobStartedEvent event) {
        log.info("Job {} started on node {}", event.getJobId(), event.getNodeId());
    }

    public void onCompleted(@Observes JobCompletedEvent event) {
        log.info("Job {} completed in {}ms",
            event.getJobId(), event.getExecutionTimeMs());
    }

    public void onFailed(@Observes JobFailedEvent event) {
        log.error("Job {} failed (attempt {}): {}",
            event.getJobId(), event.getRetryAttempt(), event.getErrorMessage());
    }

    public void onRetrying(@Observes JobRetryingEvent event) {
        log.warn("Job {} retrying (attempt {}): {}",
            event.getJobId(), event.getRetryAttempt(), event.getErrorMessage());
    }

    public void onExecutionTimeout(@Observes JobExecutionTimedOutEvent event) {
        // Dispatch external alerts on an application-managed executor: observers are synchronous.
        timeoutAlertExecutor.execute(() -> alertOps(
            "Job " + event.getJobId() + " exceeded " + event.getExecutionTimeout()));
    }

    public void onDlq(@Observes JobDlqEvent event) {
        // Alert: job exhausted all retries
        alertOps("Job " + event.getJobId() + " moved to DLQ: " + event.getErrorMessage());
    }
}

Programmatic listeners

For dynamic registration (useful in frameworks or libraries that can't use CDI observers):

java
scheduler.addEventListener(event -> {
    if (event instanceof JobFailedEvent failed) {
        log.error("Job {} failed (attempt {}): {}",
            failed.getJobId(), failed.getRetryAttempt(), failed.getErrorMessage());
    }
});

For aggregate poll-cycle and throughput metrics, use the MetricsCollector SPI (or the Micrometer module) rather than the event system. Events carry per-job context, not cycle-level summaries.

Event types

EventWhen fired
JobStartedEventWorker begins executing a job
JobCompletedEventJob finished successfully
JobFailedEventJob reached terminal FAILED state
JobRetryingEventJob failed but will be retried
JobDlqEventJob entered terminal dead-letter/FAILED handling
JobExecutionTimedOutEventRunning job exceeded its configured execution timeout
JobCancelledEventJob was cancelled via API
BatchCompletingEventAll children in a batch have finished
ChainStartedEventA chained job was triggered by its parent
WorkflowBranchTriggeredEventA conditional branch was activated

Health checks

Node heartbeat check

Query the scheduler_node table to verify all expected nodes are alive (MySQL syntax):

sql
SELECT node_id, heartbeat_ts,
       TIMESTAMPDIFF(SECOND, heartbeat_ts, NOW()) AS seconds_stale
FROM scheduler_node
WHERE heartbeat_ts < NOW() - INTERVAL 2 MINUTE;

Any rows returned indicate stale nodes. For programmatic health checks:

java
@ApplicationScoped
public class RatchetHealthCheck {

    @Inject
    NodeStore nodeStore;

    public boolean isHealthy() {
        Instant cutoff = Instant.now().minus(Duration.ofMinutes(2));
        return nodeStore.findInactiveNodesSince(cutoff).isEmpty();
    }
}

Queue depth check

Monitor the number of pending jobs to detect backlog:

sql
-- PENDING is live state on scheduler_job_queue (priority is denormalized there).
SELECT priority, COUNT(*) as pending_count
FROM scheduler_job_queue
WHERE status = 'PENDING'
GROUP BY priority;

Alert if CRITICAL or HIGH jobs have been pending for more than a few seconds.

DLQ check

Jobs in the dead-letter queue need human attention:

sql
SELECT COUNT(*)
FROM scheduler_job
WHERE terminal_status = 'FAILED';

Wire this into your alerting system. The durable scheduler_job row remains the source of truth after its live scheduler_job_queue row is removed.

Alerting recommendations

ConditionSeverityAction
DLQ count > 0CriticalInvestigate and retry or archive
Failure rate > 5% (5min window)WarningCheck logs for systematic errors
P95 duration > 2x normalWarningCheck for resource contention or slow dependencies
Node heartbeat stale > 2minCriticalNode may be down; check process health
Pending CRITICAL jobs > 30sCriticalCluster may be overloaded
Pending queue depth growingWarningScale up nodes or increase thread pool
ratchet.store.finalization.stuck > 0CriticalCheck store health; successful work is waiting for recovery
ratchet.poller.breaker.state remains 2CriticalInvestigate claim-path store failures
ratchet.encryption.integrity.violations > 0CriticalInvestigate a downgrade, lagging writer, or encryption bug
ratchet.encryption.envelope.version_skew persists after rolloutWarningFind and upgrade the lagging node

See also