feature: add a JSON observer (#38)

This commit is contained in:
Toby Lawrence 2019-07-23 12:25:49 -04:00 committed by GitHub
parent 66ba3056a7
commit ed80f3307e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 527 additions and 227 deletions

View File

@ -8,4 +8,5 @@ members = [
"metrics-exporter-http",
"metrics-observer-text",
"metrics-observer-prometheus",
"metrics-observer-json",
]

View File

@ -11,6 +11,7 @@ description = "metric exporter for serving metrics over HTTP"
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-exporter-http"
readme = "README.md"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }

View File

@ -11,6 +11,7 @@ description = "metric exporter for outputting to logs"
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-exporter-log"
readme = "README.md"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }

3
metrics-observer-json/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

View File

@ -0,0 +1,30 @@
# The Code of Conduct
This document is based on the [Rust Code of Conduct](https://www.rust-lang.org/conduct.html) and outlines the standard of conduct which is both expected and enforced as part of this project.
## Conduct
* We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
* Avoid using overtly sexual nicknames or other nicknames that might detract from a friendly, safe and welcoming environment for all.
* Please be kind and courteous. There's no need to be mean or rude.
* Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
* Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
* We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behaviour. We interpret the term "harassment" as including the definition in the [Citizen Code of Conduct](http://citizencodeofconduct.org/); if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don't tolerate behavior that excludes people in socially marginalized groups.
* Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the repository Owners immediately. Whether you're a regular contributor or a newcomer, we care about making this community a safe place for you and we've got your back.
* Likewise any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome.
## Moderation
These are the policies for upholding our community's standards of conduct. If you feel that a thread needs moderation, please use the contact information above, or mention @tobz or @LucioFranco in the thread.
1. Remarks that violate this Code of Conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.)
2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed.
In the Rust community we strive to go the extra step to look out for each other. Don't just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if they're off-topic; this all too often leads to unnecessary fights, hurt feelings, and damaged trust; worse, it can drive people away from the community entirely.
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could've communicated better — remember that it's your responsibility to make your fellow Rustaceans comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
## Contacts:
- Toby Lawrence ([toby@nuclearfurnace.com](mailto:toby@nuclearfurnace.com))
- Lucio Franco ([luciofranco14@gmail.com](mailto:luciofranco14@gmail.com))

View File

@ -0,0 +1,22 @@
[package]
name = "metrics-observer-json"
version = "0.1.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
license = "MIT"
description = "metric observer for JSON output"
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-observer-text"
readme = "README.md"
keywords = ["metrics", "telemetry", "json"]
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.3" }
hdrhistogram = "^6.1"
serde_json = "^1.0"

View File

@ -0,0 +1,17 @@
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

View File

@ -0,0 +1,18 @@
# metrics-observer-json
[![conduct-badge][]][conduct] [![downloads-badge][] ![release-badge][]][crate] [![docs-badge][]][docs] [![license-badge][]](#license)
[conduct-badge]: https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg
[downloads-badge]: https://img.shields.io/crates/d/metrics-observer-json.svg
[release-badge]: https://img.shields.io/crates/v/metrics-observer-json.svg
[license-badge]: https://img.shields.io/crates/l/metrics-observer-json.svg
[docs-badge]: https://docs.rs/metrics-observer-json/badge.svg
[conduct]: https://github.com/metrics-rs/metrics-observer-json/blob/master/CODE_OF_CONDUCT.md
[crate]: https://crates.io/crates/metrics-observer-json
[docs]: https://docs.rs/metrics-observer-json
__metrics-observer-json__ is a metric observer that outputs JSON.
## code of conduct
**NOTE**: All conversations and contributions to this project shall adhere to the [Code of Conduct][conduct].

View File

@ -0,0 +1,187 @@
//! Observes metrics in a JSON format.
//!
//! Metric scopes are used to provide the hierarchy of metrics. As an example, for a
//! snapshot with two metrics — `server.msgs_received` and `server.msgs_sent` — we would
//! expect to see this output:
//!
//! ```c
//! {"server":{"msgs_received":42,"msgs_sent":13}}
//! ```
//!
//! If we added another metric — `configuration_reloads` — we would expect to see:
//!
//! ```c
//! {"configuration_reloads":2,"server":{"msgs_received":42,"msgs_sent":13}}
//! ```
//!
//! Metrics are sorted alphabetically.
//!
//! ## Histograms
//!
//! Histograms are rendered with a configurable set of quantiles that are provided when creating an
//! instance of `JsonBuilder`. They are formatted using human-readable labels when displayed to
//! the user. For example, 0.0 is rendered as "min", 1.0 as "max", and anything in between using
//! the common "pXXX" format i.e. a quantile of 0.5 or percentile of 50 would be p50, a quantile of
//! 0.999 or percentile of 99.9 would be p999, and so on.
//!
//! All histograms have the sample count of the histogram provided in the output.
//!
//! ```c
//! {"connect_time count":15,"connect_time min":1334,"connect_time p50":1934,"connect_time
//! p99":5330,"connect_time max":139389}
//! ```
//!
#![deny(missing_docs)]
use hdrhistogram::Histogram;
use metrics_core::{Builder, Drain, Key, Label, Observer};
use metrics_util::{parse_quantiles, MetricsTree, Quantile};
use std::collections::HashMap;
/// Builder for [`JsonObserver`].
pub struct JsonBuilder {
quantiles: Vec<Quantile>,
pretty: bool,
}
impl JsonBuilder {
/// Creates a new [`JsonBuilder`] with default values.
pub fn new() -> Self {
let quantiles = parse_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]);
Self {
quantiles,
pretty: false,
}
}
/// Sets the quantiles to use when rendering histograms.
///
/// Quantiles represent a scale of 0 to 1, where percentiles represent a scale of 1 to 100, so
/// a quantile of 0.99 is the 99th percentile, and a quantile of 0.99 is the 99.9th percentile.
///
/// By default, the quantiles will be set to: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and 1.0.
pub fn set_quantiles(mut self, quantiles: &[f64]) -> Self {
self.quantiles = parse_quantiles(quantiles);
self
}
/// Sets whether or not to render the JSON as "pretty."
///
/// Pretty JSON refers to the formatting and identation, where different fields are on
/// different lines, and depending on their depth from the root object, are indented.
///
/// By default, pretty mode is not enabled.
pub fn set_pretty_json(mut self, pretty: bool) -> Self {
self.pretty = pretty;
self
}
}
impl Builder for JsonBuilder {
type Output = JsonObserver;
fn build(&self) -> Self::Output {
JsonObserver {
quantiles: self.quantiles.clone(),
pretty: self.pretty,
tree: MetricsTree::default(),
histos: HashMap::new(),
}
}
}
impl Default for JsonBuilder {
fn default() -> Self {
Self::new()
}
}
/// Records metrics in a hierarchical, text-based format.
pub struct JsonObserver {
pub(crate) quantiles: Vec<Quantile>,
pub(crate) pretty: bool,
pub(crate) tree: MetricsTree,
pub(crate) histos: HashMap<Key, Histogram<u64>>,
}
impl Observer for JsonObserver {
fn observe_counter(&mut self, key: Key, value: u64) {
let (levels, name) = key_to_parts(key);
self.tree.insert_value(levels, name, value);
}
fn observe_gauge(&mut self, key: Key, value: i64) {
let (levels, name) = key_to_parts(key);
self.tree.insert_value(levels, name, value);
}
fn observe_histogram(&mut self, key: Key, values: &[u64]) {
let entry = self
.histos
.entry(key)
.or_insert_with(|| Histogram::<u64>::new(3).expect("failed to create histogram"));
for value in values {
entry
.record(*value)
.expect("failed to observe histogram value");
}
}
}
impl Drain<String> for JsonObserver {
fn drain(&mut self) -> String {
for (key, h) in self.histos.drain() {
let (levels, name) = key_to_parts(key);
let values = hist_to_values(name, h.clone(), &self.quantiles);
self.tree.insert_values(levels, values);
}
let result = if self.pretty {
serde_json::to_string_pretty(&self.tree)
} else {
serde_json::to_string(&self.tree)
};
let rendered = result.expect("failed to render json output");
self.tree.clear();
rendered
}
}
fn key_to_parts(key: Key) -> (Vec<String>, String) {
let (name, labels) = key.into_parts();
let mut parts = name.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
let name = parts.pop().expect("name didn't have a single part");
let labels = labels
.into_iter()
.map(Label::into_parts)
.map(|(k, v)| format!("{}=\"{}\"", k, v))
.collect::<Vec<_>>()
.join(",");
let label = if labels.is_empty() {
String::new()
} else {
format!("{{{}}}", labels)
};
let fname = format!("{}{}", name, label);
(parts, fname)
}
fn hist_to_values(
name: String,
hist: Histogram<u64>,
quantiles: &[Quantile],
) -> Vec<(String, u64)> {
let mut values = Vec::new();
values.push((format!("{} count", name), hist.len()));
for quantile in quantiles {
let value = hist.value_at_quantile(quantile.value());
values.push((format!("{} {}", name, quantile.label()), value));
}
values
}

