-
Notifications
You must be signed in to change notification settings - Fork 804
Add CustomMetricsOps #7469
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
Add CustomMetricsOps #7469
Conversation
|
I spoke with @dj707chen about this. I think the basic tradeoff here is:
|
The http4s |
|
|
When it comes to the labels, there are two scenarios:
In some sense, constant labels are dynamic labels where Also, it seems that However, each metric backend treats labels differently. For example, in Prometheus, labels are key-value string entries. While working with Prometheus, you must ensure that the configured instrument labels And that's not the case for OpenTelemetry because you can use different sets of attributes (labels) with the same instrument, unlike Prometheus:
The current concept of the object Metrics {
def apply[F[_]](
ops: MetricsOps[F],
classifierF: Request[F] => Option[String] = { (_: Request[F]) => None },
)(client: Client[F])(...): Client[F] =
}Which makes it pretty limiting. I believe the backend should provide a way to build a classifier. For example: trait MetricsOps[F[_]] {
type Classifier
def classifier(request: RequestPrelude): F[Option[Classifier]]
def increaseActiveRequests(request: RequestPrelude, classifier: Option[Classifier]): F[Unit]
...
}
class OtelMetrics[F[_]] extends MetricsOps[F] {
type Classifier = Attributes
}
class PrometheusMetrics[F[_]] extends MetricsOps[F] {
type Classifier = Labels
}Or even different classifiers per metric (which might be an overkill): trait MetricsOps[F[_]] {
type Classifier
def classifier(metric: Metric, request: RequestPrelude): F[Option[Classifier]]
def increaseActiveRequests(request: RequestPrelude, classifier: Option[Classifier]): F[Unit]
...
}
sealed trait Metric
object Metrics {
case object ActiveRequests extends Metric
case object TotalTime extends Metric
case object RequestSize extends Metric
case object ResponseSize extends Metric
} |
|
@iRevive Thanks for the insightful comment, I think the changes proposed in your comment could be good candidate for next major version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a reasonable change given it has to be binary compatible.
Leaving it up to other maintainers to merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I share the concern that this is biasing the type toward Prometheus, but I have thought about this off and on and not come up with a better way.
As we work on instrumenting backends directly with otel4s (e.g., #7476), I think that the MetricsOps will become less important anyway.
I'm good with this, with a few small nitpicks below.
| } | ||
| } | ||
|
|
||
| final case class EmptyCustomLabels() extends CustomLabels[SizedSeq0[String]] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like this could be a case object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to convert, but had difficulty, see my comment below on SizedSeq0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it to abstract and the constructor of EmptyCustomLabels private.
| } | ||
|
|
||
| // format: off | ||
| final case class SizedSeq0[+A]() extends SizedSeqBase[A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could be a case object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to convert it to a case object, but encountered difficulty, I may tried a wrong way?
case object SizedSeq0 {
def toSeq[A]: Seq[A] = Seq.empty[A]
}
and EmptyCustomLabels will look like this:
case object EmptyCustomLabels {
val labels = SizedSeq0
val values = SizedSeq0
}
Then CustomMetricsOps.fromMetricsOps will looks like:
def fromMetricsOps[F[_]](ops: MetricsOps[F]): CustomMetricsOps[F, SizedSeq0.type] = {
and it got compilation error:
Type SizedSeq0.type does not conform to upper bound SizedSeq[String] of type parameter SL
Root cause is that when we define SizedSeq0 as object, it can't extend SizedSeq since object can't define a type parameter which SizedSeq requires.
So I'm leaving it as case class for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't think of that. Could do something like this:
final case class SizedSeq0[+A] private () extends SizedSeq[A] {
val toSeq: Seq[A] = Seq.empty[A]
}
final object SizedSeq0 {
private[this] val instance = SizedSeq0[Nothing]()
def apply[A](): SizedSeq0[A] = instance
}It would be the only SizedSeq that can't be created with new, but it would prevent us from creating multiple instances when a singleton does just fine. I'm not sure if it's worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made the change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it also abstract to fix the following error:
[error] /home/runner/work/http4s/http4s/core/shared/src/main/scala/org/http4s/util/SizedSeq.scala:24:1: error: [Http4sGeneralLinters.nonValidatingCopyConstructor] Case classes with private constructors should be abstract to prevent exposing a non-validating copy constructor
[error] final case class SizedSeq0[+A] private () extends SizedSeq[A] {
[error] ^
[error] (coreJS / scalafixAll) scalafix.sbt.ScalafixFailed: LinterError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
Add
CustomMetricsOpsand update client and server Metrics middleware to allow adding custom labels to metrics.Background
In a situation where a service interacts with multiple providers, a few of them do not have a lot of traffic, so when one of these light providers is down, the alert of the service is not triggered due to the small percentage counted in the failing traffic; So we want to create alerts specific to providers, and I’m searching for ways to identify the metrics entries related to a provider.
I can think of two options currently available:
MetricsOpsinstance, but the metrics of this client will be excluded from a generic Http4S metrics dashboard;org.http4s.client.middleware.Metrics.The second option is better than the first option, but not ideal.
What it does
This PR
CustomMetricsOps(extends existingMetricsOps) to allow adding custom labels to metrics records;withCustomLabelsmethod to both client and serverMetricsmiddleware to pass custom label values when they call theCustomMetricsOpswith custom label values.When
CustomMetricsOpswith some custom labels is created, the Metrics middleware calling it must supply the same number of label values, so the type parameter SL (sized list) is added to enforce this constraint.We could use
shapeless'sSizedtype to express this constraint, butshapelessis not on the dependency list and it's unlikely it will be added. So we temporarily createdSizedSeqto mimicshapeless'sSizedtype.http4s-prometheus-metricswill be updated to adapt this feature, a draft pull request onhttp4s-prometheus-metricsAllow custom labels allows creating instance ofCustomMetricsOpswith custom label. epimetheus-http4s could also be updated.Code example
First call
http4s-prometheus-metrics'sPrometheusto create an instance ofCustomMetricsOps, notice the prefixpayments:org_http4s_client, and one single custom labelpayment_provider.Then create a Metrics client middleware, passing value
Payllyto custom label.Output examples
I created SNAPSHOT version of
http4sand ran test in local, the above code generated below metrics entries; Notice that the entries with prefixpayments:org_http4s_client_have custom labelpayment_providerwith valuePaylly.