-
Notifications
You must be signed in to change notification settings - Fork 264
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
twitter-service: add metric_metadata endpoint to admin server
Problem Finagle stats are currently storing but not exposing Metric Schemas containing useful metadata about metrics. Solution Create an endpoint (/admin/metric_metadata.json) which exposes this metadata in a JSON format, and which allows for metrics to be filtered by name with a query param "name" ala "m" in /admin/metrics.json. Result Users and tools can now query the new /admin/metric_metadata.json endpoint to obtain the stored metadata about a metric. JIRA Issues: CSL-8921 Differential Revision: https://phabricator.twitter.biz/D449149
- Loading branch information
Matt Dannenberg
authored and
jenkins
committed
Mar 17, 2020
1 parent
5c8b7a7
commit 6d9c877
Showing
8 changed files
with
747 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
server/src/main/scala/com/twitter/server/handler/MetricMetadataQueryHandler.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package com.twitter.server.handler | ||
|
||
import com.twitter.finagle.Service | ||
import com.twitter.finagle.http.{MediaType, Request, Response, Uri} | ||
import com.twitter.io.Buf | ||
import com.twitter.server.util.HttpUtils.newResponse | ||
import com.twitter.server.util.{JsonConverter, MetricSchemaSource} | ||
import com.twitter.util.Future | ||
|
||
/** | ||
* A handler which accepts metrics metadata queries via http query strings and returns | ||
* json encoded metrics with the metadata contained in their MetricSchemas, as well as an indicator | ||
* of whether or not counters are latched. | ||
* | ||
* @note When passing an explicit histogram metric via ?name=, users must provide the raw histogram | ||
* name, no percentile (eg, .p99) appended. | ||
* | ||
* Example Request: | ||
* http://$HOST:$PORT/admin/metric_metadata?name=my/cool/counter&name=your/fine/gauge&name=my/only/histo | ||
* | ||
* Response: | ||
* { | ||
* "@version" : 1.0, | ||
* "counters_latched" : false, | ||
* "metrics" : [ | ||
* { | ||
* "name" : "my/cool/counter", | ||
* "kind" : "counter", | ||
* "source" : { | ||
* "class": "finagle.stats.cool", | ||
* "category": "Server", | ||
* "process_path": "dc/role/zone/service" | ||
* }, | ||
* "description" : "Counts how many cools are seen", | ||
* "unit" : "Requests", | ||
* "verbosity": "Verbosity(default)", | ||
* "key_indicator" : true | ||
* }, | ||
* { | ||
* "name" : "your/fine/gauge", | ||
* "kind" : "gauge", | ||
* "source" : { | ||
* "class": "finagle.stats.your", | ||
* "category": "Client", | ||
* "process_path": "dc/your_role/zone/your_service" | ||
* }, | ||
* "description" : "Measures how fine the downstream system is", | ||
* "unit" : "Percentage", | ||
* "verbosity": "Verbosity(debug)", | ||
* "key_indicator" : false | ||
* }, | ||
* { | ||
* "name" : "my/only/histo", | ||
* "kind" : "histogram", | ||
* "source" : { | ||
* "class": "Unspecified", | ||
* "category": "NoRoleSpecified", | ||
* "process_path": "Unspecified" | ||
* }, | ||
* "description" : "No description provided", | ||
* "unit" : "Unspecified", | ||
* "verbosity": "Verbosity(default)", | ||
* "key_indicator" : false, | ||
* "buckets" : [ | ||
* 0.5, | ||
* 0.9, | ||
* 0.99, | ||
* 0.999, | ||
* 0.9999 | ||
* ] | ||
* } | ||
* ]} | ||
*/ | ||
class MetricMetadataQueryHandler(source: MetricSchemaSource = new MetricSchemaSource) | ||
extends Service[Request, Response] { | ||
|
||
private[this] def query(keys: Iterable[String]) = | ||
for (k <- keys; e <- source.getSchema(k)) yield e | ||
|
||
def apply(req: Request): Future[Response] = { | ||
val uri = Uri.fromRequest(req) | ||
|
||
val latched = source.hasLatchedCounters | ||
val keysParam = uri.params.getAll("name") | ||
|
||
val metrics = | ||
if (keysParam.isEmpty) source.schemaList | ||
else query(keysParam) | ||
|
||
newResponse( | ||
contentType = MediaType.JsonUtf8, | ||
content = Buf.Utf8( | ||
JsonConverter.writeToString( | ||
Map( | ||
"@version" -> 1.0, | ||
"counters_latched" -> latched, | ||
"metrics" -> metrics | ||
))) | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
server/src/main/scala/com/twitter/server/util/MetricSchemaJsonModule.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.twitter.server.util | ||
|
||
import com.fasterxml.jackson.core.JsonGenerator | ||
import com.fasterxml.jackson.databind.SerializerProvider | ||
import com.fasterxml.jackson.databind.module.SimpleModule | ||
import com.fasterxml.jackson.databind.ser.std.StdSerializer | ||
import com.twitter.finagle.stats.{CounterSchema, GaugeSchema, HistogramSchema, MetricSchema} | ||
|
||
object SchemaSerializer extends StdSerializer[MetricSchema](classOf[MetricSchema]) { | ||
|
||
/** | ||
* This custom serializer is used to convert MetricSchemas to JSON for the metric_metadata | ||
* endpoint. | ||
*/ | ||
// The impetus for a customer serializer over case class serde is: | ||
// 1) The nested nature of MetricBuilder with MetricSchema means we do not have the ability to | ||
// make decisions about which MetricBuilder fields to include based on MetricSchema type | ||
// (ie, buckets should only be present for HistogramSchema). | ||
// 2) Reshaping the MetricBuilder to be the top level JSON object removes the ability to inject | ||
// the "kind" field based on the MetricSchema type. | ||
// 3) The MetricBuilder class would need to be reworked to have a Source case class | ||
// nested in it which would contain a few of the currently MetricBuilder level values. | ||
def serialize( | ||
metricSchema: MetricSchema, | ||
jsonGenerator: JsonGenerator, | ||
serializerProvider: SerializerProvider | ||
): Unit = { | ||
jsonGenerator.writeStartObject() | ||
jsonGenerator.writeStringField("name", metricSchema.metricBuilder.name.mkString("/")) | ||
val dataType = metricSchema match { | ||
case _: CounterSchema => "counter" | ||
case _: GaugeSchema => "gauge" | ||
case _: HistogramSchema => "histogram" | ||
} | ||
jsonGenerator.writeStringField("kind", dataType) | ||
jsonGenerator.writeObjectFieldStart("source") | ||
jsonGenerator.writeStringField( | ||
"class", | ||
metricSchema.metricBuilder.sourceClass.getOrElse("Unspecified")) | ||
jsonGenerator.writeStringField("category", metricSchema.metricBuilder.role.toString) | ||
jsonGenerator.writeStringField( | ||
"process_path", | ||
metricSchema.metricBuilder.processPath.getOrElse("Unspecified")) | ||
jsonGenerator.writeEndObject() | ||
jsonGenerator.writeStringField("description", metricSchema.metricBuilder.description) | ||
jsonGenerator.writeStringField("unit", metricSchema.metricBuilder.units.toString) | ||
jsonGenerator.writeStringField("verbosity", metricSchema.metricBuilder.verbosity.toString) | ||
jsonGenerator.writeBooleanField("key_indicator", metricSchema.metricBuilder.keyIndicator) | ||
if (metricSchema.isInstanceOf[HistogramSchema]) { | ||
jsonGenerator.writeArrayFieldStart("buckets") | ||
metricSchema.metricBuilder.percentiles.foreach(bucket => jsonGenerator.writeNumber(bucket)) | ||
jsonGenerator.writeEndArray() | ||
} | ||
jsonGenerator.writeEndObject() | ||
} | ||
} | ||
|
||
object MetricSchemaJsonModule extends SimpleModule { | ||
addSerializer(SchemaSerializer) | ||
} |
52 changes: 52 additions & 0 deletions
52
server/src/main/scala/com/twitter/server/util/MetricSchemaSource.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.twitter.server.util | ||
|
||
import com.twitter.finagle.stats.{MetricSchema, SchemaRegistry} | ||
import com.twitter.finagle.util.LoadService | ||
|
||
private[server] object MetricSchemaSource { | ||
lazy val registry: Seq[SchemaRegistry] = LoadService[SchemaRegistry]() | ||
} | ||
|
||
/** | ||
* A map from stats names to [[com.twitter.finagle.stats.StatEntry StatsEntries]] | ||
* which allows for stale StatEntries up to `refreshInterval`. | ||
*/ | ||
private[server] class MetricSchemaSource( | ||
registry: Seq[SchemaRegistry] = MetricSchemaSource.registry) { | ||
|
||
/** | ||
* Indicates whether or not the MetricSource is using latched Counters. | ||
* @note this relies on the fact that there is only one StatsRegistry and that it is | ||
* the finagle implementation. | ||
*/ | ||
lazy val hasLatchedCounters: Boolean = { | ||
assert(registry.length > 0) | ||
registry.head.hasLatchedCounters | ||
} | ||
|
||
/** Returns the entry for `key` if it exists */ | ||
def getSchema(key: String): Option[MetricSchema] = synchronized { | ||
registry.map(_.schemas()).find(_.contains(key)).flatMap(_.get(key)) | ||
} | ||
|
||
/** Returns all schemas */ | ||
def schemaList(): Iterable[MetricSchema] = synchronized { | ||
registry | ||
.foldLeft(IndexedSeq[MetricSchema]()) { (seq, r) => | ||
seq ++ r.schemas().values | ||
} | ||
} | ||
|
||
/** Returns true if the map contains `key` and false otherwise. */ | ||
def contains(key: String): Boolean = synchronized { | ||
registry.exists(_.schemas().contains(key)) | ||
} | ||
|
||
/** Returns the set of stat keys. */ | ||
def keySet: Set[String] = synchronized { | ||
registry | ||
.foldLeft(Set[String]()) { (set, r) => | ||
set ++ r.schemas().keySet | ||
} | ||
} | ||
} |
Oops, something went wrong.