Instruments with names which are case-insensitive equal contribute to… (#5701)

This commit is contained in:
jack-berg 2023-08-11 10:20:23 -05:00 committed by GitHub
parent ebf96dc5de
commit 7ee92eb365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 325 additions and 151 deletions

View File

@ -6,10 +6,10 @@
package io.opentelemetry.sdk.metrics.internal.descriptor;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import io.opentelemetry.sdk.metrics.InstrumentType;
import io.opentelemetry.sdk.metrics.InstrumentValueType;
import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo;
import java.util.Locale;
import javax.annotation.concurrent.Immutable;
/**
@ -23,6 +23,7 @@ import javax.annotation.concurrent.Immutable;
public abstract class InstrumentDescriptor {
private final SourceInfo sourceInfo = SourceInfo.fromCurrentStack();
private int hashcode;
public static InstrumentDescriptor create(
String name,
@ -46,6 +47,9 @@ public abstract class InstrumentDescriptor {
public abstract InstrumentValueType getValueType();
/**
* Not part of instrument identity. Ignored from {@link #hashCode()} and {@link #equals(Object)}.
*/
public abstract Advice getAdvice();
/**
@ -56,7 +60,47 @@ public abstract class InstrumentDescriptor {
return sourceInfo;
}
@Memoized
/**
* Uses case-insensitive version of {@link #getName()}, ignores {@link #getAdvice()} (not part of
* instrument identity}, ignores {@link #getSourceInfo()}.
*/
@Override
public abstract int hashCode();
public final int hashCode() {
int result = hashcode;
if (result == 0) {
result = 1;
result *= 1000003;
result ^= getName().toLowerCase(Locale.ROOT).hashCode();
result *= 1000003;
result ^= getDescription().hashCode();
result *= 1000003;
result ^= getUnit().hashCode();
result *= 1000003;
result ^= getType().hashCode();
result *= 1000003;
result ^= getValueType().hashCode();
hashcode = result;
}
return result;
}
/**
* Uses case-insensitive version of {@link #getName()}, ignores {@link #getAdvice()} (not part of
* instrument identity}, ignores {@link #getSourceInfo()}.
*/
@Override
public final boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof InstrumentDescriptor) {
InstrumentDescriptor that = (InstrumentDescriptor) o;
return this.getName().equalsIgnoreCase(that.getName())
&& this.getDescription().equals(that.getDescription())
&& this.getUnit().equals(that.getUnit())
&& this.getType().equals(that.getType())
&& this.getValueType().equals(that.getValueType());
}
return false;
}
}

View File

