Skip to content

Commit

Permalink
util-stats: Add a TranslatingStatsReceiver that will add a label and …
Browse files Browse the repository at this point in the history
…also prepend the label value as a scope

Problem

We want to be able to define metrics that can be identified by both a hierarchical name
and a dimensional name, but are otherwise the same metric. However, we normally use
"scoped" StatsReceivers which are fundamentally hierarchical only.

Solution

Add a new way of scoping a StatsReceiver that will allow us to add both the hierarchical
prefix and a label at the same time. This will let us progressively migrate our stats by
changing the scoping of StatsReceivers.

JIRA Issues: CSL-11565

Differential Revision: https://phabricator.twitter.biz/D864997
  • Loading branch information
Bryce Anderson authored and jenkins committed Apr 7, 2022
1 parent a82980a commit 67b7055
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ class MetricBuilder private (
copy(identity = nextIdentity)
}

private[finagle] def withIdentity(identity: Identity): MetricBuilder = copy(identity = identity)

def name: Seq[String] = identity.hierarchicalName

// Private for now
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.twitter.finagle.stats

import com.twitter.finagle.stats.MetricBuilder.Identity

/**
* A [[TranslatingStatsReceiver]] for working with both dimensional and hierarchical metrics.
*
* Translates the [[MetricBuilder]] to prepend the label value as a scope in addition to adding
* it to the labels map.
*/
private class ScopeTranslatingStatsReceiver(
sr: StatsReceiver,
labelName: String,
labelValue: String)
extends TranslatingStatsReceiver(sr) {

require(labelName.nonEmpty)
require(labelValue.nonEmpty)

private[this] val labelPair = labelName -> labelValue

protected def translate(builder: MetricBuilder): MetricBuilder =
builder.withIdentity(newIdentity(builder.identity))

private[this] def newIdentity(identity: Identity): Identity = identity match {
case Identity.Full(name, labels) =>
Identity.Full(labelValue +: name, labels + labelPair)
case Identity.Hierarchical(name, labels) =>
Identity.Hierarchical(labelValue +: name, labels + labelPair)
}

// We preserve this because unfortunately it is sometimes parsed
override def toString: String = s"$self/$labelValue"
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ trait StatsReceiver {
}
}

/**
* Add a label and prepend the label value to the names of the returned [[StatsReceiver]].
*
* For example:
*
* {{{
* statsReceiver.scopeAndLabel("client_name", "backend1").counter("requests")
* }}}
*
* will generate a [[Counter]] with both hierarchical name `backend1/requests` and dimensional name
* `requests {client_name="backend1"}`
*/
def scopeAndLabel(labelName: String, labelValue: String): StatsReceiver =
new ScopeTranslatingStatsReceiver(this, labelName, labelValue)

/**
* Prepend `namespace` and `namespaces` to the names of the returned [[StatsReceiver]].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,41 @@ class StatsReceiverTest extends AnyFunSuite {
assert(!receiver.supportsDimensional)
}

test("StatsReceiver.scopeAndLabel: a non-empty label name and label value are required") {
val sr = new InMemoryStatsReceiver
intercept[IllegalArgumentException] {
sr.scopeAndLabel("", "cool")
}

intercept[IllegalArgumentException] {
sr.scopeAndLabel("cool", "")
}
}

test("StatsReceiver.scopeAndLabel.counter: dimensions are supported") {
val receiver = new SupportsDimensionalStatsReceiver
val scoped = receiver.scopeAndLabel("scope", "foo")

scoped.counter("bar")
assert(receiver.supportsDimensional)
}

test("StatsReceiver.scopeAndLabel.scope.counter: dimensional support isn't magically added") {
val receiver = new SupportsDimensionalStatsReceiver
val scoped = receiver.scopeAndLabel("scope", "foo").scope("no_dimensions")

scoped.counter("bar")
assert(!receiver.supportsDimensional)
}

test("StatsReceiver.scopeAndLabel: adds both a scope and the labels") {
val sr = new InMemoryStatsReceiver
val c = sr.scopeAndLabel("scope", "good").counter("stuff")
val metricBuilder = c.metadata.toMetricBuilder.get
assert(metricBuilder.name == Seq("good", "stuff"))
assert(metricBuilder.identity.labels == Map("scope" -> "good"))
}

test("StatsReceiver.counter: varargs metrics names longer than 1 disable dimensional metrics") {
val receiver = new SupportsDimensionalStatsReceiver
receiver.counter("foo", "bar")
Expand Down

0 comments on commit 67b7055

Please sign in to comment.