diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleCounter.java
new file mode 100644
index 00000000000..624285e9758
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleCounter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleCounter;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link DoubleCounter} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedDoubleCounter#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #add(double)} calls
+ * record directly without the per-recording attribute processing and map lookup that {@link
+ * DoubleCounter#add(double, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundDoubleCounter {
+
+ /**
+ * Records a value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The increment amount. MUST be non-negative.
+ */
+ void add(double value);
+
+ /**
+ * Records a value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The increment amount. MUST be non-negative.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void add(double value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleGauge.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleGauge.java
new file mode 100644
index 00000000000..b2c1d20c142
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleGauge.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleGauge;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link DoubleGauge} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedDoubleGauge#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #set(double)} calls
+ * record directly without the per-recording attribute processing and map lookup that {@link
+ * DoubleGauge#set(double, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundDoubleGauge {
+
+ /**
+ * Records the gauge value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The current gauge value.
+ */
+ void set(double value);
+
+ /**
+ * Records the gauge value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The current gauge value.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void set(double value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleHistogram.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleHistogram.java
new file mode 100644
index 00000000000..ae3d0d24b1d
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleHistogram.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link DoubleHistogram} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedDoubleHistogram#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #record(double)} calls
+ * record directly without the per-recording attribute processing and map lookup that {@link
+ * DoubleHistogram#record(double, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundDoubleHistogram {
+
+ /**
+ * Records a value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The amount of the measurement. MUST be non-negative.
+ */
+ void record(double value);
+
+ /**
+ * Records a value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The amount of the measurement. MUST be non-negative.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void record(double value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleUpDownCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleUpDownCounter.java
new file mode 100644
index 00000000000..44ac044a300
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundDoubleUpDownCounter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleUpDownCounter;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link DoubleUpDownCounter} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedDoubleUpDownCounter#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #add(double)} calls
+ * record directly without the per-recording attribute processing and map lookup that {@link
+ * DoubleUpDownCounter#add(double, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundDoubleUpDownCounter {
+
+ /**
+ * Records a value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The increment amount. May be positive, negative or zero.
+ */
+ void add(double value);
+
+ /**
+ * Records a value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The increment amount. May be positive, negative or zero.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void add(double value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongCounter.java
new file mode 100644
index 00000000000..294b7d3de61
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongCounter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link LongCounter} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedLongCounter#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #add(long)} calls record
+ * directly without the per-recording attribute processing and map lookup that {@link
+ * LongCounter#add(long, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundLongCounter {
+
+ /**
+ * Records a value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The increment amount. MUST be non-negative.
+ */
+ void add(long value);
+
+ /**
+ * Records a value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The increment amount. MUST be non-negative.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void add(long value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongGauge.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongGauge.java
new file mode 100644
index 00000000000..6e8fab07339
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongGauge.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongGauge;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link LongGauge} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedLongGauge#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #set(long)} calls record
+ * directly without the per-recording attribute processing and map lookup that {@link
+ * LongGauge#set(long, Attributes)} performs. This is useful when the set of attribute combinations
+ * is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundLongGauge {
+
+ /**
+ * Records the gauge value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The current gauge value.
+ */
+ void set(long value);
+
+ /**
+ * Records the gauge value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The current gauge value.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void set(long value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongHistogram.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongHistogram.java
new file mode 100644
index 00000000000..63a77a0bde7
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongHistogram.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongHistogram;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link LongHistogram} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedLongHistogram#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #record(long)} calls
+ * record directly without the per-recording attribute processing and map lookup that {@link
+ * LongHistogram#record(long, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundLongHistogram {
+
+ /**
+ * Records a value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The amount of the measurement. MUST be non-negative.
+ */
+ void record(long value);
+
+ /**
+ * Records a value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The amount of the measurement. MUST be non-negative.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void record(long value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongUpDownCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongUpDownCounter.java
new file mode 100644
index 00000000000..85c2e5fbbb6
--- /dev/null
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/BoundLongUpDownCounter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongUpDownCounter;
+import io.opentelemetry.context.Context;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link LongUpDownCounter} bound to a fixed set of {@link Attributes}, obtained via {@link
+ * ExtendedLongUpDownCounter#bind(Attributes)}.
+ *
+ *
Binding resolves the underlying timeseries once, so subsequent {@link #add(long)} calls record
+ * directly without the per-recording attribute processing and map lookup that {@link
+ * LongUpDownCounter#add(long, Attributes)} performs. This is useful when the set of attribute
+ * combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+@ThreadSafe
+public interface BoundLongUpDownCounter {
+
+ /**
+ * Records a value against the bound attributes.
+ *
+ *
Note: This may use {@code Context.current()} to pull the context associated with this
+ * measurement.
+ *
+ * @param value The increment amount. May be positive, negative or zero.
+ */
+ void add(long value);
+
+ /**
+ * Records a value against the bound attributes, with an explicit {@link Context}.
+ *
+ * @param value The increment amount. May be positive, negative or zero.
+ * @param context The explicit context to associate with this measurement.
+ */
+ void add(long value, Context context);
+}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeter.java
index 8283c7bb0b6..e8ef921b202 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeter.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDefaultMeter.java
@@ -107,8 +107,85 @@ public void add(long value, Attributes attributes) {}
@Override
public void add(long value) {}
+
+ @Override
+ public BoundLongCounter bind(Attributes attributes) {
+ return NOOP_BOUND_LONG_COUNTER;
+ }
}
+ private static final BoundLongCounter NOOP_BOUND_LONG_COUNTER =
+ new BoundLongCounter() {
+ @Override
+ public void add(long value) {}
+
+ @Override
+ public void add(long value, Context context) {}
+ };
+
+ private static final BoundDoubleCounter NOOP_BOUND_DOUBLE_COUNTER =
+ new BoundDoubleCounter() {
+ @Override
+ public void add(double value) {}
+
+ @Override
+ public void add(double value, Context context) {}
+ };
+
+ private static final BoundLongUpDownCounter NOOP_BOUND_LONG_UP_DOWN_COUNTER =
+ new BoundLongUpDownCounter() {
+ @Override
+ public void add(long value) {}
+
+ @Override
+ public void add(long value, Context context) {}
+ };
+
+ private static final BoundDoubleUpDownCounter NOOP_BOUND_DOUBLE_UP_DOWN_COUNTER =
+ new BoundDoubleUpDownCounter() {
+ @Override
+ public void add(double value) {}
+
+ @Override
+ public void add(double value, Context context) {}
+ };
+
+ private static final BoundLongHistogram NOOP_BOUND_LONG_HISTOGRAM =
+ new BoundLongHistogram() {
+ @Override
+ public void record(long value) {}
+
+ @Override
+ public void record(long value, Context context) {}
+ };
+
+ private static final BoundDoubleHistogram NOOP_BOUND_DOUBLE_HISTOGRAM =
+ new BoundDoubleHistogram() {
+ @Override
+ public void record(double value) {}
+
+ @Override
+ public void record(double value, Context context) {}
+ };
+
+ private static final BoundLongGauge NOOP_BOUND_LONG_GAUGE =
+ new BoundLongGauge() {
+ @Override
+ public void set(long value) {}
+
+ @Override
+ public void set(long value, Context context) {}
+ };
+
+ private static final BoundDoubleGauge NOOP_BOUND_DOUBLE_GAUGE =
+ new BoundDoubleGauge() {
+ @Override
+ public void set(double value) {}
+
+ @Override
+ public void set(double value, Context context) {}
+ };
+
private static class NoopDoubleCounter implements ExtendedDoubleCounter {
@Override
public boolean isEnabled() {
@@ -123,6 +200,11 @@ public void add(double value, Attributes attributes) {}
@Override
public void add(double value) {}
+
+ @Override
+ public BoundDoubleCounter bind(Attributes attributes) {
+ return NOOP_BOUND_DOUBLE_COUNTER;
+ }
}
private static class NoopLongCounterBuilder implements ExtendedLongCounterBuilder {
@@ -209,6 +291,11 @@ public void add(long value, Attributes attributes) {}
@Override
public void add(long value) {}
+
+ @Override
+ public BoundLongUpDownCounter bind(Attributes attributes) {
+ return NOOP_BOUND_LONG_UP_DOWN_COUNTER;
+ }
}
private static class NoopDoubleUpDownCounter implements ExtendedDoubleUpDownCounter {
@@ -225,6 +312,11 @@ public void add(double value, Attributes attributes) {}
@Override
public void add(double value) {}
+
+ @Override
+ public BoundDoubleUpDownCounter bind(Attributes attributes) {
+ return NOOP_BOUND_DOUBLE_UP_DOWN_COUNTER;
+ }
}
private static class NoopLongUpDownCounterBuilder implements ExtendedLongUpDownCounterBuilder {
@@ -314,6 +406,11 @@ public void record(double value, Attributes attributes) {}
@Override
public void record(double value) {}
+
+ @Override
+ public BoundDoubleHistogram bind(Attributes attributes) {
+ return NOOP_BOUND_DOUBLE_HISTOGRAM;
+ }
}
private static class NoopLongHistogram implements ExtendedLongHistogram {
@@ -330,6 +427,11 @@ public void record(long value, Attributes attributes) {}
@Override
public void record(long value) {}
+
+ @Override
+ public BoundLongHistogram bind(Attributes attributes) {
+ return NOOP_BOUND_LONG_HISTOGRAM;
+ }
}
private static class NoopDoubleHistogramBuilder implements ExtendedDoubleHistogramBuilder {
@@ -428,6 +530,11 @@ public void set(double value, Attributes attributes) {}
@Override
public void set(double value, Attributes attributes, Context context) {}
+
+ @Override
+ public BoundDoubleGauge bind(Attributes attributes) {
+ return NOOP_BOUND_DOUBLE_GAUGE;
+ }
}
private static class NoopLongGaugeBuilder implements ExtendedLongGaugeBuilder {
@@ -474,6 +581,11 @@ public void set(long value, Attributes attributes) {}
@Override
public void set(long value, Attributes attributes, Context context) {}
+
+ @Override
+ public BoundLongGauge bind(Attributes attributes) {
+ return NOOP_BOUND_LONG_GAUGE;
+ }
}
private static class NoopObservableDoubleMeasurement implements ObservableDoubleMeasurement {
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounter.java
index 60043af4df8..7853163c737 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounter.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleCounter.java
@@ -5,10 +5,22 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleCounter;
/** Extended {@link DoubleCounter} with experimental APIs. */
public interface ExtendedDoubleCounter extends DoubleCounter {
+ /**
+ * Binds this counter to the given {@code attributes}, returning a {@link BoundDoubleCounter} that
+ * records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #add(double, Attributes)}. Prefer this when the
+ * set of attribute combinations is known ahead of time and the same series is recorded to
+ * repeatedly.
+ */
+ BoundDoubleCounter bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGauge.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGauge.java
index 7297f6af3be..17fe751a3dc 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGauge.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleGauge.java
@@ -5,10 +5,22 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleGauge;
/** Extended {@link DoubleGauge} with experimental APIs. */
public interface ExtendedDoubleGauge extends DoubleGauge {
+ /**
+ * Binds this gauge to the given {@code attributes}, returning a {@link BoundDoubleGauge} that
+ * records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #set(double, Attributes)}. Prefer this when the
+ * set of attribute combinations is known ahead of time and the same series is recorded to
+ * repeatedly.
+ */
+ BoundDoubleGauge bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogram.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogram.java
index 391d829a73f..b717a835970 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogram.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleHistogram.java
@@ -5,10 +5,22 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleHistogram;
/** Extended {@link DoubleHistogram} with experimental APIs. */
public interface ExtendedDoubleHistogram extends DoubleHistogram {
+ /**
+ * Binds this histogram to the given {@code attributes}, returning a {@link BoundDoubleHistogram}
+ * that records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #record(double, Attributes)}. Prefer this when
+ * the set of attribute combinations is known ahead of time and the same series is recorded to
+ * repeatedly.
+ */
+ BoundDoubleHistogram bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounter.java
index 0337e76483d..4c024d9aca2 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounter.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedDoubleUpDownCounter.java
@@ -5,10 +5,22 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleUpDownCounter;
/** Extended {@link DoubleUpDownCounter} with experimental APIs. */
public interface ExtendedDoubleUpDownCounter extends DoubleUpDownCounter {
+ /**
+ * Binds this counter to the given {@code attributes}, returning a {@link
+ * BoundDoubleUpDownCounter} that records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #add(double, Attributes)}. Prefer this when the
+ * set of attribute combinations is known ahead of time and the same series is recorded to
+ * repeatedly.
+ */
+ BoundDoubleUpDownCounter bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounter.java
index 2db89872610..c4fb903795c 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounter.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongCounter.java
@@ -5,11 +5,22 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleCounter;
import io.opentelemetry.api.metrics.LongCounter;
/** Extended {@link DoubleCounter} with experimental APIs. */
public interface ExtendedLongCounter extends LongCounter {
+ /**
+ * Binds this counter to the given {@code attributes}, returning a {@link BoundLongCounter} that
+ * records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #add(long, Attributes)}. Prefer this when the set
+ * of attribute combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+ BoundLongCounter bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGauge.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGauge.java
index e2f29146e08..f283477bdd7 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGauge.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongGauge.java
@@ -5,10 +5,21 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongGauge;
/** Extended {@link LongGauge} with experimental APIs. */
public interface ExtendedLongGauge extends LongGauge {
+ /**
+ * Binds this gauge to the given {@code attributes}, returning a {@link BoundLongGauge} that
+ * records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #set(long, Attributes)}. Prefer this when the set
+ * of attribute combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+ BoundLongGauge bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogram.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogram.java
index 888f0b68b31..4c932126099 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogram.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongHistogram.java
@@ -5,10 +5,22 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongHistogram;
/** Extended {@link LongHistogram} with experimental APIs. */
public interface ExtendedLongHistogram extends LongHistogram {
+ /**
+ * Binds this histogram to the given {@code attributes}, returning a {@link BoundLongHistogram}
+ * that records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #record(long, Attributes)}. Prefer this when the
+ * set of attribute combinations is known ahead of time and the same series is recorded to
+ * repeatedly.
+ */
+ BoundLongHistogram bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounter.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounter.java
index 24552d6c5b7..666c9f9b07f 100644
--- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounter.java
+++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/metrics/ExtendedLongUpDownCounter.java
@@ -5,10 +5,21 @@
package io.opentelemetry.api.incubator.metrics;
+import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongUpDownCounter;
/** Extended {@link LongUpDownCounter} with experimental APIs. */
public interface ExtendedLongUpDownCounter extends LongUpDownCounter {
+ /**
+ * Binds this counter to the given {@code attributes}, returning a {@link BoundLongUpDownCounter}
+ * that records to the corresponding timeseries directly.
+ *
+ *
Binding resolves the underlying timeseries once, eliminating the per-recording attribute
+ * processing and map lookup performed by {@link #add(long, Attributes)}. Prefer this when the set
+ * of attribute combinations is known ahead of time and the same series is recorded to repeatedly.
+ */
+ BoundLongUpDownCounter bind(Attributes attributes);
+
// keep this class even if it is empty, since experimental methods may be added in the future.
}
diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/BoundInstrumentUsageTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/BoundInstrumentUsageTest.java
new file mode 100644
index 00000000000..6361914ded1
--- /dev/null
+++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/metrics/BoundInstrumentUsageTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.api.incubator.metrics;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import java.util.Random;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Demonstrates usage of bound instruments for a dice-rolling scenario.
+ *
+ *
When the full set of attribute combinations is known ahead of time — as it is here, with 6
+ * fixed die faces — bound instruments eliminate the per-recording overhead of the CHM lookup
+ * (bucket traversal, {@link Attributes} equality comparison) and attribute processing by resolving
+ * the underlying timeseries once at bind time.
+ */
+class BoundInstrumentUsageTest {
+
+ private static final AttributeKey ROLL_VALUE = AttributeKey.longKey("roll.value");
+
+ // One Attributes object per die face, constructed once and reused across all recordings.
+ // With unbound instruments each call would look these up on every add().
+ private static final Attributes ROLL_1 = Attributes.of(ROLL_VALUE, 1L);
+ private static final Attributes ROLL_2 = Attributes.of(ROLL_VALUE, 2L);
+ private static final Attributes ROLL_3 = Attributes.of(ROLL_VALUE, 3L);
+ private static final Attributes ROLL_4 = Attributes.of(ROLL_VALUE, 4L);
+ private static final Attributes ROLL_5 = Attributes.of(ROLL_VALUE, 5L);
+ private static final Attributes ROLL_6 = Attributes.of(ROLL_VALUE, 6L);
+
+ @Test
+ void rollTheDice() {
+ InMemoryMetricReader reader = InMemoryMetricReader.create();
+ try (SdkMeterProvider meterProvider =
+ SdkMeterProvider.builder().registerMetricReader(reader).build()) {
+
+ Meter meter = meterProvider.get("io.opentelemetry.example.dice");
+
+ // The bind() API lives on the incubating ExtendedLongCounter, reached by casting the
+ // instrument returned from the (stable) builder.
+ ExtendedLongCounter rolls =
+ (ExtendedLongCounter)
+ meter
+ .counterBuilder("dice.rolls")
+ .setDescription("The number of times each side of the die was rolled")
+ .setUnit("{roll}")
+ .build();
+
+ // Bind one BoundLongCounter per die face. Each bind() call resolves the underlying timeseries
+ // once, so subsequent add() calls record directly without any attribute lookup.
+ //
+ // Equivalent unbound setup (no bind calls needed, but per-recording overhead is higher):
+ // // no setup — just call rolls.add(1, ROLL_N) inline below
+ BoundLongCounter face1 = rolls.bind(ROLL_1);
+ BoundLongCounter face2 = rolls.bind(ROLL_2);
+ BoundLongCounter face3 = rolls.bind(ROLL_3);
+ BoundLongCounter face4 = rolls.bind(ROLL_4);
+ BoundLongCounter face5 = rolls.bind(ROLL_5);
+ BoundLongCounter face6 = rolls.bind(ROLL_6);
+
+ // Simulate 600 rolls with a fixed seed for a reproducible distribution.
+ Random random = new Random(42);
+ long[] counts = new long[7]; // indexed 1..6; index 0 unused
+
+ for (int i = 0; i < 600; i++) {
+ int result = random.nextInt(6) + 1;
+ counts[result]++;
+ switch (result) {
+ case 1:
+ face1.add(1);
+ // Equivalent unbound: rolls.add(1, ROLL_1);
+ break;
+ case 2:
+ face2.add(1);
+ // Equivalent unbound: rolls.add(1, ROLL_2);
+ break;
+ case 3:
+ face3.add(1);
+ // Equivalent unbound: rolls.add(1, ROLL_3);
+ break;
+ case 4:
+ face4.add(1);
+ // Equivalent unbound: rolls.add(1, ROLL_4);
+ break;
+ case 5:
+ face5.add(1);
+ // Equivalent unbound: rolls.add(1, ROLL_5);
+ break;
+ case 6:
+ face6.add(1);
+ // Equivalent unbound: rolls.add(1, ROLL_6);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // A bound counter and the unbound instrument it came from record to the same timeseries; the
+ // two styles are interchangeable and can be mixed.
+ LongCounter unbound = rolls;
+ unbound.add(0, ROLL_1);
+
+ // One cumulative data point per die face, each with the exact roll count recorded above.
+ assertThat(reader.collectAllMetrics())
+ .satisfiesExactly(
+ metric ->
+ assertThat(metric)
+ .hasName("dice.rolls")
+ .hasDescription("The number of times each side of the die was rolled")
+ .hasUnit("{roll}")
+ .hasLongSumSatisfying(
+ sum ->
+ sum.isMonotonic()
+ .hasPointsSatisfying(
+ point -> point.hasAttributes(ROLL_1).hasValue(counts[1]),
+ point -> point.hasAttributes(ROLL_2).hasValue(counts[2]),
+ point -> point.hasAttributes(ROLL_3).hasValue(counts[3]),
+ point -> point.hasAttributes(ROLL_4).hasValue(counts[4]),
+ point -> point.hasAttributes(ROLL_5).hasValue(counts[5]),
+ point -> point.hasAttributes(ROLL_6).hasValue(counts[6]))));
+ }
+ }
+}
diff --git a/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java
index 13dc0667134..72e95297e34 100644
--- a/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java
+++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/MetricRecordBenchmark.java
@@ -12,6 +12,22 @@
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleCounter;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleGauge;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleHistogram;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleUpDownCounter;
+import io.opentelemetry.api.incubator.metrics.BoundLongCounter;
+import io.opentelemetry.api.incubator.metrics.BoundLongGauge;
+import io.opentelemetry.api.incubator.metrics.BoundLongHistogram;
+import io.opentelemetry.api.incubator.metrics.BoundLongUpDownCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGauge;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogram;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongGauge;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogram;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
@@ -53,6 +69,8 @@
* {@link Aggregation}, including all relevant combinations for synchronous instruments.
* {@link BenchmarkState#aggregationTemporality}
* {@link BenchmarkState#cardinality}
+ * {@link BenchmarkState#bound} whether recording goes through bound instruments ({@code
+ * Extended*#bind(Attributes)}) or unbound instruments.
* thread count
* {@link BenchmarkState#instrumentValueType}, {@link BenchmarkState#memoryMode}, and {@link
* BenchmarkState#exemplars} are disabled to reduce combinatorial explosion.
@@ -63,16 +81,23 @@
*
* The cardinality and thread count dimensions partially overlap. Cardinality dictates how many
* unique attribute sets (i.e. series) are recorded to, and thread count dictates how many threads
- * are simultaneously recording to those series. In all cases, the record path needs to look up an
- * aggregation handle for the series corresponding to the measurement's {@link Attributes} in a
- * {@link java.util.concurrent.ConcurrentHashMap}. That will be the case until otel adds support for
- * bound
- * instruments. The cardinality dictates the size of this map, which has some impact on
- * performance. However, by far the dominant bottleneck is contention. That is, the number of
- * threads simultaneously trying to record to the same series. Increasing the threads increases
- * contention. Increasing cardinality decreases contention, as the threads are now spreading their
- * record activities over more distinct series. The highest contention scenario is cardinality=1,
- * threads=4. Any scenario with threads=1 has zero contention.
+ * are simultaneously recording to those series. For unbound instruments, the record path looks up
+ * an aggregation handle for the series corresponding to the measurement's {@link Attributes} in a
+ * {@link java.util.concurrent.ConcurrentHashMap}; for bound instruments ({@link
+ * BenchmarkState#bound}) that handle is resolved once at bind time, so the record path skips the
+ * lookup and attribute processing entirely. The cardinality dictates the size of this map, which
+ * has some impact on performance. However, by far the dominant bottleneck is contention. That is,
+ * the number of threads simultaneously trying to record to the same series. Increasing the threads
+ * increases contention. Increasing cardinality decreases contention, as the threads are now
+ * spreading their record activities over more distinct series. The highest contention scenario is
+ * cardinality=1, threads=4. Any scenario with threads=1 has zero contention.
+ *
+ *
To make the cardinality dimension meaningful under contention, each thread traverses the
+ * series in its own independent (per-thread shuffled) order — see {@link ThreadState}. This models
+ * independent threads recording to arbitrary series. A naive shared {@code i % cardinality} index
+ * would instead march all threads through the same series in lockstep (contention self-synchronizes
+ * them), collapsing high-cardinality multi-thread runs into a single rotating hotspot that behaves
+ * like cardinality=1.
*
*
To make the cardinality dimension meaningful under contention, each thread traverses the
* series in its own independent (per-thread shuffled) order — see {@link ThreadState}. This models
@@ -107,6 +132,14 @@ public static class BenchmarkState {
@Param({"1", "128"})
int cardinality;
+ // Whether to record through bound instruments (Extended*#bind(Attributes)), which resolve the
+ // timeseries once up front, or unbound instruments, which look up the timeseries by Attributes
+ // on every record. Uncomment to evaluate.
+ @Param({"false", "true"})
+ boolean bound;
+
+ // boolean bound = false;
+
// The following parameters are excluded from the benchmark to reduce combinatorial explosion
// but can optionally be enabled for adhoc evaluation.
@@ -128,7 +161,10 @@ public static class BenchmarkState {
boolean exemplars = false;
OpenTelemetrySdk openTelemetry;
+ // Populated when bound == false.
private Instrument instrument;
+ // Populated when bound == true; parallel to attributesList (one bound instrument per series).
+ private List boundInstruments;
List measurements;
List attributesList;
Span span;
@@ -162,7 +198,6 @@ public void setup() {
.build();
Meter meter = openTelemetry.getMeter("benchmark");
- instrument = getInstrument(meter, instrumentType, instrumentValueType);
Tracer tracer = openTelemetry.getTracer("benchmark");
span = tracer.spanBuilder("benchmark").startSpan();
// We suppress warnings on closing here, as we rely on tests to make sure context is closed.
@@ -180,6 +215,13 @@ public void setup() {
}
Collections.shuffle(attributesList);
+ if (bound) {
+ boundInstruments =
+ bindInstruments(meter, instrumentType, instrumentValueType, attributesList);
+ } else {
+ instrument = getInstrument(meter, instrumentType, instrumentValueType);
+ }
+
measurements = new ArrayList<>(RECORDS_PER_INVOCATION);
for (int i = 0; i < RECORDS_PER_INVOCATION; i++) {
measurements.add((long) random.nextInt(2000));
@@ -252,10 +294,20 @@ public void record_MultipleThreads(BenchmarkState benchmarkState, ThreadState th
private static void record(BenchmarkState benchmarkState, ThreadState threadState) {
// Per-thread series order: at a given i, different threads hit different series (no lockstep).
int[] order = threadState.order;
- for (int i = 0; i < RECORDS_PER_INVOCATION; i++) {
- Attributes attributes = benchmarkState.attributesList.get(order[i % order.length]);
- long value = benchmarkState.measurements.get(i % benchmarkState.measurements.size());
- benchmarkState.instrument.record(value, attributes);
+ if (benchmarkState.bound) {
+ // Bound: record straight to the pre-resolved bound instrument for the series — no per-record
+ // Attributes lookup.
+ List boundInstruments = benchmarkState.boundInstruments;
+ for (int i = 0; i < RECORDS_PER_INVOCATION; i++) {
+ long value = benchmarkState.measurements.get(i % benchmarkState.measurements.size());
+ boundInstruments.get(order[i % order.length]).record(value);
+ }
+ } else {
+ for (int i = 0; i < RECORDS_PER_INVOCATION; i++) {
+ Attributes attributes = benchmarkState.attributesList.get(order[i % order.length]);
+ long value = benchmarkState.measurements.get(i % benchmarkState.measurements.size());
+ benchmarkState.instrument.record(value, attributes);
+ }
}
}
@@ -306,4 +358,95 @@ private static Instrument getInstrument(
}
throw new IllegalArgumentException();
}
+
+ @FunctionalInterface
+ private interface BoundInstrument {
+ void record(long value);
+ }
+
+ /**
+ * Builds the instrument, then binds one {@link BoundInstrument} per series in {@code
+ * attributesList}, returned in the same order so the record loop can index it in lockstep.
+ */
+ private static List bindInstruments(
+ Meter meter,
+ InstrumentType instrumentType,
+ InstrumentValueType instrumentValueType,
+ List attributesList) {
+ boolean isDouble = instrumentValueType == InstrumentValueType.DOUBLE;
+ String name = "instrument";
+ List result = new ArrayList<>(attributesList.size());
+ switch (instrumentType) {
+ case COUNTER:
+ if (isDouble) {
+ ExtendedDoubleCounter instrument =
+ (ExtendedDoubleCounter) meter.counterBuilder(name).ofDoubles().build();
+ for (Attributes attributes : attributesList) {
+ BoundDoubleCounter bound = instrument.bind(attributes);
+ result.add(bound::add);
+ }
+ } else {
+ ExtendedLongCounter instrument = (ExtendedLongCounter) meter.counterBuilder(name).build();
+ for (Attributes attributes : attributesList) {
+ BoundLongCounter bound = instrument.bind(attributes);
+ result.add(bound::add);
+ }
+ }
+ return result;
+ case UP_DOWN_COUNTER:
+ if (isDouble) {
+ ExtendedDoubleUpDownCounter instrument =
+ (ExtendedDoubleUpDownCounter) meter.upDownCounterBuilder(name).ofDoubles().build();
+ for (Attributes attributes : attributesList) {
+ BoundDoubleUpDownCounter bound = instrument.bind(attributes);
+ result.add(bound::add);
+ }
+ } else {
+ ExtendedLongUpDownCounter instrument =
+ (ExtendedLongUpDownCounter) meter.upDownCounterBuilder(name).build();
+ for (Attributes attributes : attributesList) {
+ BoundLongUpDownCounter bound = instrument.bind(attributes);
+ result.add(bound::add);
+ }
+ }
+ return result;
+ case HISTOGRAM:
+ if (isDouble) {
+ ExtendedDoubleHistogram instrument =
+ (ExtendedDoubleHistogram) meter.histogramBuilder(name).build();
+ for (Attributes attributes : attributesList) {
+ BoundDoubleHistogram bound = instrument.bind(attributes);
+ result.add(bound::record);
+ }
+ } else {
+ ExtendedLongHistogram instrument =
+ (ExtendedLongHistogram) meter.histogramBuilder(name).ofLongs().build();
+ for (Attributes attributes : attributesList) {
+ BoundLongHistogram bound = instrument.bind(attributes);
+ result.add(bound::record);
+ }
+ }
+ return result;
+ case GAUGE:
+ if (isDouble) {
+ ExtendedDoubleGauge instrument = (ExtendedDoubleGauge) meter.gaugeBuilder(name).build();
+ for (Attributes attributes : attributesList) {
+ BoundDoubleGauge bound = instrument.bind(attributes);
+ result.add(bound::set);
+ }
+ } else {
+ ExtendedLongGauge instrument =
+ (ExtendedLongGauge) meter.gaugeBuilder(name).ofLongs().build();
+ for (Attributes attributes : attributesList) {
+ BoundLongGauge bound = instrument.bind(attributes);
+ result.add(bound::set);
+ }
+ }
+ return result;
+ case OBSERVABLE_COUNTER:
+ case OBSERVABLE_UP_DOWN_COUNTER:
+ case OBSERVABLE_GAUGE:
+ }
+ throw new IllegalArgumentException();
+ }
}
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java
index 5aefcb96d25..90b4749cc99 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleCounter.java
@@ -6,18 +6,61 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
-final class ExtendedSdkDoubleCounter extends SdkDoubleCounter implements ExtendedDoubleCounter {
+final class ExtendedSdkDoubleCounter extends SdkDoubleCounter
+ implements ExtendedDoubleCounter, BoundDoubleCounter {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the add() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkDoubleCounter(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkDoubleCounter(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundDoubleCounter bind(Attributes attributes) {
+ return new ExtendedSdkDoubleCounter(
+ getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void add(double value) {
+ add(value, Context.current());
+ }
+
+ @Override
+ public void add(double value, Context context) {
+ if (!validateNonNegative(value)) {
+ return;
+ }
+ if (boundHandle != null) {
+ boundHandle.recordDouble(value, context);
+ } else {
+ storage.recordDouble(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkDoubleCounterBuilder extends SdkDoubleCounterBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java
index f65153f4a0b..d82b3756475 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleGauge.java
@@ -6,18 +6,57 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleGauge;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGauge;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder;
import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
-final class ExtendedSdkDoubleGauge extends SdkDoubleGauge implements ExtendedDoubleGauge {
+final class ExtendedSdkDoubleGauge extends SdkDoubleGauge
+ implements ExtendedDoubleGauge, BoundDoubleGauge {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the set() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkDoubleGauge(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkDoubleGauge(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundDoubleGauge bind(Attributes attributes) {
+ return new ExtendedSdkDoubleGauge(getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void set(double value) {
+ set(value, Context.current());
+ }
+
+ @Override
+ public void set(double value, Context context) {
+ if (boundHandle != null) {
+ boundHandle.recordDouble(value, context);
+ } else {
+ storage.recordDouble(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkDoubleGaugeBuilder extends SdkDoubleGaugeBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java
index 198b4160043..2a1286afbce 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleHistogram.java
@@ -6,19 +6,61 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleHistogram;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogram;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder;
import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
final class ExtendedSdkDoubleHistogram extends SdkDoubleHistogram
- implements ExtendedDoubleHistogram {
+ implements ExtendedDoubleHistogram, BoundDoubleHistogram {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the record() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
ExtendedSdkDoubleHistogram(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkDoubleHistogram(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundDoubleHistogram bind(Attributes attributes) {
+ return new ExtendedSdkDoubleHistogram(
+ getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void record(double value) {
+ record(value, Context.current());
+ }
+
+ @Override
+ public void record(double value, Context context) {
+ if (!validateNonNegative(value)) {
+ return;
+ }
+ if (boundHandle != null) {
+ boundHandle.recordDouble(value, context);
+ } else {
+ storage.recordDouble(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkDoubleHistogramBuilder extends SdkDoubleHistogramBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java
index aa991c2fcdd..b304eae6dc5 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkDoubleUpDownCounter.java
@@ -6,19 +6,58 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundDoubleUpDownCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
final class ExtendedSdkDoubleUpDownCounter extends SdkDoubleUpDownCounter
- implements ExtendedDoubleUpDownCounter {
+ implements ExtendedDoubleUpDownCounter, BoundDoubleUpDownCounter {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the add() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkDoubleUpDownCounter(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkDoubleUpDownCounter(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundDoubleUpDownCounter bind(Attributes attributes) {
+ return new ExtendedSdkDoubleUpDownCounter(
+ getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void add(double value) {
+ add(value, Context.current());
+ }
+
+ @Override
+ public void add(double value, Context context) {
+ if (boundHandle != null) {
+ boundHandle.recordDouble(value, context);
+ } else {
+ storage.recordDouble(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkDoubleUpDownCounterBuilder extends SdkDoubleUpDownCounterBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java
index eede83d2432..84d8934a034 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongCounter.java
@@ -6,18 +6,60 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundLongCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder;
import io.opentelemetry.api.incubator.metrics.ExtendedLongCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
-final class ExtendedSdkLongCounter extends SdkLongCounter implements ExtendedLongCounter {
+final class ExtendedSdkLongCounter extends SdkLongCounter
+ implements ExtendedLongCounter, BoundLongCounter {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the add() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkLongCounter(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkLongCounter(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundLongCounter bind(Attributes attributes) {
+ return new ExtendedSdkLongCounter(getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void add(long value) {
+ add(value, Context.current());
+ }
+
+ @Override
+ public void add(long value, Context context) {
+ if (!validateNonNegative(value)) {
+ return;
+ }
+ if (boundHandle != null) {
+ boundHandle.recordLong(value, context);
+ } else {
+ storage.recordLong(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkLongCounterBuilder extends SdkLongCounterBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java
index 970046d814b..4f97fe8e33f 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongGauge.java
@@ -6,18 +6,56 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundLongGauge;
import io.opentelemetry.api.incubator.metrics.ExtendedLongGauge;
import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
-final class ExtendedSdkLongGauge extends SdkLongGauge implements ExtendedLongGauge {
+final class ExtendedSdkLongGauge extends SdkLongGauge implements ExtendedLongGauge, BoundLongGauge {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the set() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkLongGauge(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkLongGauge(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundLongGauge bind(Attributes attributes) {
+ return new ExtendedSdkLongGauge(getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void set(long value) {
+ set(value, Context.current());
+ }
+
+ @Override
+ public void set(long value, Context context) {
+ if (boundHandle != null) {
+ boundHandle.recordLong(value, context);
+ } else {
+ storage.recordLong(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkLongGaugeBuilder extends SdkLongGaugeBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java
index 051f089184b..c620f58356a 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongHistogram.java
@@ -6,18 +6,61 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundLongHistogram;
import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogram;
import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.Advice;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
-final class ExtendedSdkLongHistogram extends SdkLongHistogram implements ExtendedLongHistogram {
+final class ExtendedSdkLongHistogram extends SdkLongHistogram
+ implements ExtendedLongHistogram, BoundLongHistogram {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the record() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkLongHistogram(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkLongHistogram(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundLongHistogram bind(Attributes attributes) {
+ return new ExtendedSdkLongHistogram(
+ getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void record(long value) {
+ record(value, Context.current());
+ }
+
+ @Override
+ public void record(long value, Context context) {
+ if (!validateNonNegative(value)) {
+ return;
+ }
+ if (boundHandle != null) {
+ boundHandle.recordLong(value, context);
+ } else {
+ storage.recordLong(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkLongHistogramBuilder extends SdkLongHistogramBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java
index 15b6f96790e..7abe4cf1416 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ExtendedSdkLongUpDownCounter.java
@@ -6,19 +6,58 @@
package io.opentelemetry.sdk.metrics;
import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.BoundLongUpDownCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder;
import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounter;
import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder;
+import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.WriteableMetricStorage;
import java.util.List;
+import javax.annotation.Nullable;
final class ExtendedSdkLongUpDownCounter extends SdkLongUpDownCounter
- implements ExtendedLongUpDownCounter {
+ implements ExtendedLongUpDownCounter, BoundLongUpDownCounter {
+
+ // Non-null only when this is a bound instance returned from bind(); null for the instrument
+ // itself. When set, the add() methods record straight to this handle instead of resolving the
+ // series from the storage on each call.
+ @Nullable private final BoundStorageHandle boundHandle;
private ExtendedSdkLongUpDownCounter(
InstrumentDescriptor descriptor, SdkMeter sdkMeter, WriteableMetricStorage storage) {
+ this(descriptor, sdkMeter, storage, null);
+ }
+
+ private ExtendedSdkLongUpDownCounter(
+ InstrumentDescriptor descriptor,
+ SdkMeter sdkMeter,
+ WriteableMetricStorage storage,
+ @Nullable BoundStorageHandle boundHandle) {
super(descriptor, sdkMeter, storage);
+ this.boundHandle = boundHandle;
+ }
+
+ @Override
+ public BoundLongUpDownCounter bind(Attributes attributes) {
+ return new ExtendedSdkLongUpDownCounter(
+ getDescriptor(), sdkMeter, storage, storage.bind(attributes));
+ }
+
+ @Override
+ public void add(long value) {
+ add(value, Context.current());
+ }
+
+ @Override
+ public void add(long value, Context context) {
+ if (boundHandle != null) {
+ boundHandle.recordLong(value, context);
+ } else {
+ storage.recordLong(value, Attributes.empty(), context);
+ }
}
static final class ExtendedSdkLongUpDownCounterBuilder extends SdkLongUpDownCounterBuilder
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleCounter.java
index d3ffd20f5c6..72764257d2a 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleCounter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleCounter.java
@@ -40,12 +40,7 @@ public boolean isEnabled() {
@Override
public void add(double increment, Attributes attributes, Context context) {
- if (increment < 0) {
- throttlingLogger.log(
- Level.WARNING,
- "Counters can only increase. Instrument "
- + getDescriptor().getName()
- + " has recorded a negative value.");
+ if (!validateNonNegative(increment)) {
return;
}
storage.recordDouble(increment, attributes, context);
@@ -61,6 +56,22 @@ public void add(double increment) {
add(increment, Attributes.empty());
}
+ /**
+ * Returns true if {@code increment} is non-negative, otherwise logs a warning and returns false.
+ * Shared by the unbound and bound ({@link ExtendedSdkDoubleCounter}) record paths.
+ */
+ boolean validateNonNegative(double increment) {
+ if (increment < 0) {
+ throttlingLogger.log(
+ Level.WARNING,
+ "Counters can only increase. Instrument "
+ + getDescriptor().getName()
+ + " has recorded a negative value.");
+ return false;
+ }
+ return true;
+ }
+
static class SdkDoubleCounterBuilder implements DoubleCounterBuilder {
final InstrumentBuilder builder;
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogram.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogram.java
index 56c1dd20bd9..a4ef7ebc224 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogram.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkDoubleHistogram.java
@@ -40,12 +40,7 @@ public boolean isEnabled() {
@Override
public void record(double value, Attributes attributes, Context context) {
- if (value < 0) {
- throttlingLogger.log(
- Level.WARNING,
- "Histograms can only record non-negative values. Instrument "
- + getDescriptor().getName()
- + " has recorded a negative value.");
+ if (!validateNonNegative(value)) {
return;
}
storage.recordDouble(value, attributes, context);
@@ -61,6 +56,22 @@ public void record(double value) {
record(value, Attributes.empty());
}
+ /**
+ * Returns true if {@code value} is non-negative, otherwise logs a warning and returns false.
+ * Shared by the unbound and bound ({@link ExtendedSdkDoubleHistogram}) record paths.
+ */
+ boolean validateNonNegative(double value) {
+ if (value < 0) {
+ throttlingLogger.log(
+ Level.WARNING,
+ "Histograms can only record non-negative values. Instrument "
+ + getDescriptor().getName()
+ + " has recorded a negative value.");
+ return false;
+ }
+ return true;
+ }
+
static class SdkDoubleHistogramBuilder implements DoubleHistogramBuilder {
final InstrumentBuilder builder;
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongCounter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongCounter.java
index d1834272c14..e03aa42b61c 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongCounter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongCounter.java
@@ -41,12 +41,7 @@ public boolean isEnabled() {
@Override
public void add(long increment, Attributes attributes, Context context) {
- if (increment < 0) {
- throttlingLogger.log(
- Level.WARNING,
- "Counters can only increase. Instrument "
- + getDescriptor().getName()
- + " has recorded a negative value.");
+ if (!validateNonNegative(increment)) {
return;
}
storage.recordLong(increment, attributes, context);
@@ -62,6 +57,22 @@ public void add(long increment) {
add(increment, Attributes.empty());
}
+ /**
+ * Returns true if {@code increment} is non-negative, otherwise logs a warning and returns false.
+ * Shared by the unbound and bound ({@link ExtendedSdkLongCounter}) record paths.
+ */
+ boolean validateNonNegative(long increment) {
+ if (increment < 0) {
+ throttlingLogger.log(
+ Level.WARNING,
+ "Counters can only increase. Instrument "
+ + getDescriptor().getName()
+ + " has recorded a negative value.");
+ return false;
+ }
+ return true;
+ }
+
static class SdkLongCounterBuilder implements LongCounterBuilder {
final InstrumentBuilder builder;
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongHistogram.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongHistogram.java
index 7ddcce06d8e..cb4835b571f 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongHistogram.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkLongHistogram.java
@@ -41,12 +41,7 @@ public boolean isEnabled() {
@Override
public void record(long value, Attributes attributes, Context context) {
- if (value < 0) {
- throttlingLogger.log(
- Level.WARNING,
- "Histograms can only record non-negative values. Instrument "
- + getDescriptor().getName()
- + " has recorded a negative value.");
+ if (!validateNonNegative(value)) {
return;
}
storage.recordLong(value, attributes, context);
@@ -62,6 +57,22 @@ public void record(long value) {
record(value, Attributes.empty());
}
+ /**
+ * Returns true if {@code value} is non-negative, otherwise logs a warning and returns false.
+ * Shared by the unbound and bound ({@link ExtendedSdkLongHistogram}) record paths.
+ */
+ boolean validateNonNegative(long value) {
+ if (value < 0) {
+ throttlingLogger.log(
+ Level.WARNING,
+ "Histograms can only record non-negative values. Instrument "
+ + getDescriptor().getName()
+ + " has recorded a negative value.");
+ return false;
+ }
+ return true;
+ }
+
static class SdkLongHistogramBuilder implements LongHistogramBuilder {
final InstrumentBuilder builder;
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java
index 913be6e7093..d3498408c75 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java
@@ -24,6 +24,7 @@
import io.opentelemetry.sdk.metrics.internal.descriptor.InstrumentDescriptor;
import io.opentelemetry.sdk.metrics.internal.export.RegisteredReader;
import io.opentelemetry.sdk.metrics.internal.state.AsynchronousMetricStorage;
+import io.opentelemetry.sdk.metrics.internal.state.BoundStorageHandle;
import io.opentelemetry.sdk.metrics.internal.state.CallbackRegistration;
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
import io.opentelemetry.sdk.metrics.internal.state.MetricStorage;
@@ -373,6 +374,15 @@ public void recordDouble(double value, Attributes attributes, Context context) {
}
}
+ @Override
+ public BoundStorageHandle bind(Attributes attributes) {
+ List handles = new ArrayList<>(storages.size());
+ for (WriteableMetricStorage storage : storages) {
+ handles.add(storage.bind(attributes));
+ }
+ return new MultiBoundStorageHandle(handles);
+ }
+
@Override
public boolean isEnabled() {
for (WriteableMetricStorage storage : storages) {
@@ -383,4 +393,26 @@ public boolean isEnabled() {
return false;
}
}
+
+ private static class MultiBoundStorageHandle implements BoundStorageHandle {
+ private final List handles;
+
+ private MultiBoundStorageHandle(List handles) {
+ this.handles = handles;
+ }
+
+ @Override
+ public void recordLong(long value, Context context) {
+ for (BoundStorageHandle handle : handles) {
+ handle.recordLong(value, context);
+ }
+ }
+
+ @Override
+ public void recordDouble(double value, Context context) {
+ for (BoundStorageHandle handle : handles) {
+ handle.recordDouble(value, context);
+ }
+ }
+ }
}
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/BoundStorageHandle.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/BoundStorageHandle.java
new file mode 100644
index 00000000000..18925109db7
--- /dev/null
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/BoundStorageHandle.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.metrics.internal.state;
+
+import io.opentelemetry.context.Context;
+
+/**
+ * A record target for a single timeseries, resolved once via {@link
+ * WriteableMetricStorage#bind(io.opentelemetry.api.common.Attributes)}.
+ *
+ * Records bypass the per-recording attribute processing and series lookup that {@link
+ * WriteableMetricStorage#recordLong} / {@link WriteableMetricStorage#recordDouble} perform, since
+ * the series is resolved at bind time. Backs bound instruments.
+ *
+ *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public interface BoundStorageHandle {
+
+ /** Records a long measurement against the bound series. */
+ void recordLong(long value, Context context);
+
+ /** Records a double measurement against the bound series. */
+ void recordDouble(double value, Context context);
+}
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/CumulativeSynchronousMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/CumulativeSynchronousMetricStorage.java
index 026eae1069e..8c1f60afe5e 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/CumulativeSynchronousMetricStorage.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/CumulativeSynchronousMetricStorage.java
@@ -57,6 +57,42 @@ void doRecordDouble(double value, Attributes attributes, Context context) {
getAggregatorHandle(attributes, context).recordDouble(value, attributes, context);
}
+ @Override
+ public BoundStorageHandle bind(Attributes attributes) {
+ // Cumulative handles are stable for the instrument's lifetime (the map is never swapped and
+ // handles are never reset/pooled), so resolve the handle once here and record straight onto it.
+ AggregatorHandle handle = getAggregatorHandle(attributes, Context.current());
+ return new CumulativeBoundHandle(handle, attributes);
+ }
+
+ private final class CumulativeBoundHandle implements BoundStorageHandle {
+ private final AggregatorHandle handle;
+ // Original (unprocessed) attributes, passed to the handle for exemplar sampling, matching the
+ // unbound record path.
+ private final Attributes attributes;
+
+ CumulativeBoundHandle(AggregatorHandle handle, Attributes attributes) {
+ this.handle = handle;
+ this.attributes = attributes;
+ }
+
+ @Override
+ public void recordLong(long value, Context context) {
+ if (!isEnabled()) {
+ return;
+ }
+ handle.recordLong(value, attributes, context);
+ }
+
+ @Override
+ public void recordDouble(double value, Context context) {
+ if (!shouldRecordDouble(value, attributes)) {
+ return;
+ }
+ handle.recordDouble(value, attributes, context);
+ }
+ }
+
private AggregatorHandle getAggregatorHandle(Attributes attributes, Context context) {
Objects.requireNonNull(attributes, "attributes");
attributes = attributesProcessor.process(attributes, context);
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java
index 6fd960d6bd4..d352b05e3fd 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java
@@ -95,9 +95,21 @@ public void recordLong(long value, Attributes attributes, Context context) {
@Override
public void recordDouble(double value, Attributes attributes, Context context) {
- if (!enabled) {
+ if (!shouldRecordDouble(value, attributes)) {
return;
}
+ doRecordDouble(value, attributes, context);
+ }
+
+ /**
+ * Returns true if a double {@code value} should be recorded. Returns false (dropping the
+ * measurement) when recording is disabled, or when {@code value} is NaN, logging in the latter
+ * case. Shared by the unbound and bound record paths.
+ */
+ final boolean shouldRecordDouble(double value, Attributes attributes) {
+ if (!enabled) {
+ return false;
+ }
if (Double.isNaN(value)) {
logger.log(
Level.FINE,
@@ -106,9 +118,9 @@ public void recordDouble(double value, Attributes attributes, Context context) {
+ " has recorded measurement Not-a-Number (NaN) value with attributes "
+ attributes
+ ". Dropping measurement.");
- return;
+ return false;
}
- doRecordDouble(value, attributes, context);
+ return true;
}
abstract void doRecordLong(long value, Attributes attributes, Context context);
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaSynchronousMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaSynchronousMetricStorage.java
index 78403b565d6..17a44cb3d97 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaSynchronousMetricStorage.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DeltaSynchronousMetricStorage.java
@@ -145,7 +145,7 @@ private DeltaAggregatorHandle tryAcquireHandleForRecord(
// correctness.
DeltaAggregatorHandle newDeltaHandle = aggregatorHandlePool.poll();
if (newDeltaHandle == null) {
- newDeltaHandle = new DeltaAggregatorHandle<>(aggregator.createHandle(clock.now()));
+ newDeltaHandle = new DeltaAggregatorHandle<>(this, aggregator.createHandle(clock.now()));
}
handle = aggregatorHandles.putIfAbsent(attributes, newDeltaHandle);
if (handle == null) {
@@ -161,25 +161,116 @@ private DeltaAggregatorHandle tryAcquireHandleForRecord(
}
}
+ @Override
+ public BoundStorageHandle bind(Attributes attributes) {
+ // Resolve (or create) the wrapper for these attributes once and flag it bound. The bound
+ // instrument records straight onto the wrapper from then on, skipping attribute processing and
+ // the map lookup. The bound wrapper is carried across holder swaps and rotated (rather than
+ // pooled / reset-in-place) by collect(), so this reference stays valid for the instrument's
+ // lifetime.
+ Attributes processed = attributesProcessor.process(attributes, Context.current());
+ DeltaAggregatorHandle handle = bindHandle(processed);
+ // Stash the original (unprocessed) attributes for exemplar sampling on the bound record path,
+ // matching the unbound path which passes the call-site attributes to the aggregator handle. The
+ // handle itself is the BoundStorageHandle.
+ handle.boundAttributes = attributes;
+ return handle;
+ }
+
+ @SuppressWarnings("ThreadPriorityCheck")
+ private DeltaAggregatorHandle bindHandle(Attributes attributes) {
+ while (true) {
+ AggregatorHolder holder = this.aggregatorHolder;
+ // Always coordinate through the holder gate, even for an existing series: this serializes the
+ // bound flag write with collect()'s bound-handle scan (which runs while the gate is locked),
+ // guaranteeing the handle is carried into the new holder rather than abandoned/pooled.
+ if (!holder.tryAcquireForNewSeries()) {
+ // Holder is locked for collection. Retry; once the collector installs the new holder this
+ // loop re-reads it and succeeds.
+ Thread.yield();
+ continue;
+ }
+ try {
+ ConcurrentHashMap> aggregatorHandles =
+ holder.aggregatorHandles;
+ DeltaAggregatorHandle handle = aggregatorHandles.get(attributes);
+ if (handle == null && aggregatorHandles.size() >= maxCardinality) {
+ logger.log(
+ Level.WARNING,
+ "Instrument "
+ + metricDescriptor.getSourceInstrument().getName()
+ + " has exceeded the maximum allowed cardinality ("
+ + maxCardinality
+ + ").");
+ attributes = MetricStorage.CARDINALITY_OVERFLOW;
+ handle = aggregatorHandles.get(attributes);
+ }
+ if (handle == null) {
+ DeltaAggregatorHandle newDeltaHandle = aggregatorHandlePool.poll();
+ if (newDeltaHandle == null) {
+ newDeltaHandle =
+ new DeltaAggregatorHandle<>(this, aggregator.createHandle(clock.now()));
+ }
+ DeltaAggregatorHandle existing =
+ aggregatorHandles.putIfAbsent(attributes, newDeltaHandle);
+ handle = existing != null ? existing : newDeltaHandle;
+ }
+ handle.bound = true;
+ return handle;
+ } finally {
+ holder.releaseNewSeries();
+ }
+ }
+ }
+
@Override
public MetricData collect(
Resource resource, InstrumentationScopeInfo instrumentationScopeInfo, long epochNanos) {
- ConcurrentHashMap> aggregatorHandles;
AggregatorHolder holder = this.aggregatorHolder;
- this.aggregatorHolder =
- (memoryMode == REUSABLE_DATA)
- ? new AggregatorHolder<>(previousCollectionAggregatorHandles)
- : new AggregatorHolder<>();
- // Lock out new series creation in the old holder and wait for any in-flight new-series
- // operations to complete. This guarantees the per-handle lock pass below sees every handle
- // that will ever be inserted into holder.aggregatorHandles.
+ // Lock out new series creation (and bind()) in the old holder and wait for any in-flight
+ // operations to complete. Done before scanning for bound handles and before installing the new
+ // holder, so the bound-handle scan below sees a stable set and no bind() can land in the holder
+ // we are about to stop recording into.
holder.lockForCollectAndAwait();
- // Lock each handle and wait for any in-flight recorders against it to finish.
- holder.aggregatorHandles.values().forEach(DeltaAggregatorHandle::lockForCollect);
- holder.aggregatorHandles.values().forEach(DeltaAggregatorHandle::awaitRecordersAndUnlock);
- aggregatorHandles = holder.aggregatorHandles;
+ // Seed the new holder with the bound handles, so that (a) the bound wrappers survive the swap
+ // (their bound instruments hold direct references), (b) a series recorded both bound and
+ // unbound continues to share one wrapper, and (c) bound series are collected every interval.
+ // In REUSABLE_DATA we ping-pong between two maps, so the bound wrappers are copied into the
+ // next
+ // map; in IMMUTABLE_DATA the old map is abandoned and the new map starts with only the bound
+ // wrappers.
+ ConcurrentHashMap> newHolderHandles =
+ (memoryMode == REUSABLE_DATA)
+ ? previousCollectionAggregatorHandles
+ : new ConcurrentHashMap<>();
+ holder.aggregatorHandles.forEach(
+ (attributes, handle) -> {
+ if (handle.bound) {
+ newHolderHandles.put(attributes, handle);
+ }
+ });
+ this.aggregatorHolder = new AggregatorHolder<>(newHolderHandles);
+
+ ConcurrentHashMap> aggregatorHandles =
+ holder.aggregatorHandles;
+
+ // Lock and drain unbound handles (unchanged from the unbound-only path). Bound handles are
+ // handled separately below with a tight per-handle lock window so that recording to a bound
+ // series never blocks for the duration of the whole collection.
+ aggregatorHandles.forEach(
+ (attributes, handle) -> {
+ if (!handle.bound) {
+ handle.lockForCollect();
+ }
+ });
+ aggregatorHandles.forEach(
+ (attributes, handle) -> {
+ if (!handle.bound) {
+ handle.awaitRecordersAndUnlock();
+ }
+ });
List points;
if (memoryMode == REUSABLE_DATA) {
@@ -202,11 +293,12 @@ public MetricData collect(
// aggregator handles, so on next recording cycle using this map, there will be room for newly
// recorded Attributes. This comes at the expanse of memory allocations. This can be avoided
// if the user chooses to increase the maxCardinality.
+ // Bound handles are kept unconditionally since their bound instruments hold direct references.
if (memoryMode == REUSABLE_DATA) {
if (aggregatorHandles.size() >= maxCardinality) {
aggregatorHandles.forEach(
(attribute, handle) -> {
- if (!handle.handle.hasRecordedValues()) {
+ if (!handle.bound && !handle.handle.hasRecordedValues()) {
aggregatorHandles.remove(attribute);
}
});
@@ -221,6 +313,10 @@ public MetricData collect(
// Grab aggregated points.
aggregatorHandles.forEach(
(attributes, handle) -> {
+ if (handle.bound) {
+ collectBound(handle, attributes, startEpochNanos, epochNanos, points);
+ return;
+ }
if (!handle.handle.hasRecordedValues()) {
return;
}
@@ -252,6 +348,44 @@ public MetricData collect(
resource, instrumentationScopeInfo, metricDescriptor, points, DELTA);
}
+ /**
+ * Collects a bound handle by rotation: drain in-flight recorders, swap in a fresh (already-zero)
+ * accumulator while locked, unlock so recorders resume on the fresh accumulator, then aggregate
+ * the drained accumulator off the lock (no recorders reference it anymore). The two accumulators
+ * are ping-ponged via {@link DeltaAggregatorHandle#spare} so steady-state collection allocates
+ * nothing.
+ */
+ private void collectBound(
+ DeltaAggregatorHandle handle,
+ Attributes attributes,
+ long startEpochNanos,
+ long epochNanos,
+ List points) {
+ handle.lockForCollect();
+ handle.awaitRecorders();
+ if (!handle.handle.hasRecordedValues()) {
+ // Nothing recorded this interval; no rotation needed.
+ handle.unlockAfterCollect();
+ return;
+ }
+ AggregatorHandle collected = handle.handle;
+ AggregatorHandle next = handle.spare;
+ if (next == null) {
+ next = aggregator.createHandle(clock.now());
+ }
+ handle.handle = next; // volatile: recorders resuming after unlock see the zero accumulator
+ handle.spare = collected;
+ handle.unlockAfterCollect();
+ // Aggregate (and reset) the drained accumulator off the lock; it becomes the spare for the next
+ // rotation.
+ T point =
+ collected.aggregateThenMaybeReset(
+ startEpochNanos, epochNanos, attributes, /* reset= */ true);
+ if (point != null) {
+ points.add(point);
+ }
+ }
+
private static class AggregatorHolder {
private final ConcurrentHashMap> aggregatorHandles;
// Guards new-series creation using an even/odd protocol:
@@ -301,8 +435,30 @@ void lockForCollectAndAwait() {
}
}
- private static final class DeltaAggregatorHandle {
- final AggregatorHandle handle;
+ private static final class DeltaAggregatorHandle
+ implements BoundStorageHandle {
+ // The storage this handle belongs to. Used by the bound record path (this is the
+ // BoundStorageHandle) to reach the storage-level enabled / NaN checks. Kept as an explicit
+ // back-reference rather than making this a non-static inner class, which would force
+ // AggregatorHolder to be non-static too.
+ private final DeltaSynchronousMetricStorage storage;
+ // Rotated by the collector for bound series (a fresh, already-zero handle swapped in while
+ // recorders are drained); write-once for unbound series. Not volatile: the per-handle `state`
+ // gate carries visibility — every read is preceded by a `state` acquire and every rotation
+ // write
+ // followed by a `state` release. INVARIANT: never read `handle` outside the gate.
+ AggregatorHandle handle;
+ // The off-duty accumulator used for bound rotation, ping-ponged with {@link #handle} each
+ // collect. Touched only by the (serialized) collector; volatile to publish across collect
+ // cycles. Always null for unbound handles.
+ @Nullable private volatile AggregatorHandle spare;
+ // Original (unprocessed) attributes captured at bind(), passed to the aggregator handle for
+ // exemplar sampling on the bound record path. Null until this handle is bound.
+ @Nullable private volatile Attributes boundAttributes;
+ // True once this series has been bound. Bound wrappers are carried across holder swaps (never
+ // pooled) and rotated rather than reset in place. Written under the holder gate by bind(), read
+ // by the collector.
+ private volatile boolean bound;
// Guards per-handle recording using the same even/odd protocol as
// AggregatorHolder.newSeriesGate,
// but scoped to a single series:
@@ -313,10 +469,50 @@ private static final class DeltaAggregatorHandle {
// thread decrements by 1 to restore it to even for the next cycle.
private final AtomicInteger state = new AtomicInteger(0);
- DeltaAggregatorHandle(AggregatorHandle handle) {
+ DeltaAggregatorHandle(DeltaSynchronousMetricStorage storage, AggregatorHandle handle) {
+ this.storage = storage;
this.handle = handle;
}
+ /**
+ * The bound record path ({@link BoundStorageHandle}). Records onto the current accumulator via
+ * the per-handle gate — no map lookup and no holder coordination, since this wrapper already
+ * exists and is carried across holders. Spins if the collector currently holds the lock.
+ */
+ @Override
+ @SuppressWarnings("ThreadPriorityCheck")
+ public void recordLong(long value, Context context) {
+ if (!storage.isEnabled()) {
+ return;
+ }
+ Attributes attributes = Objects.requireNonNull(boundAttributes);
+ while (!tryAcquireForRecord()) {
+ Thread.yield();
+ }
+ try {
+ handle.recordLong(value, attributes, context);
+ } finally {
+ releaseRecord();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("ThreadPriorityCheck")
+ public void recordDouble(double value, Context context) {
+ Attributes attributes = Objects.requireNonNull(boundAttributes);
+ if (!storage.shouldRecordDouble(value, attributes)) {
+ return;
+ }
+ while (!tryAcquireForRecord()) {
+ Thread.yield();
+ }
+ try {
+ handle.recordDouble(value, attributes, context);
+ } finally {
+ releaseRecord();
+ }
+ }
+
/**
* Tries to acquire a recording slot. Returns false if the collector has locked this handle (odd
* state); the caller should retry with a fresh holder.
@@ -359,5 +555,22 @@ void awaitRecordersAndUnlock() {
}
state.addAndGet(-1);
}
+
+ /**
+ * Waits for all in-flight recorders to finish but leaves the handle locked. Used by the bound
+ * rotation path, which swaps the accumulator before unlocking via {@link
+ * #unlockAfterCollect()}.
+ */
+ @SuppressWarnings("ThreadPriorityCheck")
+ void awaitRecorders() {
+ while (state.get() > 1) {
+ Thread.yield();
+ }
+ }
+
+ /** Clears the collection lock set by {@link #lockForCollect()}. */
+ void unlockAfterCollect() {
+ state.addAndGet(-1);
+ }
}
}
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java
index c8580e79f61..857b911119e 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/EmptyMetricStorage.java
@@ -37,6 +37,20 @@ public void recordLong(long value, Attributes attributes, Context context) {}
@Override
public void recordDouble(double value, Attributes attributes, Context context) {}
+ @Override
+ public BoundStorageHandle bind(Attributes attributes) {
+ return NOOP_BOUND_HANDLE;
+ }
+
+ private static final BoundStorageHandle NOOP_BOUND_HANDLE =
+ new BoundStorageHandle() {
+ @Override
+ public void recordLong(long value, Context context) {}
+
+ @Override
+ public void recordDouble(double value, Context context) {}
+ };
+
@Override
public boolean isEnabled() {
return false;
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/WriteableMetricStorage.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/WriteableMetricStorage.java
index 7191a63f1e0..6e8b42302f5 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/WriteableMetricStorage.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/WriteableMetricStorage.java
@@ -23,6 +23,13 @@ public interface WriteableMetricStorage {
/** Records a measurement. */
void recordDouble(double value, Attributes attributes, Context context);
+ /**
+ * Binds the given {@code attributes}, returning a {@link BoundStorageHandle} that records to the
+ * corresponding timeseries directly. The series is resolved once here, so subsequent records via
+ * the returned handle skip per-recording attribute processing and series lookup.
+ */
+ BoundStorageHandle bind(Attributes attributes);
+
/**
* Returns {@code true} if the storage is actively recording measurements, and {@code false}
* otherwise (i.e. noop / empty metric storage is installed).
diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java
index f287b76b05e..4ca2d8ee8a1 100644
--- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java
+++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/state/MetricStorageRegistryTest.java
@@ -112,6 +112,17 @@ public void recordLong(long value, Attributes attributes, Context context) {}
@Override
public void recordDouble(double value, Attributes attributes, Context context) {}
+ @Override
+ public BoundStorageHandle bind(Attributes attributes) {
+ return new BoundStorageHandle() {
+ @Override
+ public void recordLong(long value, Context context) {}
+
+ @Override
+ public void recordDouble(double value, Context context) {}
+ };
+ }
+
@Override
public boolean isEnabled() {
return true;
diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentStressTest.java b/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentStressTest.java
similarity index 82%
rename from sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentStressTest.java
rename to sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentStressTest.java
index 62d4cfa1e75..29be5c455b9 100644
--- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentStressTest.java
+++ b/sdk/metrics/src/testIncubating/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentStressTest.java
@@ -18,6 +18,14 @@
import com.google.common.util.concurrent.Uninterruptibles;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGauge;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogram;
+import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongCounter;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongGauge;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogram;
+import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.internal.testing.CleanupExtension;
import io.opentelemetry.sdk.common.export.MemoryMode;
@@ -56,10 +64,14 @@
/**
* {@link #stressTest(AggregationTemporality, InstrumentType, Aggregation, MemoryMode,
- * InstrumentValueType)} performs a stress test to confirm simultaneous record and collections do
- * not have concurrency issues like lost writes, partial writes, duplicate writes, etc. All
- * combinations of the following dimensions are tested: aggregation temporality, instrument type
- * (synchronous), memory mode, instrument value type.
+ * InstrumentValueType, boolean)} performs a stress test to confirm simultaneous record and
+ * collections do not have concurrency issues like lost writes, partial writes, duplicate writes,
+ * etc. All combinations of the following dimensions are tested: aggregation temporality, instrument
+ * type (synchronous), memory mode, instrument value type, and whether recording goes through bound
+ * instruments ({@code Extended*#bind(Attributes)}) or unbound instruments.
+ *
+ * Lives in {@code testIncubating} because the bound dimension exercises {@code
+ * opentelemetry-api-incubator}, which the base {@code test} source set is not allowed to depend on.
*/
class SynchronousInstrumentStressTest {
@@ -79,7 +91,7 @@ class SynchronousInstrumentStressTest {
// Can change to a higher value when making changes to internals to improve confidence of
// correctness.
- private static final int STRESS_TEST_REPETITIONS = 1;
+ private static final int STRESS_TEST_REPETITIONS = 10;
@ParameterizedTest
@MethodSource("stressTestArgs")
@@ -88,10 +100,16 @@ void stressTest(
InstrumentType instrumentType,
Aggregation aggregation,
MemoryMode memoryMode,
- InstrumentValueType instrumentValueType) {
+ InstrumentValueType instrumentValueType,
+ boolean bound) {
for (int repetition = 0; repetition < STRESS_TEST_REPETITIONS; repetition++) {
stressTestOnce(
- aggregationTemporality, instrumentType, aggregation, memoryMode, instrumentValueType);
+ aggregationTemporality,
+ instrumentType,
+ aggregation,
+ memoryMode,
+ instrumentValueType,
+ bound);
}
}
@@ -101,7 +119,8 @@ private void stressTestOnce(
InstrumentType instrumentType,
Aggregation aggregation,
MemoryMode memoryMode,
- InstrumentValueType instrumentValueType) {
+ InstrumentValueType instrumentValueType,
+ boolean bound) {
// Initialize metric SDK
DefaultAggregationSelector aggregationSelector =
DefaultAggregationSelector.getDefault().with(instrumentType, aggregation);
@@ -117,7 +136,16 @@ private void stressTestOnce(
Meter meter = meterProvider.get("test");
List attributes = Arrays.asList(ATTR_1, ATTR_2, ATTR_3, ATTR_4);
Collections.shuffle(attributes);
+ // When unbound, record through `instrument`, looking up the series by attributes on each call.
+ // When bound, bind one instrument per series up front and record straight to those (no
+ // per-record attribute lookup) — the record loop below forks accordingly.
Instrument instrument = getInstrument(meter, instrumentType, instrumentValueType);
+ List boundInstruments = new ArrayList<>();
+ if (bound) {
+ for (Attributes attr : attributes) {
+ boundInstruments.add(getBoundInstrument(meter, instrumentType, instrumentValueType, attr));
+ }
+ }
// Define list of measurements to record
// Later, we'll assert that the data collected matches these measurements, with no lost writes,
@@ -139,11 +167,20 @@ private void stressTestOnce(
new Thread(
() -> {
Uninterruptibles.awaitUninterruptibly(startSignal);
- for (Long measurement : measurements) {
- for (Attributes attr : attributes) {
- instrument.record(measurement, attr);
+ if (bound) {
+ for (Long measurement : measurements) {
+ for (BoundInstrument boundInstrument : boundInstruments) {
+ boundInstrument.record(measurement);
+ }
+ Thread.yield();
+ }
+ } else {
+ for (Long measurement : measurements) {
+ for (Attributes attr : attributes) {
+ instrument.record(measurement, attr);
+ }
+ Thread.yield();
}
- Thread.yield();
}
latch.countDown();
}));
@@ -298,20 +335,24 @@ private static Stream stressTestArgs() {
InstrumentTypeAndAggregation.values()) {
for (MemoryMode memoryMode : MemoryMode.values()) {
for (InstrumentValueType instrumentValueType : InstrumentValueType.values()) {
- argumentsList.add(
- Arguments.argumentSet(
- aggregationTemporality
- + " "
- + instrumentTypeAndAggregation.instrumentType
- + " "
- + memoryMode
- + " "
- + instrumentValueType,
- aggregationTemporality,
- instrumentTypeAndAggregation.instrumentType,
- instrumentTypeAndAggregation.aggregation,
- memoryMode,
- instrumentValueType));
+ for (boolean bound : new boolean[] {false, true}) {
+ argumentsList.add(
+ Arguments.argumentSet(
+ aggregationTemporality
+ + " "
+ + instrumentTypeAndAggregation.instrumentType
+ + " "
+ + memoryMode
+ + " "
+ + instrumentValueType
+ + (bound ? " bound" : " unbound"),
+ aggregationTemporality,
+ instrumentTypeAndAggregation.instrumentType,
+ instrumentTypeAndAggregation.aggregation,
+ memoryMode,
+ instrumentValueType,
+ bound));
+ }
}
}
}
@@ -354,10 +395,74 @@ private static Instrument getInstrument(
throw new IllegalArgumentException();
}
+ /**
+ * Builds the instrument for the given type / value type and binds it to {@code attributes},
+ * returning a {@link BoundInstrument} that records straight to that series. Mirrors {@link
+ * #getInstrument} but for the bound API.
+ */
+ private static BoundInstrument getBoundInstrument(
+ Meter meter,
+ InstrumentType instrumentType,
+ InstrumentValueType instrumentValueType,
+ Attributes attributes) {
+ boolean isDouble = instrumentValueType == InstrumentValueType.DOUBLE;
+ switch (instrumentType) {
+ case COUNTER:
+ return isDouble
+ ? ((ExtendedDoubleCounter) meter.counterBuilder(INSTRUMENT_NAME).ofDoubles().build())
+ .bind(attributes)
+ ::add
+ : ((ExtendedLongCounter) meter.counterBuilder(INSTRUMENT_NAME).build()).bind(attributes)
+ ::add;
+ case UP_DOWN_COUNTER:
+ return isDouble
+ ? ((ExtendedDoubleUpDownCounter)
+ meter.upDownCounterBuilder(INSTRUMENT_NAME).ofDoubles().build())
+ .bind(attributes)
+ ::add
+ : ((ExtendedLongUpDownCounter) meter.upDownCounterBuilder(INSTRUMENT_NAME).build())
+ .bind(attributes)
+ ::add;
+ case HISTOGRAM:
+ return isDouble
+ ? ((ExtendedDoubleHistogram)
+ meter
+ .histogramBuilder(INSTRUMENT_NAME)
+ .setExplicitBucketBoundariesAdvice(BUCKET_BOUNDARIES)
+ .build())
+ .bind(attributes)
+ ::record
+ : ((ExtendedLongHistogram)
+ meter
+ .histogramBuilder(INSTRUMENT_NAME)
+ .setExplicitBucketBoundariesAdvice(BUCKET_BOUNDARIES)
+ .ofLongs()
+ .build())
+ .bind(attributes)
+ ::record;
+ case GAUGE:
+ return isDouble
+ ? ((ExtendedDoubleGauge) meter.gaugeBuilder(INSTRUMENT_NAME).build()).bind(attributes)
+ ::set
+ : ((ExtendedLongGauge) meter.gaugeBuilder(INSTRUMENT_NAME).ofLongs().build())
+ .bind(attributes)
+ ::set;
+ case OBSERVABLE_COUNTER:
+ case OBSERVABLE_UP_DOWN_COUNTER:
+ case OBSERVABLE_GAUGE:
+ }
+ throw new IllegalArgumentException();
+ }
+
private interface Instrument {
void record(long value, Attributes attributes);
}
+ @FunctionalInterface
+ private interface BoundInstrument {
+ void record(long value);
+ }
+
private static MetricData copy(MetricData m) {
switch (m.getType()) {
case LONG_GAUGE: