Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c3deb51
New metrics validation framework fundamentals.
robsunday Nov 22, 2024
fcf2bf8
Spotless fixes
robsunday Nov 22, 2024
d833450
Improved check for not received metrics
robsunday Nov 25, 2024
8cb96ad
Cassandra integration test converted
robsunday Nov 25, 2024
f3bac7e
Fine tuning assertion messages.
robsunday Nov 25, 2024
c9eb0a7
ActiveMqIntegrationTest converted
robsunday Nov 25, 2024
2c85c38
Spotless fix
robsunday Nov 26, 2024
bd1947f
introduce 'register' API
SylvainJuge Nov 26, 2024
1e64f80
introduce dedicated assertThat for metrics
SylvainJuge Nov 26, 2024
f7c6373
refactor metrics verifier
SylvainJuge Nov 26, 2024
b10a340
add some javadoc & few comments
SylvainJuge Nov 26, 2024
65ddfb3
spotless & minor things
SylvainJuge Nov 26, 2024
db54835
add new assertion for attribute entries
SylvainJuge Nov 26, 2024
1bdf694
check for missing assertions
SylvainJuge Nov 26, 2024
9f8a394
verify attributes are checked in strict mode
SylvainJuge Nov 27, 2024
6fb7960
enhance datapoint attributes check
SylvainJuge Nov 27, 2024
a458c1c
comments, cleanup and inline a bit
SylvainJuge Nov 27, 2024
b9f054c
strict check avoids duplicate assertions
SylvainJuge Nov 28, 2024
cf72d19
remove obsolete comments in activemq yaml
SylvainJuge Nov 28, 2024
eab7c69
register -> add
SylvainJuge Nov 28, 2024
9c3390c
reformat
SylvainJuge Nov 28, 2024
9392780
refactor cassandra
SylvainJuge Nov 28, 2024
5ae735d
fix lint
SylvainJuge Nov 28, 2024
2c65318
refactor activemq
SylvainJuge Nov 28, 2024
a670561
refactor jvm metrics
SylvainJuge Nov 28, 2024
df09034
remove unused code
SylvainJuge Nov 28, 2024
06a968f
recycle assertions when we can
SylvainJuge Nov 28, 2024
0e5fd2c
Merge pull request #4 from SylvainJuge/assertions-refactoring-more
robsunday Nov 28, 2024
8062a96
Added some JavaDocs.
robsunday Nov 28, 2024
ba95efa
Merge branch 'main' into assertions-refactoring
robsunday Nov 29, 2024
4bed658
Cleanup
robsunday Nov 29, 2024
fddc5fc
Spotless fix
robsunday Nov 29, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jmxscraper.assertions;

import io.opentelemetry.proto.metrics.v1.Metric;