View File

@ -1,18 +1,21 @@
[package]
name = "metrics-observer-prometheus"
version = "0.2.2"
version = "0.3.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
license = "MIT"
description = "metric recorder for Prometheus exposition output"
description = "metric observer for Prometheus exposition output"
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-recorder-prometheus"
documentation = "https://docs.rs/metrics-observer-prometheus"
readme = "README.md"
keywords = ["metrics", "telemetry", "prometheus"]
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.2" }
metrics-util = { path = "../metrics-util", version = "^0.3" }
hdrhistogram = "^6.1"

View File

@ -1,17 +1,17 @@
# metrics-recorder-prometheus
# metrics-observer-prometheus
[![conduct-badge][]][conduct] [![downloads-badge][] ![release-badge][]][crate] [![docs-badge][]][docs] [![license-badge][]](#license)
[conduct-badge]: https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg
[downloads-badge]: https://img.shields.io/crates/d/metrics-recorder-prometheus.svg
[release-badge]: https://img.shields.io/crates/v/metrics-recorder-prometheus.svg
[license-badge]: https://img.shields.io/crates/l/metrics-recorder-prometheus.svg
[docs-badge]: https://docs.rs/metrics-recorder-prometheus/badge.svg
[conduct]: https://github.com/metrics-rs/metrics-recorder-prometheus/blob/master/CODE_OF_CONDUCT.md
[crate]: https://crates.io/crates/metrics-recorder-prometheus
[docs]: https://docs.rs/metrics-recorder-prometheus
[downloads-badge]: https://img.shields.io/crates/d/metrics-observer-prometheus.svg
[release-badge]: https://img.shields.io/crates/v/metrics-observer-prometheus.svg
[license-badge]: https://img.shields.io/crates/l/metrics-observer-prometheus.svg
[docs-badge]: https://docs.rs/metrics-observer-prometheus/badge.svg
[conduct]: https://github.com/metrics-rs/metrics-observer-prometheus/blob/master/CODE_OF_CONDUCT.md
[crate]: https://crates.io/crates/metrics-observer-prometheus
[docs]: https://docs.rs/metrics-observer-prometheus
__metrics-recorder-prometheus__ is a metric recorder that outputs a Prometheus exposition format.
__metrics-observer-prometheus__ is a metric observer that outputs a Prometheus exposition format.
## code of conduct

View File

@ -5,32 +5,28 @@ use metrics_core::{Builder, Drain, Key, Label, Observer};
use metrics_util::{parse_quantiles, Quantile};
use std::{collections::HashMap, time::SystemTime};
/// Builder for [`PrometheusRecorder`].
/// Builder for [`PrometheusObserver`].
pub struct PrometheusBuilder {
quantiles: Vec<Quantile>,
}
impl PrometheusBuilder {
/// Creates a new [`PrometheusBuilder`] with a default set of quantiles.
///
/// Configures the recorder with these default quantiles: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and
/// 1.0. If you want to customize the quantiles used, you can call
/// [`PrometheusBuilder::with_quantiles`].
///
/// The configured quantiles are used when rendering any histograms.
/// Creates a new [`PrometheusBuilder`] with default values.
pub fn new() -> Self {
Self::default()
let quantiles = parse_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]);
Self { quantiles }
}
/// Creates a new [`PrometheusBuilder`] with the given set of quantiles.
/// Sets the quantiles to use when rendering histograms.
///
/// The configured quantiles are used when rendering any histograms.
pub fn with_quantiles(quantiles: &[f64]) -> Self {
let actual_quantiles = parse_quantiles(quantiles);
Self {
quantiles: actual_quantiles,
}
/// Quantiles represent a scale of 0 to 1, where percentiles represent a scale of 1 to 100, so
/// a quantile of 0.99 is the 99th percentile, and a quantile of 0.99 is the 99.9th percentile.
///
/// By default, the quantiles will be set to: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and 1.0.
pub fn set_quantiles(mut self, quantiles: &[f64]) -> Self {
self.quantiles = parse_quantiles(quantiles);
self
}
}
@ -48,7 +44,7 @@ impl Builder for PrometheusBuilder {
impl Default for PrometheusBuilder {
fn default() -> Self {
Self::with_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0])
Self::new()
}
}
@ -139,7 +135,8 @@ impl Drain<String> for PrometheusObserver {
fn key_to_parts(key: Key) -> (String, Vec<String>) {
let (name, labels) = key.into_parts();
let name = name.replace('.', "_");
let sanitize = |c| c == '.' || c == '=' || c == '{' || c == '}' || c == '+' || c == '-';
let name = name.replace(sanitize, "_");
let labels = labels
.into_iter()
.map(Label::into_parts)

View File

@ -1,18 +1,22 @@
[package]
name = "metrics-observer-text"
version = "0.2.2"
version = "0.3.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
license = "MIT"
description = "metric recorder for hierarchical, text-based output"
description = "metric observer for hierarchical, text-based output"
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-recorder-text"
documentation = "https://docs.rs/metrics-observer-text"
readme = "README.md"
keywords = ["metrics", "telemetry", "yaml"]
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.2" }
metrics-util = { path = "../metrics-util", version = "^0.3" }
hdrhistogram = "^6.1"
serde_yaml = "^0.8"

View File

@ -1,17 +1,17 @@
# metrics-recorder-text
# metrics-observer-text
[![conduct-badge][]][conduct] [![downloads-badge][] ![release-badge][]][crate] [![docs-badge][]][docs] [![license-badge][]](#license)
[conduct-badge]: https://img.shields.io/badge/%E2%9D%A4-code%20of%20conduct-blue.svg
[downloads-badge]: https://img.shields.io/crates/d/metrics-recorder-text.svg
[release-badge]: https://img.shields.io/crates/v/metrics-recorder-text.svg
[license-badge]: https://img.shields.io/crates/l/metrics-recorder-text.svg
[docs-badge]: https://docs.rs/metrics-recorder-text/badge.svg
[conduct]: https://github.com/metrics-rs/metrics-recorder-text/blob/master/CODE_OF_CONDUCT.md
[crate]: https://crates.io/crates/metrics-recorder-text
[docs]: https://docs.rs/metrics-recorder-text
[downloads-badge]: https://img.shields.io/crates/d/metrics-observer-text.svg
[release-badge]: https://img.shields.io/crates/v/metrics-observer-text.svg
[license-badge]: https://img.shields.io/crates/l/metrics-observer-text.svg
[docs-badge]: https://docs.rs/metrics-observer-text/badge.svg
[conduct]: https://github.com/metrics-rs/metrics-observer-text/blob/master/CODE_OF_CONDUCT.md
[crate]: https://crates.io/crates/metrics-observer-text
[docs]: https://docs.rs/metrics-observer-text
__metrics-recorder-text__ is a metric recorder that outputs a hierarchical, text-based format.
__metrics-observer-text__ is a metric observer that outputs a hierarchical, text-based format.
## code of conduct

View File

@ -5,20 +5,18 @@
//! expect to see this output:
//!
//! ```c
//! root:
//! server:
//! msgs_received: 42
//! msgs_sent: 13
//! server:
//! msgs_received: 42
//! msgs_sent: 13
//! ```
//!
//! If we added another metric — `configuration_reloads` — we would expect to see:
//!
//! ```c
//! root:
//! configuration_reloads: 2
//! server:
//! msgs_received: 42
//! msgs_sent: 13
//! configuration_reloads: 2
//! server:
//! msgs_received: 42
//! msgs_sent: 13
//! ```
//!
//! Metrics are sorted alphabetically.
@ -26,7 +24,7 @@
//! ## Histograms
//!
//! Histograms are rendered with a configurable set of quantiles that are provided when creating an
//! instance of `TextObserver`. They are formatted using human-readable labels when displayed to
//! instance of `TextBuilder`. They are formatted using human-readable labels when displayed to
//! the user. For example, 0.0 is rendered as "min", 1.0 as "max", and anything in between using
//! the common "pXXX" format i.e. a quantile of 0.5 or percentile of 50 would be p50, a quantile of
//! 0.999 or percentile of 99.9 would be p999, and so on.
@ -34,49 +32,41 @@
//! All histograms have the sample count of the histogram provided in the output.
//!
//! ```c
//! root:
//! connect_time count: 15
//! connect_time min: 1334
//! connect_time p50: 1934
//! connect_time p99: 5330
//! connect_time max: 139389
//! connect_time count: 15
//! connect_time min: 1334
//! connect_time p50: 1934
//! connect_time p99: 5330
//! connect_time max: 139389
//! ```
//!
#![deny(missing_docs)]
use hdrhistogram::Histogram;
use metrics_core::{Builder, Drain, Key, Label, Observer};
use metrics_util::{parse_quantiles, Quantile};
use std::{
collections::{HashMap, VecDeque},
fmt::Display,
};
use metrics_util::{parse_quantiles, MetricsTree, Quantile};
use std::collections::HashMap;
/// Builder for [`TextRecorder`].
/// Builder for [`TextObserver`].
pub struct TextBuilder {
quantiles: Vec<Quantile>,
}
impl TextBuilder {
/// Creates a new [`TextBuilder`] with a default set of quantiles.
///
/// Configures the observer with these default quantiles: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and
/// 1.0. If you want to customize the quantiles used, you can call
/// [`TextBuilder::with_quantiles`].
///
/// The configured quantiles are used when rendering any histograms.
/// Creates a new [`TextBuilder`] with default values.
pub fn new() -> Self {
Self::default()
let quantiles = parse_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]);
Self { quantiles }
}
/// Creates a new [`TextBuilder`] with the given set of quantiles.
/// Sets the quantiles to use when rendering histograms.
///
/// The configured quantiles are used when rendering any histograms.
pub fn with_quantiles(quantiles: &[f64]) -> Self {
let actual_quantiles = parse_quantiles(quantiles);
Self {
quantiles: actual_quantiles,
}
/// Quantiles represent a scale of 0 to 1, where percentiles represent a scale of 1 to 100, so
/// a quantile of 0.99 is the 99th percentile, and a quantile of 0.99 is the 99.9th percentile.
///
/// By default, the quantiles will be set to: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and 1.0.
pub fn set_quantiles(mut self, quantiles: &[f64]) -> Self {
self.quantiles = parse_quantiles(quantiles);
self
}
}
@ -86,7 +76,7 @@ impl Builder for TextBuilder {
fn build(&self) -> Self::Output {
TextObserver {
quantiles: self.quantiles.clone(),
structure: MetricsTree::with_level(0),
tree: MetricsTree::default(),
histos: HashMap::new(),
}
}
@ -94,28 +84,26 @@ impl Builder for TextBuilder {
impl Default for TextBuilder {
fn default() -> Self {
Self::with_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0])
Self::new()
}
}
/// Records metrics in a hierarchical, text-based format.
pub struct TextObserver {
pub(crate) quantiles: Vec<Quantile>,
pub(crate) structure: MetricsTree,
pub(crate) tree: MetricsTree,
pub(crate) histos: HashMap<Key, Histogram<u64>>,
}
impl Observer for TextObserver {
fn observe_counter(&mut self, key: Key, value: u64) {
let (name_parts, name) = key_to_parts(key);
let mut values = single_value_to_values(name, value);
self.structure.insert(name_parts, &mut values);
let (levels, name) = key_to_parts(key);
self.tree.insert_value(levels, name, value);
}
fn observe_gauge(&mut self, key: Key, value: i64) {
let (name_parts, name) = key_to_parts(key);
let mut values = single_value_to_values(name, value);
self.structure.insert(name_parts, &mut values);
let (levels, name) = key_to_parts(key);
self.tree.insert_value(levels, name, value);
}
fn observe_histogram(&mut self, key: Key, values: &[u64]) {
@ -132,130 +120,24 @@ impl Observer for TextObserver {
}
}
struct MetricsTree {
level: usize,
current: Vec<String>,
next: HashMap<String, MetricsTree>,
}
impl MetricsTree {
pub fn with_level(level: usize) -> Self {
MetricsTree {
level,
current: Vec::new(),
next: HashMap::new(),
}
}
pub fn insert(&mut self, mut name_parts: VecDeque<String>, values: &mut Vec<String>) {
match name_parts.len() {
0 => {
let indent = " ".repeat(self.level);
let mut indented = values
.iter()
.map(move |x| format!("{}{}", indent, x))
.collect::<Vec<_>>();
self.current.append(&mut indented);
}
_ => {
let name = name_parts
.pop_front()
.expect("failed to get next name component");
let current_level = self.level;
let inner = self
.next
.entry(name)
.or_insert_with(move || MetricsTree::with_level(current_level + 1));
inner.insert(name_parts, values);
}
}
}
pub fn render(&mut self) -> String {
let indent = " ".repeat(self.level);
let mut output = String::new();
let mut sorted = self
.current
.drain(..)
.map(SortEntry::Inline)
.chain(self.next.drain().map(|(k, v)| SortEntry::Nested(k, v)))
.collect::<Vec<_>>();
sorted.sort();
for entry in sorted {
match entry {
SortEntry::Inline(s) => {
output.push_str(s.as_str());
output.push_str("\n");
}
SortEntry::Nested(s, mut inner) => {
output.push_str(indent.as_str());
output.push_str(s.as_str());
output.push_str(":\n");
let layer_output = inner.render();
output.push_str(layer_output.as_str());
}
}
}
output
}
}
impl Drain<String> for TextObserver {
fn drain(&mut self) -> String {
for (key, h) in self.histos.drain() {
let (name_parts, name) = key_to_parts(key);
let mut values = hist_to_values(name, h.clone(), &self.quantiles);
self.structure.insert(name_parts, &mut values);
let (levels, name) = key_to_parts(key);
let values = hist_to_values(name, h.clone(), &self.quantiles);
self.tree.insert_values(levels, values);
}
self.structure.render()
let rendered = serde_yaml::to_string(&self.tree).expect("failed to render json output");
self.tree.clear();
rendered
}
}
enum SortEntry {
Inline(String),
Nested(String, MetricsTree),
}
impl SortEntry {
fn name(&self) -> &str {
match self {
SortEntry::Inline(s) => s,
SortEntry::Nested(s, _) => s,
}
}
}
impl PartialEq for SortEntry {
fn eq(&self, other: &SortEntry) -> bool {
self.name() == other.name()
}
}
impl Eq for SortEntry {}
impl std::cmp::PartialOrd for SortEntry {
fn partial_cmp(&self, other: &SortEntry) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for SortEntry {
fn cmp(&self, other: &SortEntry) -> std::cmp::Ordering {
self.name().cmp(other.name())
}
}
fn key_to_parts(key: Key) -> (VecDeque<String>, String) {
fn key_to_parts(key: Key) -> (Vec<String>, String) {
let (name, labels) = key.into_parts();
let mut parts = name
.split('.')
.map(ToOwned::to_owned)
.collect::<VecDeque<_>>();
let name = parts.pop_back().expect("name didn't have a single part");
let mut parts = name.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
let name = parts.pop().expect("name didn't have a single part");
let labels = labels
.into_iter()
@ -274,21 +156,17 @@ fn key_to_parts(key: Key) -> (VecDeque<String>, String) {
(parts, fname)
}
fn single_value_to_values<T>(name: String, value: T) -> Vec<String>
where
T: Display,
{
let fvalue = format!("{}: {}", name, value);
vec![fvalue]
}
fn hist_to_values(name: String, hist: Histogram<u64>, quantiles: &[Quantile]) -> Vec<String> {
fn hist_to_values(
name: String,
hist: Histogram<u64>,
quantiles: &[Quantile],
) -> Vec<(String, u64)> {
let mut values = Vec::new();
values.push(format!("{} count: {}", name, hist.len()));
values.push((format!("{} count", name), hist.len()));
for quantile in quantiles {
let value = hist.value_at_quantile(quantile.value());
values.push(format!("{} {}: {}", name, quantile.label(), value));
values.push((format!("{} {}", name, quantile.label()), value));
}
values

View File

@ -19,7 +19,7 @@ keywords = ["metrics", "telemetry", "histogram", "counter", "gauge"]
[features]
default = ["exporters", "observers"]
exporters = ["metrics-exporter-log", "metrics-exporter-http"]
observers = ["metrics-observer-text", "metrics-observer-prometheus"]
observers = ["metrics-observer-text", "metrics-observer-json", "metrics-observer-prometheus"]
[[bench]]
name = "histogram"
@ -27,7 +27,7 @@ harness = false
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.2" }
metrics-util = { path = "../metrics-util", version = "^0.3" }
metrics = { path = "../metrics", version = "^0.11", features = ["std"] }
im = "^12"
arc-swap = "^0.3"
@ -38,8 +38,9 @@ futures = "^0.1"
crossbeam-utils = "^0.6"
metrics-exporter-log = { path = "../metrics-exporter-log", version = "^0.2", optional = true }
metrics-exporter-http = { path = "../metrics-exporter-http", version = "^0.1", optional = true }
metrics-observer-text = { path = "../metrics-observer-text", version = "^0.2", optional = true }
metrics-observer-prometheus = { path = "../metrics-observer-prometheus", version = "^0.2", optional = true }
metrics-observer-text = { path = "../metrics-observer-text", version = "^0.3", optional = true }
metrics-observer-prometheus = { path = "../metrics-observer-prometheus", version = "^0.3", optional = true }
metrics-observer-json = { path = "../metrics-observer-json", version = "^0.1", optional = true }
[dev-dependencies]
log = "^0.4"

View File

@ -11,7 +11,7 @@ extern crate metrics;
use getopts::Options;
use hdrhistogram::Histogram;
use metrics_runtime::Receiver;
use metrics_runtime::{exporters::HttpExporter, observers::JsonBuilder, Receiver};
use quanta::Clock;
use std::{
env,
@ -66,8 +66,8 @@ impl Generator {
0
};
counter!("ok", 1);
timing!("ok", t0, t1);
counter!("ok.gotem", 1);
timing!("ok.gotem", t0, t1);
gauge!("total", self.gauge);
if start != 0 {
@ -162,6 +162,14 @@ fn main() {
.expect("failed to build receiver");
let controller = receiver.get_controller();
let addr = "0.0.0.0:23432"
.parse()
.expect("failed to parse http listen address");
let builder = JsonBuilder::new().set_pretty_json(true);
let exporter = HttpExporter::new(controller.clone(), builder, addr);
thread::spawn(move || exporter.run());
receiver.install();
info!("receiver configured");

View File

@ -4,5 +4,8 @@
#[cfg(feature = "metrics-observer-text")]
pub use metrics_observer_text::TextBuilder;
#[cfg(feature = "metrics-observer-json")]
pub use metrics_observer_json::JsonBuilder;
#[cfg(feature = "metrics-observer-prometheus")]
pub use metrics_observer_prometheus::PrometheusBuilder;

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-util"
version = "0.2.0"
version = "0.3.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -26,6 +26,7 @@ harness = false
[dependencies]
crossbeam-epoch = "^0.7"
serde = "^1.0"
[dev-dependencies]
crossbeam = "^0.7"

View File

@ -8,3 +8,6 @@ pub use streaming::StreamingIntegers;
mod quantile;
pub use quantile::{parse_quantiles, Quantile};
mod tree;
pub use tree::{Integer, MetricsTree};

122
metrics-util/src/tree.rs Normal file
View File

@ -0,0 +1,122 @@
use serde::ser::{Serialize, Serializer};
use std::collections::HashMap;
/// An integer metric value.
pub enum Integer {
/// A signed value.
Signed(i64),
/// An unsigned value.
Unsigned(u64),
}
impl From<i64> for Integer {
fn from(i: i64) -> Integer {
Integer::Signed(i)
}
}
impl From<u64> for Integer {
fn from(i: u64) -> Integer {
Integer::Unsigned(i)
}
}
enum TreeEntry {
Value(Integer),
Nested(MetricsTree),
}
impl Serialize for TreeEntry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
TreeEntry::Value(value) => match value {
Integer::Signed(i) => serializer.serialize_i64(*i),
Integer::Unsigned(i) => serializer.serialize_u64(*i),
},
TreeEntry::Nested(tree) => tree.serialize(serializer),
}
}
}
/// A tree-structured metrics container.
///
/// Used for building a tree structure out of scoped metrics, where each level in the tree
/// represents a nested scope.
#[derive(Default)]
pub struct MetricsTree {
contents: HashMap<String, TreeEntry>,
}
impl MetricsTree {
/// Inserts a single value into the tree.
pub fn insert_value<V: Into<Integer>>(
&mut self,
mut levels: Vec<String>,
key: String,
value: V,
) {
match levels.len() {
0 => {
self.contents.insert(key, TreeEntry::Value(value.into()));
}
_ => {
let name = levels.remove(0);
let inner = self
.contents
.entry(name)
.or_insert_with(|| TreeEntry::Nested(MetricsTree::default()));
if let TreeEntry::Nested(tree) = inner {
tree.insert_value(levels, key, value);
}
}
}
}
/// Inserts multiple values into the tree.
pub fn insert_values<V: Into<Integer>>(
&mut self,
mut levels: Vec<String>,
values: Vec<(String, V)>,
) {
match levels.len() {
0 => {
for v in values.into_iter() {
self.contents.insert(v.0, TreeEntry::Value(v.1.into()));
}
}
_ => {
let name = levels.remove(0);
let inner = self
.contents
.entry(name)
.or_insert_with(|| TreeEntry::Nested(MetricsTree::default()));
if let TreeEntry::Nested(tree) = inner {
tree.insert_values(levels, values);
}
}
}
}
/// Clears all entries in the tree.
pub fn clear(&mut self) {
self.contents.clear();
}
}
impl Serialize for MetricsTree {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut sorted = self.contents.iter().collect::<Vec<_>>();
sorted.sort_by_key(|p| p.0);
serializer.collect_map(sorted)
}
}