diff --git a/metrics-statsd/pom.xml b/metrics-statsd/pom.xml new file mode 100644 index 0000000..9b7f56d --- /dev/null +++ b/metrics-statsd/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + io.avaje + avaje-metrics-parent + 9.5 + + + avaje-metrics-statsd + + + + io.avaje + avaje-metrics + 9.5 + + + com.datadoghq + java-dogstatsd-client + 4.4.4 + + + io.ebean + ebean-api + 17.0.0-RC1 + provided + true + + + + diff --git a/metrics-statsd/src/main/java/io/avaje/metrics/statsd/DatabaseReporter.java b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/DatabaseReporter.java new file mode 100644 index 0000000..e02b4fb --- /dev/null +++ b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/DatabaseReporter.java @@ -0,0 +1,102 @@ +package io.avaje.metrics.statsd; + +import com.timgroup.statsd.StatsDClient; +import io.ebean.Database; +import io.ebean.datasource.DataSourcePool; +import io.ebean.meta.MetaCountMetric; +import io.ebean.meta.MetaQueryMetric; +import io.ebean.meta.MetaTimedMetric; +import io.ebean.meta.ServerMetrics; + +import javax.sql.DataSource; + +final class DatabaseReporter implements StatsdReporter.Reporter { + + private final Database database; + + private DatabaseReporter(Database database) { + this.database = database; + } + + static StatsdReporter.Reporter reporter(Database database) { + return new DatabaseReporter(database); + } + + @Override + public void report(StatsDClient sender) { + new DReport(database, sender).report(); + } + + private static final class DReport { + + private final StatsDClient reporter; + private final ServerMetrics dbMetrics; + private final long epochSecs; + private final String dbName; + + private DReport(Database database, StatsDClient reporter) { + this.reporter = reporter; + this.dbName = database.name(); + this.dbMetrics = database.metaInfo().collectMetrics(); + this.epochSecs = System.currentTimeMillis() / 1000; + DataSource dataSource = database.dataSource(); + if (dataSource instanceof DataSourcePool) { + DataSourcePool pool = (DataSourcePool) database; + reporter.gaugeWithTimestamp("pool.size", pool.size(), epochSecs, "db", dbName, "type", "main"); + } + DataSource readDatasource = database.readOnlyDataSource(); + if (readDatasource instanceof DataSourcePool && readDatasource != dataSource) { + DataSourcePool readOnlyPool = (DataSourcePool) readDatasource; + reporter.gaugeWithTimestamp("pool.size", readOnlyPool.size(), epochSecs,"db", dbName, "type", "readonly"); + } + } + + private String nm(String name, String suffix) { + return name + suffix; + } + + private void report() { + for (MetaTimedMetric timedMetric : dbMetrics.timedMetrics()) { + reportTimedMetric(timedMetric); + } + for (MetaQueryMetric queryMetric : dbMetrics.queryMetrics()) { + reportQueryMetric(queryMetric); + } + for (MetaCountMetric countMetric : dbMetrics.countMetrics()) { + reportCountMetric(countMetric); + } + } + + private void reportCountMetric(MetaCountMetric countMetric) { + reporter.count(nm(countMetric.name(), ".count"), countMetric.count(), "db", dbName); + } + + private void reportTimedMetric(MetaTimedMetric metric) { + final String name = metric.name(); + if (name.startsWith("txn")) { + reportMetric(metric, "txn", "name", name, "db", dbName); + } else { + reportMetric(metric, name, "db", dbName); + } + } + + private void reportQueryMetric(MetaQueryMetric metric) { + final String name = metric.name(); + if (name.startsWith("orm")) { + reportMetric(metric, "db.query", "query", name, "db", dbName, "type", "orm"); + } else if (name.startsWith("sql")) { + reportMetric(metric, "db.query", "query", name, "db", dbName, "type", "orm"); + } else { + reportMetric(metric, "db.query", "query", name, "db", dbName, "type", "other"); + } + } + + private void reportMetric(MetaTimedMetric metric, String name, String... tags) { + reporter.countWithTimestamp(nm(name, ".count"), metric.count(), epochSecs, tags); + reporter.gaugeWithTimestamp(nm(name, ".max"), metric.max(), epochSecs, tags); + reporter.gaugeWithTimestamp(nm(name, ".mean"), metric.mean(), epochSecs, tags); + reporter.gaugeWithTimestamp(nm(name, ".total"), metric.total(), epochSecs, tags); + } + } + +} diff --git a/metrics-statsd/src/main/java/io/avaje/metrics/statsd/Reporter.java b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/Reporter.java new file mode 100644 index 0000000..918a8b9 --- /dev/null +++ b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/Reporter.java @@ -0,0 +1,103 @@ +package io.avaje.metrics.statsd; + +import com.timgroup.statsd.StatsDClient; +import io.avaje.metrics.*; +import org.jspecify.annotations.NullMarked; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@NullMarked +final class Reporter implements Runnable, AutoCloseable, StatsdReporter { + + private final MetricRegistry registry; + private final StatsDClient client; + private final long timedThreshold; + private final List reporters; + private final ScheduledTask scheduledTask; + private final AtomicBoolean started = new AtomicBoolean(false); + + Reporter(MetricRegistry registry, StatsDClient client, long timedThreshold, int schedule, + TimeUnit scheduleTimeUnit, List reporters) { + this.registry = registry; + this.client = client; + this.timedThreshold = timedThreshold; + this.reporters = reporters; + this.scheduledTask = ScheduledTask.builder() + .schedule(schedule, schedule, scheduleTimeUnit) + .task(this) + .build(); + } + + @Override + public void start() { + scheduledTask.start(); + started.set(true); + } + + @Override + public void close() { + if (started.get()) { + scheduledTask.cancel(true); + } + } + + @Override + public void run() { + var visitor = new AvajeMetricsVisitor(); + for (Metric.Statistics metric : registry.collectMetrics()) { + metric.visit(visitor); + } + for (StatsdReporter.Reporter reporter : reporters) { + reporter.report(client); + } + } + + private final class AvajeMetricsVisitor implements Metric.Visitor { + + private final long epochSecs = System.currentTimeMillis() / 1000; + + private void sendValues(Meter.Stats stats, String name, String... tags) { + if (stats.count() > 0) { + client.countWithTimestamp(name + ".count", stats.count(), epochSecs, tags); + client.gaugeWithTimestamp(name + ".total", stats.total(), epochSecs, tags); + client.gaugeWithTimestamp(name + ".mean", stats.mean(), epochSecs, tags); + client.gaugeWithTimestamp(name + ".max", stats.max(), epochSecs, tags); + } + } + + @Override + public void visit(Timer.Stats timed) { + if (timedThreshold == 0 || timedThreshold < timed.total()) { + if (timed.name().startsWith("web.api")) { + sendValues(timed, "web.api", "name", timed.name()); + } else if (timed.name().startsWith("app.")) { + sendValues(timed, "app.method", "name", timed.name()); + } else { + sendValues(timed, timed.name()); + } + } + } + + @Override + public void visit(Meter.Stats stats) { + sendValues(stats, stats.name()); + } + + @Override + public void visit(Counter.Stats counter) { + client.countWithTimestamp(counter.name(), counter.count(), epochSecs); + } + + @Override + public void visit(GaugeDouble.Stats gauge) { + client.gaugeWithTimestamp(gauge.name(), gauge.value(), epochSecs); + } + + @Override + public void visit(GaugeLong.Stats gauge) { + client.gaugeWithTimestamp(gauge.name(), gauge.value(), epochSecs); + } + } +} diff --git a/metrics-statsd/src/main/java/io/avaje/metrics/statsd/StatsdBuilder.java b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/StatsdBuilder.java new file mode 100644 index 0000000..9ea769c --- /dev/null +++ b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/StatsdBuilder.java @@ -0,0 +1,98 @@ +package io.avaje.metrics.statsd; + +import com.timgroup.statsd.NonBlockingStatsDClient; +import com.timgroup.statsd.NonBlockingStatsDClientBuilder; +import com.timgroup.statsd.StatsDClient; +import io.avaje.metrics.MetricRegistry; +import io.avaje.metrics.Metrics; +import io.ebean.Database; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.requireNonNull; + +final class StatsdBuilder implements StatsdReporter.Builder { + + private final List reporters = new ArrayList<>(); + private MetricRegistry registry; + private String hostname = "localhost"; + private int port = NonBlockingStatsDClient.DEFAULT_DOGSTATSD_PORT; + private StatsDClient client; + private long timedThresholdMicros; + private String[] tags; + private int schedule = 60; + private TimeUnit scheduleTimeUnit = TimeUnit.SECONDS; + + @Override + public StatsdReporter.Builder hostname(String hostname) { + this.hostname = requireNonNull(hostname); + return this; + } + + @Override + public StatsdReporter.Builder port(int port) { + this.port = port; + return this; + } + + @Override + public StatsdReporter.Builder client(StatsDClient client) { + this.client = client; + return this; + } + + @Override + public StatsdReporter.Builder timedThresholdMicros(long timedThresholdMicros) { + this.timedThresholdMicros = timedThresholdMicros; + return this; + } + + @Override + public StatsdReporter.Builder tags(String[] tags) { + this.tags = tags; + return this; + } + + @Override + public StatsdReporter.Builder schedule(int schedule, TimeUnit timeUnit) { + this.schedule = schedule; + this.scheduleTimeUnit = requireNonNull(timeUnit); + return this; + } + + @Override + public StatsdReporter.Builder registry(MetricRegistry registry) { + this.registry = requireNonNull(registry); + return this; + } + + @Override + public StatsdReporter.Builder database(Database database) { + reporters.add(DatabaseReporter.reporter(database)); + return this; + } + + @Override + public StatsdReporter.Builder reporter(StatsdReporter.Reporter reporter) { + reporters.add(requireNonNull(reporter)); + return this; + } + + @Override + public Reporter build() { + if (registry == null) { + registry = Metrics.registry(); + } + if (client == null) { + client = new NonBlockingStatsDClientBuilder() + .hostname(requireNonNull(hostname)) + .port(port) + .constantTags(tags) + .build(); + } + + return new Reporter(registry, client, timedThresholdMicros, schedule, scheduleTimeUnit, reporters); + } +} diff --git a/metrics-statsd/src/main/java/io/avaje/metrics/statsd/StatsdReporter.java b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/StatsdReporter.java new file mode 100644 index 0000000..f3d64f6 --- /dev/null +++ b/metrics-statsd/src/main/java/io/avaje/metrics/statsd/StatsdReporter.java @@ -0,0 +1,110 @@ +package io.avaje.metrics.statsd; + +import com.timgroup.statsd.StatsDClient; +import io.avaje.metrics.MetricRegistry; +import io.ebean.Database; + +import java.util.concurrent.TimeUnit; + +/** + * Interface for a StatsD reporter that can be used to report metrics. + *

+ * This interface allows for the creation of a StatsD reporter with various configurations such as hostname, + * port, client, tags, and reporting schedule. + *

+ * The reporter can be started and stopped, and it supports custom reporters that can be included in the + * reporting process. + */ +public interface StatsdReporter extends AutoCloseable { + + /** + * Create a builder for the reporter. + */ + static Builder builder() { + return new StatsdBuilder(); + } + + /** + * Start reporting. + */ + void start(); + + /** + * Shutdown and stop reporting. + */ + void close(); + + /** + * Custom reporters that can be included. + */ + interface Reporter { + + /** + * Report the metrics to the client. + */ + void report(StatsDClient statsdClient); + } + + /** + * Builder for the StatsdReporter. + */ + interface Builder { + + /** + * Specify the hostname to use. Default is localhost. + */ + Builder hostname(String hostname); + + /** + * Specify the port to use. Default is 8125. + */ + Builder port(int port); + + /** + * Specify the common tags to be used on all the reported metrics. Default is no tags. + */ + Builder tags(String[] tags); + + /** + * Specify the StatsD client to use. Default is a NonBlockingStatsDClient. + *

+ * When using a custom client, the hostname, port and tags are not used. + */ + Builder client(StatsDClient client); + + /** + * Specify the threshold in microseconds for timed metrics to be reported. Default is 0. + *

+ * Set this to reduce the number of metrics reported for timed metrics. + *

+ * For example, if set to 10_000, metrics with a duration less than 10 milliseconds will not be reported. + */ + Builder timedThresholdMicros(long timedThreshold); + + /** + * Specify the schedule in seconds. Default is 60 seconds. + */ + Builder schedule(int schedule, TimeUnit timeUnit); + + /** + * Specify the Metrics registry to use. If not specified the global registry is used. + */ + Builder registry(MetricRegistry registry); + + /** + * Add a database to report on. + */ + Builder database(Database database); + + /** + * Add an additional custom reporter. + */ + Builder reporter(Reporter reporter); + + /** + * Build the reporter. + */ + StatsdReporter build(); + + } +} diff --git a/pom.xml b/pom.xml index a424a63..38925c4 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ metrics metrics-graphite metrics-ebean + metrics-statsd