/** Dedicated Assertj extension to provide convenient fluent API for metrics testing */
public class Assertions extends org.assertj.core.api.Assertions {

public static MetricAssert assertThat(Metric metric) {
return new MetricAssert(metric);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jmxscraper.assertions;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.proto.common.v1.KeyValue;
import io.opentelemetry.proto.metrics.v1.Metric;
import io.opentelemetry.proto.metrics.v1.NumberDataPoint;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.internal.Integers;
import org.assertj.core.internal.Iterables;
import org.assertj.core.internal.Maps;
import org.assertj.core.internal.Objects;

public class MetricAssert extends AbstractAssert<MetricAssert, Metric> {

private static final Objects objects = Objects.instance();
private static final Iterables iterables = Iterables.instance();
private static final Integers integers = Integers.instance();
private static final Maps maps = Maps.instance();

private boolean strict;

private boolean descriptionChecked;
private boolean unitChecked;
private boolean typeChecked;
private boolean dataPointAttributesChecked;

MetricAssert(Metric actual) {
super(actual, MetricAssert.class);
}

public void setStrict(boolean strict) {
this.strict = strict;
}

public void strictCheck() {
strictCheck("description", /* expectedCheckStatus= */ true, descriptionChecked);
strictCheck("unit", /* expectedCheckStatus= */ true, unitChecked);
strictCheck("type", /* expectedCheckStatus= */ true, typeChecked);
strictCheck(
"data point attributes", /* expectedCheckStatus= */ true, dataPointAttributesChecked);
}

private void strictCheck(
String metricProperty, boolean expectedCheckStatus, boolean actualCheckStatus) {
if (!strict) {
return;
}
String failMsgPrefix = expectedCheckStatus ? "duplicate" : "missing";
info.description(
"%s assertion on %s for metric '%s'", failMsgPrefix, metricProperty, actual.getName());
objects.assertEqual(info, actualCheckStatus, expectedCheckStatus);
}

/**
* Verifies metric description
*
* @param description expected description
* @return this
*/
@CanIgnoreReturnValue
public MetricAssert hasDescription(String description) {
isNotNull();

info.description("unexpected description for metric '%s'", actual.getName());
objects.assertEqual(info, actual.getDescription(), description);
strictCheck("description", /* expectedCheckStatus= */ false, descriptionChecked);
descriptionChecked = true;
return this;
}

/**
* Verifies metric unit
*
* @param unit expected unit
* @return this
*/
@CanIgnoreReturnValue
public MetricAssert hasUnit(String unit) {
isNotNull();

info.description("unexpected unit for metric '%s'", actual.getName());
objects.assertEqual(info, actual.getUnit(), unit);
strictCheck("unit", /* expectedCheckStatus= */ false, unitChecked);
unitChecked = true;
return this;
}

/**
* Verifies the metric is a gauge
*
* @return this
*/
@CanIgnoreReturnValue
public MetricAssert isGauge() {
isNotNull();

info.description("gauge expected for metric '%s'", actual.getName());
objects.assertEqual(info, actual.hasGauge(), true);
strictCheck("type", /* expectedCheckStatus= */ false, typeChecked);
typeChecked = true;
return this;
}

@CanIgnoreReturnValue
private MetricAssert hasSum(boolean monotonic) {
isNotNull();

info.description("sum expected for metric '%s'", actual.getName());
objects.assertEqual(info, actual.hasSum(), true);

String prefix = monotonic ? "monotonic" : "non-monotonic";
info.description(prefix + " sum expected for metric '%s'", actual.getName());
objects.assertEqual(info, actual.getSum().getIsMonotonic(), monotonic);
return this;
}

/**
* Verifies the metric is a counter
*
* @return this
*/
@CanIgnoreReturnValue
public MetricAssert isCounter() {
// counters have a monotonic sum as their value can't decrease
hasSum(true);
strictCheck("type", /* expectedCheckStatus= */ false, typeChecked);
typeChecked = true;
return this;
}

/**
* Verifies the metric is an up-down counter
*
* @return this
*/
@CanIgnoreReturnValue
public MetricAssert isUpDownCounter() {
// up down counters are non-monotonic as their value can increase & decrease
hasSum(false);
strictCheck("type", /* expectedCheckStatus= */ false, typeChecked);
typeChecked = true;
return this;
}

@CanIgnoreReturnValue
public MetricAssert hasDataPointsWithoutAttributes() {
isNotNull();

return checkDataPoints(
dataPoints -> {
dataPointsCommonCheck(dataPoints);

// all data points must not have any attribute
for (NumberDataPoint dataPoint : dataPoints) {
info.description(
"no attribute expected on data point for metric '%s'", actual.getName());
iterables.assertEmpty(info, dataPoint.getAttributesList());
}
});
}

@CanIgnoreReturnValue
private MetricAssert checkDataPoints(Consumer<List<NumberDataPoint>> listConsumer) {
// in practice usually one set of data points is provided but the
// protobuf does not enforce that, so we have to ensure checking at least one
int count = 0;
if (actual.hasGauge()) {
count++;
listConsumer.accept(actual.getGauge().getDataPointsList());
}
if (actual.hasSum()) {
count++;
listConsumer.accept(actual.getSum().getDataPointsList());
}
info.description("at least one set of data points expected for metric '%s'", actual.getName());
integers.assertGreaterThan(info, count, 0);

strictCheck("data point attributes", /* expectedCheckStatus= */ false, dataPointAttributesChecked);
dataPointAttributesChecked = true;
return this;
}

@CanIgnoreReturnValue
public MetricAssert hasTypedDataPoints(Collection<String> types) {
return checkDataPoints(
dataPoints -> {
dataPointsCommonCheck(dataPoints);

Set<String> foundValues = new HashSet<>();
for (NumberDataPoint dataPoint : dataPoints) {
List<KeyValue> attributes = dataPoint.getAttributesList();

info.description(
"expected exactly one 'name' attribute for typed data point in metric '%s'",
actual.getName());
iterables.assertHasSize(info, attributes, 1);

objects.assertEqual(info, attributes.get(0).getKey(), "name");
foundValues.add(attributes.get(0).getValue().getStringValue());
}
info.description(
"missing or unexpected type attribute for metric '%s'", actual.getName());
iterables.assertContainsExactlyInAnyOrder(info, foundValues, types.toArray());
});
}

private void dataPointsCommonCheck(List<NumberDataPoint> dataPoints) {
info.description("unable to retrieve data points from metric '%s'", actual.getName());
objects.assertNotNull(info, dataPoints);

// at least one data point must be reported
info.description("at least one data point expected for metric '%s'", actual.getName());
iterables.assertNotEmpty(info, dataPoints);
}

/**
* Verifies that all data points have all the expected attributes
*
* @param attributes expected attributes
* @return this
*/
@SafeVarargs
@CanIgnoreReturnValue
public final MetricAssert hasDataPointsAttributes(Map.Entry<String, String>... attributes) {
return checkDataPoints(
dataPoints -> {
dataPointsCommonCheck(dataPoints);

Map<String, String> attributesMap = new HashMap<>();
for (Map.Entry<String, String> attributeEntry : attributes) {
attributesMap.put(attributeEntry.getKey(), attributeEntry.getValue());
}
for (NumberDataPoint dataPoint : dataPoints) {
Map<String, String> dataPointAttributes = toMap(dataPoint.getAttributesList());

// all attributes must match
info.description(
"missing/unexpected data points attributes for metric '%s'", actual.getName());
containsExactly(dataPointAttributes, attributes);
maps.assertContainsAllEntriesOf(info, dataPointAttributes, attributesMap);
}
});
}

/**
* Verifies that all data points have their attributes match one of the attributes set and that
* all provided attributes sets matched at least once.
*
* @param attributeSets sets of attributes as maps
* @return this
*/
@SafeVarargs
@CanIgnoreReturnValue
@SuppressWarnings("varargs") // required to avoid warning
public final MetricAssert hasDataPointsAttributes(Map<String, String>... attributeSets) {
return checkDataPoints(
dataPoints -> {
dataPointsCommonCheck(dataPoints);

boolean[] matchedSets = new boolean[attributeSets.length];

// validate each datapoint attributes match exactly one of the provided attributes set
for (NumberDataPoint dataPoint : dataPoints) {
Map<String, String> map = toMap(dataPoint.getAttributesList());

int matchCount = 0;
for (int i = 0; i < attributeSets.length; i++) {
if (mapEquals(map, attributeSets[i])) {
matchedSets[i] = true;
matchCount++;
}
}

info.description(
"data point attributes '%s' for metric '%s' must match exactly one of the attribute sets '%s'",
map, actual.getName(), Arrays.asList(attributeSets));
integers.assertEqual(info, matchCount, 1);
}

// check that all attribute sets matched at least once
for (int i = 0; i < matchedSets.length; i++) {
info.description(
"no data point matched attribute set '%s' for metric '%s'",
attributeSets[i], actual.getName());
objects.assertEqual(info, matchedSets[i], true);
}
});
}

/**
* Map equality utility
*
* @param m1 first map
* @param m2 second map
* @return true if the maps have exactly the same keys and values
*/
private static boolean mapEquals(Map<String, String> m1, Map<String, String> m2) {
if (m1.size() != m2.size()) {
return false;
}
return m1.entrySet().stream().allMatch(e -> e.getValue().equals(m2.get(e.getKey())));
}

@SafeVarargs
@SuppressWarnings("varargs") // required to avoid warning
private final void containsExactly(
Map<String, String> map, Map.Entry<String, String>... entries) {
maps.assertContainsExactly(info, map, entries);
}

private static Map<String, String> toMap(List<KeyValue> list) {
return list.stream()
.collect(Collectors.toMap(KeyValue::getKey, kv -> kv.getValue().getStringValue()));
}
}
Loading