Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions products/metrics/metrics-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ description = "Metrics API"

dependencies {
implementation(libs.slf4j)

testImplementation(libs.bundles.junit5)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package datadog.metrics.api.statsd;

/**
* Reports a drained {@code long[]} of counter values -- e.g. from {@code
* Accumulator.accumulateAndReset(data)} -- to a {@link StatsDClient}, one {@link
* StatsDClient#count} call per enum constant whose value changed.
*
* @see StatsDCounterKey
*/
public final class StatsDCountReporter {
private StatsDCountReporter() {}

/**
* @param values the enum constants naming each counter, e.g. {@code MyCounters.values()}
* @param counts one value per constant, indexed by {@link Enum#ordinal()} -- e.g. the array
* returned by {@code Accumulator.accumulateAndReset(data)}
* @param tags tags applied uniformly to every reported counter
*/
public static <E extends Enum<E> & StatsDCounterKey> void report(
StatsDClient statsDClient, E[] values, long[] counts, String... tags) {
for (E value : values) {
long delta = counts[value.ordinal()];
if (delta != 0) {
statsDClient.count(value.getMetricName(), delta, tags);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package datadog.metrics.api.statsd;

/**
* Lets an {@code enum} used as an {@code Accumulator} key declare its own dogstatsd metric name at
* the declaration site, so the schema and its reporting name can't drift apart.
*
* <pre>{@code
* enum MyCounters implements StatsDCounterKey {
* FOO("my.counters.foo"),
* BAR("my.counters.bar");
*
* private final String metricName;
* MyCounters(String metricName) { this.metricName = metricName; }
* @Override public String getMetricName() { return metricName; }
* }
* }</pre>
*
* @see StatsDCountReporter
*/
public interface StatsDCounterKey {
String getMetricName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package datadog.metrics.api.statsd;

import java.util.ArrayList;
import java.util.List;

/** Test fake that records every {@link #count} call; every other method is a no-op. */
final class RecordingStatsDClient implements StatsDClient {

static final class Count {
final String metricName;
final long delta;
final String[] tags;

Count(String metricName, long delta, String[] tags) {
this.metricName = metricName;
this.delta = delta;
this.tags = tags;
}
}

final List<Count> counts = new ArrayList<>();

@Override
public void incrementCounter(String metricName, String... tags) {}

@Override
public void count(String metricName, long delta, String... tags) {
counts.add(new Count(metricName, delta, tags));
}

@Override
public void gauge(String metricName, long value, String... tags) {}

@Override
public void gauge(String metricName, double value, String... tags) {}

@Override
public void histogram(String metricName, long value, String... tags) {}

@Override
public void histogram(String metricName, double value, String... tags) {}

@Override
public void distribution(String metricName, long value, String... tags) {}

@Override
public void distribution(String metricName, double value, String... tags) {}

@Override
public void serviceCheck(
String serviceCheckName, String status, String message, String... tags) {}

@Override
public void error(Exception error) {}

@Override
public int getErrorCount() {
return 0;
}

@Override
public void close() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package datadog.metrics.api.statsd;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class StatsDCountReporterTest {

private enum TestCounters implements StatsDCounterKey {
FOO("test.counters.foo"),
BAR("test.counters.bar");

private final String metricName;

TestCounters(String metricName) {
this.metricName = metricName;
}

@Override
public String getMetricName() {
return metricName;
}
}

@Test
void reportsNonZeroCounters() {
RecordingStatsDClient client = new RecordingStatsDClient();
long[] counts = new long[TestCounters.values().length];
counts[TestCounters.FOO.ordinal()] = 3L;

StatsDCountReporter.report(client, TestCounters.values(), counts);

assertEquals(1, client.counts.size());
assertEquals("test.counters.foo", client.counts.get(0).metricName);
assertEquals(3L, client.counts.get(0).delta);
}

@Test
void skipsZeroDeltaCounters() {
RecordingStatsDClient client = new RecordingStatsDClient();
long[] counts = new long[TestCounters.values().length];
counts[TestCounters.FOO.ordinal()] = 0L;
counts[TestCounters.BAR.ordinal()] = 5L;

StatsDCountReporter.report(client, TestCounters.values(), counts);

assertEquals(1, client.counts.size());
assertEquals("test.counters.bar", client.counts.get(0).metricName);
}

@Test
void reportsNegativeDeltas() {
RecordingStatsDClient client = new RecordingStatsDClient();
long[] counts = new long[TestCounters.values().length];
counts[TestCounters.FOO.ordinal()] = -2L;

StatsDCountReporter.report(client, TestCounters.values(), counts);

assertEquals(1, client.counts.size());
assertEquals(-2L, client.counts.get(0).delta);
}

@Test
void passesTagsThrough() {
RecordingStatsDClient client = new RecordingStatsDClient();
long[] counts = new long[TestCounters.values().length];
counts[TestCounters.FOO.ordinal()] = 1L;

StatsDCountReporter.report(client, TestCounters.values(), counts, "env:test", "service:foo");

assertArrayEquals(new String[] {"env:test", "service:foo"}, client.counts.get(0).tags);
}

@Test
void reportsNothingWhenAllCountersAreZero() {
RecordingStatsDClient client = new RecordingStatsDClient();
long[] counts = new long[TestCounters.values().length];

StatsDCountReporter.report(client, TestCounters.values(), counts);

assertTrue(client.counts.isEmpty());
}
}
Loading