Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sdk-metrics: add InstrumentDescriptor #572

Merged
merged 1 commit into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ val Http4sVersion = "0.23.26"
val CirceVersion = "0.14.6"
val EpollcatVersion = "0.1.6"
val ScalaPBCirceVersion = "0.15.1"
val CaseInsensitiveVersion = "1.4.0"

lazy val scalaReflectDependency = Def.settings(
libraryDependencies ++= {
Expand Down Expand Up @@ -227,6 +228,7 @@ lazy val `sdk-metrics` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % CatsEffectVersion,
"org.scodec" %%% "scodec-bits" % ScodecVersion,
"org.typelevel" %%% "case-insensitive" % CaseInsensitiveVersion,
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
"org.typelevel" %%% "discipline-munit" % MUnitDisciplineVersion % Test,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2024 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
package internal

import cats.Hash
import cats.Show
import cats.syntax.foldable._
import org.typelevel.ci.CIString

/** A description of an instrument that was registered to record measurements.
*/
private[metrics] sealed trait InstrumentDescriptor {

/** The name of the instrument.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/metrics/sdk/#name-conflict]]
*/
def name: CIString

/** The description of the instrument.
*/
def description: Option[String]

/** The unit of the instrument.
*/
def unit: Option[String]

/** The type of the instrument.
*/
def instrumentType: InstrumentType

override final def hashCode(): Int =
Hash[InstrumentDescriptor].hash(this)

override final def equals(obj: Any): Boolean =
obj match {
case other: InstrumentDescriptor =>
Hash[InstrumentDescriptor].eqv(this, other)
case _ => false
}

override final def toString: String =
Show[InstrumentDescriptor].show(this)
}

private[metrics] object InstrumentDescriptor {

sealed trait Synchronous extends InstrumentDescriptor {
def instrumentType: InstrumentType.Synchronous
}

sealed trait Asynchronous extends InstrumentDescriptor {
def instrumentType: InstrumentType.Asynchronous
}

/** Creates an [[InstrumentDescriptor]] for a synchronous instrument.
*/
def synchronous(
name: CIString,
description: Option[String],
unit: Option[String],
instrumentType: InstrumentType.Synchronous
): InstrumentDescriptor.Synchronous =
SynchronousImpl(name, description, unit, instrumentType)

/** Creates an [[InstrumentDescriptor]] for a asynchronous instrument.
*/
def asynchronous(
name: CIString,
description: Option[String],
unit: Option[String],
instrumentType: InstrumentType.Asynchronous
): InstrumentDescriptor.Asynchronous =
AsynchronousImpl(name, description, unit, instrumentType)

implicit val instrumentDescriptorHash: Hash[InstrumentDescriptor] =
Hash.by { descriptor =>
(
descriptor.name,
descriptor.description,
descriptor.unit,
descriptor.instrumentType
)
}

implicit val instrumentDescriptorShow: Show[InstrumentDescriptor] =
Show.show { descriptor =>
val description = descriptor.description.foldMap(d => s"description=$d, ")
val unit = descriptor.unit.foldMap(d => s"unit=$d, ")
s"InstrumentDescriptor{name=${descriptor.name}, $description${unit}type=${descriptor.instrumentType}}"
}

private final case class SynchronousImpl(
name: CIString,
description: Option[String],
unit: Option[String],
instrumentType: InstrumentType.Synchronous
) extends InstrumentDescriptor.Synchronous

private final case class AsynchronousImpl(
name: CIString,
description: Option[String],
unit: Option[String],
instrumentType: InstrumentType.Asynchronous
) extends InstrumentDescriptor.Asynchronous

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 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.internal

import cats.Show
import cats.kernel.laws.discipline.HashTests
import munit.DisciplineSuite
import org.scalacheck.Prop
import org.typelevel.otel4s.sdk.metrics.scalacheck.Arbitraries._
import org.typelevel.otel4s.sdk.metrics.scalacheck.Cogens._
import org.typelevel.otel4s.sdk.metrics.scalacheck.Gens

class InstrumentDescriptorSuite extends DisciplineSuite {

checkAll(
"InstrumentDescriptor.HashLaws",
HashTests[InstrumentDescriptor].hash
)

test("Show[InstrumentDescriptor]") {
Prop.forAll(Gens.instrumentDescriptor) { descriptor =>
val description = descriptor.description match {
case Some(value) => s"description=$value, "
case None => ""
}

val unit = descriptor.unit match {
case Some(value) => s"unit=$value, "
case None => ""
}

val expected =
s"InstrumentDescriptor{name=${descriptor.name}, $description${unit}type=${descriptor.instrumentType}}"

assertEquals(Show[InstrumentDescriptor].show(descriptor), expected)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ import org.scalacheck.Arbitrary
import org.typelevel.otel4s.sdk.metrics.data.AggregationTemporality
import org.typelevel.otel4s.sdk.metrics.data.ExemplarData
import org.typelevel.otel4s.sdk.metrics.data.TimeWindow
import org.typelevel.otel4s.sdk.metrics.internal.InstrumentDescriptor

trait Arbitraries extends org.typelevel.otel4s.sdk.scalacheck.Arbitraries {

implicit val aggregationTemporalityArb: Arbitrary[AggregationTemporality] =
Arbitrary(Gens.aggregationTemporality)

implicit val instrumentDescriptorArbitrary: Arbitrary[InstrumentDescriptor] =
Arbitrary(Gens.instrumentDescriptor)

implicit val timeWindowArbitrary: Arbitrary[TimeWindow] =
Arbitrary(Gens.timeWindow)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,34 @@
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.metrics.scalacheck
package org.typelevel.otel4s.sdk.metrics
package scalacheck

import org.scalacheck.Cogen
import org.typelevel.ci.CIString
import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.sdk.metrics.data.AggregationTemporality
import org.typelevel.otel4s.sdk.metrics.data.ExemplarData
import org.typelevel.otel4s.sdk.metrics.data.TimeWindow
import org.typelevel.otel4s.sdk.metrics.internal.InstrumentDescriptor

import scala.concurrent.duration.FiniteDuration

trait Cogens extends org.typelevel.otel4s.sdk.scalacheck.Cogens {

implicit val ciStringCogen: Cogen[CIString] =
Cogen[String].contramap(_.toString)

implicit val aggregationTemporalityCogen: Cogen[AggregationTemporality] =
Cogen[String].contramap(_.toString)

implicit val instrumentTypeCogen: Cogen[InstrumentType] =
Cogen[String].contramap(_.toString)

implicit val instrumentDescriptorCogen: Cogen[InstrumentDescriptor] =
Cogen[(CIString, Option[String], Option[String], InstrumentType)]
.contramap(d => (d.name, d.description, d.unit, d.instrumentType))

implicit val timeWindowCogen: Cogen[TimeWindow] =
Cogen[(FiniteDuration, FiniteDuration)].contramap(w => (w.start, w.end))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.metrics.scalacheck
package org.typelevel.otel4s.sdk.metrics
package scalacheck

import org.scalacheck.Gen
import org.typelevel.ci.CIString
import org.typelevel.otel4s.sdk.metrics.data.AggregationTemporality
import org.typelevel.otel4s.sdk.metrics.data.ExemplarData
import org.typelevel.otel4s.sdk.metrics.data.TimeWindow
import org.typelevel.otel4s.sdk.metrics.internal.InstrumentDescriptor
import scodec.bits.ByteVector

import scala.concurrent.duration._
Expand All @@ -29,6 +32,41 @@ trait Gens extends org.typelevel.otel4s.sdk.scalacheck.Gens {
val aggregationTemporality: Gen[AggregationTemporality] =
Gen.oneOf(AggregationTemporality.Delta, AggregationTemporality.Cumulative)

val ciString: Gen[CIString] =
Gens.nonEmptyString.map(CIString(_))

val instrumentType: Gen[InstrumentType] =
Gen.oneOf(InstrumentType.values)

val synchronousInstrumentType: Gen[InstrumentType.Synchronous] =
Gen.oneOf(InstrumentType.values.collect {
case tpe: InstrumentType.Synchronous => tpe
})

val asynchronousInstrumentType: Gen[InstrumentType.Asynchronous] =
Gen.oneOf(InstrumentType.values.collect {
case tpe: InstrumentType.Asynchronous => tpe
})

val synchronousInstrumentDescriptor: Gen[InstrumentDescriptor] =
for {
tpe <- Gens.synchronousInstrumentType
name <- Gens.ciString
description <- Gen.option(Gen.alphaNumStr)
unit <- Gen.option(Gen.alphaNumStr)
} yield InstrumentDescriptor.synchronous(name, description, unit, tpe)

val asynchronousInstrumentDescriptor: Gen[InstrumentDescriptor] =
for {
tpe <- Gens.asynchronousInstrumentType
name <- Gens.ciString
description <- Gen.option(Gen.alphaNumStr)
unit <- Gen.option(Gen.alphaNumStr)
} yield InstrumentDescriptor.asynchronous(name, description, unit, tpe)

val instrumentDescriptor: Gen[InstrumentDescriptor] =
Gen.oneOf(synchronousInstrumentDescriptor, asynchronousInstrumentDescriptor)

val timeWindow: Gen[TimeWindow] =
for {
start <- Gen.chooseNum(1L, Long.MaxValue - 5)
Expand Down