Add experimental synchronous gauge (#5506)

This commit is contained in:
jack-berg 2023-09-07 14:58:51 -05:00 committed by GitHub
parent 43ee51ca4a
commit a5889a685d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 902 additions and 360 deletions

View File

@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.extension.incubator.metrics;
import io.opentelemetry.api.common.Attributes;
import javax.annotation.concurrent.ThreadSafe;
/** A gauge instrument that synchronously records {@code double} values. */
@ThreadSafe
public interface DoubleGauge {
/**
* Set the gauge value.
*
* @param value The current gauge value.
*/
void set(double value);
/**
* Records a value with a set of attributes.
*
* @param value The current gauge value.
* @param attributes A set of attributes to associate with the value.
*/
void set(double value, Attributes attributes);
// TODO(jack-berg): should we add overload with Context argument?
}

View File

@ -11,6 +11,17 @@ import java.util.function.Consumer;
/** Extended {@link DoubleGaugeBuilder} with experimental APIs. */
public interface ExtendedDoubleGaugeBuilder extends DoubleGaugeBuilder {
/**
* Builds and returns a DoubleGauge instrument with the configuration.
*
* <p>NOTE: This produces a synchronous gauge which records gauge values as they occur. Most users
* will want to instead register an {@link #buildWithCallback(Consumer)} to asynchronously observe
* the value of the gauge when metrics are collected.
*
* @return The DoubleGauge instrument.
*/
DoubleGauge build();
/** Specify advice for gauge implementations. */
default DoubleGaugeBuilder setAdvice(Consumer<DoubleGaugeAdviceConfigurer> adviceConsumer) {
return this;

View File

@ -11,6 +11,17 @@ import java.util.function.Consumer;
/** Extended {@link LongGaugeBuilder} with experimental APIs. */
public interface ExtendedLongGaugeBuilder extends LongGaugeBuilder {
/**
* Builds and returns a LongGauge instrument with the configuration.
*
* <p>NOTE: This produces a synchronous gauge which records gauge values as they occur. Most users
* will want to instead register an {@link #buildWithCallback(Consumer)} to asynchronously observe
* the value of the gauge when metrics are collected.
*
* @return The LongGauge instrument.
*/
LongGauge build();
/** Specify advice for gauge implementations. */
default LongGaugeBuilder setAdvice(Consumer<LongGaugeAdviceConfigurer> adviceConsumer) {
return this;

View File

@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.extension.incubator.metrics;
import io.opentelemetry.api.common.Attributes;
import javax.annotation.concurrent.ThreadSafe;
/** A gauge instrument that synchronously records {@code long} values. */
@ThreadSafe
public interface LongGauge {
/**
* Set the gauge value.
*
* @param value The current gauge value.
*/
void set(long value);
/**
* Records a value with a set of attributes.
*
* @param value The current gauge value.
* @param attributes A set of attributes to associate with the value.
*/
void set(long value, Attributes attributes);
// TODO(jack-berg): should we add overload with Context argument?
}

View File

@ -0,0 +1,101 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleGaugeBuilder;
import io.opentelemetry.api.metrics.LongGaugeBuilder;
import io.opentelemetry.api.metrics.ObservableDoubleGauge;
import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
import io.opentelemetry.context.Context;
import io.opentelemetry.extension.incubator.metrics.DoubleGauge;
import io.opentelemetry.extension.incubator.metrics.DoubleGaugeAdviceConfigurer;
import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
import io.opentelemetry.sdk.metrics.internal.state.MeterSharedState;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
import java.util.function.Consumer;
final class SdkDoubleGauge extends AbstractInstrument implements DoubleGauge {
private final WriteableMetricStorage storage;
private SdkDoubleGauge(InstrumentDescriptor descriptor, WriteableMetricStorage storage) {
super(descriptor);
this.storage = storage;
}
@Override
public void set(double increment, Attributes attributes) {
storage.recordDouble(increment, attributes, Context.root());
}
@Override
public void set(double increment) {
set(increment, Attributes.empty());
}
static final class SdkDoubleGaugeBuilder extends AbstractInstrumentBuilder<SdkDoubleGaugeBuilder>
implements ExtendedDoubleGaugeBuilder, DoubleGaugeAdviceConfigurer {
SdkDoubleGaugeBuilder(
MeterProviderSharedState meterProviderSharedState,
MeterSharedState meterSharedState,
String name) {
super(
meterProviderSharedState,
meterSharedState,
// TODO: use InstrumentType.GAUGE when available
InstrumentType.OBSERVABLE_GAUGE,
InstrumentValueType.DOUBLE,
name,
"",
DEFAULT_UNIT);
}
@Override
protected SdkDoubleGaugeBuilder getThis() {
return this;
}
@Override
public SdkDoubleGauge build() {
return buildSynchronousInstrument(SdkDoubleGauge::new);
}
@Override
public DoubleGaugeBuilder setAdvice(Consumer<DoubleGaugeAdviceConfigurer> adviceConsumer) {
adviceConsumer.accept(this);
return this;
}
@Override
public DoubleGaugeAdviceConfigurer setAttributes(List<AttributeKey<?>> attributes) {
adviceBuilder.setAttributes(attributes);
return this;
}
@Override
public LongGaugeBuilder ofLongs() {
return swapBuilder(SdkLongGauge.SdkLongGaugeBuilder::new);
}
@Override
public ObservableDoubleGauge buildWithCallback(Consumer<ObservableDoubleMeasurement> callback) {
// TODO: use InstrumentType.GAUGE when available
return registerDoubleAsynchronousInstrument(InstrumentType.OBSERVABLE_GAUGE, callback);
}
@Override
public ObservableDoubleMeasurement buildObserver() {
// TODO: use InstrumentType.GAUGE when available
return buildObservableMeasurement(InstrumentType.OBSERVABLE_GAUGE);
}
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.metrics.DoubleGaugeBuilder;
import io.opentelemetry.api.metrics.LongGaugeBuilder;
import io.opentelemetry.api.metrics.ObservableDoubleGauge;
import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
import io.opentelemetry.extension.incubator.metrics.DoubleGaugeAdviceConfigurer;
import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder;
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
import io.opentelemetry.sdk.metrics.internal.state.MeterSharedState;
import java.util.List;
import java.util.function.Consumer;
final class SdkDoubleGaugeBuilder extends AbstractInstrumentBuilder<SdkDoubleGaugeBuilder>
implements ExtendedDoubleGaugeBuilder, DoubleGaugeAdviceConfigurer {
SdkDoubleGaugeBuilder(
MeterProviderSharedState meterProviderSharedState,
MeterSharedState meterSharedState,
String name) {
super(
meterProviderSharedState,
meterSharedState,
InstrumentType.OBSERVABLE_GAUGE,
InstrumentValueType.DOUBLE,
name,
"",
DEFAULT_UNIT);
}
@Override
protected SdkDoubleGaugeBuilder getThis() {
return this;
}
@Override
public DoubleGaugeBuilder setAdvice(Consumer<DoubleGaugeAdviceConfigurer> adviceConsumer) {
adviceConsumer.accept(this);
return this;
}
@Override
public LongGaugeBuilder ofLongs() {
return swapBuilder(SdkLongGaugeBuilder::new);
}
@Override
public ObservableDoubleGauge buildWithCallback(Consumer<ObservableDoubleMeasurement> callback) {
return registerDoubleAsynchronousInstrument(InstrumentType.OBSERVABLE_GAUGE, callback);
}
@Override
public ObservableDoubleMeasurement buildObserver() {
return buildObservableMeasurement(InstrumentType.OBSERVABLE_GAUGE);
}
@Override
public DoubleGaugeAdviceConfigurer setAttributes(List<AttributeKey<?>> attributes) {
adviceBuilder.setAttributes(attributes);
return this;
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongGaugeBuilder;
import io.opentelemetry.api.metrics.ObservableLongGauge;
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
import io.opentelemetry.context.Context;
import io.opentelemetry.extension.incubator.metrics.ExtendedLongGaugeBuilder;
import io.opentelemetry.extension.incubator.metrics.LongGauge;
import io.opentelemetry.extension.incubator.metrics.LongGaugeAdviceConfigurer;
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
import io.opentelemetry.sdk.metrics.internal.state.MeterSharedState;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
import java.util.function.Consumer;
final class SdkLongGauge extends AbstractInstrument implements LongGauge {
private final WriteableMetricStorage storage;
private SdkLongGauge(InstrumentDescriptor descriptor, WriteableMetricStorage storage) {
super(descriptor);
this.storage = storage;
}
@Override
public void set(long increment, Attributes attributes) {
storage.recordLong(increment, attributes, Context.root());
}
@Override
public void set(long increment) {
set(increment, Attributes.empty());
}
static final class SdkLongGaugeBuilder extends AbstractInstrumentBuilder<SdkLongGaugeBuilder>
implements ExtendedLongGaugeBuilder, LongGaugeAdviceConfigurer {
SdkLongGaugeBuilder(
MeterProviderSharedState meterProviderSharedState,
MeterSharedState sharedState,
String name,
String description,
String unit,
Advice.AdviceBuilder adviceBuilder) {
super(
meterProviderSharedState,
sharedState,
// TODO: use InstrumentType.GAUGE when available
InstrumentType.OBSERVABLE_GAUGE,
InstrumentValueType.LONG,
name,
description,
unit,
adviceBuilder);
}
@Override
protected SdkLongGaugeBuilder getThis() {
return this;
}
@Override
public SdkLongGauge build() {
return buildSynchronousInstrument(SdkLongGauge::new);
}
@Override
public LongGaugeBuilder setAdvice(Consumer<LongGaugeAdviceConfigurer> adviceConsumer) {
adviceConsumer.accept(this);
return this;
}
@Override
public LongGaugeAdviceConfigurer setAttributes(List<AttributeKey<?>> attributes) {
adviceBuilder.setAttributes(attributes);
return this;
}
@Override
public ObservableLongGauge buildWithCallback(Consumer<ObservableLongMeasurement> callback) {
// TODO: use InstrumentType.GAUGE when available
return registerLongAsynchronousInstrument(InstrumentType.OBSERVABLE_GAUGE, callback);
}
@Override
public ObservableLongMeasurement buildObserver() {
// TODO: use InstrumentType.GAUGE when available
return buildObservableMeasurement(InstrumentType.OBSERVABLE_GAUGE);
}
}
}

View File

@ -1,67 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.metrics.LongGaugeBuilder;
import io.opentelemetry.api.metrics.ObservableLongGauge;
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
import io.opentelemetry.extension.incubator.metrics.ExtendedLongGaugeBuilder;
import io.opentelemetry.extension.incubator.metrics.LongGaugeAdviceConfigurer;
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
import io.opentelemetry.sdk.metrics.internal.state.MeterSharedState;
import java.util.List;
import java.util.function.Consumer;
final class SdkLongGaugeBuilder extends AbstractInstrumentBuilder<SdkLongGaugeBuilder>
implements ExtendedLongGaugeBuilder, LongGaugeAdviceConfigurer {
SdkLongGaugeBuilder(
MeterProviderSharedState meterProviderSharedState,
MeterSharedState sharedState,
String name,
String description,
String unit,
Advice.AdviceBuilder adviceBuilder) {
super(
meterProviderSharedState,
sharedState,
InstrumentType.OBSERVABLE_GAUGE,
InstrumentValueType.LONG,
name,
description,
unit,
adviceBuilder);
}
@Override
protected SdkLongGaugeBuilder getThis() {
return this;
}
@Override
public LongGaugeBuilder setAdvice(Consumer<LongGaugeAdviceConfigurer> adviceConsumer) {
adviceConsumer.accept(this);
return this;
}
@Override
public ObservableLongGauge buildWithCallback(Consumer<ObservableLongMeasurement> callback) {
return registerLongAsynchronousInstrument(InstrumentType.OBSERVABLE_GAUGE, callback);
}
@Override
public ObservableLongMeasurement buildObserver() {
return buildObservableMeasurement(InstrumentType.OBSERVABLE_GAUGE);
}
@Override
public LongGaugeAdviceConfigurer setAttributes(List<AttributeKey<?>> attributes) {
adviceBuilder.setAttributes(attributes);
return this;
}
}

View File

@ -108,7 +108,8 @@ final class SdkMeter implements Meter {
public DoubleGaugeBuilder gaugeBuilder(String name) {
return !checkValidInstrumentName(name)
? NOOP_METER.gaugeBuilder(NOOP_INSTRUMENT_NAME)
: new SdkDoubleGaugeBuilder(meterProviderSharedState, meterSharedState, name);
: new SdkDoubleGauge.SdkDoubleGaugeBuilder(
meterProviderSharedState, meterSharedState, name);
}
@Override

View File

@ -1,113 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableDoubleGauge;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import io.opentelemetry.sdk.testing.time.TestClock;
import java.time.Duration;
import org.junit.jupiter.api.Test;
/** Unit tests for SDK {@link ObservableDoubleGauge}. */
class SdkDoubleGaugeBuilderTest {
private static final Resource RESOURCE =
Resource.create(Attributes.of(stringKey("resource_key"), "resource_value"));
private static final InstrumentationScopeInfo INSTRUMENTATION_SCOPE_INFO =
InstrumentationScopeInfo.create(SdkDoubleGaugeBuilderTest.class.getName());
private final TestClock testClock = TestClock.create();
private final InMemoryMetricReader sdkMeterReader = InMemoryMetricReader.create();
private final SdkMeterProvider sdkMeterProvider =
SdkMeterProvider.builder()
.setClock(testClock)
.setResource(RESOURCE)
.registerMetricReader(sdkMeterReader)
.build();
private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName());
@Test
void removeCallback() {
ObservableDoubleGauge gauge =
sdkMeter.gaugeBuilder("testGauge").buildWithCallback(measurement -> measurement.record(10));
assertThat(sdkMeterReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasName("testGauge")
.hasDoubleGaugeSatisfying(
doubleGauge -> doubleGauge.hasPointsSatisfying(poit -> {})));
gauge.close();
assertThat(sdkMeterReader.collectAllMetrics()).hasSize(0);
}
@Test
void collectMetrics_NoRecords() {
sdkMeter
.gaugeBuilder("testObserver")
.setDescription("My own DoubleValueObserver")
.setUnit("ms")
.buildWithCallback(result -> {});
assertThat(sdkMeterReader.collectAllMetrics()).isEmpty();
}
@Test
void collectMetrics_WithOneRecord() {
sdkMeter
.gaugeBuilder("testObserver")
.setDescription("My own DoubleValueObserver")
.setUnit("ms")
.buildWithCallback(
result -> result.record(12.1d, Attributes.builder().put("k", "v").build()));
testClock.advance(Duration.ofSeconds(1));
assertThat(sdkMeterReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testObserver")
.hasDescription("My own DoubleValueObserver")
.hasUnit("ms")
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now() - 1000000000L)
.hasEpochNanos(testClock.now())
.hasAttributes(attributeEntry("k", "v"))
.hasValue(12.1d))));
testClock.advance(Duration.ofSeconds(1));
assertThat(sdkMeterReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testObserver")
.hasDescription("My own DoubleValueObserver")
.hasUnit("ms")
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now() - 2000000000L)
.hasEpochNanos(testClock.now())
.hasAttributes(attributeEntry("k", "v"))
.hasValue(12.1d))));
}
}

View File

@ -0,0 +1,305 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableDoubleGauge;
import io.opentelemetry.extension.incubator.metrics.DoubleGauge;
import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import io.opentelemetry.sdk.testing.time.TestClock;
import java.time.Duration;
import java.util.stream.IntStream;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link SdkDoubleGauge}. */
class SdkDoubleGaugeTest {
private static final long SECOND_NANOS = 1_000_000_000;
private static final Resource RESOURCE =
Resource.create(Attributes.of(stringKey("resource_key"), "resource_value"));
private static final InstrumentationScopeInfo INSTRUMENTATION_SCOPE_INFO =
InstrumentationScopeInfo.create(SdkDoubleGaugeTest.class.getName());
private final TestClock testClock = TestClock.create();
private final InMemoryMetricReader cumulativeReader = InMemoryMetricReader.create();
private final InMemoryMetricReader deltaReader = InMemoryMetricReader.createDelta();
private final SdkMeterProvider sdkMeterProvider =
SdkMeterProvider.builder()
.setClock(testClock)
.registerMetricReader(cumulativeReader)
.registerMetricReader(deltaReader)
.setResource(RESOURCE)
.build();
private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName());
@Test
void set_PreventNullAttributes() {
assertThatThrownBy(
() ->
((ExtendedDoubleGaugeBuilder) sdkMeter.gaugeBuilder("testGauge"))
.build()
.set(1.0, null))
.isInstanceOf(NullPointerException.class)
.hasMessage("attributes");
}
@Test
void observable_RemoveCallback() {
ObservableDoubleGauge gauge =
sdkMeter.gaugeBuilder("testGauge").buildWithCallback(measurement -> measurement.record(10));
Assertions.assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasName("testGauge")
.hasDoubleGaugeSatisfying(
doubleGauge -> doubleGauge.hasPointsSatisfying(point -> {})));
gauge.close();
Assertions.assertThat(cumulativeReader.collectAllMetrics()).hasSize(0);
}
@Test
void collectMetrics_NoRecords() {
((ExtendedDoubleGaugeBuilder) sdkMeter.gaugeBuilder("testGauge")).build();
assertThat(cumulativeReader.collectAllMetrics()).isEmpty();
}
@Test
void collectMetrics_WithEmptyAttributes() {
DoubleGauge doubleGauge =
((ExtendedDoubleGaugeBuilder)
sdkMeter.gaugeBuilder("testGauge").setDescription("description").setUnit("K"))
.build();
testClock.advance(Duration.ofNanos(SECOND_NANOS));
doubleGauge.set(12d, Attributes.empty());
doubleGauge.set(13d);
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDescription("description")
.hasUnit("K")
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now() - SECOND_NANOS)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(13d))));
}
@Test
void collectMetrics_WithMultipleCollects() {
long startTime = testClock.now();
DoubleGauge doubleGauge =
((ExtendedDoubleGaugeBuilder) sdkMeter.gaugeBuilder("testGauge")).build();
doubleGauge.set(12.1d, Attributes.empty());
doubleGauge.set(123.3d, Attributes.builder().put("K", "V").build());
doubleGauge.set(21.4d, Attributes.empty());
// Advancing time here should not matter.
testClock.advance(Duration.ofNanos(SECOND_NANOS));
doubleGauge.set(321.5d, Attributes.builder().put("K", "V").build());
doubleGauge.set(111.1d, Attributes.builder().put("K", "V").build());
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDescription("")
.hasUnit("")
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(21.4d),
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasValue(111.1d)
.hasAttributes(attributeEntry("K", "V")))));
assertThat(deltaReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDescription("")
.hasUnit("")
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(21.4d),
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasValue(111.1d)
.hasAttributes(attributeEntry("K", "V")))));
// Repeat to prove we keep previous values.
testClock.advance(Duration.ofNanos(SECOND_NANOS));
doubleGauge.set(222d, Attributes.builder().put("K", "V").build());
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(21.4d),
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasValue(222d)
.hasAttributes(attributeEntry("K", "V")))));
// Delta reader should only have point for {K=V} series, since the {} did not have any
// measurements
assertThat(deltaReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime + SECOND_NANOS)
.hasEpochNanos(testClock.now())
.hasValue(222d)
.hasAttributes(attributeEntry("K", "V")))));
}
@Test
void stressTest() {
DoubleGauge doubleGauge =
((ExtendedDoubleGaugeBuilder) sdkMeter.gaugeBuilder("testGauge")).build();
StressTestRunner.Builder stressTestBuilder =
StressTestRunner.builder().setCollectionIntervalMs(100);
for (int i = 0; i < 4; i++) {
stressTestBuilder.addOperation(
StressTestRunner.Operation.create(
1_000,
1,
() -> {
doubleGauge.set(10, Attributes.builder().put("K", "V").build());
doubleGauge.set(11, Attributes.builder().put("K", "V").build());
}));
}
stressTestBuilder.build().run();
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry("K", "V")))));
}
@Test
void stressTest_WithDifferentLabelSet() {
String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"};
String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"};
DoubleGauge doubleGauge =
((ExtendedDoubleGaugeBuilder) sdkMeter.gaugeBuilder("testGauge")).build();
StressTestRunner.Builder stressTestBuilder =
StressTestRunner.builder().setCollectionIntervalMs(100);
IntStream.range(0, 4)
.forEach(
i ->
stressTestBuilder.addOperation(
StressTestRunner.Operation.create(
2_000,
1,
() -> {
doubleGauge.set(10, Attributes.builder().put(keys[i], values[i]).build());
doubleGauge.set(11, Attributes.builder().put(keys[i], values[i]).build());
})));
stressTestBuilder.build().run();
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasDoubleGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[0], values[0])),
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[1], values[1])),
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[2], values[2])),
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[3], values[3])))));
}
}

