dev.responsive.kafka.internal.metrics.CassandraMetricsFactory Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2024 Responsive Computing, Inc.
*
* This source code is licensed under the Responsive Business Source License Agreement v1.0
* available at:
*
* https://www.responsive.dev/legal/responsive-bsl-10
*
* This software requires a valid Commercial License Key for production use. Trial and commercial
* licenses can be obtained at https://www.responsive.dev
*/
package dev.responsive.kafka.internal.metrics;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.metadata.Node;
import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric;
import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric;
import com.datastax.oss.driver.api.core.metrics.Metrics;
import com.datastax.oss.driver.api.core.metrics.NodeMetric;
import com.datastax.oss.driver.api.core.metrics.SessionMetric;
import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
import com.datastax.oss.driver.internal.core.metrics.MetricUpdater;
import com.datastax.oss.driver.internal.core.metrics.MetricsFactory;
import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater;
import com.datastax.oss.driver.internal.core.metrics.NoopNodeMetricUpdater;
import com.datastax.oss.driver.internal.core.metrics.NoopSessionMetricUpdater;
import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.metrics.Sensor;
import org.apache.kafka.common.metrics.stats.CumulativeCount;
import org.apache.kafka.common.metrics.stats.CumulativeSum;
/**
* This class adapts the metrics generated by the Cassandra client to be exported through
* Kafka metrics.
*
* See here
* for a description of metrics usage in the Cassandra driver.
*
*
* For a description of each metric exposed by the driver see the
* reference documentation.
*
*/
public class CassandraMetricsFactory implements MetricsFactory {
static final String CASSANDRA_METRICS_GROUP = "cassandra-client";
private final SessionMetricUpdater sessionMetricUpdater;
private final NodeMetricUpdater nodeMetricUpdater;
public CassandraMetricsFactory(final DriverContext driverContext) {
final Optional metricsOpt = fetchMetrics(driverContext);
if (metricsOpt.isPresent()) {
final ResponsiveMetrics metrics = metricsOpt.get();
this.sessionMetricUpdater = new ResponsiveSessionMetricUpdater(metrics);
this.nodeMetricUpdater = new ResponsiveNodeMetricUpdater(metrics);
} else {
this.sessionMetricUpdater = NoopSessionMetricUpdater.INSTANCE;
this.nodeMetricUpdater = NoopNodeMetricUpdater.INSTANCE;
}
}
private static Optional fetchMetrics(DriverContext driverContext) {
// Note that there seems to be no alternative to accessing the metric registry
// than `InternalDriverContext`. Each of the included metric factory classes
// cast in order to get access.
if (driverContext instanceof InternalDriverContext) {
final InternalDriverContext internalDriverContext = (InternalDriverContext) driverContext;
final Object metricRegistry = internalDriverContext.getMetricRegistry();
if (metricRegistry instanceof ResponsiveMetrics) {
return Optional.of((ResponsiveMetrics) internalDriverContext.getMetricRegistry());
}
}
return Optional.empty();
}
@Override
public Optional getMetrics() {
return Optional.empty();
}
@Override
public SessionMetricUpdater getSessionUpdater() {
return sessionMetricUpdater;
}
@Override
public NodeMetricUpdater newNodeUpdater(final Node node) {
return nodeMetricUpdater;
}
private abstract static class ResponsiveMetricUpdater implements MetricUpdater {
protected abstract Sensor getOrCreateSensor(T metric);
@Override
public void incrementCounter(
final T metric,
@Nullable final String profileName,
final long amount
) {
getOrCreateSensor(metric).record(amount);
}
@Override
public void updateHistogram(
final T metric,
@Nullable final String s,
final long value
) {
getOrCreateSensor(metric).record(value);
}
@Override
public void markMeter(
final T metric,
@Nullable final String profileName,
final long amount
) {
getOrCreateSensor(metric).record(amount);
}
@Override
public void updateTimer(
final T metric,
@Nullable final String profileName,
final long duration,
final TimeUnit timeUnit
) {
getOrCreateSensor(metric)
.record(TimeUnit.MILLISECONDS.convert(duration, timeUnit));
}
}
private static class ResponsiveSessionMetricUpdater
extends ResponsiveMetricUpdater
implements SessionMetricUpdater {
private final ResponsiveMetrics metrics;
private ResponsiveSessionMetricUpdater(final ResponsiveMetrics metrics) {
this.metrics = metrics;
}
@Override
public boolean isEnabled(
final SessionMetric metric,
@Nullable final String profileName
) {
switch (asDefaultSessionMetric(metric)) {
case BYTES_SENT:
case BYTES_RECEIVED:
case CQL_REQUESTS:
case CQL_CLIENT_TIMEOUTS:
case THROTTLING_DELAY:
case THROTTLING_ERRORS:
return true;
default:
return false;
}
}
private String sensorName(SessionMetric metric) {
return "cassandra-" + metric.getPath();
}
private DefaultSessionMetric asDefaultSessionMetric(SessionMetric metric) {
if (metric instanceof DefaultSessionMetric) {
return (DefaultSessionMetric) metric;
} else {
return DefaultSessionMetric.fromPath(metric.getPath());
}
}
@Override
protected Sensor getOrCreateSensor(SessionMetric metric) {
final DefaultSessionMetric sessionMetric = asDefaultSessionMetric(metric);
final String sensorName = sensorName(sessionMetric);
Sensor sensor = metrics.getSensor(sensorName);
if (sensor == null) {
sensor = metrics.addSensor(sensorName);
switch (sessionMetric) {
case BYTES_SENT:
// Exposed as a Meter
sensor.add(new MetricName(
"bytes-sent",
CASSANDRA_METRICS_GROUP,
"Cumulative bytes sent",
Collections.emptyMap()
), new CumulativeSum());
break;
case BYTES_RECEIVED:
// Exposed as a Meter
sensor.add(new MetricName(
"bytes-received",
CASSANDRA_METRICS_GROUP,
"Cumulative bytes received",
Collections.emptyMap()
), new CumulativeSum());
break;
case CQL_REQUESTS:
// Exposed as a Timer
sensor.add(new MetricName(
"cql-requests-count",
CASSANDRA_METRICS_GROUP,
"Cumulative CQL request count",
Collections.emptyMap()
), new CumulativeCount());
sensor.add(new MetricName(
"cql-requests-cumulative-latency",
CASSANDRA_METRICS_GROUP,
"Cumulative CQL request latency in milliseconds",
Collections.emptyMap()
), new CumulativeSum());
break;
case CQL_CLIENT_TIMEOUTS:
// Exposed as a Counter
sensor.add(new MetricName(
"cql-request-timeouts-count",
CASSANDRA_METRICS_GROUP,
"Cumulative count of CQL request timeouts",
Collections.emptyMap()
), new CumulativeSum());
break;
case THROTTLING_DELAY:
// Exposed as a Timer
sensor.add(new MetricName(
"throttling-cumulative-delay",
CASSANDRA_METRICS_GROUP,
"Cumulative throttling delay in milliseconds",
Collections.emptyMap()
), new CumulativeSum());
break;
case THROTTLING_ERRORS:
// Exposed as a Counter
sensor.add(new MetricName(
"throttling-errors-count",
CASSANDRA_METRICS_GROUP,
"Cumulative count of throttling errors",
Collections.emptyMap()
), new CumulativeSum());
break;
default:
break;
}
}
return sensor;
}
}
// Note: we use a `NodeMetricUpdater` to get access to some of the other client
// metrics, but we do not expose node-level information. Each exposed metric
// is rolled up into a session-level metric to avoid exploding the number of
// metrics collected. Similarly, we roll up related metrics into totals.
private static class ResponsiveNodeMetricUpdater
extends ResponsiveMetricUpdater
implements NodeMetricUpdater {
private final ResponsiveMetrics metrics;
private ResponsiveNodeMetricUpdater(final ResponsiveMetrics metrics) {
this.metrics = metrics;
}
private DefaultNodeMetric asDefaultNodeMetric(NodeMetric metric) {
if (metric instanceof DefaultNodeMetric) {
return (DefaultNodeMetric) metric;
} else {
return DefaultNodeMetric.fromPath(metric.getPath());
}
}
@Override
public boolean isEnabled(
final NodeMetric metric,
@Nullable final String profileName
) {
switch (asDefaultNodeMetric(metric)) {
case AUTHENTICATION_ERRORS:
case CONNECTION_INIT_ERRORS:
case UNSENT_REQUESTS:
case ABORTED_REQUESTS:
case WRITE_TIMEOUTS:
case READ_TIMEOUTS:
case UNAVAILABLES:
case OTHER_ERRORS:
case RETRIES:
case IGNORES:
return true;
default:
return false;
}
}
private String sensorName(DefaultNodeMetric metric) {
switch (metric) {
case AUTHENTICATION_ERRORS:
case CONNECTION_INIT_ERRORS:
return "cassandra-connection-errors";
case UNSENT_REQUESTS:
case ABORTED_REQUESTS:
case WRITE_TIMEOUTS:
case READ_TIMEOUTS:
case UNAVAILABLES:
case OTHER_ERRORS:
return "cassandra-request-errors";
case RETRIES:
case IGNORES:
default:
return "cassandra-" + metric.getPath();
}
}
@Override
protected Sensor getOrCreateSensor(final NodeMetric metric) {
final DefaultNodeMetric nodeMetric = asDefaultNodeMetric(metric);
final String sensorName = sensorName(nodeMetric);
Sensor sensor = metrics.getSensor(sensorName);
if (sensor == null) {
sensor = metrics.addSensor(sensorName);
switch (nodeMetric) {
case AUTHENTICATION_ERRORS:
case CONNECTION_INIT_ERRORS:
// Exposed as a Counter
sensor.add(new MetricName(
"connection-errors-count",
CASSANDRA_METRICS_GROUP,
"Cumulative count of all connection errors",
Collections.emptyMap()
), new CumulativeSum());
break;
case UNSENT_REQUESTS:
case ABORTED_REQUESTS:
case WRITE_TIMEOUTS:
case READ_TIMEOUTS:
case UNAVAILABLES:
case OTHER_ERRORS:
// Exposed as a Counter
// Unlike ignores and retries, the driver does not expose an error total,
// so we do the work here instead.
sensor.add(new MetricName(
"cql-request-errors-count",
CASSANDRA_METRICS_GROUP,
"Cumulative count of all request errors",
Collections.emptyMap()
), new CumulativeSum());
break;
case RETRIES:
// From reference doc:
// # The total number of errors on this node that caused the RetryPolicy to trigger a
// # retry (exposed as a Counter).
// #
// # This is a sum of all the other retries.* metrics.
sensor.add(new MetricName(
"cql-request-retries-count",
CASSANDRA_METRICS_GROUP,
"Cumulative count of all request retries",
Collections.emptyMap()
), new CumulativeSum());
break;
case IGNORES:
// From reference doc:
// # The total number of errors on this node that were ignored by the RetryPolicy
// (exposed as a Counter).
// #
// # This is a sum of all the other ignores.* metrics.
sensor.add(new MetricName(
"cql-request-ignores-count",
CASSANDRA_METRICS_GROUP,
"Cumulative count of all request ignores",
Collections.emptyMap()
), new CumulativeSum());
break;
default:
break;
}
}
return sensor;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy