Skip to content
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

feat(metrics): Allow ingestion of custom metric units [INGEST-1131] #1256

Merged
merged 7 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Add platform, op, http.method and status tag to all extracted transaction metrics. ([#1227](https://github.com/getsentry/relay/pull/1227))
- Add units in built-in measurements. ([#1229](https://github.com/getsentry/relay/pull/1229))
- Add protocol support for custom units on transaction measurements. ([#1256](https://github.com/getsentry/relay/pull/1256))

**Bug Fixes**:

Expand All @@ -20,7 +21,7 @@
- Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235))
- Remove the unused "internal" data category. ([#1245](https://github.com/getsentry/relay/pull/1245))
- Add the client and version as `sdk` tag to extracted session metrics in the format `name/version`. ([#1248](https://github.com/getsentry/relay/pull/1248))
- - Expose `shutdown_timeout` in `OverridableConfig` ([#1247](https://github.com/getsentry/relay/pull/1247))
- Expose `shutdown_timeout` in `OverridableConfig` ([#1247](https://github.com/getsentry/relay/pull/1247))

## 22.4.0

Expand Down
4 changes: 4 additions & 0 deletions py/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Add protocol support for custom units on transaction measurements. ([#1256](https://github.com/getsentry/relay/pull/1256))

## 0.8.10

- Map Windows version from raw_description to version name (XP, Vista, 11, ...). ([#1219](https://github.com/getsentry/relay/pull/1219))
Expand Down
290 changes: 290 additions & 0 deletions relay-common/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,293 @@ impl fmt::Display for SpanStatus {
}
}
}

/// Time duration units used in [`MetricUnit::Duration`].
///
/// Defaults to `millisecond`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum DurationUnit {
/// Nanosecond (`"nanosecond"`), 10^-9 seconds.
NanoSecond,
/// Microsecond (`"microsecond"`), 10^-6 seconds.
MicroSecond,
/// Millisecond (`"millisecond"`), 10^-3 seconds.
MilliSecond,
/// Full second (`"second"`).
Second,
/// Minute (`"minute"`), 60 seconds.
Minute,
/// Hour (`"hour"`), 3600 seconds.
Hour,
/// Day (`"day"`), 86,400 seconds.
Day,
/// Week (`"week"`), 604,800 seconds.
Week,
}

impl Default for DurationUnit {
fn default() -> Self {
Self::MilliSecond
}
}

impl fmt::Display for DurationUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NanoSecond => f.write_str("nanosecond"),
Self::MicroSecond => f.write_str("microsecond"),
Self::MilliSecond => f.write_str("millisecond"),
Self::Second => f.write_str("second"),
Self::Minute => f.write_str("minute"),
Self::Hour => f.write_str("hour"),
Self::Day => f.write_str("day"),
Self::Week => f.write_str("week"),
}
}
}

/// An error parsing a [`MetricUnit`] or one of its variants.
#[derive(Clone, Copy, Debug)]
pub struct ParseMetricUnitError(());

/// Size of information derived from bytes, used in [`MetricUnit::Information`].
///
/// Defaults to `byte`. See also [Units of
/// information](https://en.wikipedia.org/wiki/Units_of_information).
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum InformationUnit {
/// Bit (`"bit"`), corresponding to 1/8 of a byte.
///
/// Note that there are computer systems with a different number of bits per byte.
Bit,
/// Byte (`"byte"`).
Byte,
/// Kilobyte (`"kilobyte"`), 10^3 bytes.
KiloByte,
/// Kibibyte (`"kibibyte"`), 2^10 bytes.
KibiByte,
/// Megabyte (`"megabyte"`), 10^6 bytes.
MegaByte,
/// Mebibyte (`"mebibyte"`), 2^20 bytes.
MebiByte,
/// Gigabyte (`"gigabyte"`), 10^9 bytes.
GigaByte,
/// Gibibyte (`"gibibyte"`), 2^30 bytes.
GibiByte,
/// Terabyte (`"terabyte"`), 10^12 bytes.
TeraByte,
/// Tebibyte (`"tebibyte"`), 2^40 bytes.
TebiByte,
/// Petabyte (`"petabyte"`), 10^15 bytes.
PetaByte,
/// Pebibyte (`"pebibyte"`), 2^50 bytes.
PebiByte,
/// Exabyte (`"exabyte"`), 10^18 bytes.
ExaByte,
/// Exbibyte (`"exbibyte"`), 2^60 bytes.
ExbiByte,
}

impl Default for InformationUnit {
fn default() -> Self {
Self::Byte
}
}

impl fmt::Display for InformationUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bit => f.write_str("bit"),
Self::Byte => f.write_str("byte"),
Self::KiloByte => f.write_str("kilobyte"),
Self::KibiByte => f.write_str("kibibyte"),
Self::MegaByte => f.write_str("megabyte"),
Self::MebiByte => f.write_str("mebibyte"),
Self::GigaByte => f.write_str("gigabyte"),
Self::GibiByte => f.write_str("gibibyte"),
Self::TeraByte => f.write_str("terabyte"),
Self::TebiByte => f.write_str("tebibyte"),
Self::PetaByte => f.write_str("petabyte"),
Self::PebiByte => f.write_str("pebibyte"),
Self::ExaByte => f.write_str("exabyte"),
Self::ExbiByte => f.write_str("exbibyte"),
}
}
}

/// Units of fraction used in [`MetricUnit::Fraction`].
///
/// Defaults to `ratio`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum FractionUnit {
/// Floating point fraction of `1`.
Ratio,
/// Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`.
Percent,
}

impl Default for FractionUnit {
fn default() -> Self {
Self::Ratio
}
}

impl fmt::Display for FractionUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ratio => f.write_str("ratio"),
Self::Percent => f.write_str("percent"),
}
}
}

/// Custom user-defined units without builtin conversion.
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct CustomUnit([u8; 15]);

impl CustomUnit {
/// Parses a `CustomUnit` from a string.
pub fn parse(s: &str) -> Result<Self, ParseMetricUnitError> {
if s.len() > 15 || !s.is_ascii() {
return Err(ParseMetricUnitError(()));
}

let mut unit = Self(Default::default());
unit.0.copy_from_slice(s.as_bytes());
unit.0.make_ascii_lowercase();
Ok(unit)
}

/// Returns the string representation of this unit.
#[inline]
pub fn as_str(&self) -> &str {
// Safety: The string is already validated to be of length 32 and valid ASCII when
// constructing `ProjectKey`.
unsafe { std::str::from_utf8_unchecked(&self.0) }
}
}

impl fmt::Debug for CustomUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}

impl fmt::Display for CustomUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}

impl std::str::FromStr for CustomUnit {
type Err = ParseMetricUnitError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}

impl std::ops::Deref for CustomUnit {
type Target = str;

fn deref(&self) -> &Self::Target {
self.as_str()
}
}

/// The unit of measurement of a metric value.
///
/// Units augment metric values by giving them a magnitude and semantics. There are certain types of
/// units that are subdivided in their precision, such as the [`DurationUnit`] for time
/// measurements.
///
/// Units and their precisions are uniquely represented by a string identifier.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum MetricUnit {
/// A time duration, defaulting to `"millisecond"`.
Duration(DurationUnit),
/// Size of information derived from bytes, defaulting to `"byte"`.
Information(InformationUnit),
/// Fractions such as percentages, defaulting to `"ratio"`.
Fraction(FractionUnit),
/// user-defined units without builtin conversion or default.
Custom(CustomUnit),
/// Untyped value without a unit (`""`).
None,
}