View File

@ -1,111 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableLongGauge;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import io.opentelemetry.sdk.testing.time.TestClock;
import java.time.Duration;
import org.junit.jupiter.api.Test;
/** Unit tests for SDK {@link ObservableLongGauge}. */
class SdkLongGaugeBuilderTest {
private static final Resource RESOURCE =
Resource.create(Attributes.of(stringKey("resource_key"), "resource_value"));
private static final InstrumentationScopeInfo INSTRUMENTATION_SCOPE_INFO =
InstrumentationScopeInfo.create(SdkLongGaugeBuilderTest.class.getName());
private final TestClock testClock = TestClock.create();
private final InMemoryMetricReader sdkMeterReader = InMemoryMetricReader.create();
private final SdkMeterProvider sdkMeterProvider =
SdkMeterProvider.builder()
.setClock(testClock)
.setResource(RESOURCE)
.registerMetricReader(sdkMeterReader)
.build();
private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName());
@Test
void removeCallback() {
ObservableLongGauge gauge =
sdkMeter
.gaugeBuilder("testGauge")
.ofLongs()
.buildWithCallback(measurement -> measurement.record(10));
assertThat(sdkMeterReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasName("testGauge")
.hasLongGaugeSatisfying(
longGauge -> longGauge.hasPointsSatisfying(point -> {})));
gauge.close();
assertThat(sdkMeterReader.collectAllMetrics()).hasSize(0);
}
@Test
void collectMetrics_NoRecords() {
sdkMeter
.gaugeBuilder("testObserver")
.ofLongs()
.setDescription("My own LongValueObserver")
.setUnit("ms")
.buildWithCallback(result -> {});
assertThat(sdkMeterReader.collectAllMetrics()).isEmpty();
}
@Test
void collectMetrics_WithOneRecord() {
sdkMeter
.gaugeBuilder("testObserver")
.ofLongs()
.buildWithCallback(result -> result.record(12, Attributes.builder().put("k", "v").build()));
testClock.advance(Duration.ofSeconds(1));
assertThat(sdkMeterReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testObserver")
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now() - 1000000000L)
.hasEpochNanos(testClock.now())
.hasAttributes(attributeEntry("k", "v"))
.hasValue(12))));
testClock.advance(Duration.ofSeconds(1));
assertThat(sdkMeterReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testObserver")
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now() - 2000000000L)
.hasEpochNanos(testClock.now())
.hasAttributes(attributeEntry("k", "v"))
.hasValue(12))));
}
}

