diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/build.gradle.kts b/instrumentation/twitter-util-stats-23.11/javaagent/build.gradle.kts index 455d6e9567b8..da96a9d18265 100644 --- a/instrumentation/twitter-util-stats-23.11/javaagent/build.gradle.kts +++ b/instrumentation/twitter-util-stats-23.11/javaagent/build.gradle.kts @@ -30,8 +30,6 @@ val scalified = fun(pack: String): String { } dependencies { - bootstrap(project(":instrumentation:executors:bootstrap")) - compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/Helpers.java b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/Helpers.java new file mode 100644 index 000000000000..6665c5e08d6f --- /dev/null +++ b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/Helpers.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.twitterutilstats.v23_11; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.twitter.finagle.stats.Bytes$; +import com.twitter.finagle.stats.CustomUnit; +import com.twitter.finagle.stats.Kilobytes$; +import com.twitter.finagle.stats.Megabytes$; +import com.twitter.finagle.stats.MetricBuilder; +import com.twitter.finagle.stats.MetricUnit; +import com.twitter.finagle.stats.Microseconds$; +import com.twitter.finagle.stats.Milliseconds$; +import com.twitter.finagle.stats.Percentage$; +import com.twitter.finagle.stats.Requests$; +import com.twitter.finagle.stats.Seconds$; +import com.twitter.finagle.stats.Unspecified$; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import scala.jdk.CollectionConverters; + +public class Helpers { + + private static final Logger logger = Logger.getLogger(Helpers.class.getName()); + + private static final AttributeKey> HIERARCHICAL_NAME_LABEL_KEY = + AttributeKey.stringArrayKey("twitter-util-stats/hierarchical_name"); + + private static final Pattern NAME_REGEX = Pattern.compile("^[A-Za-z][_.-/A-Za-z0-9]{0,254}$"); + + private static final String DEFAULT_DELIM = "."; + + public static final String delimiter; + + static { + String delim = + AgentInstrumentationConfig.get() + .getString( + "otel.instrumentation.twitter-util-stats.metrics.name.delimiter", DEFAULT_DELIM); + + if (delim.isEmpty()) { + logger.warning("Delimiter must be non-empty"); + delim = DEFAULT_DELIM; + } else if (!delim.equals(".") && !delim.equals("/") && !delim.equals("-")) { + logger.warning("Unsupported delimiter detected: " + delim); + delim = DEFAULT_DELIM; + } + + delimiter = delim; + } + + private Helpers() {} + + public static Attributes attributesFromLabels(MetricBuilder builder) { + return builder + .identity() + .labels() + .foldLeft( + // put the hierarchical name as a label bc hierarchical names are not cleanly mapped + // to dimensional names via twitter stats or the final usage in finagle in a way otel + // can concisely reason about; by adding the label, downstream systems can aggregate + // on their own -- or not -- achieving a finer granularity without compromising + // the general patterns applied herein, and without the complexities applied in + // the finagle PrometheusExporter (implicitly, how the MetricsView & registries work, + // etc.), for example + Attributes.builder() + .put( + HIERARCHICAL_NAME_LABEL_KEY, + scala.jdk.CollectionConverters.SeqHasAsJava( + builder.identity().hierarchicalName()) + .asJava()), + (v1, v2) -> v1.put(v2._1(), v2._2())) + .build(); + } + + @VisibleForTesting + public static String nameConversion(MetricBuilder schema) { + return schema.identity().dimensionalName().mkString(delimiter); + } + + /* + Always use "byte" for size units to avoid precision loss. + Why: the aggregating services treat unit scales uniformly, so this should present little issue. + */ + // unchecked casting to efficiently handle the generic number conversion for long vs float + @SuppressWarnings("unchecked") + public static UnitConversion unitConverter( + MetricUnit unit, MetricBuilder.MetricType metricType) { + if (unit instanceof CustomUnit) { + return UnitConversion.create(((CustomUnit) unit).name().toLowerCase(Locale.getDefault())); + } else if (unit instanceof Unspecified$) { + return UnitConversion.create(); + } else if (unit instanceof Bytes$) { + return UnitConversion.create("byte"); + } else if (unit instanceof Kilobytes$) { + // base-10 to base-2 translation + if (metricType == MetricBuilder.CounterType$.MODULE$) { + return (UnitConversion) UnitConversion.create("byte", (Long x) -> (x * 1000)); + } else { + return (UnitConversion) UnitConversion.create("byte", (Float x) -> (x * 1000)); + } + } else if (unit instanceof Megabytes$) { + // base-10 to base-2 translation + if (metricType == MetricBuilder.CounterType$.MODULE$) { + return (UnitConversion) UnitConversion.create("byte", (Long x) -> (x * 1000 * 1000)); + } else { + return (UnitConversion) UnitConversion.create("byte", (Float x) -> (x * 1000 * 1000)); + } + } else if (unit instanceof Seconds$) { + return UnitConversion.create("second"); + } else if (unit instanceof Milliseconds$) { + return UnitConversion.create("millisecond"); + } else if (unit instanceof Microseconds$) { + return UnitConversion.create("microsecond"); + } else if (unit instanceof Requests$) { + return UnitConversion.create("request"); + } else if (unit instanceof Percentage$) { + return UnitConversion.create("percent"); + } else { + throw new IllegalArgumentException("unsupported metric unit: " + unit.toString()); + } + } + + /* + Identical semantics to the finagle-stats PrometheusExporter. + */ + @VisibleForTesting + public static boolean shouldEmit(MetricBuilder schema) { + return schema + .identity() + .identityType() + .bias(com.twitter.finagle.stats.MetricBuilder$IdentityType$HierarchicalOnly$.MODULE$) + == com.twitter.finagle.stats.MetricBuilder$IdentityType$Full$.MODULE$; + } + + /* + Guard against avoidable, incorrect metric names. + Finagle produces and emits a number of metrics which contain invalid chars and entirely free-form, + making them hard to adapt to the otel spec, whitespace, in particular. This, along with names + containing other identifying attributes which are unpredictable except in specific known cases + but which are indistinguishable from other potential metrics. IOW, a metric bearing some pattern + of identifying attributes in its name is indistinguishable from others created for a different + purpose and in a different context and is therefore not reasonably translated into otel metrics. + + Suggestion: write another instrumentation that adapts those specific cases. + */ + @VisibleForTesting + public static boolean canEmit(MetricBuilder schema) { + return CollectionConverters.SeqHasAsJava(schema.identity().dimensionalName()).asJava().stream() + .allMatch(NAME_REGEX.asPredicate()); + } + + @AutoValue + public abstract static class UnitConversion { + public abstract Optional getUnits(); + + public abstract Function getConverter(); + + static UnitConversion create(String units, Function converter) { + return new AutoValue_Helpers_UnitConversion<>(Optional.of(units), converter); + } + + static UnitConversion create(String units) { + return create(units, Function.identity()); + } + + static UnitConversion create() { + return new AutoValue_Helpers_UnitConversion<>(Optional.empty(), Function.identity()); + } + } +} diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/LoadedStatsReceiverInstrumentation.java b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/LoadedStatsReceiverInstrumentation.java index d87db17b728f..e2ec6c1899b3 100644 --- a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/LoadedStatsReceiverInstrumentation.java +++ b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/LoadedStatsReceiverInstrumentation.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.javaagent.instrumentation.twitterutilstats.v23_11; import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer; diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiver.java b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiver.java index fbaf8195e55f..9737417b037b 100644 --- a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiver.java +++ b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiver.java @@ -1,27 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.javaagent.instrumentation.twitterutilstats.v23_11; import com.google.auto.value.AutoValue; -import com.google.auto.value.extension.memoized.Memoized; -import com.google.common.annotations.VisibleForTesting; -import com.twitter.finagle.stats.Bytes$; import com.twitter.finagle.stats.Counter; -import com.twitter.finagle.stats.CustomUnit; import com.twitter.finagle.stats.Gauge; -import com.twitter.finagle.stats.Kilobytes$; -import com.twitter.finagle.stats.Megabytes$; import com.twitter.finagle.stats.Metadata; import com.twitter.finagle.stats.MetricBuilder; -import com.twitter.finagle.stats.MetricUnit; -import com.twitter.finagle.stats.Microseconds$; -import com.twitter.finagle.stats.Milliseconds$; -import com.twitter.finagle.stats.Percentage$; -import com.twitter.finagle.stats.Requests$; -import com.twitter.finagle.stats.Seconds$; import com.twitter.finagle.stats.Stat; import com.twitter.finagle.stats.StatsReceiver; -import com.twitter.finagle.stats.Unspecified$; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleGaugeBuilder; import io.opentelemetry.api.metrics.DoubleHistogram; @@ -30,55 +21,21 @@ import io.opentelemetry.api.metrics.LongCounterBuilder; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableDoubleGauge; -import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; import java.util.logging.Logger; -import java.util.regex.Pattern; -import java.util.stream.Collectors; import javax.annotation.Nullable; -import scala.jdk.CollectionConverters; public class OtelStatsReceiver implements StatsReceiver { private static final Logger logger = Logger.getLogger(OtelStatsReceiver.class.getName()); - private static final String DEFAULT_DELIM = "."; - - private static final Pattern NAME_REGEX = Pattern.compile("^[A-Za-z][_.-/A-Za-z0-9]{0,254}$"); - private static final Map HISTO_MAP = new ConcurrentHashMap<>(); private static final Map GAUGE_MAP = new ConcurrentHashMap<>(); private static final Map COUNTER_MAP = new ConcurrentHashMap<>(); private static final Meter meter = GlobalOpenTelemetry.get().getMeter("twitter-util-stats"); - private static final AttributeKey> HIERARCHICAL_NAME_LABEL_KEY = - AttributeKey.stringArrayKey("twitter-util-stats/hierarchical_name"); - - private static final String delimiter; - - static { - String delim = - AgentInstrumentationConfig.get() - .getString( - "otel.instrumentation.twitter-util-stats.metrics.name.delimiter", DEFAULT_DELIM); - - if (delim.isEmpty()) { - logger.warning("Delimiter must be non-empty"); - delim = DEFAULT_DELIM; - } else if (!delim.equals(".") && !delim.equals("/") && !delim.equals("-")) { - logger.warning("Unsupported delimiter detected: " + delim); - delim = DEFAULT_DELIM; - } - - delimiter = delim; - } - public OtelStatsReceiver() {} @Override @@ -86,158 +43,54 @@ public Object repr() { return this; } - @VisibleForTesting - static String nameConversion(MetricBuilder schema) { - return schema.identity().dimensionalName().mkString(delimiter); - } - - /* - Identical semantics to the finagle-stats PrometheusExporter. - */ - @VisibleForTesting - static boolean shouldEmit(MetricBuilder schema) { - return schema - .identity() - .identityType() - .bias(com.twitter.finagle.stats.MetricBuilder$IdentityType$HierarchicalOnly$.MODULE$) - == com.twitter.finagle.stats.MetricBuilder$IdentityType$Full$.MODULE$; - } - - /* - Guard against avoidable, incorrect metric names. - Finagle produces and emits a number of metrics which contain invalid chars and entirely free-form, - making them hard to adapt to the otel spec, whitespace, in particular. This, along with names - containing other identifying attributes which are unpredictable except in specific known cases - but which are indistinguishable from other potential metrics. IOW, a metric bearing some pattern - of identifying attributes in its name is indistinguishable from others created for a different - purpose and in a different context and is therefore not reasonably translated into otel metrics. - - Suggestion: write another instrumentation that adapts those specific cases. - */ - @VisibleForTesting - static boolean canEmit(MetricBuilder schema) { - return CollectionConverters.SeqHasAsJava(schema.identity().dimensionalName()).asJava().stream() - .allMatch(NAME_REGEX.asPredicate()); - } - - @VisibleForTesting - static Attributes attributesFromLabels(MetricBuilder builder) { - return builder - .identity() - .labels() - .foldLeft( - // put the hierarchical name as a label bc hierarchical names are not cleanly mapped - // to dimensional names via twitter stats or the final usage in finagle in a way otel - // can concisely reason about; by adding the label, downstream systems can aggregate - // on their own -- or not -- achieving a finer granularity without compromising - // the general patterns applied herein, and without the complexities applied in - // the finagle PrometheusExporter (implicitly, how the MetricsView & registries work, - // etc.), for example - Attributes.builder() - .put( - HIERARCHICAL_NAME_LABEL_KEY, - scala.jdk.CollectionConverters.SeqHasAsJava( - builder.identity().hierarchicalName()) - .asJava()), - (v1, v2) -> v1.put(v2._1(), v2._2())) - .build(); - } - - /* - Always use "byte" for size units to avoid precision loss. - Why: the aggregating services treat unit scales uniformly, so this should present little issue. - */ - // unchecked casting to efficiently handle the generic number conversion for long vs float - @SuppressWarnings("unchecked") - private static UnitConversion unitConverter( - MetricUnit unit, MetricBuilder.MetricType metricType) { - if (unit instanceof CustomUnit) { - return UnitConversion.create(((CustomUnit) unit).name().toLowerCase(Locale.getDefault())); - } else if (unit instanceof Unspecified$) { - return UnitConversion.create(); - } else if (unit instanceof Bytes$) { - return UnitConversion.create("byte"); - } else if (unit instanceof Kilobytes$) { - // base-10 to base-2 translation - if (metricType == MetricBuilder.CounterType$.MODULE$) { - return (UnitConversion) UnitConversion.create("byte", (Long x) -> (x * 1000)); - } else { - return (UnitConversion) UnitConversion.create("byte", (Float x) -> (x * 1000)); - } - } else if (unit instanceof Megabytes$) { - // base-10 to base-2 translation - if (metricType == MetricBuilder.CounterType$.MODULE$) { - return (UnitConversion) UnitConversion.create("byte", (Long x) -> (x * 1000 * 1000)); - } else { - return (UnitConversion) UnitConversion.create("byte", (Float x) -> (x * 1000 * 1000)); - } - } else if (unit instanceof Seconds$) { - return UnitConversion.create("second"); - } else if (unit instanceof Milliseconds$) { - return UnitConversion.create("millisecond"); - } else if (unit instanceof Microseconds$) { - return UnitConversion.create("microsecond"); - } else if (unit instanceof Requests$) { - return UnitConversion.create("request"); - } else if (unit instanceof Percentage$) { - return UnitConversion.create("percent"); - } else { - throw new IllegalArgumentException("unsupported metric unit: " + unit.toString()); - } - } - @Override public Stat stat(MetricBuilder schema) { - Attributes attributes = attributesFromLabels(schema); - UnitConversion conversion = unitConverter(schema.units(), schema.metricType()); + Attributes attributes = Helpers.attributesFromLabels(schema); + Helpers.UnitConversion conversion = + Helpers.unitConverter(schema.units(), schema.metricType()); - if (!canEmit(schema) && !shouldEmit(schema)) { - logger.fine("skipping stat: " + nameConversion(schema)); + if (!Helpers.canEmit(schema) && !Helpers.shouldEmit(schema)) { + logger.fine("skipping stat: " + Helpers.nameConversion(schema)); return OtelStat.createNull(schema, attributes, conversion); } - logger.fine("instrumenting stat: " + nameConversion(schema)); + logger.fine("instrumenting stat: " + Helpers.nameConversion(schema)); return OtelStat.create( schema, attributes, conversion, HISTO_MAP.computeIfAbsent( - nameConversion(schema), + // NOTE: histograms aren't native to twitter-util-stats but we're using the stat name + Helpers.nameConversion(schema), name -> { DoubleHistogramBuilder builder = meter.histogramBuilder(name).setDescription(schema.description()); if (conversion.getUnits().isPresent()) { builder = builder.setUnit(conversion.getUnits().get()); } - if (schema.percentiles().nonEmpty()) { - builder = - builder.setExplicitBucketBoundariesAdvice( - CollectionConverters.SeqHasAsJava(schema.percentiles()).asJava().stream() - .map(scala.Double::unbox) - .collect(Collectors.toList())); - } return builder.build(); })); } @Override public Gauge addGauge(MetricBuilder metricBuilder, scala.Function0 f) { - if (!canEmit(metricBuilder) && !shouldEmit(metricBuilder)) { - logger.fine("skipping gauge: " + nameConversion(metricBuilder)); - return OtelGauge.createNull(metricBuilder); + Attributes attributes = Helpers.attributesFromLabels(metricBuilder); + Helpers.UnitConversion conversion = + Helpers.unitConverter(metricBuilder.units(), metricBuilder.metricType()); + + if (!Helpers.canEmit(metricBuilder) && !Helpers.shouldEmit(metricBuilder)) { + logger.fine("skipping gauge: " + Helpers.nameConversion(metricBuilder)); + return OtelGauge.createNull(metricBuilder, attributes, conversion); } - logger.fine("instrumenting gauge: " + nameConversion(metricBuilder)); + logger.fine("instrumenting gauge: " + Helpers.nameConversion(metricBuilder)); return OtelGauge.create( metricBuilder, + attributes, + conversion, GAUGE_MAP.computeIfAbsent( - nameConversion(metricBuilder), + Helpers.nameConversion(metricBuilder), name -> { - Attributes attributes = attributesFromLabels(metricBuilder); - - UnitConversion conversion = - unitConverter(metricBuilder.units(), metricBuilder.metricType()); - DoubleGaugeBuilder builder = meter.gaugeBuilder(name).setDescription(metricBuilder.description()); if (conversion.getUnits().isPresent()) { @@ -251,23 +104,24 @@ public Gauge addGauge(MetricBuilder metricBuilder, scala.Function0 f) { @Override public Counter counter(MetricBuilder schema) { - Attributes attributes = attributesFromLabels(schema); - UnitConversion conversion = unitConverter(schema.units(), schema.metricType()); + Attributes attributes = Helpers.attributesFromLabels(schema); + Helpers.UnitConversion conversion = + Helpers.unitConverter(schema.units(), schema.metricType()); - if (!canEmit(schema) && !shouldEmit(schema)) { - logger.fine("skipping counter: " + nameConversion(schema)); + if (!Helpers.canEmit(schema) && !Helpers.shouldEmit(schema)) { + logger.fine("skipping counter: " + Helpers.nameConversion(schema)); return OtelCounter.createNull(schema, attributes, conversion); } - logger.fine("instrumenting counter: " + nameConversion(schema)); + logger.fine("instrumenting counter: " + Helpers.nameConversion(schema)); return OtelCounter.create( schema, attributes, conversion, COUNTER_MAP.computeIfAbsent( - nameConversion(schema), + Helpers.nameConversion(schema), name -> { - logger.fine("creating counter: " + nameConversion(schema)); + logger.fine("creating counter: " + Helpers.nameConversion(schema)); LongCounterBuilder builder = meter.counterBuilder(name).setDescription(schema.description()); if (conversion.getUnits().isPresent()) { @@ -277,42 +131,30 @@ public Counter counter(MetricBuilder schema) { })); } - @AutoValue - abstract static class UnitConversion { - abstract Optional getUnits(); - - abstract Function getConverter(); - - static UnitConversion create(String units, Function converter) { - return new AutoValue_OtelStatsReceiver_UnitConversion<>(Optional.of(units), converter); - } - - static UnitConversion create(String units) { - return create(units, Function.identity()); - } - - static UnitConversion create() { - return new AutoValue_OtelStatsReceiver_UnitConversion<>( - Optional.empty(), Function.identity()); - } - } - @AutoValue abstract static class OtelGauge implements Gauge { abstract MetricBuilder getSchema(); + abstract Attributes getAttributes(); + + abstract Helpers.UnitConversion getConversion(); + @Nullable abstract ObservableDoubleGauge getGauge(); - static OtelGauge createNull(MetricBuilder schema) { - return new AutoValue_OtelStatsReceiver_OtelGauge(schema, null); + static OtelGauge createNull( + MetricBuilder schema, Attributes attributes, Helpers.UnitConversion conversion) { + return new AutoValue_OtelStatsReceiver_OtelGauge(schema, attributes, conversion, null); } - static OtelGauge create(MetricBuilder schema, ObservableDoubleGauge gauge) { - return new AutoValue_OtelStatsReceiver_OtelGauge(schema, gauge); + static OtelGauge create( + MetricBuilder schema, + Attributes attributes, + Helpers.UnitConversion conversion, + ObservableDoubleGauge gauge) { + return new AutoValue_OtelStatsReceiver_OtelGauge(schema, attributes, conversion, gauge); } - @Memoized boolean isEmitted() { return getGauge() != null; } @@ -337,25 +179,24 @@ abstract static class OtelCounter implements Counter { abstract Attributes getAttributes(); - abstract UnitConversion getConversion(); + abstract Helpers.UnitConversion getConversion(); @Nullable abstract LongCounter getCounter(); static OtelCounter createNull( - MetricBuilder schema, Attributes attributes, UnitConversion conversion) { + MetricBuilder schema, Attributes attributes, Helpers.UnitConversion conversion) { return new AutoValue_OtelStatsReceiver_OtelCounter(schema, attributes, conversion, null); } static OtelCounter create( MetricBuilder schema, Attributes attributes, - UnitConversion conversion, + Helpers.UnitConversion conversion, LongCounter counter) { return new AutoValue_OtelStatsReceiver_OtelCounter(schema, attributes, conversion, counter); } - @Memoized boolean isEmitted() { return getCounter() != null; } @@ -380,25 +221,24 @@ abstract static class OtelStat implements Stat { abstract Attributes getAttributes(); - abstract UnitConversion getConversion(); + abstract Helpers.UnitConversion getConversion(); @Nullable abstract DoubleHistogram getHistogram(); static OtelStat createNull( - MetricBuilder schema, Attributes attributes, UnitConversion conversion) { + MetricBuilder schema, Attributes attributes, Helpers.UnitConversion conversion) { return new AutoValue_OtelStatsReceiver_OtelStat(schema, attributes, conversion, null); } static OtelStat create( MetricBuilder schema, Attributes attributes, - UnitConversion conversion, + Helpers.UnitConversion conversion, DoubleHistogram histogram) { return new AutoValue_OtelStatsReceiver_OtelStat(schema, attributes, conversion, histogram); } - @Memoized boolean isEmitted() { return getHistogram() != null; } diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiverProxy.java b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiverProxy.java index c82f21bd755a..8b8788c5f630 100644 --- a/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiverProxy.java +++ b/instrumentation/twitter-util-stats-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/OtelStatsReceiverProxy.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.javaagent.instrumentation.twitterutilstats.v23_11; import com.google.common.annotations.VisibleForTesting; diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/ClientTest.java b/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/ClientTest.java index 7fa72e30336d..c1ad295edd60 100644 --- a/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/ClientTest.java +++ b/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/ClientTest.java @@ -9,12 +9,14 @@ import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest.READ_TIMEOUT; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import com.google.common.collect.ImmutableMap; import com.twitter.finagle.Http; import com.twitter.finagle.ListeningServer; import com.twitter.finagle.Service; import com.twitter.finagle.http.Method; import com.twitter.finagle.http.Request; import com.twitter.finagle.http.Response; +import com.twitter.finagle.netty4.HashedWheelTimer$; import com.twitter.finagle.service.RetryBudget; import com.twitter.finagle.stats.Counter; import com.twitter.finagle.stats.CustomUnit; @@ -26,9 +28,10 @@ import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.testing.assertj.DoublePointAssert; +import io.opentelemetry.sdk.testing.assertj.HistogramPointAssert; import io.opentelemetry.sdk.testing.assertj.LongPointAssert; -import io.opentelemetry.testing.internal.armeria.internal.shaded.guava.collect.ImmutableMap; import java.net.URI; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -38,6 +41,7 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import scala.jdk.CollectionConverters; class ClientTest { private static final Logger logger = Logger.getLogger(ClientTest.class.getName()); @@ -207,5 +211,85 @@ public Future apply(Request request) { .hasDoubleGaugeSatisfying( sum -> sum.hasPointsSatisfying(collect)))); }); + + // stop this otherwise some metrics will keep emitting + HashedWheelTimer$.MODULE$.stop(); + + TestStatsReceiver.getInstance() + .getStats() + .asMap() + .forEach( + (name, stats) -> { + List> collect = + stats.stream() + .filter( + stat -> { + if (!stat.isInitialized()) { + logger.info("uninitialized: " + name); + // skip uninitialized bc they won't have + // default values assigned in otel + return false; + } + if (!stat.getCounterpart().isEmitted()) { + logger.info("skipped: " + name); + return false; + } + logger.info(name + ": " + stat.getStat()); + return true; + }) + .map( + stat -> { + List collected = + CollectionConverters.SeqHasAsJava(stat.getStat().apply()) + .asJava() + .stream() + .mapToDouble(x -> (float) x) + .boxed() + .collect(Collectors.toList()); + System.out.println( + "stat: " + + stat.getStat() + + " -> " + + stat.getStat().apply() + + " { count=" + + collected.size() + + ", sum=" + + collected.stream().reduce(0d, Double::sum) + + ", min=" + + collected.stream().min(Comparator.naturalOrder()).orElse(0d) + + ", max=" + + collected.stream().max(Comparator.naturalOrder()).orElse(0d) + + " }"); + return (Consumer) + points -> + points + .hasMin( + collected.stream() + .min(Comparator.naturalOrder()) + .orElse(0d)) + .hasMax( + collected.stream() + .max(Comparator.naturalOrder()) + .orElse(0d)); + }) + .collect(Collectors.toList()); + + if (collect.isEmpty()) { + logger.info("unset; skipping all assertions: " + name); + return; + } + + // NOTE: choosing to be very lax with the histogram assertions as twitter-util-stats + // only supports Summary types + testing.waitAndAssertMetrics( + "twitter-util-stats", + name, + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasHistogramSatisfying( + histo -> histo.hasPointsSatisfying(collect)))); + }); } } diff --git a/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/TestStatsReceiver.java b/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/TestStatsReceiver.java index 6843c6da6166..e80f18e21b84 100644 --- a/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/TestStatsReceiver.java +++ b/instrumentation/twitter-util-stats-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twitterutilstats/v23_11/TestStatsReceiver.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.javaagent.instrumentation.twitterutilstats.v23_11; import com.google.common.collect.ImmutableSetMultimap; @@ -42,7 +47,7 @@ public Multimap getCounters() { return counters.entrySet().stream() .collect( ImmutableSetMultimap.toImmutableSetMultimap( - k -> OtelStatsReceiver.nameConversion(k.getKey()), Map.Entry::getValue)); + k -> Helpers.nameConversion(k.getKey()), Map.Entry::getValue)); } @Override @@ -50,7 +55,7 @@ public Multimap getStats() { return stats.entrySet().stream() .collect( ImmutableSetMultimap.toImmutableSetMultimap( - k -> OtelStatsReceiver.nameConversion(k.getKey()), Map.Entry::getValue)); + k -> Helpers.nameConversion(k.getKey()), Map.Entry::getValue)); } @Override @@ -58,7 +63,7 @@ public Multimap getGauges() { return gauges.entrySet().stream() .collect( ImmutableSetMultimap.toImmutableSetMultimap( - k -> OtelStatsReceiver.nameConversion(k.getKey()), Map.Entry::getValue)); + k -> Helpers.nameConversion(k.getKey()), Map.Entry::getValue)); } @Override