diff --git a/opentelemetry-sdk/src/metrics/internal/aggregate.rs b/opentelemetry-sdk/src/metrics/internal/aggregate.rs index e004240c60..4e589e6ced 100644 --- a/opentelemetry-sdk/src/metrics/internal/aggregate.rs +++ b/opentelemetry-sdk/src/metrics/internal/aggregate.rs @@ -16,6 +16,11 @@ pub(crate) fn is_under_cardinality_limit(size: usize) -> bool { size < STREAM_CARDINALITY_LIMIT as usize } +/// Checks whether aggregator has hit cardinality limit for metric streams +pub(crate) fn cardinality_limit() -> usize { + STREAM_CARDINALITY_LIMIT as usize +} + /// Receives measurements to be aggregated. pub(crate) trait Measure: Send + Sync + 'static { fn call(&self, measurement: T, attrs: &[KeyValue]); diff --git a/opentelemetry-sdk/src/metrics/internal/exponential_histogram.rs b/opentelemetry-sdk/src/metrics/internal/exponential_histogram.rs index c23b441663..3c1baa61db 100644 --- a/opentelemetry-sdk/src/metrics/internal/exponential_histogram.rs +++ b/opentelemetry-sdk/src/metrics/internal/exponential_histogram.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, f64::consts::LOG2_E, sync::Mutex, time::SystemTime}; use once_cell::sync::Lazy; -use opentelemetry::{metrics::MetricsError, KeyValue}; +use opentelemetry::{otel_error, KeyValue}; use crate::{ metrics::data::{self, Aggregation, Temporality}, @@ -100,9 +100,22 @@ impl ExpoHistogramDataPoint { if (self.scale - scale_delta as i8) < EXPO_MIN_SCALE { // With a scale of -10 there is only two buckets for the whole range of f64 values. // This can only happen if there is a max size of 1. - opentelemetry::global::handle_error(MetricsError::Other( - "exponential histogram scale underflow".into(), - )); + + // This error is logged when a measurement is dropped because its value is either too high + // or too low, falling outside the allowable range defined by the scale and max_size. + // If these values are expected, the user should adjust the histogram configuration + // to accommodate the range. + otel_error!( + name: "ExponentialHistogramDataPoint.Scale.Underflow", + current_scale = self.scale, + scale_delta = scale_delta, + max_size = self.max_size, + min_scale = EXPO_MIN_SCALE, + record_min_max = self.record_min_max, + record_sum = self.record_sum, + value = format!("{:?}", v), + error = format!("The measurement will be dropped due to scale underflow. Check the histogram configuration") + ); return; } // Downscale diff --git a/opentelemetry-sdk/src/metrics/internal/mod.rs b/opentelemetry-sdk/src/metrics/internal/mod.rs index 7a79fe4653..271c1d6e79 100644 --- a/opentelemetry-sdk/src/metrics/internal/mod.rs +++ b/opentelemetry-sdk/src/metrics/internal/mod.rs @@ -12,12 +12,11 @@ use std::ops::{Add, AddAssign, Sub}; use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; -use aggregate::is_under_cardinality_limit; +use aggregate::{cardinality_limit, is_under_cardinality_limit}; pub(crate) use aggregate::{AggregateBuilder, ComputeAggregation, Measure}; pub(crate) use exponential_histogram::{EXPO_MAX_SCALE, EXPO_MIN_SCALE}; use once_cell::sync::Lazy; -use opentelemetry::metrics::MetricsError; -use opentelemetry::{global, otel_warn, KeyValue}; +use opentelemetry::{otel_warn, KeyValue}; use crate::metrics::AttributeSet; @@ -146,9 +145,10 @@ impl, T: Number, O: Operation> ValueMap { let new_tracker = AU::new_atomic_tracker(self.buckets_count); O::update_tracker(&new_tracker, measurement, index); trackers.insert(STREAM_OVERFLOW_ATTRIBUTES.clone(), Arc::new(new_tracker)); - global::handle_error(MetricsError::Other("Warning: Maximum data points for metric stream exceeded. Entry added to overflow. Subsequent overflows to same metric until next collect will not be logged.".into())); - otel_warn!( name: "ValueMap.measure", - message = "Maximum data points for metric stream exceeded. Entry added to overflow. Subsequent overflows to same metric until next collect will not be logged." + //TODO - include name of meter, instrument + otel_warn!( name: "MetricCardinalityLimitReached", + message = format!("{}", "Maximum data points for metric stream exceeded. Entry added to overflow. Subsequent overflows to same metric will not be logged until next collect."), + cardinality_limit = cardinality_limit() as u64, ); } } diff --git a/opentelemetry-sdk/src/metrics/manual_reader.rs b/opentelemetry-sdk/src/metrics/manual_reader.rs index e1cb571ec3..abc0bcdc98 100644 --- a/opentelemetry-sdk/src/metrics/manual_reader.rs +++ b/opentelemetry-sdk/src/metrics/manual_reader.rs @@ -4,8 +4,8 @@ use std::{ }; use opentelemetry::{ - global, metrics::{MetricsError, Result}, + otel_debug, }; use super::{ @@ -76,10 +76,11 @@ impl MetricReader for ManualReader { // Only register once. If producer is already set, do nothing. if inner.sdk_producer.is_none() { inner.sdk_producer = Some(pipeline); - } else { - global::handle_error(MetricsError::Config( - "duplicate reader registration, did not register manual reader".into(), - )) + } else { + otel_debug!( + name: "ManualReader.RegisterPipeline.DuplicateRegistration", + error = "The pipeline is already registered to the Reader. Registering pipeline multiple times is not allowed." + ); } }); } diff --git a/opentelemetry-sdk/src/metrics/meter.rs b/opentelemetry-sdk/src/metrics/meter.rs index 45131ed611..c01c65487b 100644 --- a/opentelemetry-sdk/src/metrics/meter.rs +++ b/opentelemetry-sdk/src/metrics/meter.rs @@ -2,12 +2,12 @@ use core::fmt; use std::{borrow::Cow, sync::Arc}; use opentelemetry::{ - global, metrics::{ AsyncInstrumentBuilder, Counter, Gauge, Histogram, HistogramBuilder, InstrumentBuilder, InstrumentProvider, MetricsError, ObservableCounter, ObservableGauge, ObservableUpDownCounter, Result, UpDownCounter, }, + otel_error, }; use crate::instrumentation::Scope; @@ -75,14 +75,18 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the counter will be ignored. Reason: {}", err)); return Counter::new(Arc::new(NoopSyncInstrument::new())); } match resolver .lookup( InstrumentKind::Counter, - builder.name, + builder.name.clone(), builder.description, builder.unit, None, @@ -91,7 +95,12 @@ impl SdkMeter { { Ok(counter) => counter, Err(err) => { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err) + ); Counter::new(Arc::new(NoopSyncInstrument::new())) } } @@ -107,34 +116,41 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateObservableCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err)); return ObservableCounter::new(); } match resolver.measures( InstrumentKind::ObservableCounter, - builder.name, + builder.name.clone(), builder.description, builder.unit, None, ) { Ok(ms) => { if ms.is_empty() { + otel_error!( + name: "SdkMeter.CreateObservableCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + message = "Measurements from the instrument will be ignored. Reason: View Configuration / Drop Aggregation" + ); return ObservableCounter::new(); } - let observable = Arc::new(Observable::new(ms)); - for callback in builder.callbacks { let cb_inst = Arc::clone(&observable); self.pipes .register_callback(move || callback(cb_inst.as_ref())); } - ObservableCounter::new() } - Err(err) => { - global::handle_error(err); + Err(_err) => { + // TODO - Add error handling ObservableCounter::new() } } @@ -150,34 +166,41 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateObservableUpDownCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err)); return ObservableUpDownCounter::new(); } match resolver.measures( InstrumentKind::ObservableUpDownCounter, - builder.name, + builder.name.clone(), builder.description, builder.unit, None, ) { Ok(ms) => { if ms.is_empty() { + otel_error!( + name: "SdkMeter.CreateUpDownObservableCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + message = "Measurements from the instrument will be ignored. Reason: View Configuration / Drop Aggregation" + ); return ObservableUpDownCounter::new(); } - let observable = Arc::new(Observable::new(ms)); - for callback in builder.callbacks { let cb_inst = Arc::clone(&observable); self.pipes .register_callback(move || callback(cb_inst.as_ref())); } - ObservableUpDownCounter::new() } - Err(err) => { - global::handle_error(err); + Err(_err) => { + // TBD - Add error handling ObservableUpDownCounter::new() } } @@ -193,34 +216,41 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateObservableGauge.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err)); return ObservableGauge::new(); } match resolver.measures( InstrumentKind::ObservableGauge, - builder.name, + builder.name.clone(), builder.description, builder.unit, None, ) { Ok(ms) => { if ms.is_empty() { + otel_error!( + name: "SdkMeter.CreateObservableGauge.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + message = "Measurements from the instrument will be ignored. Reason: View Configuration / Drop Aggregation" + ); return ObservableGauge::new(); } - let observable = Arc::new(Observable::new(ms)); - for callback in builder.callbacks { let cb_inst = Arc::clone(&observable); self.pipes .register_callback(move || callback(cb_inst.as_ref())); } - ObservableGauge::new() } - Err(err) => { - global::handle_error(err); + Err(_err) => { + //TBD - Add error handling ObservableGauge::new() } } @@ -236,14 +266,18 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateUpDownCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err)); return UpDownCounter::new(Arc::new(NoopSyncInstrument::new())); } match resolver .lookup( InstrumentKind::UpDownCounter, - builder.name, + builder.name.clone(), builder.description, builder.unit, None, @@ -252,7 +286,12 @@ impl SdkMeter { { Ok(updown_counter) => updown_counter, Err(err) => { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateUpDownCounter.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err) + ); UpDownCounter::new(Arc::new(NoopSyncInstrument::new())) } } @@ -268,14 +307,18 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateGauge.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err)); return Gauge::new(Arc::new(NoopSyncInstrument::new())); } match resolver .lookup( InstrumentKind::Gauge, - builder.name, + builder.name.clone(), builder.description, builder.unit, None, @@ -284,7 +327,12 @@ impl SdkMeter { { Ok(gauge) => gauge, Err(err) => { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateGauge.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err) + ); Gauge::new(Arc::new(NoopSyncInstrument::new())) } } @@ -300,14 +348,19 @@ impl SdkMeter { { let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); if let Err(err) = validation_result { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateHistogram.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err)); + return Histogram::new(Arc::new(NoopSyncInstrument::new())); } match resolver .lookup( InstrumentKind::Histogram, - builder.name, + builder.name.clone(), builder.description, builder.unit, builder.boundaries, @@ -316,7 +369,12 @@ impl SdkMeter { { Ok(histogram) => histogram, Err(err) => { - global::handle_error(err); + otel_error!( + name: "SdkMeter.CreateHistogram.InstrumentCreationFailed", + meter_name = self.scope.name.as_ref(), + instrument_name = builder.name.as_ref(), + error = format!("Measurements from the instrument will be ignored. Reason: {}", err) + ); Histogram::new(Arc::new(NoopSyncInstrument::new())) } } diff --git a/opentelemetry-sdk/src/metrics/meter_provider.rs b/opentelemetry-sdk/src/metrics/meter_provider.rs index 1468ed0f30..bde9b27289 100644 --- a/opentelemetry-sdk/src/metrics/meter_provider.rs +++ b/opentelemetry-sdk/src/metrics/meter_provider.rs @@ -8,9 +8,8 @@ use std::{ }; use opentelemetry::{ - global, metrics::{Meter, MeterProvider, MetricsError, Result}, - KeyValue, + otel_error, KeyValue, }; use crate::{instrumentation::Scope, Resource}; @@ -139,7 +138,7 @@ impl Drop for SdkMeterProviderInner { // shutdown(), then we don't need to call shutdown again. if !self.is_shutdown.load(Ordering::Relaxed) { if let Err(err) = self.shutdown() { - global::handle_error(err); + otel_error!(name: "SdkMeterProvider.Drop.ShutdownError", error = format!("{}",err)); } } } diff --git a/opentelemetry-sdk/src/metrics/periodic_reader.rs b/opentelemetry-sdk/src/metrics/periodic_reader.rs index 67e887d1eb..181d87d1da 100644 --- a/opentelemetry-sdk/src/metrics/periodic_reader.rs +++ b/opentelemetry-sdk/src/metrics/periodic_reader.rs @@ -14,7 +14,7 @@ use futures_util::{ use opentelemetry::{ global, metrics::{MetricsError, Result}, - otel_error, + otel_debug, otel_error, }; use crate::runtime::Runtime; @@ -245,13 +245,11 @@ impl PeriodicReaderWorker { Either::Left((res, _)) => { res // return the status of export. } - Either::Right(_) => { - otel_error!( - name: "collect_and_export", - status = "timed_out" - ); - Err(MetricsError::Other("export timed out".into())) - } + Either::Right(_) => Err(MetricsError::ExportTimeout( + "PeriodicReader".into(), + self.timeout.as_nanos(), + ) + .into()), } } @@ -259,22 +257,44 @@ impl PeriodicReaderWorker { match message { Message::Export => { if let Err(err) = self.collect_and_export().await { - global::handle_error(err) + match err { + MetricsError::ExportTimeout(_, _) => { + otel_error!( name: "PeriodicReader.ExportFailed", error = format!("{:?}", err)); + } + MetricsError::ReaderShutdown => { + otel_debug!( name: "PeriodicReader.ReaderShutdown", error = format!("{:?}", err)); + } + MetricsError::ReaderNotRegistered => { + otel_debug!( name: "PeriodicReader.ReaderNotRegistered", error = format!("{:?}", err)); + } + _ => { + // TBD: This includes errors from both collection and export. Need to be made more specific + // and identify the levels to log them. + otel_error!( name: "PeriodicReader.ExportFailed", error = format!("{:?}", err)); + } + } } } Message::Flush(ch) => { let res = self.collect_and_export().await; - if ch.send(res).is_err() { - global::handle_error(MetricsError::Other("flush channel closed".into())) + if let Err(send_error) = ch.send(res) { + otel_debug!( + name: "PeriodicReader.Flush.SendResultError", + error = format!("{:?}", send_error), + ); } } Message::Shutdown(ch) => { let res = self.collect_and_export().await; let _ = self.reader.exporter.shutdown(); - if ch.send(res).is_err() { - global::handle_error(MetricsError::Other("shutdown channel closed".into())) + if let Err(send_error) = ch.send(res) { + otel_debug!( + name: "PeriodicReader.Flush.SendResultError", + error = format!("{:?}", send_error), + ); + //Return false to break the loop and shutdown the worker. + return false; } - return false; } } @@ -300,9 +320,7 @@ impl MetricReader for PeriodicReader { let worker = match &mut inner.sdk_producer_or_worker { ProducerOrWorker::Producer(_) => { // Only register once. If producer is already set, do nothing. - global::handle_error(MetricsError::Other( - "duplicate meter registration, did not register manual reader".into(), - )); + otel_debug!(name: "PeriodicReader.RegisterPipeline.DuplicateRegistration"); return; } ProducerOrWorker::Worker(w) => mem::replace(w, Box::new(|_| {})), @@ -315,7 +333,7 @@ impl MetricReader for PeriodicReader { fn collect(&self, rm: &mut ResourceMetrics) -> Result<()> { let inner = self.inner.lock()?; if inner.is_shutdown { - return Err(MetricsError::Other("reader is shut down".into())); + return Err(MetricsError::ReaderShutdown); } if let Some(producer) = match &inner.sdk_producer_or_worker { @@ -324,7 +342,7 @@ impl MetricReader for PeriodicReader { } { producer.produce(rm)?; } else { - return Err(MetricsError::Other("reader is not registered".into())); + return Err(MetricsError::ReaderNotRegistered); } Ok(()) diff --git a/opentelemetry-sdk/src/metrics/pipeline.rs b/opentelemetry-sdk/src/metrics/pipeline.rs index a140f65dec..7f15eeca92 100644 --- a/opentelemetry-sdk/src/metrics/pipeline.rs +++ b/opentelemetry-sdk/src/metrics/pipeline.rs @@ -6,9 +6,8 @@ use std::{ }; use opentelemetry::{ - global, metrics::{MetricsError, Result}, - KeyValue, + otel_warn, KeyValue, }; use crate::{ @@ -414,15 +413,18 @@ where if existing == id { return; } - - global::handle_error(MetricsError::Other(format!( - "duplicate metric stream definitions, names: ({} and {}), descriptions: ({} and {}), kinds: ({:?} and {:?}), units: ({:?} and {:?}), and numbers: ({} and {})", - existing.name, id.name, - existing.description, id.description, - existing.kind, id.kind, - existing.unit, id.unit, - existing.number, id.number, - ))) + otel_warn!(name: "Instrument.DuplicateMetricStreamDefinitions", + name = format!("{}",id.name), + description = format!("{}", id.description), + kind = format!("{:?}", id.kind), + unit = format!("{}",id.unit), + number = format!("{}", id.number), + existing_name = format!("{}", existing.name), + existing_desc = format!("{}", existing.description), + existing_kind = format!("{:?}", existing.kind), + existing_unit = format!("{}", existing.unit), + existing_number = format!("{}", existing.number), + ); } } } @@ -710,8 +712,7 @@ where if errs.is_empty() { if measures.is_empty() { - // TODO: Emit internal log that measurements from the instrument - // are being dropped due to view configuration + // Error is logged elsewhere. } Ok(measures) } else { diff --git a/opentelemetry-sdk/src/metrics/view.rs b/opentelemetry-sdk/src/metrics/view.rs index d9f256bd2b..176470ac3f 100644 --- a/opentelemetry-sdk/src/metrics/view.rs +++ b/opentelemetry-sdk/src/metrics/view.rs @@ -1,8 +1,8 @@ use super::instrument::{Instrument, Stream}; use glob::Pattern; use opentelemetry::{ - global, metrics::{MetricsError, Result}, + otel_error, }; fn empty_view(_inst: &Instrument) -> Option { @@ -102,9 +102,11 @@ impl View for Box { /// ``` pub fn new_view(criteria: Instrument, mask: Stream) -> Result> { if criteria.is_empty() { - global::handle_error(MetricsError::Config(format!( - "no criteria provided, dropping view. mask: {mask:?}" - ))); + otel_error!(name: "CreateView.ValidationError", error = format!("{}",MetricsError::Config( + "no criteria provided, dropping view".to_string())), + criteria = format!("{:?}", criteria), + mask = format!("{:?}", mask) + ); return Ok(Box::new(empty_view)); } let contains_wildcard = criteria.name.contains(['*', '?']); @@ -112,9 +114,11 @@ pub fn new_view(criteria: Instrument, mask: Stream) -> Result> { let match_fn: Box bool + Send + Sync> = if contains_wildcard { if mask.name != "" { - global::handle_error(MetricsError::Config(format!( - "name replacement for multiple instruments, dropping view, criteria: {criteria:?}, mask: {mask:?}" - ))); + otel_error!(name: "CreateView.ValidationError", error = format!("{}", MetricsError::Config( + "name replacement for wildcard instrument, dropping view".to_string())), + criteria = format!("{:?}", criteria), + mask = format!("{:?}", mask) + ); return Ok(Box::new(empty_view)); } @@ -138,10 +142,11 @@ pub fn new_view(criteria: Instrument, mask: Stream) -> Result> { match ma.validate() { Ok(_) => agg = Some(ma.clone()), Err(err) => { - global::handle_error(MetricsError::Other(format!( - "{}, proceeding as if view did not exist. criteria: {:?}, mask: {:?}", - err, err_msg_criteria, mask - ))); + otel_error!(name: "CreateView.ValidationError", + error = format!("{}",err), + criteria = format!("{:?}", err_msg_criteria), + mask = format!("{:?}", mask), + ); return Ok(Box::new(empty_view)); } } diff --git a/opentelemetry/src/metrics/mod.rs b/opentelemetry/src/metrics/mod.rs index 417a37ff5e..0b58a690d4 100644 --- a/opentelemetry/src/metrics/mod.rs +++ b/opentelemetry/src/metrics/mod.rs @@ -1,5 +1,4 @@ //! # OpenTelemetry Metrics API - use std::hash::{Hash, Hasher}; use std::result; use std::sync::Arc; @@ -34,9 +33,18 @@ pub enum MetricsError { /// Invalid configuration #[error("Config error {0}")] Config(String), + /// Timeout + #[error("Metrics reader {0} failed with timeout. Max configured timeout: {1} ns")] + ExportTimeout(String, u128), /// Fail to export metrics #[error("Metrics exporter {} failed with {0}", .0.exporter_name())] ExportErr(Box), + /// Metrics Reader shutdown + #[error("Metrics reader is shutdown")] + ReaderShutdown, + /// Metrics reader is not registered + #[error("Metrics reader is not registered")] + ReaderNotRegistered, /// Invalid instrument configuration such invalid instrument name, invalid instrument description, invalid instrument unit, etc. /// See [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#general-characteristics) /// for full list of requirements.