impl MetricUnit {
/// Returns `true` if the metric_unit is [`None`].
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}

impl Default for MetricUnit {
fn default() -> Self {
MetricUnit::None
}
}

impl fmt::Display for MetricUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetricUnit::Duration(u) => u.fmt(f),
MetricUnit::Information(u) => u.fmt(f),
MetricUnit::Fraction(u) => u.fmt(f),
MetricUnit::Custom(u) => u.fmt(f),
MetricUnit::None => f.write_str("none"),
}
}
}

impl std::str::FromStr for MetricUnit {
type Err = ParseMetricUnitError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"nanosecond" | "ns" => Self::Duration(DurationUnit::NanoSecond),
"microsecond" => Self::Duration(DurationUnit::MicroSecond),
"millisecond" | "ms" => Self::Duration(DurationUnit::MilliSecond),
"second" | "s" => Self::Duration(DurationUnit::Second),
"minute" => Self::Duration(DurationUnit::Minute),
"hour" => Self::Duration(DurationUnit::Hour),
"day" => Self::Duration(DurationUnit::Day),
"week" => Self::Duration(DurationUnit::Week),

"bit" => Self::Information(InformationUnit::Bit),
"byte" => Self::Information(InformationUnit::Byte),
"kilobyte" => Self::Information(InformationUnit::KiloByte),
"kibibyte" => Self::Information(InformationUnit::KibiByte),
"megabyte" => Self::Information(InformationUnit::MegaByte),
"mebibyte" => Self::Information(InformationUnit::MebiByte),
"gigabyte" => Self::Information(InformationUnit::GigaByte),
"gibibyte" => Self::Information(InformationUnit::GibiByte),
"terabyte" => Self::Information(InformationUnit::TeraByte),
"tebibyte" => Self::Information(InformationUnit::TebiByte),
"petabyte" => Self::Information(InformationUnit::PetaByte),
"pebibyte" => Self::Information(InformationUnit::PebiByte),
"exabyte" => Self::Information(InformationUnit::ExaByte),
"exbibyte" => Self::Information(InformationUnit::ExbiByte),

"ratio" => Self::Fraction(FractionUnit::Ratio),
"percent" => Self::Fraction(FractionUnit::Percent),

"" | "unit" | "none" => Self::None,
Copy link
Member

@AbhiPrasad AbhiPrasad May 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a discussion offline about if "unit" should pattern matched to Self::None. The reasoning that this was put in place was to track the unit type () that exists in different languages. Does this still make sense? I would rather remove it.

Also worthwhile syncing to see if we can better match OpenTelemetry in general here, particularly around defining dimensionless units, and opting into custom units (like recording X number of Y - for ex with a counter for network packets, 100 (measure) packets (unit)).

https://opentelemetry.io/docs/reference/specification/metrics/semantic_conventions/#instrument-units

Units should follow the Unified Code for Units of Measure (need more clarification in #705).

  • Instruments for utilization metrics (that measure the fraction out of a total) are dimensionless and SHOULD use the default unit 1 (the unity).
  • Instruments that measure an integer count of something SHOULD only use annotations with curly braces to give additional meaning without the leading default unit (1). For example, use {packets}, {errors}, {faults}, etc.

Copy link
Member Author

@jan-auer jan-auer May 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, agreed to remove "unit" from here. This has not been used actively, so we can safely remove it.

Other than that, this PR merely moves the existing unit definition and derives new traits. We can ideally discuss units in a new DACI or GitHub issue. Units are definitely not ideal yet, and we're also not able to support X per Y units (e.g. frames per second). Note that this is different from fractions.

_ => Self::Custom(CustomUnit::parse(s)?),
})
}
}

impl_str_serde!(MetricUnit, "a metric unit string");

#[cfg(feature = "jsonschema")]
impl schemars::JsonSchema for MetricUnit {
fn schema_name() -> String {
std::any::type_name::<Self>().to_owned()
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(gen)
}
}
Loading