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

Measure metrics using otel4s #396

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
501f8e1
Measure metrics using `otel4s`
iRevive Jun 9, 2022
67462d9
Merge branch 'main' into otel4s
iRevive Jun 9, 2022
8502140
Merge branch 'main' into otel4s
iRevive Jun 13, 2022
0a99a7b
Update otel4s
iRevive Jun 13, 2022
734fedf
Use `CrossType.Full`
iRevive Jun 13, 2022
4a178e8
Use `otel4s-testkit`
iRevive Jun 18, 2022
0aa2dd4
Merge branch 'main' into otel4s
iRevive Jul 3, 2022
a3b253d
Update `otel4s-testkit`
iRevive Jul 3, 2022
1e16c63
Generate headers, update `ci.yml`
iRevive Jul 3, 2022
7f172fb
Use snapshot version of `otel4s`
iRevive Aug 3, 2022
c5e6572
Merge branch 'main' into otel4s
iRevive Aug 3, 2022
016178b
Use `Resolver.sonatypeOssRepos`
iRevive Aug 3, 2022
3fd620a
Merge branch 'main' into otel4s
iRevive Jan 25, 2023
831588c
Use `otel4s` snapshot
iRevive Jan 25, 2023
e17b987
Merge branch 'main' into otel4s
iRevive Feb 8, 2023
369482e
Use `0.1.0` of `otel4s`
iRevive Feb 8, 2023
bbee8bf
Merge branch 'main' into otel4s
iRevive Mar 30, 2024
8792c34
Update otel4s to `0.5.0`
iRevive Mar 30, 2024
8afd322
Move otel4s integration to a separate module
iRevive Mar 31, 2024
00e75ee
generate CI workflow
iRevive Mar 31, 2024
8212c5b
Update build.sbt
iRevive Apr 16, 2024
8e12526
build.sbt: update previous mima artifacts for otel4s
iRevive Apr 16, 2024
98da94d
add scaladocs, update tests
iRevive Apr 16, 2024
f7a16b7
fix compilation issue
iRevive Apr 16, 2024
041bd81
make mima happy
iRevive Apr 16, 2024
6bfb557
Use otel4s `0.7.0`
iRevive May 5, 2024
0aae98b
Merge branch 'main' into otel4s
iRevive May 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p target .js/target site/target core/.js/target core/.jvm/target .jvm/target .native/target project/target
run: mkdir -p target .js/target site/target core/js/target core/jvm/target .jvm/target .native/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar target .js/target site/target core/.js/target core/.jvm/target .jvm/target .native/target project/target
run: tar cf targets.tar target .js/target site/target core/js/target core/jvm/target .jvm/target .native/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
14 changes: 12 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import com.typesafe.tools.mima.core._
val Scala213 = "2.13.8"

