Skip to content
New issue

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

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

Already on GitHub? # to your account

Add exemplar support #65

Merged
merged 14 commits into from
Aug 8, 2023
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import laika.rewrite.link._
import org.typelevel.sbt.site.TypelevelProject

// https://typelevel.org/sbt-typelevel/faq.html#what-is-a-base-version-anyway
ThisBuild / tlBaseVersion := "1.0" // your current series x.y
ThisBuild / tlBaseVersion := "2.0" // your current series x.y

ThisBuild / organization := "com.permutive"
ThisBuild / organizationName := "Permutive"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2022 Permutive
*
* 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 prometheus4cats.internal

import prometheus4cats.Exemplar

import scala.reflect.macros.blackbox

private[prometheus4cats] trait ExemplarLabelNameFromStringLiteral {

def apply(t: String): Exemplar.LabelName =
macro ExemplarLabelNameMacros.fromStringLiteral

implicit def fromStringLiteral(t: String): Exemplar.LabelName =
macro ExemplarLabelNameMacros.fromStringLiteral

}

private[prometheus4cats] class ExemplarLabelNameMacros(val c: blackbox.Context) extends MacroUtils {

def fromStringLiteral(t: c.Expr[String]): c.Expr[Exemplar.LabelName] = {
val string: String = literal(t, or = "Exemplar.LabelName.from({string})")

Exemplar.LabelName
.from(string)
.fold(
abort,
_ => c.universe.reify(Exemplar.LabelName.from(t.splice).toOption.get)
)
}

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

import prometheus4cats._

import scala.quoted.*

private[prometheus4cats] trait ExemplarLabelNameFromStringLiteral {

inline def apply(inline t: String): Exemplar.LabelName = ${
ExemplarLabelNameFromStringLiteral.nameLiteral('t)
}

implicit inline def fromStringLiteral(inline t: String): Exemplar.LabelName = ${
ExemplarLabelNameFromStringLiteral.nameLiteral('t)
}

}

private[prometheus4cats] object ExemplarLabelNameFromStringLiteral extends MacroUtils {
def nameLiteral(s: Expr[String])(using q: Quotes): Expr[Exemplar.LabelName] =
s.value match {
case Some(string) =>
Exemplar.LabelName
.from(string)
.fold(
error,
_ =>
'{
Exemplar.LabelName.from(${ Expr(string) }).toOption.get
}
)
case None =>
abort("Exemplar.LabelName.from")
'{ ??? }
}
}
104 changes: 66 additions & 38 deletions core/src/main/scala/prometheus4cats/Counter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,34 @@

package prometheus4cats

import cats.syntax.flatMap._
import cats.{Applicative, Contravariant, FlatMap, Monad, ~>}

import java.util.regex.Pattern

import cats.{Applicative, Contravariant, ~>}
sealed abstract class Counter[F[_]: FlatMap, -A] extends Metric[A] { self =>

sealed abstract class Counter[F[_], -A] extends Metric[A] { self =>
final def inc: F[Unit] = incWithExemplar(None)
final def inc(n: A): F[Unit] = incWithExemplar(n, None)
final def incWithExemplar(implicit exemplar: Exemplar[F]): F[Unit] = exemplar.get.flatMap(incWithExemplar)
final def incWithExemplar(n: A)(implicit exemplar: Exemplar[F]): F[Unit] = exemplar.get.flatMap(incWithExemplar(n, _))

def inc: F[Unit]
def inc(n: A): F[Unit]
def incWithExemplar(n: A, exemplar: Option[Exemplar.Labels]): F[Unit]
def incWithExemplar(exemplar: Option[Exemplar.Labels]): F[Unit]

def contramap[B](f: B => A): Counter[F, B] = new Counter[F, B] {
override def inc: F[Unit] = self.inc

override def inc(n: B): F[Unit] = self.inc(f(n))
override def incWithExemplar(n: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
self.incWithExemplar(f(n), exemplar)
override def incWithExemplar(exemplar: Option[Exemplar.Labels]): F[Unit] = self.incWithExemplar(exemplar)
}

final def mapK[G[_]](fk: F ~> G): Counter[G, A] = new Counter[G, A] {
override def inc: G[Unit] = fk(self.inc)
override def inc(n: A): G[Unit] = fk(self.inc(n))
final def mapK[G[_]: FlatMap](fk: F ~> G): Counter[G, A] = new Counter[G, A] {
override def incWithExemplar(n: A, exemplar: Option[Exemplar.Labels]): G[Unit] = fk(
self.incWithExemplar(n, exemplar)
)
override def incWithExemplar(exemplar: Option[Exemplar.Labels]): G[Unit] = fk(
self.incWithExemplar(exemplar)
)
}
}

Expand All @@ -54,45 +64,56 @@ object Counter {
override def contramap[A, B](fa: Counter[F, A])(f: B => A): Counter[F, B] = fa.contramap(f)
}

def make[F[_], A](default: A, _inc: A => F[Unit]): Counter[F, A] = new Counter[F, A] {
override def inc: F[Unit] = inc(default)
def make[F[_]: FlatMap, A](default: A, _inc: (A, Option[Exemplar.Labels]) => F[Unit]): Counter[F, A] =
new Counter[F, A] {
override def incWithExemplar(n: A, exemplar: Option[Exemplar.Labels]): F[Unit] = _inc(n, exemplar)

override def inc(n: A): F[Unit] = _inc(n)
}
override def incWithExemplar(exemplar: Option[Exemplar.Labels]): F[Unit] = _inc(default, exemplar)
}

def make[F[_], A](_inc: A => F[Unit])(implicit A: Numeric[A]): Counter[F, A] = make(A.one, _inc)
def make[F[_]: FlatMap, A](_inc: (A, Option[Exemplar.Labels]) => F[Unit])(implicit A: Numeric[A]): Counter[F, A] =
make(A.one, _inc)

def noop[F[_]: Applicative, A]: Counter[F, A] = new Counter[F, A] {
override def inc: F[Unit] = Applicative[F].unit
def noop[F[_]: Monad, A]: Counter[F, A] = new Counter[F, A] {
override def incWithExemplar(n: A, exemplar: Option[Exemplar.Labels]): F[Unit] = Applicative[F].unit

override def inc(n: A): F[Unit] = Applicative[F].unit
override def incWithExemplar(exemplar: Option[Exemplar.Labels]): F[Unit] = Applicative[F].unit
}

sealed abstract class Labelled[F[_], -A, -B] extends Metric[A] with Metric.Labelled[B] {
sealed abstract class Labelled[F[_]: FlatMap, -A, -B] extends Metric[A] with Metric.Labelled[B] {
self =>
def inc(labels: B): F[Unit]
final def inc(labels: B): F[Unit] = incWithExemplar(labels, None)
final def inc(n: A, labels: B): F[Unit] = incWithExemplar(n, labels, None)
final def incWithExemplar(labels: B)(implicit exemplar: Exemplar[F]): F[Unit] =
exemplar.get.flatMap(incWithExemplar(labels, _))
final def incWithExemplar(n: A, labels: B)(implicit exemplar: Exemplar[F]): F[Unit] =
exemplar.get.flatMap(incWithExemplar(n, labels, _))

def inc(n: A, labels: B): F[Unit]
def incWithExemplar(labels: B, exemplar: Option[Exemplar.Labels]): F[Unit]
def incWithExemplar(n: A, labels: B, exemplar: Option[Exemplar.Labels]): F[Unit]

def contramap[C](f: C => A): Labelled[F, C, B] = new Labelled[F, C, B] {
override def inc(labels: B): F[Unit] = self.inc(labels)
override def incWithExemplar(labels: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
self.incWithExemplar(labels, exemplar)

override def inc(n: C, labels: B): F[Unit] = self.inc(f(n), labels)
override def incWithExemplar(n: C, labels: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
self.incWithExemplar(f(n), labels, exemplar)
}

def contramapLabels[C](f: C => B): Labelled[F, A, C] = new Labelled[F, A, C] {
override def inc(labels: C): F[Unit] = self.inc(f(labels))

override def inc(n: A, labels: C): F[Unit] = self.inc(n, f(labels))
override def incWithExemplar(labels: C, exemplar: Option[Exemplar.Labels]): F[Unit] =
self.incWithExemplar(f(labels), exemplar)
override def incWithExemplar(n: A, labels: C, exemplar: Option[Exemplar.Labels]): F[Unit] =
self.incWithExemplar(n, f(labels), exemplar)
}

final def mapK[G[_]](fk: F ~> G): Counter.Labelled[G, A, B] =
final def mapK[G[_]: FlatMap](fk: F ~> G): Counter.Labelled[G, A, B] =
new Labelled[G, A, B] {
override def inc(labels: B): G[Unit] = fk(self.inc(labels))
override def incWithExemplar(labels: B, exemplar: Option[Exemplar.Labels]): G[Unit] =
fk(self.incWithExemplar(labels, exemplar))

override def inc(n: A, labels: B): G[Unit] = fk(
self.inc(n, labels)
)
override def incWithExemplar(n: A, labels: B, exemplar: Option[Exemplar.Labels]): G[Unit] =
fk(self.incWithExemplar(n, labels, exemplar))
}
}

Expand All @@ -107,19 +128,26 @@ object Counter {
override def contramapLabels[A, B](fa: Labelled[F, C, A])(f: B => A): Labelled[F, C, B] = fa.contramapLabels(f)
}

def make[F[_], A, B](default: A, _inc: (A, B) => F[Unit]): Labelled[F, A, B] =
def make[F[_]: FlatMap, A, B](default: A, _inc: (A, B, Option[Exemplar.Labels]) => F[Unit]): Labelled[F, A, B] =
new Labelled[F, A, B] {
override def inc(labels: B): F[Unit] = inc(default, labels)
override def incWithExemplar(labels: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
_inc(default, labels, exemplar)

override def inc(n: A, labels: B): F[Unit] = _inc(n, labels)
override def incWithExemplar(n: A, labels: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
_inc(n, labels, exemplar)
}

def make[F[_], A, B](_inc: (A, B) => F[Unit])(implicit A: Numeric[A]): Labelled[F, A, B] = make(A.one, _inc)
def make[F[_]: FlatMap, A, B](_inc: (A, B, Option[Exemplar.Labels]) => F[Unit])(implicit
A: Numeric[A]
): Labelled[F, A, B] =
make(A.one, _inc)

def noop[F[_]: Applicative, A, B]: Labelled[F, A, B] = new Labelled[F, A, B] {
override def inc(labels: B): F[Unit] = Applicative[F].unit
def noop[F[_]: Monad, A, B]: Labelled[F, A, B] = new Labelled[F, A, B] {
override def incWithExemplar(labels: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
Applicative[F].unit

override def inc(n: A, labels: B): F[Unit] = Applicative[F].unit
override def incWithExemplar(n: A, labels: B, exemplar: Option[Exemplar.Labels]): F[Unit] =
Applicative[F].unit
}
}

Expand Down
85 changes: 85 additions & 0 deletions core/src/main/scala/prometheus4cats/Exemplar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2022 Permutive
*
* 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 prometheus4cats

import cats.Applicative
import cats.syntax.show._
import prometheus4cats.internal.ExemplarLabelNameFromStringLiteral

import java.util.regex.Pattern
import scala.collection.immutable.SortedMap

/** A typeclass to provide exemplars to counters and histograms, which may be used by [[MetricRegistry]]
* implementations.
*/
trait Exemplar[F[_]] {
def get: F[Option[Exemplar.Labels]]
}

object Exemplar {
def apply[F[_]: Exemplar]: Exemplar[F] = implicitly

object Implicits {
implicit def noop[F[_]: Applicative]: Exemplar[F] = new Exemplar[F] {
override def get: F[Option[Labels]] = Applicative[F].pure(None)
}
}

/** Refined value class for an exemplar label name that has been parsed from a string
*/
final class LabelName private (val value: String) extends AnyVal with internal.Refined.Value[String] {
override def toString: String = s"""Exemplar.LabelName("$value")"""
}

object LabelName extends internal.Refined.StringRegexRefinement[LabelName] with ExemplarLabelNameFromStringLiteral {
protected val regex: Pattern = "^[a-zA-Z_][a-zA-Z_0-9]*$".r.pattern

implicit val ordering: Ordering[LabelName] = catsInstances.toOrdering
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You should be able to remove it since cats' Order already provides this as an implicit conversion.

This comment follows the conventionalcomments.org standard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep it does, but we're going the other way as SortedMap needs Ordering


override protected def make(a: String): LabelName = new LabelName(a)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why this method?

This comment follows the conventionalcomments.org standard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That comes from our internal refinement code

}

/** Refined value class for a set of exemplar labels.
*
* Validation will fail if the labels are empty or if the length of all the label names and values does not exceed
* 128 UTF-8 characters
*/
final class Labels private (val value: SortedMap[LabelName, String])
extends AnyVal
with internal.Refined.Value[SortedMap[LabelName, String]] {
override def toString: String = s"""Exemplar.Labels("${value.show}")"""
}

object Labels extends internal.Refined[SortedMap[LabelName, String], Labels] {
def of(first: (LabelName, String), rest: (LabelName, String)*): Either[String, Labels] =
from(rest.foldLeft(SortedMap.empty[LabelName, String].updated(first._1, first._2)) { case (acc, (k, v)) =>
acc.updated(k, v)
})

def fromMap(a: Map[LabelName, String]): Either[String, Labels] = from(
a.foldLeft(SortedMap.empty[LabelName, String]) { case (acc, (k, v)) => acc.updated(k, v) }
)

override protected def make(a: SortedMap[LabelName, String]): Labels = new Labels(a)

override protected def test(a: SortedMap[LabelName, String]): Boolean =
a.nonEmpty && a.map { case (k, v) => s"${k.value}$v".length }.sum <= 128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Very clever!

This comment follows the conventionalcomments.org standard


override protected def nonMatchMessage(a: SortedMap[LabelName, String]): String =
"exemplar labels must not be empty and the combined length of the label names and values must not exceed 128 UTF-8 characters"
}
}
Loading