Skip to content

Commit 7ee92eb

Browse files
authored
Instruments with names which are case-insensitive equal contribute to… (#5701)
1 parent ebf96dc commit 7ee92eb

File tree

6 files changed

+325
-151
lines changed

6 files changed

+325
-151
lines changed

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/descriptor/InstrumentDescriptor.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
package io.opentelemetry.sdk.metrics.internal.descriptor;
77

88
import com.google.auto.value.AutoValue;
9-
import com.google.auto.value.extension.memoized.Memoized;
109
import io.opentelemetry.sdk.metrics.InstrumentType;
1110
import io.opentelemetry.sdk.metrics.InstrumentValueType;
1211
import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo;
12+
import java.util.Locale;
1313
import javax.annotation.concurrent.Immutable;
1414

1515
/**
@@ -23,6 +23,7 @@
2323
public abstract class InstrumentDescriptor {
2424

2525
private final SourceInfo sourceInfo = SourceInfo.fromCurrentStack();
26+
private int hashcode;
2627

2728
public static InstrumentDescriptor create(
2829
String name,
@@ -46,6 +47,9 @@ public static InstrumentDescriptor create(
4647

4748
public abstract InstrumentValueType getValueType();
4849

50+
/**
51+
* Not part of instrument identity. Ignored from {@link #hashCode()} and {@link #equals(Object)}.
52+
*/
4953
public abstract Advice getAdvice();
5054

5155
/**
@@ -56,7 +60,47 @@ public final SourceInfo getSourceInfo() {
5660
return sourceInfo;
5761
}
5862

59-
@Memoized
63+
/**
64+
* Uses case-insensitive version of {@link #getName()}, ignores {@link #getAdvice()} (not part of
65+
* instrument identity}, ignores {@link #getSourceInfo()}.
66+
*/
67+
@Override
68+
public final int hashCode() {
69+
int result = hashcode;
70+
if (result == 0) {
71+
result = 1;
72+
result *= 1000003;
73+
result ^= getName().toLowerCase(Locale.ROOT).hashCode();
74+
result *= 1000003;
75+
result ^= getDescription().hashCode();
76+
result *= 1000003;
77+
result ^= getUnit().hashCode();
78+
result *= 1000003;
79+
result ^= getType().hashCode();
80+
result *= 1000003;
81+
result ^= getValueType().hashCode();
82+
hashcode = result;
83+
}
84+
return result;
85+
}
86+
87+
/**
88+
* Uses case-insensitive version of {@link #getName()}, ignores {@link #getAdvice()} (not part of
89+
* instrument identity}, ignores {@link #getSourceInfo()}.
90+
*/
6091
@Override
61-
public abstract int hashCode();
92+
public final boolean equals(Object o) {
93+
if (o == this) {
94+
return true;
95+
}
96+
if (o instanceof InstrumentDescriptor) {
97+
InstrumentDescriptor that = (InstrumentDescriptor) o;
98+
return this.getName().equalsIgnoreCase(that.getName())
99+
&& this.getDescription().equals(that.getDescription())
100+
&& this.getUnit().equals(that.getUnit())
101+
&& this.getType().equals(that.getType())
102+
&& this.getValueType().equals(that.getValueType());
103+
}
104+
return false;
105+
}
62106
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/descriptor/MetricDescriptor.java

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
package io.opentelemetry.sdk.metrics.internal.descriptor;
77

88
import com.google.auto.value.AutoValue;
9-
import com.google.auto.value.extension.memoized.Memoized;
109
import io.opentelemetry.sdk.metrics.Aggregation;
1110
import io.opentelemetry.sdk.metrics.InstrumentType;
1211
import io.opentelemetry.sdk.metrics.InstrumentValueType;
1312
import io.opentelemetry.sdk.metrics.View;
1413
import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil;
1514
import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo;
15+
import java.util.Locale;
1616
import java.util.concurrent.atomic.AtomicReference;
1717
import javax.annotation.concurrent.Immutable;
1818

@@ -29,6 +29,7 @@
2929
public abstract class MetricDescriptor {
3030

3131
private final AtomicReference<SourceInfo> viewSourceInfo = new AtomicReference<>();
32+
private int hashcode;
3233

3334
/**
3435
* Constructs a metric descriptor with no instrument and default view.
@@ -94,36 +95,38 @@ public String getAggregationName() {
9495
return AggregationUtil.aggregationName(getView().getAggregation());
9596
}
9697

97-
@Memoized
98+
/** Uses case-insensitive version of {@link #getName()}. */
9899
@Override
99-
public abstract int hashCode();
100+
public final int hashCode() {
101+
int result = hashcode;
102+
if (result == 0) {
103+
result = 1;
104+
result *= 1000003;
105+
result ^= getName().toLowerCase(Locale.ROOT).hashCode();
106+
result *= 1000003;
107+
result ^= getDescription().hashCode();
108+
result *= 1000003;
109+
result ^= getView().hashCode();
110+
result *= 1000003;
111+
result ^= getSourceInstrument().hashCode();
112+
hashcode = result;
113+
}
114+
return result;
115+
}
100116

101-
/**
102-
* Returns true if another metric descriptor is compatible with this one.
103-
*
104-
* <p>A metric descriptor is compatible with another if the following are true:
105-
*
106-
* <ul>
107-
* <li>{@link #getName()} is equal
108-
* <li>{@link #getDescription()} is equal
109-
* <li>{@link #getAggregationName()} is equal
110-
* <li>{@link InstrumentDescriptor#getName()} is equal
111-
* <li>{@link InstrumentDescriptor#getDescription()} is equal
112-
* <li>{@link InstrumentDescriptor#getUnit()} is equal
113-
* <li>{@link InstrumentDescriptor#getType()} is equal
114-
* <li>{@link InstrumentDescriptor#getValueType()} is equal
115-
* </ul>
116-
*/
117-
public boolean isCompatibleWith(MetricDescriptor other) {
118-
return getName().equals(other.getName())
119-
&& getDescription().equals(other.getDescription())
120-
&& getAggregationName().equals(other.getAggregationName())
121-
&& getSourceInstrument().getName().equals(other.getSourceInstrument().getName())
122-
&& getSourceInstrument()
123-
.getDescription()
124-
.equals(other.getSourceInstrument().getDescription())
125-
&& getSourceInstrument().getUnit().equals(other.getSourceInstrument().getUnit())
126-
&& getSourceInstrument().getType().equals(other.getSourceInstrument().getType())
127-
&& getSourceInstrument().getValueType().equals(other.getSourceInstrument().getValueType());
117+
/** Uses case-insensitive version of {@link #getName()}. */
118+
@Override
119+
public final boolean equals(Object o) {
120+
if (o == this) {
121+
return true;
122+
}
123+
if (o instanceof MetricDescriptor) {
124+
MetricDescriptor that = (MetricDescriptor) o;
125+
return this.getName().equalsIgnoreCase(that.getName())
126+
&& this.getDescription().equals(that.getDescription())
127+
&& this.getView().equals(that.getView())
128+
&& this.getSourceInstrument().equals(that.getSourceInstrument());
129+
}
130+
return false;
128131
}
129132
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DebugUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ private DebugUtils() {}
2222
* Creates a detailed error message comparing two {@link MetricDescriptor}s.
2323
*
2424
* <p>Called when the metrics with the descriptors have the same name, but {@link
25-
* MetricDescriptor#isCompatibleWith(MetricDescriptor)} is {@code false}.
25+
* MetricDescriptor#equals(Object)} is {@code false}.
2626
*
2727
* <p>This should identify all issues between the descriptor and log information on where they are
2828
* defined. Users should be able to find/fix issues based on this error.

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistry.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
*
2323
* <p>Each descriptor in the registry results in an exported metric stream. Under normal
2424
* circumstances each descriptor shares a unique {@link MetricDescriptor#getName()}. When multiple
25-
* descriptors share the same name, an identity conflict has occurred. The registry detects identity
26-
* conflicts on {@link #register(MetricStorage)} and logs diagnostic information when they occur.
27-
* See {@link MetricDescriptor#isCompatibleWith(MetricDescriptor)} for definition of compatibility.
25+
* descriptors share the same name, but have some difference in identifying fields, an identity
26+
* conflict has occurred. The registry detects identity conflicts on {@link
27+
* #register(MetricStorage)} and logs diagnostic information when they occur. See {@link
28+
* MetricDescriptor#equals(Object)} for definition of identity equality.
2829
*
2930
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
3031
* at any time.
@@ -75,9 +76,9 @@ public <I extends MetricStorage> I register(I newStorage) {
7576
continue;
7677
}
7778
MetricDescriptor existing = storage.getMetricDescriptor();
79+
// TODO: consider this alternative
7880
// Check compatibility of metrics which share the same case-insensitive name
79-
if (existing.getName().equalsIgnoreCase(descriptor.getName())
80-
&& !existing.isCompatibleWith(descriptor)) {
81+
if (existing.getName().equalsIgnoreCase(descriptor.getName())) {
8182
logger.log(Level.WARNING, DebugUtils.duplicateMetricErrorMessage(existing, descriptor));
8283
break; // Only log information about the first conflict found to reduce noise
8384
}

sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/IdentityTest.java

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
99

1010
import io.github.netmikey.logunit.api.LogCapturer;
11+
import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder;
1112
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
1213
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
1314
import io.opentelemetry.sdk.metrics.internal.state.MetricStorageRegistry;
1415
import io.opentelemetry.sdk.metrics.internal.view.ViewRegistry;
1516
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
17+
import java.util.Arrays;
1618
import java.util.concurrent.atomic.AtomicLong;
1719
import org.junit.jupiter.api.BeforeEach;
1820
import org.junit.jupiter.api.Test;
@@ -106,7 +108,7 @@ void sameMeterSameInstrumentNoViews() {
106108
}
107109

108110
@Test
109-
void sameMeterDifferentInstrumentNoViews() {
111+
void sameMeterDifferentInstrumentNameNoViews() {
110112
SdkMeterProvider meterProvider = builder.build();
111113

112114
meterProvider.get("meter1").counterBuilder("counter1").build().add(10);
@@ -130,6 +132,139 @@ void sameMeterDifferentInstrumentNoViews() {
130132
assertThat(metricStorageRegistryLogs.getEvents()).hasSize(0);
131133
}
132134

135+
@Test
136+
void sameMeterDifferentInstrumentNameCaseNoViews() {
137+
SdkMeterProvider meterProvider = builder.build();
138+
139+
meterProvider.get("meter1").counterBuilder("Counter1").build().add(10);
140+
meterProvider.get("meter1").counterBuilder("counter1").build().add(10);
141+
142+
assertThat(reader.collectAllMetrics())
143+
.satisfiesExactlyInAnyOrder(
144+
metricData ->
145+
assertThat(metricData)
146+
.hasInstrumentationScope(forMeter("meter1"))
147+
.hasName("Counter1")
148+
.hasLongSumSatisfying(
149+
sum -> sum.hasPointsSatisfying(point -> point.hasValue(20))));
150+
151+
assertThat(metricStorageRegistryLogs.getEvents()).hasSize(0);
152+
}
153+
154+
@Test
155+
@SuppressLogger(MetricStorageRegistry.class)
156+
void sameMeterSameInstrumentNameDifferentIdentifyingFieldsNoViews() {
157+
SdkMeterProvider meterProvider = builder.build();
158+
159+
meterProvider.get("meter1").counterBuilder("counter1").build().add(10);
160+
// Same name, different unit
161+
meterProvider.get("meter1").counterBuilder("counter1").setUnit("unit1").build().add(10);
162+
// Same name, different description
163+
meterProvider
164+
.get("meter1")
165+
.counterBuilder("counter1")
166+
.setDescription("description1")
167+
.build()
168+
.add(10);
169+
// Same name, different value type
170+
meterProvider.get("meter1").counterBuilder("counter1").ofDoubles().build().add(10);
171+
// Same name, different instrument type
172+
meterProvider.get("meter1").upDownCounterBuilder("counter1").build().add(10);
173+
174+
// When name is the same, but some identifying field is different (unit, description, value
175+
// type, instrument type) we produce different metric streams are produced and log a warning
176+
assertThat(reader.collectAllMetrics())
177+
.satisfiesExactlyInAnyOrder(
178+
metricData ->
179+
assertThat(metricData)
180+
.hasInstrumentationScope(forMeter("meter1"))
181+
.hasName("counter1")
182+
.hasLongSumSatisfying(
183+
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
184+
metricData ->
185+
assertThat(metricData)
186+
.hasInstrumentationScope(forMeter("meter1"))
187+
.hasName("counter1")
188+
.hasUnit("unit1")
189+
.hasLongSumSatisfying(
190+
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
191+
metricData ->
192+
assertThat(metricData)
193+
.hasInstrumentationScope(forMeter("meter1"))
194+
.hasName("counter1")
195+
.hasDescription("description1")
196+
.hasLongSumSatisfying(
197+
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
198+
metricData ->
199+
assertThat(metricData)
200+
.hasInstrumentationScope(forMeter("meter1"))
201+
.hasName("counter1")
202+
.hasDoubleSumSatisfying(
203+
sum -> sum.isMonotonic().hasPointsSatisfying(point -> point.hasValue(10))),
204+
metricData ->
205+
assertThat(metricData)
206+
.hasInstrumentationScope(forMeter("meter1"))
207+
.hasName("counter1")
208+
.hasLongSumSatisfying(
209+
sum ->
210+
sum.isNotMonotonic().hasPointsSatisfying(point -> point.hasValue(10))));
211+
212+
assertThat(metricStorageRegistryLogs.getEvents())
213+
.allSatisfy(
214+
logEvent ->
215+
assertThat(logEvent.getMessage()).contains("Found duplicate metric definition"))
216+
.hasSize(4);
217+
}
218+
219+
@Test
220+
void sameMeterSameInstrumentNameDifferentNonIdentifyingFieldsNoViews() {
221+
SdkMeterProvider meterProvider = builder.build();
222+
223+
// Register histogram1, with and without advice. First registration without advice wins.
224+
meterProvider.get("meter1").histogramBuilder("histogram1").build().record(8);
225+
((ExtendedDoubleHistogramBuilder) meterProvider.get("meter1").histogramBuilder("histogram1"))
226+
.setAdvice(advice -> advice.setExplicitBucketBoundaries(Arrays.asList(10.0, 20.0, 30.0)))
227+
.build()
228+
.record(8);
229+
230+
// Register histogram2, with and without advice. First registration with advice wins.
231+
((ExtendedDoubleHistogramBuilder) meterProvider.get("meter1").histogramBuilder("histogram2"))
232+
.setAdvice(advice -> advice.setExplicitBucketBoundaries(Arrays.asList(10.0, 20.0, 30.0)))
233+
.build()
234+
.record(8);
235+
meterProvider.get("meter1").histogramBuilder("histogram2").build().record(8);
236+
237+
assertThat(reader.collectAllMetrics())
238+
.satisfiesExactlyInAnyOrder(
239+
metricData ->
240+
assertThat(metricData)
241+
.hasInstrumentationScope(forMeter("meter1"))
242+
.hasName("histogram1")
243+
.hasHistogramSatisfying(
244+
histogram ->
245+
histogram.hasPointsSatisfying(
246+
point ->
247+
point
248+
.hasBucketCounts(
249+
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
250+
.hasBucketBoundaries(
251+
0d, 5d, 10d, 25d, 50d, 75d, 100d, 250d, 500d, 750d,
252+
1_000d, 2_500d, 5_000d, 7_500d, 10_000d))),
253+
metricData ->
254+
assertThat(metricData)
255+
.hasInstrumentationScope(forMeter("meter1"))
256+
.hasName("histogram2")
257+
.hasHistogramSatisfying(
258+
histogram ->
259+
histogram.hasPointsSatisfying(
260+
point ->
261+
point
262+
.hasBucketCounts(2, 0, 0, 0)
263+
.hasBucketBoundaries(10.0, 20.0, 30.0))));
264+
265+
assertThat(metricStorageRegistryLogs.getEvents()).hasSize(0);
266+
}
267+
133268
@Test
134269
void differentMeterSameInstrumentNoViews() {
135270
// Meters are the same if their name, version, and scope are all equals

0 commit comments

Comments
 (0)