ThisBuild / tlBaseVersion := "0.4"
ThisBuild / crossScalaVersions := Seq("2.12.16", Scala213, "3.0.2")
ThisBuild / crossScalaVersions := Seq("2.12.16", Scala213, "3.1.2")
ThisBuild / tlVersionIntroduced := Map("3" -> "0.4.3")
ThisBuild / developers += tlGitHubDev("ChristopherDavenport", "Christopher Davenport")
ThisBuild / startYear := Some(2019)
Expand All @@ -13,7 +13,7 @@ ThisBuild / tlSiteApiUrl := Some(url("https://www.javadoc.io/doc/org.typelevel/k
lazy val root = tlCrossRootProject.aggregate(core)

lazy val core = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.crossType(CrossType.Full)
.in(file("core"))
.settings(commonSettings)
.settings(
Expand All @@ -23,6 +23,12 @@ lazy val core = crossProject(JVMPlatform, JSPlatform)
.jsSettings(
tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.4.6").toMap
)
.jvmSettings(
libraryDependencies ++= Seq(
"org.typelevel" %% "otel4s-java" % otel4sV % Test,
"org.typelevel" %% "otel4s-testkit" % otel4sV % Test
)
)
.settings(
mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("org.typelevel.keypool.KeyPool.destroy"),
Expand Down Expand Up @@ -52,6 +58,8 @@ lazy val docs = project
val catsV = "2.7.0"
val catsEffectV = "3.3.13"

val otel4sV = "0.0-2daac91-SNAPSHOT"

val munitV = "0.7.29"
val munitCatsEffectV = "1.0.7"

Expand All @@ -61,9 +69,11 @@ val betterMonadicForV = "0.3.1"
// General Settings
lazy val commonSettings = Seq(
Test / parallelExecution := false,
resolvers += "Sonatype OSS Snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots",
iRevive marked this conversation as resolved.
Show resolved Hide resolved
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsV,
"org.typelevel" %%% "cats-effect-std" % catsEffectV,
"org.typelevel" %%% "otel4s-core" % otel4sV,
"org.typelevel" %%% "cats-effect-testkit" % catsEffectV % Test,
"org.scalameta" %%% "munit" % munitV % Test,
"org.typelevel" %%% "munit-cats-effect-3" % munitCatsEffectV % Test
Expand Down
256 changes: 256 additions & 0 deletions core/jvm/src/test/scala/org/typelevel/keypool/PoolMetricsSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/*
* Copyright (c) 2019 Typelevel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package org.typelevel.keypool

import cats.effect._
import cats.effect.testkit._
import io.opentelemetry.sdk.metrics.{
Aggregation,
InstrumentSelector,
InstrumentType,
SdkMeterProviderBuilder,
View
}
import munit.CatsEffectSuite
import org.typelevel.otel4s.Otel4s
import org.typelevel.otel4s.java.OtelJava
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.testkit.{HistogramPointData, MetricData, Sdk}
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._
import scala.util.control.NoStackTrace

class PoolMetricsSpec extends CatsEffectSuite {
import PoolMetricsSpec._

test("Metrics should be empty for unused pool") {
val sdk = createSdk
val expectedSnapshot =
MetricsSnapshot(Nil, Nil, Nil, Nil, Nil)

for {
snapshot <- mkPool(sdk.otel.meterProvider).use(_ => sdk.snapshot)
} yield assertEquals(snapshot, expectedSnapshot)
}

test("In use: increment on acquire and decrement on release") {
poolTest() { (sdk, pool) =>
for {
inUse <- pool.take.use(_ => sdk.snapshot)
afterUse <- sdk.snapshot
} yield {
assertEquals(inUse.inUse, List(1L))
assertEquals(afterUse.inUse, List(0L))
}
}
}

test("In use: increment on acquire and decrement on release (failure)") {
val exception = new RuntimeException("Something went wrong") with NoStackTrace

poolTest() { (sdk, pool) =>
for {
deferred <- IO.deferred[MetricsSnapshot]
_ <- pool.take
.use(_ => sdk.snapshot.flatMap(deferred.complete) >> IO.raiseError(exception))
.attempt
inUse <- deferred.get
afterUse <- sdk.snapshot
} yield {
assertEquals(inUse.inUse, List(1L))
assertEquals(afterUse.inUse, List(0L))
}
}
}

test("Idle: keep 0 when `maxIdle` is 0") {
poolTest(_.withMaxIdle(0)) { (sdk, pool) =>
for {
inUse <- pool.take.use(_ => sdk.snapshot)
afterUse <- sdk.snapshot
} yield {
assertEquals(inUse.idle, Nil)
assertEquals(afterUse.idle, Nil)
}
}
}

test("Idle: keep 1 when `maxIdle` is 1") {
poolTest(_.withMaxIdle(1)) { (sdk, pool) =>
for {
inUse <- pool.take.use(_ => sdk.snapshot)
afterUse <- sdk.snapshot
} yield {
assertEquals(inUse.idle, Nil)
assertEquals(afterUse.idle, List(1L))
}
}
}

test("Idle: decrement on reaper cleanup") {
TestControl.executeEmbed {
poolTest(_.withMaxIdle(1).withIdleTimeAllowedInPool(1.second)) { (sdk, pool) =>
for {
inUse <- pool.take.use(_ => sdk.snapshot)
afterUse <- sdk.snapshot
afterSleep <- sdk.snapshot.delayBy(6.seconds)
} yield {
assertEquals(inUse.idle, Nil)
assertEquals(afterUse.idle, List(1L))
assertEquals(afterSleep.idle, List(0L))
}
}
}
}

test("Generate valid metric snapshots") {
val sdk = createSdk

TestControl.executeEmbed {
mkPool(sdk.otel.meterProvider)
.use(pool => pool.take.use(_ => sdk.snapshot.delayBy(500.millis)).product(sdk.snapshot))
.map { case (inUse, afterUse) =>
val acquireDuration =
List(
new HistogramPointData(0, 1, HistogramBuckets, List(1, 0, 0, 0, 0))
)

val expectedInUse = MetricsSnapshot(
idle = Nil,
inUse = List(1),
inUseDuration = Nil,
acquiredTotal = List(1),
acquireDuration = acquireDuration
)

val expectedAfterUser = MetricsSnapshot(
idle = List(1),
inUse = List(0),
inUseDuration = List(
new HistogramPointData(500.0, 1, HistogramBuckets, List(0, 0, 0, 1, 0))
),
acquiredTotal = List(1),
acquireDuration = acquireDuration
)

assertEquals(inUse, expectedInUse)
assertEquals(afterUse, expectedAfterUser)
}
}
}

private def poolTest(
customize: Pool.Builder[IO, Ref[IO, Int]] => Pool.Builder[IO, Ref[IO, Int]] = identity
)(scenario: (OtelSdk[IO], Pool[IO, Ref[IO, Int]]) => IO[Unit]): IO[Unit] = {
val sdk = createSdk

val builder =
Pool.Builder(Ref.of[IO, Int](1), nothing).withMeterProvider(sdk.otel.meterProvider)

customize(builder).build.use(pool => scenario(sdk, pool))
}

private def mkPool(meterProvider: MeterProvider[IO]) =
Pool
.Builder(
Ref.of[IO, Int](1),
nothing
)
.withMeterProvider(meterProvider)
.withMaxTotal(10)
.build

private def createSdk: OtelSdk[IO] = {
def customize(builder: SdkMeterProviderBuilder) =
builder
.registerView(
InstrumentSelector.builder().setType(InstrumentType.HISTOGRAM).build(),
View
.builder()
.setAggregation(
Aggregation.explicitBucketHistogram(HistogramBuckets.map(Double.box).asJava)
)
.build()
)

val sdk = Sdk.create[IO](customize)

new OtelSdk[IO] {
val otel: Otel4s[IO] = OtelJava.forSync[IO](sdk.sdk)

def snapshot: IO[MetricsSnapshot] =
for {
metrics <- sdk.metrics
} yield {
def counterValue(name: String): List[Long] =
metrics
.find(_.name == name)
.map(_.data)
.collectFirst { case MetricData.LongSum(points) =>
points.map(_.value)
}
.getOrElse(Nil)

def histogramSnapshot(name: String): List[HistogramPointData] =
metrics
.find(_.name == name)
.map(_.data)
.collectFirst { case MetricData.Histogram(points) =>
points.map(_.value)
}
.getOrElse(Nil)

MetricsSnapshot(
counterValue("idle"),
counterValue("in_use"),
histogramSnapshot("in_use_duration"),
counterValue("acquired_total"),
histogramSnapshot("acquire_duration")
)
}
}
}

private val HistogramBuckets: List[Double] =
List(0.01, 1, 100, 1000)

private def nothing(ref: Ref[IO, Int]): IO[Unit] =
ref.get.void

}

object PoolMetricsSpec {

trait OtelSdk[F[_]] {
def otel: Otel4s[F]
def snapshot: F[MetricsSnapshot]
}

final case class MetricsSnapshot(
idle: List[Long],
inUse: List[Long],
inUseDuration: List[HistogramPointData],
acquiredTotal: List[Long],
acquireDuration: List[HistogramPointData]
)

}