diff --git a/build.sbt b/build.sbt index a7d5c6c52..b29ae16d1 100644 --- a/build.sbt +++ b/build.sbt @@ -225,7 +225,10 @@ lazy val `sdk-common` = crossProject(JVMPlatform, JSPlatform, NativePlatform) lazy val `sdk-metrics` = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("sdk/metrics")) - .dependsOn(`sdk-common` % "compile->compile;test->test", `core-metrics`) + .dependsOn( + `sdk-common` % "compile->compile;test->test", + `core-metrics` % "compile->compile;test->test" + ) .settings( name := "otel4s-sdk-metrics", startYear := Some(2024), @@ -300,7 +303,8 @@ lazy val sdk = crossProject(JVMPlatform, JSPlatform, NativePlatform) .dependsOn( core, `sdk-common`, - `sdk-metrics`, + `sdk-metrics` % "compile->compile;test->test", + `sdk-metrics-testkit` % Test, `sdk-trace` % "compile->compile;test->test", `sdk-trace-testkit` % Test ) @@ -459,7 +463,10 @@ lazy val `oteljava-common-testkit` = project lazy val `oteljava-metrics` = project .in(file("oteljava/metrics")) - .dependsOn(`oteljava-common`, `core-metrics`.jvm) + .dependsOn( + `oteljava-common`, + `core-metrics`.jvm % "compile->compile;test->test" + ) .settings(munitDependencies) .settings( name := "otel4s-oteljava-metrics", @@ -520,7 +527,7 @@ lazy val oteljava = project .in(file("oteljava/all")) .dependsOn( core.jvm, - `oteljava-metrics`, + `oteljava-metrics` % "compile->compile;test->test", `oteljava-metrics-testkit` % Test, `oteljava-trace` % "compile->compile;test->test", `oteljava-trace-testkit` % Test diff --git a/core/metrics/src/test/scala/org/typelevel/otel4s/metrics/BaseMeterSuite.scala b/core/metrics/src/test/scala/org/typelevel/otel4s/metrics/BaseMeterSuite.scala new file mode 100644 index 000000000..7400d145a --- /dev/null +++ b/core/metrics/src/test/scala/org/typelevel/otel4s/metrics/BaseMeterSuite.scala @@ -0,0 +1,429 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.metrics + +import cats.effect.IO +import cats.effect.Resource +import cats.effect.testkit.TestControl +import cats.mtl.Local +import cats.syntax.traverse._ +import munit.CatsEffectSuite +import munit.Location +import munit.TestOptions +import org.typelevel.otel4s.Attributes + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration._ + +abstract class BaseMeterSuite extends CatsEffectSuite { + import BaseMeterSuite._ + + type Ctx + + sdkTest("Counter - accept only positive values") { sdk => + for { + meter <- sdk.provider.get("meter") + counter <- meter.counter[Long]("counter").create + _ <- counter.add(-1L) + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, Nil) + } + + sdkTest("Counter - record values") { sdk => + val expected = MetricData.sum("counter", monotonic = true, 5L, Some(5L)) + + for { + meter <- sdk.provider.get("meter") + counter <- meter.counter[Long]("counter").create + _ <- counter.add(5L) + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("Counter - increment") { sdk => + val expected = MetricData.sum("counter", monotonic = true, 1L, Some(1L)) + + for { + meter <- sdk.provider.get("meter") + counter <- meter.counter[Long]("counter").create + _ <- counter.inc() + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("UpDownCounter - record values") { sdk => + val expected = MetricData.sum("counter", monotonic = false, 3L, Some(3L)) + + for { + meter <- sdk.provider.get("meter") + counter <- meter.upDownCounter[Long]("counter").create + _ <- counter.add(3L) + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("UpDownCounter - increment") { sdk => + val expected = MetricData.sum("counter", monotonic = false, 1L, Some(1L)) + + for { + meter <- sdk.provider.get("meter") + counter <- meter.upDownCounter[Long]("counter").create + _ <- counter.inc() + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("UpDownCounter - decrement") { sdk => + val expected = MetricData.sum("counter", monotonic = false, -1L, Some(-1L)) + + for { + meter <- sdk.provider.get("meter") + counter <- meter.upDownCounter[Long]("counter").create + _ <- counter.dec() + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("Histogram - allow only non-negative values") { sdk => + for { + meter <- sdk.provider.get("meter") + histogram <- meter.histogram[Double]("histogram").create + _ <- histogram.record(-1.0) + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, Nil) + } + + sdkTest("Histogram - record values") { sdk => + val values = List(1.0, 2.0, 3.0) + val expected = MetricData.histogram("histogram", values, Some(3.0)) + + for { + meter <- sdk.provider.get("meter") + histogram <- meter.histogram[Double]("histogram").create + _ <- values.traverse(value => histogram.record(value)) + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("Histogram - record duration") { sdk => + val duration = 100.nanos + val expected = MetricData.histogram( + "histogram", + List(duration.toNanos.toDouble) + ) + + TestControl.executeEmbed { + for { + meter <- sdk.provider.get("meter") + histogram <- meter.histogram[Double]("histogram").create + _ <- histogram + .recordDuration(TimeUnit.NANOSECONDS) + .surround(IO.sleep(duration)) + metrics <- sdk.collectMetrics + } yield assertEquals(metrics, List(expected)) + } + } + + sdkTest("ObservableCounter - record values") { sdk => + val expected = MetricData.sum("counter", monotonic = true, 1L) + + for { + meter <- sdk.provider.get("meter") + metrics <- meter + .observableCounter[Long]("counter") + .createWithCallback(cb => cb.record(1L)) + .surround(sdk.collectMetrics) + } yield assertEquals(metrics, List(expected)) + } + + sdkTest( + "ObservableCounter - multiple values for same attributes - retain first" + ) { sdk => + val expected = MetricData.sum("counter", monotonic = true, 1L) + + for { + meter <- sdk.provider.get("meter") + metrics <- meter + .observableCounter[Long]("counter") + .createWithCallback { cb => + cb.record(1L) >> cb.record(2L) >> cb.record(3L) + } + .surround(sdk.collectMetrics) + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("ObservableUpDownCounter - record values") { sdk => + val expected = MetricData.sum("counter", monotonic = false, 1L) + + for { + meter <- sdk.provider.get("meter") + metrics <- meter + .observableUpDownCounter[Long]("counter") + .createWithCallback(cb => cb.record(1L)) + .surround(sdk.collectMetrics) + } yield assertEquals(metrics, List(expected)) + } + + sdkTest( + "ObservableUpDownCounter - multiple values for same attributes - retain first" + ) { sdk => + val expected = MetricData.sum("counter", monotonic = false, 1L) + + for { + meter <- sdk.provider.get("meter") + metrics <- meter + .observableUpDownCounter[Long]("counter") + .createWithCallback { cb => + cb.record(1L) >> cb.record(2L) >> cb.record(3L) + } + .surround(sdk.collectMetrics) + } yield assertEquals(metrics, List(expected)) + } + + sdkTest("ObservableGauge - record values") { sdk => + val expected = MetricData.gauge("gauge", 1L) + + for { + meter <- sdk.provider.get("meter") + metrics <- meter + .observableGauge[Long]("gauge") + .createWithCallback(cb => cb.record(1L)) + .surround(sdk.collectMetrics) + } yield assertEquals(metrics, List(expected)) + } + + sdkTest( + "ObservableGauge - multiple values for same attributes - retain first" + ) { sdk => + val expected = MetricData.gauge("gauge", 1L) + + for { + meter <- sdk.provider.get("meter") + metrics <- meter + .observableGauge[Long]("gauge") + .createWithCallback { cb => + cb.record(1L) >> cb.record(2L) >> cb.record(3L) + } + .surround(sdk.collectMetrics) + } yield assertEquals(metrics, List(expected)) + } + + private def sdkTest[A]( + options: TestOptions + )(body: Sdk[Ctx] => IO[A])(implicit loc: Location): Unit = { + val io = makeSdk.use { sdk => + sdk.local.scope(body(sdk))(tracedContext(TraceId, SpanId)) + } + test(options)(transform(io)) + } + + protected def tracedContext(traceId: String, spanId: String): Ctx + + protected def transform[A](io: IO[A]): IO[A] = io + + protected def makeSdk: Resource[IO, Sdk[Ctx]] + +} + +object BaseMeterSuite { + + private val TraceId = "c0d6e01941825730ffedcd00768eb663" + private val SpanId = "69568a2f0ba45094" + + sealed trait AggregationTemporality + object AggregationTemporality { + case object Delta extends AggregationTemporality + case object Cumulative extends AggregationTemporality + } + + final case class Exemplar( + attributes: Attributes, + timestamp: FiniteDuration, + traceId: Option[String], + spanId: Option[String], + value: Either[Long, Double] + ) + + sealed trait PointData { + def start: FiniteDuration + def end: FiniteDuration + def attributes: Attributes + } + + object PointData { + final case class NumberPoint( + start: FiniteDuration, + end: FiniteDuration, + attributes: Attributes, + value: Either[Long, Double], + exemplars: Vector[Exemplar] + ) extends PointData + + final case class Histogram( + start: FiniteDuration, + end: FiniteDuration, + attributes: Attributes, + sum: Option[Double], + min: Option[Double], + max: Option[Double], + count: Option[Long], + boundaries: BucketBoundaries, + counts: Vector[Long], + exemplars: Vector[Exemplar] + ) extends PointData + } + + sealed trait MetricPoints + object MetricPoints { + final case class Sum( + points: Vector[PointData.NumberPoint], + monotonic: Boolean, + aggregationTemporality: AggregationTemporality + ) extends MetricPoints + + final case class Gauge(points: Vector[PointData.NumberPoint]) + extends MetricPoints + + final case class Histogram( + points: Vector[PointData.Histogram], + aggregationTemporality: AggregationTemporality + ) extends MetricPoints + } + + final case class MetricData( + name: String, + description: Option[String], + unit: Option[String], + data: MetricPoints + ) + + object MetricData { + private val DefaultBoundaries = BucketBoundaries( + Vector( + 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, + 2500.0, 5000.0, 7500.0, 10000.0 + ) + ) + + def sum( + name: String, + monotonic: Boolean, + value: Long, + exemplarValue: Option[Long] = None + ): MetricData = + MetricData( + name, + None, + None, + MetricPoints.Sum( + points = Vector( + PointData.NumberPoint( + Duration.Zero, + Duration.Zero, + Attributes.empty, + Left(value), + exemplarValue.toVector.map { exemplar => + Exemplar( + Attributes.empty, + Duration.Zero, + Some(TraceId), + Some(SpanId), + Left(exemplar) + ) + } + ) + ), + monotonic = monotonic, + aggregationTemporality = AggregationTemporality.Cumulative + ) + ) + + def gauge(name: String, value: Long): MetricData = + MetricData( + name, + None, + None, + MetricPoints.Gauge( + points = Vector( + PointData.NumberPoint( + Duration.Zero, + Duration.Zero, + Attributes.empty, + Left(value), + Vector.empty + ) + ) + ) + ) + + def histogram( + name: String, + values: List[Double], + exemplarValue: Option[Double] = None + ): MetricData = { + val boundaries = DefaultBoundaries + + val counts: Vector[Long] = + values.foldLeft(Vector.fill(boundaries.length + 1)(0L)) { + case (acc, value) => + val i = boundaries.boundaries.indexWhere(b => value <= b) + val idx = if (i == -1) boundaries.length else i + + acc.updated(idx, acc(idx) + 1L) + } + + MetricData( + name, + None, + None, + MetricPoints.Histogram( + points = Vector( + PointData.Histogram( + Duration.Zero, + Duration.Zero, + Attributes.empty, + Some(values.sum), + Some(values.min), + Some(values.max), + Some(values.size.toLong), + boundaries, + counts, + exemplarValue.toVector.map { exemplar => + Exemplar( + Attributes.empty, + Duration.Zero, + Some(TraceId), + Some(SpanId), + Right(exemplar) + ) + } + ) + ), + aggregationTemporality = AggregationTemporality.Cumulative + ) + ) + } + + } + + trait Sdk[Ctx] { + def provider: MeterProvider[IO] + def collectMetrics: IO[List[MetricData]] + def local: Local[IO, Ctx] + } + +} diff --git a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/BatchCallbackSuite.scala b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/BatchCallbackSuite.scala index 30438f80f..2a058058a 100644 --- a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/BatchCallbackSuite.scala +++ b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/BatchCallbackSuite.scala @@ -17,14 +17,19 @@ package org.typelevel.otel4s.oteljava.metrics import cats.effect.IO +import cats.mtl.Ask import munit.CatsEffectSuite import org.typelevel.otel4s.Attribute import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.oteljava.context.AskContext +import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.testkit.metrics.MetricsTestkit import org.typelevel.otel4s.oteljava.testkit.metrics.data.Metric class BatchCallbackSuite extends CatsEffectSuite { + private implicit val askContext: AskContext[IO] = Ask.const(Context.root) + test("update multiple observers") { MetricsTestkit.inMemory[IO]().use { metrics => for { diff --git a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/CounterSuite.scala b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/CounterSuite.scala index 216d73d18..a1367e6d5 100644 --- a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/CounterSuite.scala +++ b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/CounterSuite.scala @@ -20,8 +20,11 @@ package metrics import cats.effect.IO import cats.effect.Resource +import cats.mtl.Ask import io.opentelemetry.sdk.metrics._ import munit.CatsEffectSuite +import org.typelevel.otel4s.oteljava.context.AskContext +import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.testkit.InstrumentationScope import org.typelevel.otel4s.oteljava.testkit.TelemetryResource import org.typelevel.otel4s.oteljava.testkit.metrics.MetricsTestkit @@ -81,6 +84,8 @@ class CounterSuite extends CatsEffectSuite { } private def makeSdk: Resource[IO, MetricsTestkit[IO]] = { + implicit val askContext: AskContext[IO] = Ask.const(Context.root) + def customize(builder: SdkMeterProviderBuilder): SdkMeterProviderBuilder = builder.registerView( InstrumentSelector.builder().setType(InstrumentType.HISTOGRAM).build(), diff --git a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/MeterSuite.scala b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/MeterSuite.scala new file mode 100644 index 000000000..71a3e5548 --- /dev/null +++ b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/MeterSuite.scala @@ -0,0 +1,200 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.oteljava.metrics + +import cats.effect.IO +import cats.effect.Resource +import cats.mtl.Local +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.TraceFlags +import io.opentelemetry.api.trace.TraceState +import io.opentelemetry.context.{Context => JContext} +import io.opentelemetry.sdk.common.Clock +import io.opentelemetry.sdk.metrics.data.AggregationTemporality +import io.opentelemetry.sdk.metrics.data.DoubleExemplarData +import io.opentelemetry.sdk.metrics.data.DoublePointData +import io.opentelemetry.sdk.metrics.data.ExemplarData +import io.opentelemetry.sdk.metrics.data.HistogramPointData +import io.opentelemetry.sdk.metrics.data.LongExemplarData +import io.opentelemetry.sdk.metrics.data.LongPointData +import io.opentelemetry.sdk.metrics.data.MetricDataType +import io.opentelemetry.sdk.metrics.data.PointData +import org.typelevel.otel4s.context.LocalProvider +import org.typelevel.otel4s.metrics.BaseMeterSuite +import org.typelevel.otel4s.metrics.BucketBoundaries +import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.oteljava.AttributeConverters._ +import org.typelevel.otel4s.oteljava.context.Context +import org.typelevel.otel4s.oteljava.testkit.metrics.MetricsTestkit + +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class MeterSuite extends BaseMeterSuite { + + type MetricData = io.opentelemetry.sdk.metrics.data.MetricData + type Ctx = Context + + protected def tracedContext(traceId: String, spanId: String): Context = { + val spanContext = SpanContext.create( + traceId, + spanId, + TraceFlags.getSampled, + TraceState.getDefault + ) + + Context.wrap( + Span.wrap(spanContext).storeInContext(JContext.root()) + ) + } + + protected def makeSdk: Resource[IO, BaseMeterSuite.Sdk[Ctx]] = + Resource + .eval(LocalProvider[IO, Context].local) + .flatMap { implicit localContext => + val clock = new Clock { + def now(): Long = 0L + def nanoTime(): Long = 0L + } + + MetricsTestkit + .inMemory[IO](_.setClock(clock)) + .map { metrics => + new BaseMeterSuite.Sdk[Ctx] { + def provider: MeterProvider[IO] = + metrics.meterProvider + + def collectMetrics: IO[List[BaseMeterSuite.MetricData]] = + metrics.collectMetrics[MetricData].map(_.map(toMetricData)) + + def local: Local[IO, Context] = + localContext + } + } + } + + private def toMetricData(md: MetricData): BaseMeterSuite.MetricData = { + def temporality( + aggregationTemporality: AggregationTemporality + ): BaseMeterSuite.AggregationTemporality = + aggregationTemporality match { + case AggregationTemporality.DELTA => + BaseMeterSuite.AggregationTemporality.Delta + case AggregationTemporality.CUMULATIVE => + BaseMeterSuite.AggregationTemporality.Cumulative + } + + def toExemplar(exemplar: ExemplarData) = { + val value = exemplar match { + case long: LongExemplarData => Left(long.getValue) + case double: DoubleExemplarData => Right(double.getValue) + case other => sys.error(s"unknown exemplar $other") + } + + // OtelJava exemplar reservoirs always use Clock.getDefault + // this makes testing difficult, so we always use 0L + + BaseMeterSuite.Exemplar( + exemplar.getFilteredAttributes.toScala, + Duration.Zero, // exemplar.getEpochNanos.nanos, + Option(exemplar.getSpanContext.getTraceId), + Option(exemplar.getSpanContext.getSpanId), + value + ) + } + + def toNumberPoint(number: PointData) = { + val value = number match { + case long: LongPointData => Left(long.getValue) + case double: DoublePointData => Right(double.getValue) + case other => sys.error(s"unknown point data $other") + } + + BaseMeterSuite.PointData.NumberPoint( + number.getStartEpochNanos.nanos, + number.getEpochNanos.nanos, + number.getAttributes.toScala, + value, + number.getExemplars.asScala.toVector.map(toExemplar) + ) + } + + def toHistogramPoint(histogram: HistogramPointData) = + BaseMeterSuite.PointData.Histogram( + histogram.getStartEpochNanos.nanos, + histogram.getEpochNanos.nanos, + histogram.getAttributes.toScala, + Option.when(histogram.getCount > 0L)(histogram.getSum), + Option.when(histogram.hasMin)(histogram.getMin), + Option.when(histogram.hasMax)(histogram.getMax), + Option.when(histogram.getCount > 0L)(histogram.getCount), + BucketBoundaries( + histogram.getBoundaries.asScala.toVector.map(Double.unbox) + ), + histogram.getCounts.asScala.toVector.map(Long.unbox), + histogram.getExemplars.asScala.toVector.map(toExemplar) + ) + + val data = md.getType match { + case MetricDataType.LONG_GAUGE => + BaseMeterSuite.MetricPoints.Gauge( + md.getLongGaugeData.getPoints.asScala.toVector.map(toNumberPoint) + ) + + case MetricDataType.DOUBLE_GAUGE => + BaseMeterSuite.MetricPoints.Gauge( + md.getDoubleGaugeData.getPoints.asScala.toVector.map(toNumberPoint) + ) + + case MetricDataType.LONG_SUM => + val sum = md.getLongSumData + BaseMeterSuite.MetricPoints.Sum( + sum.getPoints.asScala.toVector.map(toNumberPoint), + sum.isMonotonic, + temporality(sum.getAggregationTemporality) + ) + + case MetricDataType.DOUBLE_SUM => + val sum = md.getDoubleSumData + BaseMeterSuite.MetricPoints.Sum( + sum.getPoints.asScala.toVector.map(toNumberPoint), + sum.isMonotonic, + temporality(sum.getAggregationTemporality) + ) + + case MetricDataType.HISTOGRAM => + val histogram = md.getHistogramData + + BaseMeterSuite.MetricPoints.Histogram( + histogram.getPoints.asScala.toVector.map(toHistogramPoint), + temporality(histogram.getAggregationTemporality) + ) + + case other => + sys.error(s"unsupported metric data type $other") + } + + BaseMeterSuite.MetricData( + md.getName, + Option(md.getDescription).filter(_.nonEmpty), + Option(md.getUnit).filter(_.nonEmpty), + data + ) + } + +} diff --git a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/ObservableSuite.scala b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/ObservableSuite.scala index 0c9ee6663..027653f13 100644 --- a/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/ObservableSuite.scala +++ b/oteljava/all/src/test/scala/org/typelevel/otel4s/oteljava/metrics/ObservableSuite.scala @@ -19,13 +19,18 @@ package oteljava package metrics import cats.effect.IO +import cats.mtl.Ask import munit.CatsEffectSuite import org.typelevel.otel4s.metrics.Measurement +import org.typelevel.otel4s.oteljava.context.AskContext +import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.oteljava.testkit.metrics.MetricsTestkit import org.typelevel.otel4s.oteljava.testkit.metrics.data.Metric class ObservableSuite extends CatsEffectSuite { + private implicit val askContext: AskContext[IO] = Ask.const(Context.root) + test("gauge test") { MetricsTestkit.inMemory[IO]().use { sdk => for { diff --git a/oteljava/metrics-testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/metrics/MetricsTestkit.scala b/oteljava/metrics-testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/metrics/MetricsTestkit.scala index c0e461ec9..6a0153a0a 100644 --- a/oteljava/metrics-testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/metrics/MetricsTestkit.scala +++ b/oteljava/metrics-testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/metrics/MetricsTestkit.scala @@ -22,6 +22,7 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.oteljava.context.AskContext import org.typelevel.otel4s.oteljava.metrics.MeterProviderImpl import scala.jdk.CollectionConverters._ @@ -57,7 +58,7 @@ object MetricsTestkit { * @param customize * the customization of the builder */ - def inMemory[F[_]: Async]( + def inMemory[F[_]: Async: AskContext]( customize: SdkMeterProviderBuilder => SdkMeterProviderBuilder = identity ): Resource[F, MetricsTestkit[F]] = { diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/ContextUtils.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/ContextUtils.scala new file mode 100644 index 000000000..87d10d8fa --- /dev/null +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/ContextUtils.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.oteljava.metrics + +import cats.effect.Sync +import cats.mtl.Ask +import cats.syntax.flatMap._ +import org.typelevel.otel4s.oteljava.context.AskContext +import org.typelevel.otel4s.oteljava.context.Context + +private object ContextUtils { + + def delayWithContext[F[_]: Sync: AskContext, A](f: () => A): F[A] = + Ask[F, Context].ask.flatMap { ctx => + Sync[F].delay { + // make the current context active + val scope = ctx.underlying.makeCurrent() + try { + f() + } finally { + scope.close() // release the context + } + } + } + +} diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/CounterBuilderImpl.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/CounterBuilderImpl.scala index b5fb40331..4917fcccf 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/CounterBuilderImpl.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/CounterBuilderImpl.scala @@ -23,6 +23,7 @@ import io.opentelemetry.api.metrics.{Meter => JMeter} import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.metrics._ import org.typelevel.otel4s.oteljava.AttributeConverters._ +import org.typelevel.otel4s.oteljava.context.AskContext import scala.collection.immutable @@ -46,7 +47,7 @@ private[oteljava] case class CounterBuilderImpl[F[_], A]( private[oteljava] object CounterBuilderImpl { - def apply[F[_]: Sync, A: MeasurementValue]( + def apply[F[_]: Sync: AskContext, A: MeasurementValue]( jMeter: JMeter, name: String ): Counter.Builder[F, A] = @@ -66,7 +67,7 @@ private[oteljava] object CounterBuilderImpl { ): F[Counter[F, A]] } - private def longFactory[F[_]: Sync, A]( + private def longFactory[F[_]: Sync: AskContext, A]( jMeter: JMeter, cast: A => Long ): Factory[F, A] = @@ -89,21 +90,25 @@ private[oteljava] object CounterBuilderImpl { value: A, attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = - Sync[F].delay( - counter.add(cast(value), attributes.toJavaAttributes) - ) + record(cast(value), attributes) def inc(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = - Sync[F].delay( - counter.add(1L, attributes.toJavaAttributes) - ) + record(1L, attributes) + + private def record( + value: Long, + attributes: immutable.Iterable[Attribute[_]] + ): F[Unit] = + ContextUtils.delayWithContext { () => + counter.add(value, attributes.toJavaAttributes) + } } Counter.fromBackend(backend) } } - private def doubleFactory[F[_]: Sync, A]( + private def doubleFactory[F[_]: Sync: AskContext, A]( jMeter: JMeter, cast: A => Double ): Factory[F, A] = @@ -126,14 +131,18 @@ private[oteljava] object CounterBuilderImpl { value: A, attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = - Sync[F].delay( - counter.add(cast(value), attributes.toJavaAttributes) - ) + record(cast(value), attributes) def inc(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = - Sync[F].delay( - counter.add(1.0, attributes.toJavaAttributes) - ) + record(1.0, attributes) + + private def record( + value: Double, + attributes: immutable.Iterable[Attribute[_]] + ): F[Unit] = + ContextUtils.delayWithContext { () => + counter.add(value, attributes.toJavaAttributes) + } } Counter.fromBackend(backend) diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/HistogramBuilderImpl.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/HistogramBuilderImpl.scala index 9cec82746..248b5e498 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/HistogramBuilderImpl.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/HistogramBuilderImpl.scala @@ -25,6 +25,7 @@ import cats.syntax.functor._ import io.opentelemetry.api.metrics.{Meter => JMeter} import org.typelevel.otel4s.metrics._ import org.typelevel.otel4s.oteljava.AttributeConverters._ +import org.typelevel.otel4s.oteljava.context.AskContext import scala.collection.immutable import scala.concurrent.duration.TimeUnit @@ -56,7 +57,7 @@ private[oteljava] case class HistogramBuilderImpl[F[_], A]( object HistogramBuilderImpl { - def apply[F[_]: Sync, A: MeasurementValue]( + def apply[F[_]: Sync: AskContext, A: MeasurementValue]( jMeter: JMeter, name: String ): Histogram.Builder[F, A] = @@ -77,7 +78,7 @@ object HistogramBuilderImpl { ): F[Histogram[F, A]] } - private def longFactory[F[_]: Sync, A]( + private def longFactory[F[_]: Sync: AskContext, A]( jMeter: JMeter, cast: A => Long ): Factory[F, A] = @@ -106,12 +107,7 @@ object HistogramBuilderImpl { value: A, attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = - Sync[F].delay( - histogram.record( - cast(value), - attributes.toJavaAttributes - ) - ) + doRecord(cast(value), attributes) def recordDuration( timeUnit: TimeUnit, @@ -121,24 +117,28 @@ object HistogramBuilderImpl { .makeCase(Sync[F].monotonic) { case (start, ec) => for { end <- Sync[F].monotonic - _ <- Sync[F].delay( - histogram.record( - (end - start).toUnit(timeUnit).toLong, - ( - attributes ++ Histogram.causeAttributes(ec) - ).toJavaAttributes - ) + _ <- doRecord( + (end - start).toUnit(timeUnit).toLong, + attributes ++ Histogram.causeAttributes(ec) ) } yield () } .void + + private def doRecord( + value: Long, + attributes: immutable.Iterable[Attribute[_]] + ): F[Unit] = + ContextUtils.delayWithContext { () => + histogram.record(value, attributes.toJavaAttributes) + } } Histogram.fromBackend(backend) } } - private def doubleFactory[F[_]: Sync, A]( + private def doubleFactory[F[_]: Sync: AskContext, A]( jMeter: JMeter, cast: A => Double ): Factory[F, A] = @@ -167,12 +167,7 @@ object HistogramBuilderImpl { value: A, attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = - Sync[F].delay( - histogram.record( - cast(value), - attributes.toJavaAttributes - ) - ) + doRecord(cast(value), attributes) def recordDuration( timeUnit: TimeUnit, @@ -182,17 +177,21 @@ object HistogramBuilderImpl { .makeCase(Sync[F].monotonic) { case (start, ec) => for { end <- Sync[F].monotonic - _ <- Sync[F].delay( - histogram.record( - (end - start).toUnit(timeUnit), - ( - attributes ++ Histogram.causeAttributes(ec) - ).toJavaAttributes - ) + _ <- doRecord( + (end - start).toUnit(timeUnit), + attributes ++ Histogram.causeAttributes(ec) ) } yield () } .void + + private def doRecord( + value: Double, + attributes: immutable.Iterable[Attribute[_]] + ): F[Unit] = + ContextUtils.delayWithContext { () => + histogram.record(value, attributes.toJavaAttributes) + } } Histogram.fromBackend(backend) diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterBuilderImpl.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterBuilderImpl.scala index 6c9a2438a..3842ddb89 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterBuilderImpl.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterBuilderImpl.scala @@ -21,8 +21,9 @@ package metrics import cats.effect.kernel.Async import io.opentelemetry.api.metrics.{MeterProvider => JMeterProvider} import org.typelevel.otel4s.metrics._ +import org.typelevel.otel4s.oteljava.context.AskContext -private[oteljava] case class MeterBuilderImpl[F[_]]( +private[oteljava] case class MeterBuilderImpl[F[_]: AskContext]( jMeterProvider: JMeterProvider, name: String, version: Option[String] = None, diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterImpl.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterImpl.scala index ffd79d521..c3ed10473 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterImpl.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterImpl.scala @@ -21,8 +21,9 @@ package metrics import cats.effect.kernel.Async import io.opentelemetry.api.metrics.{Meter => JMeter} import org.typelevel.otel4s.metrics._ +import org.typelevel.otel4s.oteljava.context.AskContext -private[oteljava] class MeterImpl[F[_]: Async](jMeter: JMeter) +private[oteljava] class MeterImpl[F[_]: Async: AskContext](jMeter: JMeter) extends Meter[F] { def counter[A: MeasurementValue](name: String): Counter.Builder[F, A] = diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterProviderImpl.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterProviderImpl.scala index 475619686..cfb774cac 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterProviderImpl.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/MeterProviderImpl.scala @@ -22,8 +22,9 @@ import cats.effect.kernel.Async import io.opentelemetry.api.metrics.{MeterProvider => JMeterProvider} import org.typelevel.otel4s.metrics.MeterBuilder import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.oteljava.context.AskContext -private[oteljava] class MeterProviderImpl[F[_]: Async]( +private[oteljava] class MeterProviderImpl[F[_]: Async: AskContext]( jMeterProvider: JMeterProvider ) extends MeterProvider[F] { diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala index 65230bd10..e63ae8518 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/Metrics.scala @@ -19,6 +19,7 @@ package org.typelevel.otel4s.oteljava.metrics import cats.effect.kernel.Async import io.opentelemetry.api.{OpenTelemetry => JOpenTelemetry} import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.oteljava.context.AskContext trait Metrics[F[_]] { def meterProvider: MeterProvider[F] @@ -26,7 +27,7 @@ trait Metrics[F[_]] { object Metrics { - def forAsync[F[_]: Async](jOtel: JOpenTelemetry): Metrics[F] = + def forAsync[F[_]: Async: AskContext](jOtel: JOpenTelemetry): Metrics[F] = new Metrics[F] { val meterProvider: MeterProvider[F] = new MeterProviderImpl[F](jOtel.getMeterProvider) diff --git a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/UpDownCounterBuilderImpl.scala b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/UpDownCounterBuilderImpl.scala index 8fd284069..36cac42e3 100644 --- a/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/UpDownCounterBuilderImpl.scala +++ b/oteljava/metrics/src/main/scala/org/typelevel/otel4s/oteljava/metrics/UpDownCounterBuilderImpl.scala @@ -23,6 +23,7 @@ import io.opentelemetry.api.metrics.{Meter => JMeter} import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.metrics._ import org.typelevel.otel4s.oteljava.AttributeConverters._ +import org.typelevel.otel4s.oteljava.context.AskContext import scala.collection.immutable @@ -45,7 +46,7 @@ private[oteljava] case class UpDownCounterBuilderImpl[F[_], A]( private[oteljava] object UpDownCounterBuilderImpl { - def apply[F[_]: Sync, A: MeasurementValue]( + def apply[F[_]: Sync: AskContext, A: MeasurementValue]( jMeter: JMeter, name: String ): UpDownCounter.Builder[F, A] = @@ -65,7 +66,7 @@ private[oteljava] object UpDownCounterBuilderImpl { ): F[UpDownCounter[F, A]] } - private def longFactory[F[_]: Sync, A]( + private def longFactory[F[_]: Sync: AskContext, A]( jMeter: JMeter, cast: A => Long ): Factory[F, A] = @@ -88,26 +89,28 @@ private[oteljava] object UpDownCounterBuilderImpl { value: A, attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = - Sync[F].delay( - counter.add(cast(value), attributes.toJavaAttributes) - ) + record(cast(value), attributes) def inc(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = - Sync[F].delay( - counter.add(1L, attributes.toJavaAttributes) - ) + record(1L, attributes) def dec(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = - Sync[F].delay( - counter.add(-1L, attributes.toJavaAttributes) - ) + record(-1L, attributes) + + private def record( + value: Long, + attributes: immutable.Iterable[Attribute[_]] + ): F[Unit] = + ContextUtils.delayWithContext { () => + counter.add(value, attributes.toJavaAttributes) + } } UpDownCounter.fromBackend(backend) } } - private def doubleFactory[F[_]: Sync, A]( + private def doubleFactory[F[_]: Sync: AskContext, A]( jMeter: JMeter, cast: A => Double ): Factory[F, A] = @@ -130,19 +133,21 @@ private[oteljava] object UpDownCounterBuilderImpl { value: A, attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = - Sync[F].delay( - counter.add(cast(value), attributes.toJavaAttributes) - ) + record(cast(value), attributes) def inc(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = - Sync[F].delay( - counter.add(1.0, attributes.toJavaAttributes) - ) + record(1.0, attributes) def dec(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = - Sync[F].delay( - counter.add(-1.0, attributes.toJavaAttributes) - ) + record(-1.0, attributes) + + private def record( + value: Double, + attributes: immutable.Iterable[Attribute[_]] + ): F[Unit] = + ContextUtils.delayWithContext { () => + counter.add(value, attributes.toJavaAttributes) + } } UpDownCounter.fromBackend(backend) diff --git a/oteljava/testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/OtelJavaTestkit.scala b/oteljava/testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/OtelJavaTestkit.scala index 78b33b4af..a3817a9bc 100644 --- a/oteljava/testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/OtelJavaTestkit.scala +++ b/oteljava/testkit/src/main/scala/org/typelevel/otel4s/oteljava/testkit/OtelJavaTestkit.scala @@ -24,6 +24,7 @@ import io.opentelemetry.context.propagation.{ } import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder +import org.typelevel.otel4s.context.LocalProvider import org.typelevel.otel4s.context.propagation.ContextPropagators import org.typelevel.otel4s.metrics.MeterProvider import org.typelevel.otel4s.oteljava.context.Context @@ -67,13 +68,15 @@ object OtelJavaTestkit { identity, textMapPropagators: Iterable[JTextMapPropagator] = Nil ): Resource[F, OtelJavaTestkit[F]] = - for { - metrics <- MetricsTestkit.inMemory(customizeMeterProviderBuilder) - traces <- TracesTestkit.inMemory( - customizeTracerProviderBuilder, - textMapPropagators - ) - } yield new Impl[F](metrics, traces) + Resource.eval(LocalProvider[F, Context].local).flatMap { implicit local => + for { + metrics <- MetricsTestkit.inMemory(customizeMeterProviderBuilder) + traces <- TracesTestkit.inMemory( + customizeTracerProviderBuilder, + textMapPropagators + )(Async[F], LocalProvider.fromLocal(local)) + } yield new Impl[F](metrics, traces) + } private final class Impl[F[_]]( metrics: MetricsTestkit[F], diff --git a/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkMeterSuite.scala b/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkMeterSuite.scala new file mode 100644 index 000000000..362498bb8 --- /dev/null +++ b/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkMeterSuite.scala @@ -0,0 +1,165 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.sdk.metrics + +import cats.effect.IO +import cats.effect.Resource +import cats.effect.SyncIO +import cats.effect.std.Console +import cats.effect.testkit.TestControl +import cats.mtl.Local +import org.typelevel.otel4s.context.LocalProvider +import org.typelevel.otel4s.metrics.BaseMeterSuite +import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.sdk.context.Context +import org.typelevel.otel4s.sdk.metrics.data.AggregationTemporality +import org.typelevel.otel4s.sdk.metrics.data.ExemplarData +import org.typelevel.otel4s.sdk.metrics.data.ExemplarData.TraceContext +import org.typelevel.otel4s.sdk.metrics.data.MetricPoints +import org.typelevel.otel4s.sdk.metrics.data.PointData +import org.typelevel.otel4s.sdk.test.NoopConsole +import org.typelevel.otel4s.sdk.testkit.metrics.MetricsTestkit +import scodec.bits.ByteVector + +class SdkMeterSuite extends BaseMeterSuite { + + type MetricData = org.typelevel.otel4s.sdk.metrics.data.MetricData + type Ctx = Context + + private val traceContextKey = Context.Key + .unique[SyncIO, TraceContext]("trace-context") + .unsafeRunSync() + + protected def tracedContext(traceId: String, spanId: String): Context = { + val spanContext = TraceContext( + traceId = ByteVector.fromValidHex(traceId), + spanId = ByteVector.fromValidHex(spanId), + sampled = true + ) + + Context.root.updated(traceContextKey, spanContext) + } + + override protected def transform[A](io: IO[A]): IO[A] = + TestControl.executeEmbed(io) + + protected def makeSdk: Resource[IO, BaseMeterSuite.Sdk[Ctx]] = + Resource + .eval(LocalProvider[IO, Context].local) + .flatMap { implicit localContext => + implicit val noopConsole: Console[IO] = new NoopConsole + + MetricsTestkit + .inMemory[IO](_.withTraceContextLookup(_.get(traceContextKey))) + .map { metrics => + new BaseMeterSuite.Sdk[Ctx] { + def provider: MeterProvider[IO] = + metrics.meterProvider + + def collectMetrics: IO[List[BaseMeterSuite.MetricData]] = + metrics.collectMetrics.map(_.map(toWrapper)) + + def local: Local[IO, Context] = + localContext + } + } + } + + private def toWrapper(md: MetricData): BaseMeterSuite.MetricData = { + def temporality( + aggregationTemporality: AggregationTemporality + ): BaseMeterSuite.AggregationTemporality = + aggregationTemporality match { + case AggregationTemporality.Delta => + BaseMeterSuite.AggregationTemporality.Delta + case AggregationTemporality.Cumulative => + BaseMeterSuite.AggregationTemporality.Cumulative + } + + def toExemplar(exemplar: ExemplarData) = { + val value = exemplar match { + case long: ExemplarData.LongExemplar => Left(long.value) + case double: ExemplarData.DoubleExemplar => Right(double.value) + } + + BaseMeterSuite.Exemplar( + exemplar.filteredAttributes, + exemplar.timestamp, + exemplar.traceContext.map(_.traceId.toHex), + exemplar.traceContext.map(_.spanId.toHex), + value + ) + } + + def toNumberPoint(number: PointData.NumberPoint) = { + val value = number match { + case long: PointData.LongNumber => Left(long.value) + case double: PointData.DoubleNumber => Right(double.value) + } + + BaseMeterSuite.PointData.NumberPoint( + number.timeWindow.start, + number.timeWindow.end, + number.attributes, + value, + number.exemplars.map(toExemplar) + ) + } + + def toHistogramPoint(histogram: PointData.Histogram) = + BaseMeterSuite.PointData.Histogram( + histogram.timeWindow.start, + histogram.timeWindow.end, + histogram.attributes, + histogram.stats.map(_.sum), + histogram.stats.map(_.min), + histogram.stats.map(_.max), + histogram.stats.map(_.count), + histogram.boundaries, + histogram.counts, + histogram.exemplars.map(toExemplar) + ) + + val data = md.data match { + case sum: MetricPoints.Sum => + BaseMeterSuite.MetricPoints.Sum( + sum.points.toVector.map(toNumberPoint), + sum.monotonic, + temporality(sum.aggregationTemporality) + ) + + case gauge: MetricPoints.Gauge => + BaseMeterSuite.MetricPoints.Gauge( + gauge.points.toVector.map(toNumberPoint) + ) + + case histogram: MetricPoints.Histogram => + BaseMeterSuite.MetricPoints.Histogram( + histogram.points.toVector.map(toHistogramPoint), + temporality(histogram.aggregationTemporality) + ) + } + + BaseMeterSuite.MetricData( + md.name, + md.description, + md.unit, + data + ) + } + +} diff --git a/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkHistogramSuite.scala b/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkHistogramSuite.scala index d4910d586..4d96c825b 100644 --- a/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkHistogramSuite.scala +++ b/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/SdkHistogramSuite.scala @@ -42,7 +42,7 @@ class SdkHistogramSuite extends CatsEffectSuite with ScalaCheckEffectSuite { private implicit val askContext: Ask[IO, Context] = Ask.const(Context.root) - test("allow only non-positive values") { + test("allow only non-negative values") { PropF.forAllF( Gens.telemetryResource, Gens.instrumentationScope,