@ -6,13 +6,13 @@
package io.opentelemetry.sdk.metrics.internal.descriptor;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import io.opentelemetry.sdk.metrics.Aggregation;
import io.opentelemetry.sdk.metrics.InstrumentType;
import io.opentelemetry.sdk.metrics.InstrumentValueType;
import io.opentelemetry.sdk.metrics.View;
import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil;
import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.concurrent.Immutable;
@ -29,6 +29,7 @@ import javax.annotation.concurrent.Immutable;
public abstract class MetricDescriptor {
private final AtomicReference<SourceInfo> viewSourceInfo = new AtomicReference<>();
private int hashcode;
/**
* Constructs a metric descriptor with no instrument and default view.
@ -94,36 +95,38 @@ public abstract class MetricDescriptor {
return AggregationUtil.aggregationName(getView().getAggregation());
}
@Memoized
/** Uses case-insensitive version of {@link #getName()}. */
@Override
public abstract int hashCode();
public final int hashCode() {
int result = hashcode;
if (result == 0) {
result = 1;
result *= 1000003;
result ^= getName().toLowerCase(Locale.ROOT).hashCode();
result *= 1000003;
result ^= getDescription().hashCode();
result *= 1000003;
result ^= getView().hashCode();
result *= 1000003;
result ^= getSourceInstrument().hashCode();
hashcode = result;
}
return result;
}
/**
* Returns true if another metric descriptor is compatible with this one.
*
* <p>A metric descriptor is compatible with another if the following are true:
*
* <ul>
* <li>{@link #getName()} is equal
* <li>{@link #getDescription()} is equal
* <li>{@link #getAggregationName()} is equal
* <li>{@link InstrumentDescriptor#getName()} is equal
* <li>{@link InstrumentDescriptor#getDescription()} is equal
* <li>{@link InstrumentDescriptor#getUnit()} is equal
* <li>{@link InstrumentDescriptor#getType()} is equal
* <li>{@link InstrumentDescriptor#getValueType()} is equal
* </ul>
*/
public boolean isCompatibleWith(MetricDescriptor other) {
return getName().equals(other.getName())
&& getDescription().equals(other.getDescription())
&& getAggregationName().equals(other.getAggregationName())
&& getSourceInstrument().getName().equals(other.getSourceInstrument().getName())
&& getSourceInstrument()
.getDescription()
.equals(other.getSourceInstrument().getDescription())
&& getSourceInstrument().getUnit().equals(other.getSourceInstrument().getUnit())
&& getSourceInstrument().getType().equals(other.getSourceInstrument().getType())
&& getSourceInstrument().getValueType().equals(other.getSourceInstrument().getValueType());
/** Uses case-insensitive version of {@link #getName()}. */
@Override
public final boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof MetricDescriptor) {
MetricDescriptor that = (MetricDescriptor) o;
return this.getName().equalsIgnoreCase(that.getName())
&& this.getDescription().equals(that.getDescription())
&& this.getView().equals(that.getView())
&& this.getSourceInstrument().equals(that.getSourceInstrument());
}
return false;
}
}

View File