View File

@ -0,0 +1,312 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableLongGauge;
import io.opentelemetry.extension.incubator.metrics.ExtendedLongGaugeBuilder;
import io.opentelemetry.extension.incubator.metrics.LongGauge;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import io.opentelemetry.sdk.testing.time.TestClock;
import java.time.Duration;
import java.util.stream.IntStream;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link SdkLongGauge}. */
class SdkLongGaugeTest {
private static final long SECOND_NANOS = 1_000_000_000;
private static final Resource RESOURCE =
Resource.create(Attributes.of(stringKey("resource_key"), "resource_value"));
private static final InstrumentationScopeInfo INSTRUMENTATION_SCOPE_INFO =
InstrumentationScopeInfo.create(SdkLongGaugeTest.class.getName());
private final TestClock testClock = TestClock.create();
private final InMemoryMetricReader cumulativeReader = InMemoryMetricReader.create();
private final InMemoryMetricReader deltaReader = InMemoryMetricReader.createDelta();
private final SdkMeterProvider sdkMeterProvider =
SdkMeterProvider.builder()
.setClock(testClock)
.registerMetricReader(cumulativeReader)
.registerMetricReader(deltaReader)
.setResource(RESOURCE)
.build();
private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName());
@Test
void set_PreventNullAttributes() {
assertThatThrownBy(
() ->
((ExtendedLongGaugeBuilder) sdkMeter.gaugeBuilder("testGauge").ofLongs())
.build()
.set(1, null))
.isInstanceOf(NullPointerException.class)
.hasMessage("attributes");
}
@Test
void observable_RemoveCallback() {
ObservableLongGauge gauge =
sdkMeter
.gaugeBuilder("testGauge")
.ofLongs()
.buildWithCallback(measurement -> measurement.record(10));
Assertions.assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasName("testGauge")
.hasLongGaugeSatisfying(
longGauge -> longGauge.hasPointsSatisfying(point -> {})));
gauge.close();
Assertions.assertThat(cumulativeReader.collectAllMetrics()).hasSize(0);
}
@Test
void collectMetrics_NoRecords() {
((ExtendedLongGaugeBuilder) sdkMeter.gaugeBuilder("testGauge").ofLongs()).build();
assertThat(cumulativeReader.collectAllMetrics()).isEmpty();
}
@Test
void collectMetrics_WithEmptyAttributes() {
LongGauge longGauge =
((ExtendedLongGaugeBuilder)
sdkMeter
.gaugeBuilder("testGauge")
.ofLongs()
.setDescription("description")
.setUnit("K"))
.build();
testClock.advance(Duration.ofNanos(SECOND_NANOS));
longGauge.set(12, Attributes.empty());
longGauge.set(13);
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDescription("description")
.hasUnit("K")
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now() - SECOND_NANOS)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(13))));
}
@Test
void collectMetrics_WithMultipleCollects() {
long startTime = testClock.now();
LongGauge longGauge =
((ExtendedLongGaugeBuilder) sdkMeter.gaugeBuilder("testGauge").ofLongs()).build();
longGauge.set(12, Attributes.empty());
longGauge.set(12, Attributes.builder().put("K", "V").build());
longGauge.set(21, Attributes.empty());
// Advancing time here should not matter.
testClock.advance(Duration.ofNanos(SECOND_NANOS));
longGauge.set(321, Attributes.builder().put("K", "V").build());
longGauge.set(111, Attributes.builder().put("K", "V").build());
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDescription("")
.hasUnit("")
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(21),
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasValue(111)
.hasAttributes(attributeEntry("K", "V")))));
assertThat(deltaReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasDescription("")
.hasUnit("")
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(21),
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasValue(111)
.hasAttributes(attributeEntry("K", "V")))));
// Repeat to prove we keep previous values.
testClock.advance(Duration.ofNanos(SECOND_NANOS));
longGauge.set(222, Attributes.builder().put("K", "V").build());
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasAttributes(Attributes.empty())
.hasValue(21),
point ->
point
.hasStartEpochNanos(startTime)
.hasEpochNanos(testClock.now())
.hasValue(222)
.hasAttributes(attributeEntry("K", "V")))));
// Delta reader should only have point for {K=V} series, since the {} did not have any
// measurements
assertThat(deltaReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(startTime + SECOND_NANOS)
.hasEpochNanos(testClock.now())
.hasValue(222)
.hasAttributes(attributeEntry("K", "V")))));
}
@Test
void stressTest() {
LongGauge longGauge =
((ExtendedLongGaugeBuilder) sdkMeter.gaugeBuilder("testGauge").ofLongs()).build();
StressTestRunner.Builder stressTestBuilder =
StressTestRunner.builder().setCollectionIntervalMs(100);
for (int i = 0; i < 4; i++) {
stressTestBuilder.addOperation(
StressTestRunner.Operation.create(
1_000,
1,
() -> {
longGauge.set(10, Attributes.builder().put("K", "V").build());
longGauge.set(11, Attributes.builder().put("K", "V").build());
}));
}
stressTestBuilder.build().run();
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasName("testGauge")
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry("K", "V")))));
}
@Test
void stressTest_WithDifferentLabelSet() {
String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"};
String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"};
LongGauge longGauge =
((ExtendedLongGaugeBuilder) sdkMeter.gaugeBuilder("testGauge").ofLongs()).build();
StressTestRunner.Builder stressTestBuilder =
StressTestRunner.builder().setCollectionIntervalMs(100);
IntStream.range(0, 4)
.forEach(
i ->
stressTestBuilder.addOperation(
StressTestRunner.Operation.create(
2_000,
1,
() -> {
longGauge.set(10, Attributes.builder().put(keys[i], values[i]).build());
longGauge.set(11, Attributes.builder().put(keys[i], values[i]).build());
})));
stressTestBuilder.build().run();
assertThat(cumulativeReader.collectAllMetrics())
.satisfiesExactly(
metric ->
assertThat(metric)
.hasResource(RESOURCE)
.hasInstrumentationScope(INSTRUMENTATION_SCOPE_INFO)
.hasLongGaugeSatisfying(
gauge ->
gauge.hasPointsSatisfying(
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[0], values[0])),
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[1], values[1])),
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[2], values[2])),
point ->
point
.hasStartEpochNanos(testClock.now())
.hasEpochNanos(testClock.now())
.hasValue(11)
.hasAttributes(attributeEntry(keys[3], values[3])))));
}
}