Skip to content

Commit

Permalink
Add exemplar support
Browse files Browse the repository at this point in the history
Exemplars allow us to attach transient labels like trace IDs to
metrics
  • Loading branch information
janstenpickle committed Jul 24, 2023
1 parent fad5f09 commit a6ef2a3
Show file tree
Hide file tree
Showing 7 changed files with 474 additions and 90 deletions.
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")
'{ ??? }
}
}
76 changes: 76 additions & 0 deletions core/src/main/scala/prometheus4cats/Exemplar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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

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

override protected def make(a: String): LabelName = new LabelName(a)
}

/** 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 fromMap(a: Map[LabelName, String]): Either[String, Labels] = from(SortedMap.from(a))

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

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"
}
}
2 changes: 2 additions & 0 deletions docs/implementations/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
The Java registry implements both [`MetricRegistry`] and [`CallbackRegistry`], wrapping the [Prometheus Java library].
This provides interoperability with anything that depends on the Java library.

> ℹ️ As of version `1.1.0` of Prometheus4Cats, the Java registry now supports [Exemplars](../interface/exemplars.md)
> ℹ️ The Java Registry does add a runtime constraint that go beyond constraints that Prometheus itself imposes:
> You cannot have two metrics of the same name with different labels.
> [This issue](https://github.com/prometheus/client_java/issues/696) describes the problem.
Expand Down
8 changes: 8 additions & 0 deletions docs/interface/exemplars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Exemplars

Exemplars provide a way of linking trace information to metrics. For a detailed explainer see the
[Grafana labs introduction to exemplars](https://grafana.com/docs/grafana/latest/fundamentals/exemplars/).

Exemplars are implemented in Prometheus4Cats using the `Exemplar` typeclass, which returns an optional map of exemplar
labels. You can provide your own implementation for this, but beware that your label names and values can't be any
longer than 128 UTF-8 characters.
Loading

0 comments on commit a6ef2a3

Please # to comment.