@ -22,7 +22,7 @@ public final class DebugUtils {
* Creates a detailed error message comparing two {@link MetricDescriptor}s.
*
* <p>Called when the metrics with the descriptors have the same name, but {@link
* MetricDescriptor#isCompatibleWith(MetricDescriptor)} is {@code false}.
* MetricDescriptor#equals(Object)} is {@code false}.
*
* <p>This should identify all issues between the descriptor and log information on where they are
* defined. Users should be able to find/fix issues based on this error.

View File

@ -22,9 +22,10 @@ import java.util.logging.Logger;
*
* <p>Each descriptor in the registry results in an exported metric stream. Under normal
* circumstances each descriptor shares a unique {@link MetricDescriptor#getName()}. When multiple
* descriptors share the same name, an identity conflict has occurred. The registry detects identity
* conflicts on {@link #register(MetricStorage)} and logs diagnostic information when they occur.
* See {@link MetricDescriptor#isCompatibleWith(MetricDescriptor)} for definition of compatibility.
* descriptors share the same name, but have some difference in identifying fields, an identity
* conflict has occurred. The registry detects identity conflicts on {@link
* #register(MetricStorage)} and logs diagnostic information when they occur. See {@link
* MetricDescriptor#equals(Object)} for definition of identity equality.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
@ -75,9 +76,9 @@ public class MetricStorageRegistry {
continue;
}
MetricDescriptor existing = storage.getMetricDescriptor();
// TODO: consider this alternative
// Check compatibility of metrics which share the same case-insensitive name
if (existing.getName().equalsIgnoreCase(descriptor.getName())
&& !existing.isCompatibleWith(descriptor)) {
if (existing.getName().equalsIgnoreCase(descriptor.getName())) {
logger.log(Level.WARNING, DebugUtils.duplicateMetricErrorMessage(existing, descriptor));
break; // Only log information about the first conflict found to reduce noise
}

View File

@ -8,11 +8,13 @@ package io.opentelemetry.sdk.metrics;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder;
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.metrics.internal.state.MetricStorageRegistry;
import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -106,7 +108,7 @@ class IdentityTest {
}
@Test
void sameMeterDifferentInstrumentNoViews() {
void sameMeterDifferentInstrumentNameNoViews() {
SdkMeterProvider meterProvider = builder.build();
meterProvider.get("meter1").counterBuilder("counter1").build().add(10);
@ -130,6 +132,139 @@ class IdentityTest {
assertThat(metricStorageRegistryLogs.getEvents()).hasSize(0);
}
@Test
void sameMeterDifferentInstrumentNameCaseNoViews() {
SdkMeterProvider meterProvider = builder.build();
meterProvider.get("meter1").counterBuilder("Counter1").build().add(10);
meterProvider.get("meter1").counterBuilder("counter1").build().add(10);
assertThat(reader.collectAllMetrics())
.satisfiesExactlyInAnyOrder(
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("Counter1")
.hasLongSumSatisfying(
sum -> sum.hasPointsSatisfying(point -> point.hasValue(20))));
assertThat(metricStorageRegistryLogs.getEvents()).hasSize(0);
}
@Test
@SuppressLogger(MetricStorageRegistry.class)
void sameMeterSameInstrumentNameDifferentIdentifyingFieldsNoViews() {
SdkMeterProvider meterProvider = builder.build();
meterProvider.get("meter1").counterBuilder("counter1").build().add(10);
// Same name, different unit
meterProvider.get("meter1").counterBuilder("counter1").setUnit("unit1").build().add(10);
// Same name, different description
meterProvider
.get("meter1")
.counterBuilder("counter1")
.setDescription("description1")
.build()
.add(10);
// Same name, different value type
meterProvider.get("meter1").counterBuilder("counter1").ofDoubles().build().add(10);
// Same name, different instrument type
meterProvider.get("meter1").upDownCounterBuilder("counter1").build().add(10);
// When name is the same, but some identifying field is different (unit, description, value
// type, instrument type) we produce different metric streams are produced and log a warning
assertThat(reader.collectAllMetrics())
.satisfiesExactlyInAnyOrder(
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("counter1")
.hasLongSumSatisfying(
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("counter1")
.hasUnit("unit1")
.hasLongSumSatisfying(
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("counter1")
.hasDescription("description1")
.hasLongSumSatisfying(
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("counter1")
.hasDoubleSumSatisfying(
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("counter1")
.hasLongSumSatisfying(
sum ->
sum.isNotMonotonic().hasPointsSatisfying(point -> point.hasValue(10))));
assertThat(metricStorageRegistryLogs.getEvents())
.allSatisfy(
logEvent ->
assertThat(logEvent.getMessage()).contains("Found duplicate metric definition"))
.hasSize(4);
}
@Test
void sameMeterSameInstrumentNameDifferentNonIdentifyingFieldsNoViews() {
SdkMeterProvider meterProvider = builder.build();
// Register histogram1, with and without advice. First registration without advice wins.
meterProvider.get("meter1").histogramBuilder("histogram1").build().record(8);
((ExtendedDoubleHistogramBuilder) meterProvider.get("meter1").histogramBuilder("histogram1"))
.setAdvice(advice -> advice.setExplicitBucketBoundaries(Arrays.asList(10.0, 20.0, 30.0)))
.build()
.record(8);
// Register histogram2, with and without advice. First registration with advice wins.
((ExtendedDoubleHistogramBuilder) meterProvider.get("meter1").histogramBuilder("histogram2"))
.setAdvice(advice -> advice.setExplicitBucketBoundaries(Arrays.asList(10.0, 20.0, 30.0)))
.build()
.record(8);
meterProvider.get("meter1").histogramBuilder("histogram2").build().record(8);
assertThat(reader.collectAllMetrics())
.satisfiesExactlyInAnyOrder(
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("histogram1")
.hasHistogramSatisfying(
histogram ->
histogram.hasPointsSatisfying(
point ->
point
.hasBucketCounts(
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
.hasBucketBoundaries(
0d, 5d, 10d, 25d, 50d, 75d, 100d, 250d, 500d, 750d,
1_000d, 2_500d, 5_000d, 7_500d, 10_000d))),
metricData ->
assertThat(metricData)
.hasInstrumentationScope(forMeter("meter1"))
.hasName("histogram2")
.hasHistogramSatisfying(
histogram ->
histogram.hasPointsSatisfying(
point ->
point
.hasBucketCounts(2, 0, 0, 0)
.hasBucketBoundaries(10.0, 20.0, 30.0))));
assertThat(metricStorageRegistryLogs.getEvents()).hasSize(0);
}
@Test
void differentMeterSameInstrumentNoViews() {
// Meters are the same if their name, version, and scope are all equals

View File

@ -12,6 +12,7 @@ import io.opentelemetry.sdk.metrics.InstrumentType;
import io.opentelemetry.sdk.metrics.InstrumentValueType;
import io.opentelemetry.sdk.metrics.View;
import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
class MetricDescriptorTest {
@ -57,7 +58,7 @@ class MetricDescriptorTest {
}
@Test
void metricDescriptor_isCompatible() {
void metricDescriptor_equals() {
View view = View.builder().build();
InstrumentDescriptor instrument =
InstrumentDescriptor.create(
@ -67,116 +68,106 @@ class MetricDescriptorTest {
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty());
MetricDescriptor descriptor =
MetricDescriptor descriptor1 =
MetricDescriptor.create(view, SourceInfo.fromCurrentStack(), instrument);
// Same name, description, source name, source description, source unit, source type, and source
// value type is compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
View.builder().build(),
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()))))
.isTrue();
// Different name overridden by view is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
View.builder().setName("bar").build(),
SourceInfo.fromCurrentStack(),
instrument)))
.isFalse();
// Different description overridden by view is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
View.builder().setDescription("foo").build(),
SourceInfo.fromCurrentStack(),
instrument)))
.isFalse();
// Different aggregation overridden by view is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
View.builder().setAggregation(Aggregation.lastValue()).build(),
SourceInfo.fromCurrentStack(),
instrument)))
.isFalse();
// Different instrument source name is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"foo",
"description",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()))))
.isFalse();
// Different instrument source description is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"foo",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()))))
.isFalse();
// Different instrument source unit is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"foo",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()))))
.isFalse();
// Different instrument source type is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"unit",
InstrumentType.HISTOGRAM,
InstrumentValueType.DOUBLE,
Advice.empty()))))
.isFalse();
// Different instrument source value type is not compatible
assertThat(
descriptor.isCompatibleWith(
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.LONG,
Advice.empty()))))
.isFalse();
// Same name (case-insensitive), description, view, source name (case-insensitive), source unit,
// source description, source type, and source value type is equal. Advice is not part of
// equals.
MetricDescriptor descriptor2 =
MetricDescriptor.create(
View.builder().build(),
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"Name",
"description",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.create(Arrays.asList(1.0, 2.0))));
assertThat(descriptor1).isEqualTo(descriptor2).hasSameHashCodeAs(descriptor2);
// Different name overridden by view is not equal
descriptor2 =
MetricDescriptor.create(
View.builder().setName("bar").build(), SourceInfo.fromCurrentStack(), instrument);
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different description overridden by view is not equal
descriptor2 =
MetricDescriptor.create(
View.builder().setDescription("foo").build(),
SourceInfo.fromCurrentStack(),
instrument);
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different aggregation overridden by view is not equal
descriptor2 =
MetricDescriptor.create(
View.builder().setAggregation(Aggregation.lastValue()).build(),
SourceInfo.fromCurrentStack(),
instrument);
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different instrument source name is not equal
descriptor2 =
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"foo",
"description",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()));
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different instrument source description is not equal
descriptor2 =
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"foo",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()));
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different instrument source unit is not equal
descriptor2 =
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"foo",
InstrumentType.COUNTER,
InstrumentValueType.DOUBLE,
Advice.empty()));
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different instrument source type is not equal
descriptor2 =
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"unit",
InstrumentType.HISTOGRAM,
InstrumentValueType.DOUBLE,
Advice.empty()));
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
// Different instrument source value type is not equal
descriptor2 =
MetricDescriptor.create(
view,
SourceInfo.fromCurrentStack(),
InstrumentDescriptor.create(
"name",
"description",
"unit",
InstrumentType.COUNTER,
InstrumentValueType.LONG,
Advice.empty()));
assertThat(descriptor1).isNotEqualTo(descriptor2).doesNotHaveSameHashCodeAs(descriptor2);
}
}