Skip to content

Commit

Permalink
metrics: support synchronous gauge
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed May 10, 2024
1 parent 9a61034 commit 37fc291
Show file tree
Hide file tree
Showing 19 changed files with 708 additions and 71 deletions.
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]

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

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

}

object GaugeMacro {
import scala.reflect.macros.blackbox

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

def setColl[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.set($value, $attributes) else $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]

/** Sets the gauge value with a set of attributes.
*
* @param value
* the value to set
*
* @param attributes
* the set of attributes to associate with the value
*/
def set(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 set(
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.set(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

0 comments on commit 37fc291

Please sign in to comment.