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

metrics: support synchronous gauge #677

Merged
merged 1 commit into from
May 22, 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: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ThisBuild / tlBaseVersion := "0.7"
ThisBuild / tlBaseVersion := "0.8"

ThisBuild / organization := "org.typelevel"
ThisBuild / organizationName := "Typelevel"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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
package metrics

import scala.collection.immutable

private[otel4s] trait GaugeMacro[F[_], A] {
def backend: Gauge.Backend[F, A]

/** Records a value with a set of attributes.
*
* @param value
* the value to record
*
* @param attributes
* the set of attributes to associate with the value
*/
def record(value: A, attributes: Attribute[_]*): F[Unit] =
macro GaugeMacro.record[A]

/** Records a value with a set of attributes.
*
* @param value
* the value to record
*
* @param attributes
* the set of attributes to associate with the value
*/
def record(value: A, attributes: immutable.Iterable[Attribute[_]]): F[Unit] =
macro GaugeMacro.recordColl[A]

}

object GaugeMacro {
import scala.reflect.macros.blackbox

def record[A](c: blackbox.Context)(
value: c.Expr[A],
attributes: c.Expr[Attribute[_]]*
): c.universe.Tree = {
import c.universe._
recordColl(c)(value, c.Expr(q"_root_.scala.Seq(..$attributes)"))
}

def recordColl[A](c: blackbox.Context)(
value: c.Expr[A],
attributes: c.Expr[immutable.Iterable[Attribute[_]]]
): c.universe.Tree = {
import c.universe._
val backend = q"${c.prefix}.backend"
val meta = q"$backend.meta"

q"if ($meta.isEnabled) $backend.record($value, $attributes) else $meta.unit"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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
package metrics

import scala.collection.immutable
import scala.quoted.*

private[otel4s] trait GaugeMacro[F[_], A] {
def backend: Gauge.Backend[F, A]

/** Records a value with a set of attributes.
*
* @param value
* the value to record
*
* @param attributes
* the set of attributes to associate with the value
*/
inline def record(
inline value: A,
inline attributes: Attribute[_]*
): F[Unit] =
${ GaugeMacro.record('backend, 'value, 'attributes) }

/** Records a value with a set of attributes.
*
* @param value
* the value to record
*
* @param attributes
* the set of attributes to associate with the value
*/
inline def record(
inline value: A,
inline attributes: immutable.Iterable[Attribute[_]]
): F[Unit] =
${ GaugeMacro.record('backend, 'value, 'attributes) }

}

object GaugeMacro {

def record[F[_], A](
backend: Expr[Gauge.Backend[F, A]],
value: Expr[A],
attributes: Expr[immutable.Iterable[Attribute[_]]]
)(using Quotes, Type[F], Type[A]) =
'{
if ($backend.meta.isEnabled) $backend.record($value, $attributes)
else $backend.meta.unit
}

}
114 changes: 114 additions & 0 deletions core/metrics/src/main/scala/org/typelevel/otel4s/metrics/Gauge.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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
package metrics

import cats.Applicative
import org.typelevel.otel4s.meta.InstrumentMeta

import scala.collection.immutable

/** A `Gauge` instrument that records non-additive values of type `A`.
*
* @see
* See [[UpDownCounter]] to record additive values
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/metrics/api/#gauge]]
*
* @tparam F
* the higher-kinded type of a polymorphic effect
*
* @tparam A
* the type of the values to record. The type must have an instance of
* [[MeasurementValue]]. [[scala.Long]] and [[scala.Double]] are supported
* out of the box.
*/
trait Gauge[F[_], A] extends GaugeMacro[F, A]

object Gauge {

/** A builder of [[Gauge]].
*
* @tparam F
* the higher-kinded type of a polymorphic effect
*
* @tparam A
* the type of the values to record. The type must have an instance of
* [[MeasurementValue]]. [[scala.Long]] and [[scala.Double]] are supported
* out of the box.
*/
trait Builder[F[_], A] {

/** Sets the unit of measure for this counter.
*
* @see
* [[https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-unit Instrument Unit]]
*
* @param unit
* the measurement unit. Must be 63 or fewer ASCII characters.
*/
def withUnit(unit: String): Builder[F, A]

/** Sets the description for this gauge.
*
* @see
* [[https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-description Instrument Description]]
*
* @param description
* the description
*/
def withDescription(description: String): Builder[F, A]

/** Creates a [[Gauge]] with the given `unit` and `description` (if any).
*/
def create: F[Gauge[F, A]]
}

trait Backend[F[_], A] {
def meta: InstrumentMeta[F]

/** Records a value with a set of attributes.
*
* @param value
* the value to record
*
* @param attributes
* the set of attributes to associate with the value
*/
def record(value: A, attributes: immutable.Iterable[Attribute[_]]): F[Unit]

}

def noop[F[_], A](implicit F: Applicative[F]): Gauge[F, A] =
new Gauge[F, A] {
val backend: Backend[F, A] =
new Backend[F, A] {
val meta: InstrumentMeta[F] = InstrumentMeta.disabled
def record(
value: A,
attributes: immutable.Iterable[Attribute[_]]
): F[Unit] = meta.unit
}
}

private[otel4s] def fromBackend[F[_], A](b: Backend[F, A]): Gauge[F, A] =
new Gauge[F, A] {
def backend: Backend[F, A] = b
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,37 @@ trait Meter[F[_]] {
name: String
): UpDownCounter.Builder[F, A]

/** Creates a builder of [[Gauge]] instrument that records values of type `A`.
*
* The [[Gauge]] records non-additive values.
*
* @note
* the `A` type must be provided explicitly, for example
* `meter.gauge[Long]` or `meter.gauge[Double]`
*
* @example
* {{{
* val meter: Meter[F] = ???
*
* val doubleGauge: F[Gauge[F, Double]] =
* meter.gauge[Double]("double-gauge").create
*
* val longGauge: F[Gauge[F, Long]] =
* meter.gauge[Long]("long-gauge").create
* }}}
*
* @see
* See [[upDownCounter]] to record additive values
*
* @param name
* the name of the instrument
*
* @tparam A
* the type of the measurement. [[scala.Long]] and [[scala.Double]] are
* supported out of the box
*/
def gauge[A: MeasurementValue](name: String): Gauge.Builder[F, A]

/** Creates a builder of [[ObservableGauge]] instrument that collects values
* of type `A` from the given callback.
*
Expand Down Expand Up @@ -326,6 +357,13 @@ object Meter {
def create: F[UpDownCounter[F, A]] = F.pure(UpDownCounter.noop)
}

def gauge[A: MeasurementValue](name: String): Gauge.Builder[F, A] =
new Gauge.Builder[F, A] {
def withUnit(unit: String): Gauge.Builder[F, A] = this
def withDescription(description: String): Gauge.Builder[F, A] = this
def create: F[Gauge[F, A]] = F.pure(Gauge.noop)
}

def observableGauge[A: MeasurementValue](
name: String
): ObservableGauge.Builder[F, A] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ abstract class BaseMeterSuite extends CatsEffectSuite {
} yield assertEquals(metrics, List(expected))
}

sdkTest("Gauge - record values") { sdk =>
val expected = MetricData.gauge("gauge", 1L, Some(1L))

for {
meter <- sdk.provider.get("meter")
gauge <- meter.gauge[Long]("gauge").create
_ <- gauge.record(1L)
metrics <- sdk.collectMetrics
} yield assertEquals(metrics, List(expected))
}

sdkTest("UpDownCounter - record values") { sdk =>
val expected = MetricData.sum("counter", monotonic = false, 3L, Some(3L))

Expand Down Expand Up @@ -376,7 +387,11 @@ object BaseMeterSuite {
)
)

def gauge(name: String, value: Long): MetricData =
def gauge(
name: String,
value: Long,
exemplarValue: Option[Long] = None
): MetricData =
MetricData(
name,
None,
Expand All @@ -388,7 +403,15 @@ object BaseMeterSuite {
Duration.Zero,
Attributes.empty,
Left(value),
Vector.empty
exemplarValue.toVector.map { exemplar =>
Exemplar(
Attributes.empty,
Duration.Zero,
Some(TraceId),
Some(SpanId),
Left(exemplar)
)
}
)
)
)
Expand Down
Loading