diff --git a/dd-java-agent/agent-otel/otel-shim/build.gradle b/dd-java-agent/agent-otel/otel-shim/build.gradle index 428f19de65d..a077e01236f 100644 --- a/dd-java-agent/agent-otel/otel-shim/build.gradle +++ b/dd-java-agent/agent-otel/otel-shim/build.gradle @@ -7,5 +7,6 @@ dependencies { // minimum OpenTelemetry API version this shim is compatible with compileOnly group: 'io.opentelemetry', name: 'opentelemetry-api', version: '1.47.0' + implementation project(':products:metrics:metrics-api') implementation project(':internal-api') } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/OtelInstrumentationScope.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/OtelInstrumentationScope.java index 407fd0edbd0..68869e54ff9 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/OtelInstrumentationScope.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/OtelInstrumentationScope.java @@ -17,6 +17,20 @@ public OtelInstrumentationScope( this.schemaUrl = schemaUrl; } + public String getName() { + return scopeName; + } + + @Nullable + public String getVersion() { + return scopeVersion; + } + + @Nullable + public String getSchemaUrl() { + return schemaUrl; + } + @Override public boolean equals(Object o) { if (!(o instanceof OtelInstrumentationScope)) { @@ -36,4 +50,15 @@ public int hashCode() { result = 31 * result + Objects.hashCode(schemaUrl); return result; } + + @Override + public String toString() { + // use same property names as OTel in toString + return "OtelInstrumentationScope{" + + "name='" + + scopeName + + (scopeVersion != null ? "', version='" + scopeVersion : "") + + (schemaUrl != null ? "', schemaUrl='" + schemaUrl : "") + + "'}"; + } } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java index c1584ed24c4..3ad72a0689c 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleCounter.java @@ -5,55 +5,76 @@ import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_INSTRUMENT_NAME; import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_METER; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; +import datadog.trace.relocate.api.RatelimitedLogger; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleCounter; import io.opentelemetry.api.metrics.DoubleCounterBuilder; import io.opentelemetry.api.metrics.ObservableDoubleCounter; import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; import io.opentelemetry.context.Context; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.annotation.ParametersAreNonnullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ParametersAreNonnullByDefault -final class OtelDoubleCounter implements DoubleCounter { +final class OtelDoubleCounter extends OtelInstrument implements DoubleCounter { + private static final Logger LOGGER = LoggerFactory.getLogger(OtelDoubleCounter.class); + private static final RatelimitedLogger RATELIMITED_LOGGER = + new RatelimitedLogger(LOGGER, 5, TimeUnit.MINUTES); + + OtelDoubleCounter(OtelMetricStorage storage) { + super(storage); + } @Override public void add(double value) { - // FIXME: implement recording + add(value, Attributes.empty()); } @Override public void add(double value, Attributes attributes) { - // FIXME: implement recording + if (value < 0) { + RATELIMITED_LOGGER.warn( + "Counters can only increase. Instrument {} has recorded a negative value.", + storage.getInstrumentName()); + } else { + storage.recordDouble(value, attributes); + } } @Override - public void add(double value, Attributes attributes, Context context) { - // FIXME: implement recording + public void add(double value, Attributes attributes, Context unused) { + add(value, attributes); } static final class Builder implements DoubleCounterBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; - Builder(OtelInstrumentBuilder builder) { - this.instrumentBuilder = ofDoubles(builder, COUNTER); + Builder(OtelMeter meter, OtelInstrumentBuilder builder) { + this.meter = meter; + this.builder = ofDoubles(builder, COUNTER); } @Override public DoubleCounterBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public DoubleCounterBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override public DoubleCounter build() { - return new OtelDoubleCounter(); + return new OtelDoubleCounter( + meter.registerStorage(builder.descriptor(), OtelMetricStorage::newDoubleSumStorage)); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleGauge.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleGauge.java index 1929175be72..e680d69d611 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleGauge.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleGauge.java @@ -5,6 +5,7 @@ import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_INSTRUMENT_NAME; import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_METER; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleGauge; import io.opentelemetry.api.metrics.DoubleGaugeBuilder; @@ -16,50 +17,56 @@ import javax.annotation.ParametersAreNonnullByDefault; @ParametersAreNonnullByDefault -final class OtelDoubleGauge implements DoubleGauge { +final class OtelDoubleGauge extends OtelInstrument implements DoubleGauge { + OtelDoubleGauge(OtelMetricStorage storage) { + super(storage); + } @Override public void set(double value) { - // FIXME: implement recording + set(value, Attributes.empty()); } @Override public void set(double value, Attributes attributes) { - // FIXME: implement recording + storage.recordDouble(value, attributes); } @Override - public void set(double value, Attributes attributes, Context context) { - // FIXME: implement recording + public void set(double value, Attributes attributes, Context unused) { + set(value, attributes); } static final class Builder implements DoubleGaugeBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; Builder(OtelMeter meter, String instrumentName) { - this.instrumentBuilder = ofDoubles(meter, instrumentName, GAUGE); + this.meter = meter; + this.builder = ofDoubles(instrumentName, GAUGE); } @Override public DoubleGaugeBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public DoubleGaugeBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override public LongGaugeBuilder ofLongs() { - return new OtelLongGauge.Builder(instrumentBuilder); + return new OtelLongGauge.Builder(meter, builder); } @Override public DoubleGauge build() { - return new OtelDoubleGauge(); + return new OtelDoubleGauge( + meter.registerStorage(builder.descriptor(), OtelMetricStorage::newDoubleValueStorage)); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleHistogram.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleHistogram.java index bbc3612c5ec..f3b54923c75 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleHistogram.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleHistogram.java @@ -2,66 +2,135 @@ import static datadog.opentelemetry.shim.metrics.OtelInstrumentBuilder.ofDoubles; import static datadog.opentelemetry.shim.metrics.OtelInstrumentType.HISTOGRAM; +import static datadog.opentelemetry.shim.metrics.data.OtelMetricStorage.newHistogramStorage; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; +import datadog.trace.relocate.api.RatelimitedLogger; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.LongHistogramBuilder; import io.opentelemetry.context.Context; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import javax.annotation.ParametersAreNonnullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ParametersAreNonnullByDefault -final class OtelDoubleHistogram implements DoubleHistogram { +final class OtelDoubleHistogram extends OtelInstrument implements DoubleHistogram { + private static final Logger LOGGER = LoggerFactory.getLogger(OtelDoubleHistogram.class); + private static final RatelimitedLogger RATELIMITED_LOGGER = + new RatelimitedLogger(LOGGER, 5, TimeUnit.MINUTES); + + OtelDoubleHistogram(OtelMetricStorage storage) { + super(storage); + } @Override public void record(double value) { - // FIXME: implement recording + record(value, Attributes.empty()); } @Override public void record(double value, Attributes attributes) { - // FIXME: implement recording + if (value < 0) { + RATELIMITED_LOGGER.warn( + "Histograms can only record non-negative values. Instrument {} has recorded a negative value.", + storage.getInstrumentName()); + } else { + storage.recordDouble(value, attributes); + } } @Override - public void record(double value, Attributes attributes, Context context) { - // FIXME: implement recording + public void record(double value, Attributes attributes, Context unused) { + record(value, attributes); } static final class Builder implements DoubleHistogramBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private static final List DEFAULT_BOUNDARIES = + asList( + 0d, 5d, 10d, 25d, 50d, 75d, 100d, 250d, 500d, 750d, 1_000d, 2_500d, 5_000d, 7_500d, + 10_000d); + + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; + private List bucketBoundaries; Builder(OtelMeter meter, String instrumentName) { - this.instrumentBuilder = ofDoubles(meter, instrumentName, HISTOGRAM); + this.meter = meter; + this.builder = ofDoubles(instrumentName, HISTOGRAM); + this.bucketBoundaries = DEFAULT_BOUNDARIES; } @Override public DoubleHistogramBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public DoubleHistogramBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override + @SuppressFBWarnings("DCN") // match OTel in catching and logging NPE public DoubleHistogramBuilder setExplicitBucketBoundariesAdvice(List bucketBoundaries) { - // FIXME: implement boundary advice + try { + Objects.requireNonNull(bucketBoundaries, "bucketBoundaries must not be null"); + this.bucketBoundaries = validateBoundaries(new ArrayList<>(bucketBoundaries)); + } catch (IllegalArgumentException | NullPointerException e) { + LOGGER.warn("Error setting explicit bucket boundaries advice: {}", e.getMessage()); + } return this; } @Override public LongHistogramBuilder ofLongs() { - return new OtelLongHistogram.Builder(instrumentBuilder); + return new OtelLongHistogram.Builder(meter, builder, bucketBoundaries); } @Override public DoubleHistogram build() { - return new OtelDoubleHistogram(); + return new OtelDoubleHistogram( + meter.registerStorage( + builder.descriptor(), + descriptor -> newHistogramStorage(descriptor, bucketBoundaries))); + } + + static List validateBoundaries(List boundaries) { + if (boundaries.isEmpty()) { + return emptyList(); + } + if (boundaries.get(0) == Double.NEGATIVE_INFINITY) { + throw new IllegalArgumentException("invalid bucket boundary: -Inf"); + } + if (boundaries.get(boundaries.size() - 1) == Double.POSITIVE_INFINITY) { + throw new IllegalArgumentException("invalid bucket boundary: +Inf"); + } + Double previousBoundary = null; + for (Double boundary : boundaries) { + if (boundary.isNaN()) { + throw new IllegalArgumentException("invalid bucket boundary: NaN"); + } + if (previousBoundary != null && previousBoundary >= boundary) { + throw new IllegalArgumentException( + "Bucket boundaries must be in increasing order: " + + previousBoundary + + " >= " + + boundary); + } + previousBoundary = boundary; + } + return boundaries; } } } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java index 32b68931659..6e0d2159ee0 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelDoubleUpDownCounter.java @@ -5,6 +5,7 @@ import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_INSTRUMENT_NAME; import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_METER; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleUpDownCounter; import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; @@ -15,45 +16,51 @@ import javax.annotation.ParametersAreNonnullByDefault; @ParametersAreNonnullByDefault -final class OtelDoubleUpDownCounter implements DoubleUpDownCounter { +final class OtelDoubleUpDownCounter extends OtelInstrument implements DoubleUpDownCounter { + OtelDoubleUpDownCounter(OtelMetricStorage storage) { + super(storage); + } @Override public void add(double value) { - // FIXME: implement recording + add(value, Attributes.empty()); } @Override public void add(double value, Attributes attributes) { - // FIXME: implement recording + storage.recordDouble(value, attributes); } @Override - public void add(double value, Attributes attributes, Context context) { - // FIXME: implement recording + public void add(double value, Attributes attributes, Context unused) { + add(value, attributes); } static final class Builder implements DoubleUpDownCounterBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; - Builder(OtelInstrumentBuilder builder) { - this.instrumentBuilder = ofDoubles(builder, UP_DOWN_COUNTER); + Builder(OtelMeter meter, OtelInstrumentBuilder builder) { + this.meter = meter; + this.builder = ofDoubles(builder, UP_DOWN_COUNTER); } @Override public DoubleUpDownCounterBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public DoubleUpDownCounterBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override public DoubleUpDownCounter build() { - return new OtelDoubleUpDownCounter(); + return new OtelDoubleUpDownCounter( + meter.registerStorage(builder.descriptor(), OtelMetricStorage::newDoubleSumStorage)); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrument.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrument.java new file mode 100644 index 00000000000..3b7e727550d --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrument.java @@ -0,0 +1,32 @@ +package datadog.opentelemetry.shim.metrics; + +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; + +/** Ensure all instruments implement the same equivalency. */ +abstract class OtelInstrument { + final OtelMetricStorage storage; + + OtelInstrument(OtelMetricStorage storage) { + this.storage = storage; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof OtelInstrument)) { + return false; + } + + OtelInstrument that = (OtelInstrument) o; + return storage.getDescriptor().equals(that.storage.getDescriptor()); + } + + @Override + public final int hashCode() { + return storage.getDescriptor().hashCode(); + } + + @Override + public final String toString() { + return getClass().getSimpleName() + "{descriptor=" + storage.getDescriptor() + '}'; + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentBuilder.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentBuilder.java index 494cefea84f..4ee187fa654 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentBuilder.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentBuilder.java @@ -3,8 +3,6 @@ import javax.annotation.Nullable; final class OtelInstrumentBuilder { - - private final OtelMeter meter; private final String instrumentName; private final OtelInstrumentType instrumentType; private final boolean longValues; @@ -15,14 +13,12 @@ final class OtelInstrumentBuilder { /** * Starts building an instrument of long values with the given name and type. * - * @param meter the owning mete * @param instrumentName the name of the instrument * @param instrumentType the type of the instrument * @return new instrument builder */ - static OtelInstrumentBuilder ofLongs( - OtelMeter meter, String instrumentName, OtelInstrumentType instrumentType) { - return new OtelInstrumentBuilder(meter, instrumentName, instrumentType, true); + static OtelInstrumentBuilder ofLongs(String instrumentName, OtelInstrumentType instrumentType) { + return new OtelInstrumentBuilder(instrumentName, instrumentType, true); } /** @@ -34,20 +30,18 @@ static OtelInstrumentBuilder ofLongs( */ static OtelInstrumentBuilder ofLongs( OtelInstrumentBuilder builder, OtelInstrumentType instrumentType) { - return new OtelInstrumentBuilder(builder.meter, builder.instrumentName, instrumentType, true); + return new OtelInstrumentBuilder(builder.instrumentName, instrumentType, true); } /** * Starts building an instrument of double values with the given name and type. * - * @param meter the owning mete * @param instrumentName the name of the instrument * @param instrumentType the type of the instrument * @return new instrument builder */ - static OtelInstrumentBuilder ofDoubles( - OtelMeter meter, String instrumentName, OtelInstrumentType instrumentType) { - return new OtelInstrumentBuilder(meter, instrumentName, instrumentType, false); + static OtelInstrumentBuilder ofDoubles(String instrumentName, OtelInstrumentType instrumentType) { + return new OtelInstrumentBuilder(instrumentName, instrumentType, false); } /** @@ -59,15 +53,11 @@ static OtelInstrumentBuilder ofDoubles( */ static OtelInstrumentBuilder ofDoubles( OtelInstrumentBuilder builder, OtelInstrumentType instrumentType) { - return new OtelInstrumentBuilder(builder.meter, builder.instrumentName, instrumentType, false); + return new OtelInstrumentBuilder(builder.instrumentName, instrumentType, false); } private OtelInstrumentBuilder( - OtelMeter meter, - String instrumentName, - OtelInstrumentType instrumentType, - boolean longValues) { - this.meter = meter; + String instrumentName, OtelInstrumentType instrumentType, boolean longValues) { this.instrumentName = instrumentName; this.instrumentType = instrumentType; this.longValues = longValues; @@ -80,4 +70,9 @@ void setDescription(String description) { void setUnit(String unit) { this.unit = unit; } + + OtelInstrumentDescriptor descriptor() { + return new OtelInstrumentDescriptor( + instrumentName, instrumentType, longValues, description, unit); + } } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentDescriptor.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentDescriptor.java new file mode 100644 index 00000000000..2ba90bc1a0c --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentDescriptor.java @@ -0,0 +1,88 @@ +package datadog.opentelemetry.shim.metrics; + +import java.util.Locale; +import java.util.Objects; +import javax.annotation.Nullable; + +/** Uniquely describes an instrument for the Meter that created it. */ +public final class OtelInstrumentDescriptor { + private final String instrumentName; + private final OtelInstrumentType instrumentType; + private final boolean longValues; + @Nullable private final String description; + @Nullable private final String unit; + + OtelInstrumentDescriptor( + String instrumentName, + OtelInstrumentType instrumentType, + boolean longValues, + @Nullable String description, + @Nullable String unit) { + this.instrumentName = instrumentName; + this.instrumentType = instrumentType; + this.longValues = longValues; + this.description = description; + this.unit = unit; + } + + public String getName() { + return instrumentName; + } + + public OtelInstrumentType getType() { + return instrumentType; + } + + public boolean hasLongValues() { + return longValues; + } + + @Nullable + public String getDescription() { + return description; + } + + @Nullable + public String getUnit() { + return unit; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof OtelInstrumentDescriptor)) { + return false; + } + + OtelInstrumentDescriptor that = (OtelInstrumentDescriptor) o; + return instrumentName.equalsIgnoreCase(that.instrumentName) + && instrumentType == that.instrumentType + && longValues == that.longValues + && Objects.equals(description, that.description) + && Objects.equals(unit, that.unit); + } + + @Override + public int hashCode() { + int result = instrumentName.toLowerCase(Locale.ROOT).hashCode(); + result = 31 * result + instrumentType.hashCode(); + result = 31 * result + Boolean.hashCode(longValues); + result = 31 * result + Objects.hashCode(description); + result = 31 * result + Objects.hashCode(unit); + return result; + } + + @Override + public String toString() { + // use same property names as OTel in toString + return "OtelInstrumentDescriptor{" + + "name='" + + instrumentName + + (description != null ? "', description='" + description : "") + + (unit != null ? "', unit='" + unit : "") + + "', type=" + + instrumentType + + ", valueType=" + + (longValues ? "LONG" : "DOUBLE") + + "}"; + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentType.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentType.java index 338b842ed87..95cc54d5f71 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentType.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelInstrumentType.java @@ -1,8 +1,12 @@ package datadog.opentelemetry.shim.metrics; -enum OtelInstrumentType { +public enum OtelInstrumentType { + // same order as io.opentelemetry.sdk.metrics.InstrumentType COUNTER, UP_DOWN_COUNTER, HISTOGRAM, - GAUGE + OBSERVABLE_COUNTER, + OBSERVABLE_UP_DOWN_COUNTER, + OBSERVABLE_GAUGE, + GAUGE, } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java index c387c5b8104..8c6d77fcfa6 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongCounter.java @@ -5,6 +5,8 @@ import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_INSTRUMENT_NAME; import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_METER; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; +import datadog.trace.relocate.api.RatelimitedLogger; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleCounterBuilder; import io.opentelemetry.api.metrics.LongCounter; @@ -12,54 +14,73 @@ import io.opentelemetry.api.metrics.ObservableLongCounter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.context.Context; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.annotation.ParametersAreNonnullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ParametersAreNonnullByDefault -final class OtelLongCounter implements LongCounter { +final class OtelLongCounter extends OtelInstrument implements LongCounter { + private static final Logger LOGGER = LoggerFactory.getLogger(OtelLongCounter.class); + private static final RatelimitedLogger RATELIMITED_LOGGER = + new RatelimitedLogger(LOGGER, 5, TimeUnit.MINUTES); + + OtelLongCounter(OtelMetricStorage storage) { + super(storage); + } @Override public void add(long value) { - // FIXME: implement recording + add(value, Attributes.empty()); } @Override public void add(long value, Attributes attributes) { - // FIXME: implement recording + if (value < 0) { + RATELIMITED_LOGGER.warn( + "Counters can only increase. Instrument {} has recorded a negative value.", + storage.getInstrumentName()); + } else { + storage.recordLong(value, attributes); + } } @Override - public void add(long value, Attributes attributes, Context context) { - // FIXME: implement recording + public void add(long value, Attributes attributes, Context unused) { + add(value, attributes); } static final class Builder implements LongCounterBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; Builder(OtelMeter meter, String instrumentName) { - this.instrumentBuilder = ofLongs(meter, instrumentName, COUNTER); + this.meter = meter; + this.builder = ofLongs(instrumentName, COUNTER); } @Override public LongCounterBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public LongCounterBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override public DoubleCounterBuilder ofDoubles() { - return new OtelDoubleCounter.Builder(instrumentBuilder); + return new OtelDoubleCounter.Builder(meter, builder); } @Override public LongCounter build() { - return new OtelLongCounter(); + return new OtelLongCounter( + meter.registerStorage(builder.descriptor(), OtelMetricStorage::newLongSumStorage)); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongGauge.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongGauge.java index 97f708d49e5..2b0057a92fd 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongGauge.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongGauge.java @@ -5,6 +5,7 @@ import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_INSTRUMENT_NAME; import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_METER; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongGauge; import io.opentelemetry.api.metrics.LongGaugeBuilder; @@ -15,45 +16,51 @@ import javax.annotation.ParametersAreNonnullByDefault; @ParametersAreNonnullByDefault -final class OtelLongGauge implements LongGauge { +final class OtelLongGauge extends OtelInstrument implements LongGauge { + OtelLongGauge(OtelMetricStorage storage) { + super(storage); + } @Override public void set(long value) { - // FIXME: implement recording + set(value, Attributes.empty()); } @Override public void set(long value, Attributes attributes) { - // FIXME: implement recording + storage.recordLong(value, attributes); } @Override - public void set(long value, Attributes attributes, Context context) { - // FIXME: implement recording + public void set(long value, Attributes attributes, Context unused) { + set(value, attributes); } static final class Builder implements LongGaugeBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; - Builder(OtelInstrumentBuilder builder) { - this.instrumentBuilder = ofLongs(builder, GAUGE); + Builder(OtelMeter meter, OtelInstrumentBuilder builder) { + this.meter = meter; + this.builder = ofLongs(builder, GAUGE); } @Override public LongGaugeBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public LongGaugeBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override public LongGauge build() { - return new OtelLongGauge(); + return new OtelLongGauge( + meter.registerStorage(builder.descriptor(), OtelMetricStorage::newLongValueStorage)); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongHistogram.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongHistogram.java index c005bc10806..730134ba4d4 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongHistogram.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongHistogram.java @@ -1,61 +1,98 @@ package datadog.opentelemetry.shim.metrics; +import static datadog.opentelemetry.shim.metrics.OtelDoubleHistogram.Builder.validateBoundaries; import static datadog.opentelemetry.shim.metrics.OtelInstrumentBuilder.ofLongs; import static datadog.opentelemetry.shim.metrics.OtelInstrumentType.HISTOGRAM; +import static datadog.opentelemetry.shim.metrics.data.OtelMetricStorage.newHistogramStorage; +import static java.util.stream.Collectors.toList; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; +import datadog.trace.relocate.api.RatelimitedLogger; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.LongHistogramBuilder; import io.opentelemetry.context.Context; import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import javax.annotation.ParametersAreNonnullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ParametersAreNonnullByDefault -final class OtelLongHistogram implements LongHistogram { +final class OtelLongHistogram extends OtelInstrument implements LongHistogram { + private static final Logger LOGGER = LoggerFactory.getLogger(OtelLongHistogram.class); + private static final RatelimitedLogger RATELIMITED_LOGGER = + new RatelimitedLogger(LOGGER, 5, TimeUnit.MINUTES); + + OtelLongHistogram(OtelMetricStorage storage) { + super(storage); + } @Override public void record(long value) { - // FIXME: implement recording + record(value, Attributes.empty()); } @Override public void record(long value, Attributes attributes) { - // FIXME: implement recording + if (value < 0) { + RATELIMITED_LOGGER.warn( + "Histograms can only record non-negative values. Instrument {} has recorded a negative value.", + storage.getInstrumentName()); + } else { + storage.recordLong(value, attributes); + } } @Override - public void record(long value, Attributes attributes, Context context) { - // FIXME: implement recording + public void record(long value, Attributes attributes, Context unused) { + record(value, attributes); } static final class Builder implements LongHistogramBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; + private List bucketBoundaries; - Builder(OtelInstrumentBuilder builder) { - this.instrumentBuilder = ofLongs(builder, HISTOGRAM); + Builder(OtelMeter meter, OtelInstrumentBuilder builder, List bucketBoundaries) { + this.meter = meter; + this.builder = ofLongs(builder, HISTOGRAM); + this.bucketBoundaries = bucketBoundaries; } @Override public LongHistogramBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public LongHistogramBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override + @SuppressFBWarnings("DCN") // match OTel in catching and logging NPE public LongHistogramBuilder setExplicitBucketBoundariesAdvice(List bucketBoundaries) { - // FIXME: implement boundary advice + try { + Objects.requireNonNull(bucketBoundaries, "bucketBoundaries must not be null"); + this.bucketBoundaries = + validateBoundaries(bucketBoundaries.stream().map(Long::doubleValue).collect(toList())); + } catch (IllegalArgumentException | NullPointerException e) { + LOGGER.warn("Error setting explicit bucket boundaries advice: {}", e.getMessage()); + } return this; } @Override public LongHistogram build() { - return new OtelLongHistogram(); + return new OtelLongHistogram( + meter.registerStorage( + builder.descriptor(), + descriptor -> newHistogramStorage(descriptor, bucketBoundaries))); } } } diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java index d53eb468592..52b07b0392c 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelLongUpDownCounter.java @@ -5,6 +5,7 @@ import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_INSTRUMENT_NAME; import static datadog.opentelemetry.shim.metrics.OtelMeter.NOOP_METER; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; import io.opentelemetry.api.metrics.LongUpDownCounter; @@ -16,50 +17,56 @@ import javax.annotation.ParametersAreNonnullByDefault; @ParametersAreNonnullByDefault -final class OtelLongUpDownCounter implements LongUpDownCounter { +final class OtelLongUpDownCounter extends OtelInstrument implements LongUpDownCounter { + OtelLongUpDownCounter(OtelMetricStorage storage) { + super(storage); + } @Override public void add(long value) { - // FIXME: implement recording + add(value, Attributes.empty()); } @Override public void add(long value, Attributes attributes) { - // FIXME: implement recording + storage.recordLong(value, attributes); } @Override - public void add(long value, Attributes attributes, Context context) { - // FIXME: implement recording + public void add(long value, Attributes attributes, Context unused) { + add(value, attributes); } static final class Builder implements LongUpDownCounterBuilder { - private final OtelInstrumentBuilder instrumentBuilder; + private final OtelMeter meter; + private final OtelInstrumentBuilder builder; Builder(OtelMeter meter, String instrumentName) { - this.instrumentBuilder = ofLongs(meter, instrumentName, UP_DOWN_COUNTER); + this.meter = meter; + this.builder = ofLongs(instrumentName, UP_DOWN_COUNTER); } @Override public LongUpDownCounterBuilder setDescription(String description) { - instrumentBuilder.setDescription(description); + builder.setDescription(description); return this; } @Override public LongUpDownCounterBuilder setUnit(String unit) { - instrumentBuilder.setUnit(unit); + builder.setUnit(unit); return this; } @Override public DoubleUpDownCounterBuilder ofDoubles() { - return new OtelDoubleUpDownCounter.Builder(instrumentBuilder); + return new OtelDoubleUpDownCounter.Builder(meter, builder); } @Override public LongUpDownCounter build() { - return new OtelLongUpDownCounter(); + return new OtelLongUpDownCounter( + meter.registerStorage(builder.descriptor(), OtelMetricStorage::newLongSumStorage)); } @Override diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeter.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeter.java index 39345aef96e..6213d9084f4 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeter.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeter.java @@ -1,6 +1,8 @@ package datadog.opentelemetry.shim.metrics; import datadog.opentelemetry.shim.OtelInstrumentationScope; +import datadog.opentelemetry.shim.metrics.data.OtelMetricStorage; +import datadog.opentelemetry.shim.metrics.export.OtelMeterVisitor; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.DoubleGaugeBuilder; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; @@ -9,6 +11,9 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.metrics.ObservableMeasurement; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; @@ -27,6 +32,9 @@ final class OtelMeter implements Meter { private final OtelInstrumentationScope instrumentationScope; + private final Map storage = + new ConcurrentHashMap<>(); + OtelMeter(OtelInstrumentationScope instrumentationScope) { this.instrumentationScope = instrumentationScope; } @@ -77,6 +85,16 @@ public String toString() { return "OtelMeter{instrumentationScope=" + instrumentationScope + "}"; } + OtelMetricStorage registerStorage( + OtelInstrumentDescriptor descriptor, + Function storageFactory) { + return storage.computeIfAbsent(descriptor, storageFactory); + } + + void collect(OtelMeterVisitor visitor) { + storage.forEach((descriptor, storage) -> storage.collect(visitor.visitInstrument(descriptor))); + } + private static boolean validInstrumentName(@Nullable String instrumentName) { if (instrumentName != null && VALID_INSTRUMENT_NAME_PATTERN.matcher(instrumentName).matches()) { return true; diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeterProvider.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeterProvider.java index fa2b641bbdb..c133eb9368d 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeterProvider.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/OtelMeterProvider.java @@ -1,6 +1,7 @@ package datadog.opentelemetry.shim.metrics; import datadog.opentelemetry.shim.OtelInstrumentationScope; +import datadog.opentelemetry.shim.metrics.export.OtelMetricVisitor; import datadog.trace.util.Strings; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterBuilder; @@ -32,6 +33,10 @@ public MeterBuilder meterBuilder(String instrumentationScopeName) { return new OtelMeterBuilder(this, instrumentationScopeName); } + public void collect(OtelMetricVisitor visitor) { + meters.forEach((scope, meter) -> meter.collect(visitor.visitMeter(scope))); + } + OtelMeter getMeterShim( String instrumentationScopeName, @Nullable String instrumentationScopeVersion, diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelAggregator.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelAggregator.java new file mode 100644 index 00000000000..282186ef36d --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelAggregator.java @@ -0,0 +1,40 @@ +package datadog.opentelemetry.shim.metrics.data; + +/** Common behaviour shared across all aggregators. */ +abstract class OtelAggregator { + private volatile boolean empty = true; + + final boolean isEmpty() { + return empty; + } + + final void recordDouble(double value) { + doRecordDouble(value); + empty = false; + } + + final void recordLong(long value) { + doRecordLong(value); + empty = false; + } + + final OtelPoint collect() { + return doCollect(false); + } + + final OtelPoint collectAndReset() { + OtelPoint point = doCollect(true); + empty = true; + return point; + } + + void doRecordDouble(double value) { + throw new UnsupportedOperationException(); + } + + void doRecordLong(long value) { + throw new UnsupportedOperationException(); + } + + abstract OtelPoint doCollect(boolean reset); +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoublePoint.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoublePoint.java new file mode 100644 index 00000000000..0404543b538 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoublePoint.java @@ -0,0 +1,9 @@ +package datadog.opentelemetry.shim.metrics.data; + +public final class OtelDoublePoint extends OtelPoint { + public final double value; + + OtelDoublePoint(double value) { + this.value = value; + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoubleSum.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoubleSum.java new file mode 100644 index 00000000000..d1dd1ae6a62 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoubleSum.java @@ -0,0 +1,17 @@ +package datadog.opentelemetry.shim.metrics.data; + +import java.util.concurrent.atomic.DoubleAdder; + +final class OtelDoubleSum extends OtelAggregator { + private final DoubleAdder total = new DoubleAdder(); + + @Override + void doRecordDouble(double value) { + total.add(value); + } + + @Override + OtelPoint doCollect(boolean reset) { + return new OtelDoublePoint(reset ? total.sumThenReset() : total.sum()); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoubleValue.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoubleValue.java new file mode 100644 index 00000000000..9bf5f8e6b98 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelDoubleValue.java @@ -0,0 +1,15 @@ +package datadog.opentelemetry.shim.metrics.data; + +final class OtelDoubleValue extends OtelAggregator { + private volatile double value; + + @Override + void doRecordDouble(double value) { + this.value = value; + } + + @Override + OtelPoint doCollect(boolean reset) { + return new OtelDoublePoint(value); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelHistogramPoint.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelHistogramPoint.java new file mode 100644 index 00000000000..efa6cdd3c7a --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelHistogramPoint.java @@ -0,0 +1,18 @@ +package datadog.opentelemetry.shim.metrics.data; + +import java.util.List; + +public final class OtelHistogramPoint extends OtelPoint { + public final double count; + public final List bucketBoundaries; + public final List bucketCounts; + public final double sum; + + OtelHistogramPoint( + double count, List bucketBoundaries, List bucketCounts, double sum) { + this.count = count; + this.bucketBoundaries = bucketBoundaries; + this.bucketCounts = bucketCounts; + this.sum = sum; + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelHistogramSketch.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelHistogramSketch.java new file mode 100644 index 00000000000..7fc0f182bac --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelHistogramSketch.java @@ -0,0 +1,56 @@ +package datadog.opentelemetry.shim.metrics.data; + +import datadog.metrics.api.Histogram; +import java.util.List; + +final class OtelHistogramSketch extends OtelAggregator { + private final Histogram histogram; + private volatile double totalSum; + + OtelHistogramSketch(List bucketBoundaries) { + this.histogram = Histogram.newHistogram(bucketBoundaries); + } + + @Override + void doRecordDouble(double value) { + synchronized (histogram) { + histogram.accept(value); + totalSum += value; + } + } + + @Override + void doRecordLong(long value) { + doRecordDouble(fixedPrecision(value)); + } + + @Override + OtelPoint doCollect(boolean reset) { + double count; + List binBoundaries; + List binCounts; + double sum; + synchronized (histogram) { + count = histogram.getCount(); + binBoundaries = histogram.getBinBoundaries(); + binCounts = histogram.getBinCounts(); + sum = totalSum; + if (reset) { + histogram.clear(); + totalSum = 0; + } + } + return new OtelHistogramPoint(count, binBoundaries, binCounts, sum); + } + + /** Truncate IEEE-754 floating-point value to 10 bits precision. */ + private static double fixedPrecision(long value) { + long bits = Double.doubleToRawLongBits(value); + // the mask include 1 bit sign 11 bits exponent (0xfff) + // then we filter the mantissa to 10bits (0xff8) (9 bits as it has implicit value of 1) + // 10 bits precision (any value will be +/- 1/1024) + // https://en.wikipedia.org/wiki/Double-precision_floating-point_format + bits &= 0xfffff80000000000L; + return Double.longBitsToDouble(bits); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongPoint.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongPoint.java new file mode 100644 index 00000000000..453f60eab31 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongPoint.java @@ -0,0 +1,9 @@ +package datadog.opentelemetry.shim.metrics.data; + +public final class OtelLongPoint extends OtelPoint { + public final long value; + + OtelLongPoint(long value) { + this.value = value; + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongSum.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongSum.java new file mode 100644 index 00000000000..91da9c054bb --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongSum.java @@ -0,0 +1,17 @@ +package datadog.opentelemetry.shim.metrics.data; + +import java.util.concurrent.atomic.LongAdder; + +final class OtelLongSum extends OtelAggregator { + private final LongAdder total = new LongAdder(); + + @Override + void doRecordLong(long value) { + total.add(value); + } + + @Override + OtelPoint doCollect(boolean reset) { + return new OtelLongPoint(reset ? total.sumThenReset() : total.sum()); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongValue.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongValue.java new file mode 100644 index 00000000000..31e1719a89c --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelLongValue.java @@ -0,0 +1,15 @@ +package datadog.opentelemetry.shim.metrics.data; + +final class OtelLongValue extends OtelAggregator { + private volatile long value; + + @Override + void doRecordLong(long value) { + this.value = value; + } + + @Override + OtelPoint doCollect(boolean reset) { + return new OtelLongPoint(value); + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelMetricStorage.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelMetricStorage.java new file mode 100644 index 00000000000..39b6edd9fd6 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelMetricStorage.java @@ -0,0 +1,216 @@ +package datadog.opentelemetry.shim.metrics.data; + +import datadog.opentelemetry.shim.metrics.OtelInstrumentDescriptor; +import datadog.opentelemetry.shim.metrics.export.OtelInstrumentVisitor; +import datadog.trace.api.Config; +import datadog.trace.api.config.OtlpConfig; +import datadog.trace.relocate.api.RatelimitedLogger; +import io.opentelemetry.api.common.Attributes; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.function.Function; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Stores and aggregates metrics data for a given instrument. */ +public final class OtelMetricStorage { + private static final Logger LOGGER = LoggerFactory.getLogger(OtelMetricStorage.class); + private static final RatelimitedLogger RATELIMITED_LOGGER = + new RatelimitedLogger(LOGGER, 5, TimeUnit.MINUTES); + + private static final int DEFAULT_MAX_CARDINALITY = 2_000; + + private static final Attributes CARDINALITY_OVERFLOW = + Attributes.builder().put("otel.metric.overflow", true).build(); + + private static final boolean RESET_ON_COLLECT = + Config.get().getOtlpMetricsTemporalityPreference() == OtlpConfig.Temporality.DELTA; + + private final OtelInstrumentDescriptor descriptor; + private final Function aggregatorSupplier; + private volatile Recording currentRecording; + + // only used with DELTA temporality + private Recording previousRecording; + + private OtelMetricStorage( + OtelInstrumentDescriptor descriptor, Supplier aggregatorSupplier) { + this.descriptor = descriptor; + this.aggregatorSupplier = unused -> aggregatorSupplier.get(); + this.currentRecording = new Recording(); + if (RESET_ON_COLLECT) { + this.previousRecording = new Recording(); + } + } + + public static OtelMetricStorage newDoubleSumStorage(OtelInstrumentDescriptor descriptor) { + return new OtelMetricStorage(descriptor, OtelDoubleSum::new); + } + + public static OtelMetricStorage newDoubleValueStorage(OtelInstrumentDescriptor descriptor) { + return new OtelMetricStorage(descriptor, OtelDoubleValue::new); + } + + public static OtelMetricStorage newLongSumStorage(OtelInstrumentDescriptor descriptor) { + return new OtelMetricStorage(descriptor, OtelLongSum::new); + } + + public static OtelMetricStorage newLongValueStorage(OtelInstrumentDescriptor descriptor) { + return new OtelMetricStorage(descriptor, OtelLongValue::new); + } + + public static OtelMetricStorage newHistogramStorage( + OtelInstrumentDescriptor descriptor, List bucketBoundaries) { + return new OtelMetricStorage(descriptor, () -> new OtelHistogramSketch(bucketBoundaries)); + } + + public String getInstrumentName() { + return descriptor.getName(); + } + + public OtelInstrumentDescriptor getDescriptor() { + return descriptor; + } + + public void recordLong(long value, Attributes attributes) { + Recording recording = acquireRecordingForWrite(); + try { + aggregator(recording.aggregators, attributes).recordLong(value); + } finally { + releaseRecordingAfterWrite(recording); + } + } + + public void recordDouble(double value, Attributes attributes) { + Recording recording = acquireRecordingForWrite(); + try { + aggregator(recording.aggregators, attributes).recordDouble(value); + } finally { + releaseRecordingAfterWrite(recording); + } + } + + private OtelAggregator aggregator( + Map aggregators, Attributes attributes) { + Objects.requireNonNull(attributes, "attributes"); + OtelAggregator aggregator = aggregators.get(attributes); + if (null != aggregator) { + return aggregator; + } + if (aggregators.size() >= DEFAULT_MAX_CARDINALITY) { + RATELIMITED_LOGGER.warn( + "Instrument {} has exceeded the maximum allowed cardinality ({}).", + descriptor.getName(), + DEFAULT_MAX_CARDINALITY); + attributes = CARDINALITY_OVERFLOW; // write data to overflow + } + return aggregators.computeIfAbsent(attributes, aggregatorSupplier); + } + + public void collect(OtelInstrumentVisitor visitor) { + if (RESET_ON_COLLECT) { + doCollectAndReset(visitor); + } else { + doCollect(visitor); + } + } + + /** Collect data for CUMULATIVE temporality, keeping aggregators for future writes. */ + private void doCollect(OtelInstrumentVisitor visitor) { + // no need to hold writers back if we are not resetting metrics on collect + currentRecording.aggregators.forEach( + (attributes, aggregator) -> { + if (!aggregator.isEmpty()) { + visitor.visitPoint(attributes, aggregator.collect()); + } + }); + } + + /** + * Collect data for DELTA temporality, resetting aggregators for future writes. + * + *

Each collect request toggles between two groups of aggregators: current / previous. + */ + private void doCollectAndReset(OtelInstrumentVisitor visitor) { + + // capture _current_ recording for collection, its aggregators will be reset at the end + final Recording recording = currentRecording; + + // publish fresh recording for new writers, using aggregators from _previous_ recording + currentRecording = new Recording(previousRecording); + + // notify writers that the captured recording is about to be reset + ACTIVITY.addAndGet(recording, RESET_PENDING); + while (recording.activity > 1) { + Thread.yield(); // other threads are still writing to this recording + } + + Map aggregators = recording.aggregators; + + // avoid churn: only remove empty aggregators if we're over cardinality + if (aggregators.size() >= DEFAULT_MAX_CARDINALITY) { + aggregators.values().removeIf(OtelAggregator::isEmpty); + } + + aggregators.forEach( + (attributes, aggregator) -> { + if (!aggregator.isEmpty()) { + visitor.visitPoint(attributes, aggregator.collectAndReset()); + } + }); + + previousRecording = recording; + } + + private Recording acquireRecordingForWrite() { + if (RESET_ON_COLLECT) { + // busy loop to limit impact on caller + while (true) { + final Recording recording = currentRecording; + // atomically notify collector of write activity and check state + if ((ACTIVITY.addAndGet(recording, WRITER) & RESET_PENDING) == 0) { + return recording; + } else { + // reset pending: rollback and check again for a fresh recording + ACTIVITY.addAndGet(recording, -WRITER); + } + } + } else { + return currentRecording; + } + } + + private void releaseRecordingAfterWrite(Recording recording) { + if (RESET_ON_COLLECT) { + ACTIVITY.addAndGet(recording, -WRITER); + } + } + + static final AtomicIntegerFieldUpdater ACTIVITY = + AtomicIntegerFieldUpdater.newUpdater(Recording.class, "activity"); + + // first activity bit indicates if this recording is about to be reset + private static final int RESET_PENDING = 1; + + // the other activity bits indicate how many threads are writing to it + private static final int WRITER = 2; + + static final class Recording { + final Map aggregators; + + transient volatile int activity; + + Recording() { + this.aggregators = new ConcurrentHashMap<>(); + } + + Recording(Recording previous) { + this.aggregators = previous.aggregators; + } + } +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelPoint.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelPoint.java new file mode 100644 index 00000000000..1c0a8afd070 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/data/OtelPoint.java @@ -0,0 +1,3 @@ +package datadog.opentelemetry.shim.metrics.data; + +public abstract class OtelPoint {} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelInstrumentVisitor.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelInstrumentVisitor.java new file mode 100644 index 00000000000..6067761e9aa --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelInstrumentVisitor.java @@ -0,0 +1,9 @@ +package datadog.opentelemetry.shim.metrics.export; + +import datadog.opentelemetry.shim.metrics.data.OtelPoint; +import io.opentelemetry.api.common.Attributes; + +public interface OtelInstrumentVisitor { + /** Visits a data point collected by the instrument. */ + void visitPoint(Attributes attributes, OtelPoint point); +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelMeterVisitor.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelMeterVisitor.java new file mode 100644 index 00000000000..311d2666a0f --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelMeterVisitor.java @@ -0,0 +1,8 @@ +package datadog.opentelemetry.shim.metrics.export; + +import datadog.opentelemetry.shim.metrics.OtelInstrumentDescriptor; + +public interface OtelMeterVisitor { + /** Visits an instrument created by the meter. */ + OtelInstrumentVisitor visitInstrument(OtelInstrumentDescriptor descriptor); +} diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelMetricVisitor.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelMetricVisitor.java new file mode 100644 index 00000000000..ac2a5ac2e16 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/metrics/export/OtelMetricVisitor.java @@ -0,0 +1,8 @@ +package datadog.opentelemetry.shim.metrics.export; + +import datadog.opentelemetry.shim.OtelInstrumentationScope; + +public interface OtelMetricVisitor { + /** Visits a meter created by the OpenTelemetry API. */ + OtelMeterVisitor visitMeter(OtelInstrumentationScope scope); +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/main/java/datadog/trace/instrumentation/opentelemetry147/OpenTelemetryMetricsInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/main/java/datadog/trace/instrumentation/opentelemetry147/OpenTelemetryMetricsInstrumentation.java index 41aa14afc7e..61d959cc249 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/main/java/datadog/trace/instrumentation/opentelemetry147/OpenTelemetryMetricsInstrumentation.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.47/src/main/java/datadog/trace/instrumentation/opentelemetry147/OpenTelemetryMetricsInstrumentation.java @@ -65,6 +65,41 @@ public String[] helperClassNames() { "datadog.opentelemetry.shim.metrics.OtelMeter", "datadog.opentelemetry.shim.metrics.OtelMeterBuilder", "datadog.opentelemetry.shim.metrics.OtelMeterProvider", + "datadog.opentelemetry.shim.metrics.OtelInstrumentType", + "datadog.opentelemetry.shim.metrics.OtelInstrumentDescriptor", + "datadog.opentelemetry.shim.metrics.OtelInstrumentBuilder", + "datadog.opentelemetry.shim.metrics.OtelInstrument", + "datadog.opentelemetry.shim.metrics.data.OtelPoint", + "datadog.opentelemetry.shim.metrics.data.OtelAggregator", + "datadog.opentelemetry.shim.metrics.data.OtelDoublePoint", + "datadog.opentelemetry.shim.metrics.data.OtelDoubleSum", + "datadog.opentelemetry.shim.metrics.data.OtelDoubleValue", + "datadog.opentelemetry.shim.metrics.data.OtelHistogramPoint", + "datadog.opentelemetry.shim.metrics.data.OtelHistogramSketch", + "datadog.opentelemetry.shim.metrics.data.OtelLongPoint", + "datadog.opentelemetry.shim.metrics.data.OtelLongSum", + "datadog.opentelemetry.shim.metrics.data.OtelLongValue", + "datadog.opentelemetry.shim.metrics.data.OtelMetricStorage", + "datadog.opentelemetry.shim.metrics.data.OtelMetricStorage$Recording", + "datadog.opentelemetry.shim.metrics.export.OtelInstrumentVisitor", + "datadog.opentelemetry.shim.metrics.export.OtelMeterVisitor", + "datadog.opentelemetry.shim.metrics.export.OtelMetricVisitor", + "datadog.opentelemetry.shim.metrics.OtelDoubleCounter", + "datadog.opentelemetry.shim.metrics.OtelDoubleCounter$Builder", + "datadog.opentelemetry.shim.metrics.OtelDoubleGauge", + "datadog.opentelemetry.shim.metrics.OtelDoubleGauge$Builder", + "datadog.opentelemetry.shim.metrics.OtelDoubleHistogram", + "datadog.opentelemetry.shim.metrics.OtelDoubleHistogram$Builder", + "datadog.opentelemetry.shim.metrics.OtelDoubleUpDownCounter", + "datadog.opentelemetry.shim.metrics.OtelDoubleUpDownCounter$Builder", + "datadog.opentelemetry.shim.metrics.OtelLongCounter", + "datadog.opentelemetry.shim.metrics.OtelLongCounter$Builder", + "datadog.opentelemetry.shim.metrics.OtelLongGauge", + "datadog.opentelemetry.shim.metrics.OtelLongGauge$Builder", + "datadog.opentelemetry.shim.metrics.OtelLongHistogram", + "datadog.opentelemetry.shim.metrics.OtelLongHistogram$Builder", + "datadog.opentelemetry.shim.metrics.OtelLongUpDownCounter", + "datadog.opentelemetry.shim.metrics.OtelLongUpDownCounter$Builder", }; }