-
Notifications
You must be signed in to change notification settings - Fork 452
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
Allow precreation of AttributeSets for metrics #1421
Allow precreation of AttributeSets for metrics #1421
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #1421 +/- ##
=======================================
+ Coverage 61.3% 63.2% +1.8%
=======================================
Files 146 147 +1
Lines 19459 19664 +205
=======================================
+ Hits 11938 12428 +490
+ Misses 7521 7236 -285 ☔ View full report in Codecov by Sentry. |
@@ -266,7 +269,6 @@ mod tests { | |||
// "multi_thread" tokio flavor must be used else flush won't | |||
// be able to make progress! | |||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] | |||
#[ignore = "Spatial aggregation is not yet implemented."] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is one more ignored test to be un-ignored now.
|
||
impl Ord for HashKeyValue { | ||
fn cmp(&self, other: &Self) -> Ordering { | ||
match self.0.key.cmp(&other.0.key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand this is not introduced in this PR, but I am not able to follow the logic behind this comparison.. Wont a simple "self.0.key.cmp(&other.0.key)" be sufficient here? The question of Key's being equal should not occur as we de-dedup before sorting...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@KallDrexx Gentle reminder for this! Not a blocker for this PR.
@KallDrexx can you do: pub fn add(&self, value: T, attributes: impl Into<AttributeSet>) {
self.0.add(value, attributes.into())
} to avoid the api break? |
@@ -253,25 +253,25 @@ pub(crate) struct ResolvedMeasures<T> { | |||
} | |||
|
|||
impl<T: Copy + 'static> SyncCounter<T> for ResolvedMeasures<T> { | |||
fn add(&self, val: T, attrs: &[KeyValue]) { | |||
fn add(&self, val: T, attrs: AttributeSet) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on making adding a generic function here to replace AttributeSet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made the outer Counter
accept the Into<AttributeSet>
but left the inner Sync*
types alone.
I really think we should do this, and we can |
Sorry been a bit busy the last few days, will give this a try tomorrow. |
The Specifically, do we wish to retain being able to pass in a The problem is the current impl<I: IntoIterator<Item = KeyValue>> From<I> for AttributeSet
where
<I as IntoIterator>::IntoIter: DoubleEndedIterator + ExactSizeIterator,
{
fn from(values: I) -> Self {
AttributeSet(Arc::new(InternalAttributeSet::from(values)))
}
} This works well for But you can't have I can fix this by removing these implementations and go for more concrete implementations, such as impl<const N: usize> From<&[KeyValue; N]> for AttributeSet {
fn from(value: &[KeyValue; N]) -> Self {
AttributeSet(Arc::new(InternalAttributeSet::from(value.iter().cloned())))
}
}
impl<const N: usize> From<[KeyValue; N]> for AttributeSet {
fn from(value: [KeyValue; N]) -> Self {
AttributeSet(Arc::new(InternalAttributeSet::from(value.into_iter())))
}
} However, this means you can only pass in references or owned fixed size arrays to create an attribute set. You can't pass in a So the question becomes, do we want to support shared references and keep the API backward compatible, but enforce a limited set of iterator types that can be utilized, or make the API more broad? |
Take away from SIG call:
I'm still open for discussion on it, but with that I'm going to go with the first code block I posted ( |
…into an attribute set
Well unfortunately I missed the call because I was assuming we were going to meet at the later time of 9am 😓 I'll try to read over this. |
Note that the fn register_callback(
&self,
instruments: &[Arc<dyn Any>],
callbacks: Box<MultiInstrumentCallback>,
) -> Result<Box<dyn CallbackRegistration>>;
type MultiInstrumentCallback = dyn Fn(&dyn Observer) + Send + Sync; Since we are just passing in a function which can take in any observer, there's no way for it to resolve at compile time what type of generic we will use for So I'm going to leave that as taking an |
opentelemetry/src/attributes/set.rs
Outdated
#[derive(Clone, Debug, Hash, PartialEq, Eq)] | ||
pub struct AttributeSet(Arc<InternalAttributeSet>); | ||
|
||
impl<'a, I: IntoIterator<Item = &'a KeyValue>> From<I> for AttributeSet |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little unfortunate to require this to always be references from a caller perspective. One alternative could be to make a new trait so this can accept both iterators over references and owned values? e.g.
trait ToKv {
fn to_kv(self) -> KeyValue;
}
impl ToKv for KeyValue {
fn to_kv(self) -> KeyValue {
self
}
}
impl ToKv for &'_ KeyValue {
fn to_kv(self) -> KeyValue {
self.clone()
}
}
impl<'a, KV, I> From<I> for AttributeSet
where
KV: ToKv,
I: IntoIterator<Item = KV>,
<I as IntoIterator>::IntoIter: DoubleEndedIterator + ExactSizeIterator,
{
fn from(values: I) -> Self {
AttributeSet(Arc::new(InternalAttributeSet::from(
values.into_iter().map(ToKv::to_kv),
)))
}
}
That would allow for a greater variety of arguments
let _ = AttributeSet::from(vec![KeyValue::new("owned", true)]);
let _ = AttributeSet::from(&[KeyValue::new("owned", false)]);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good trick. Done.
In order to do this. I had to slightly re-arrange the Resource
to AttributeSet
conversion, since my previous From<&Resource> to AttributeSet
implementation ended up causing an ambiguous reference. So that code was moved into the Resource
struct itself (which probably makes more sense anyway).
examples/metrics-basic/src/main.rs
Outdated
@@ -1,3 +1,4 @@ | |||
use opentelemetry::attributes::AttributeSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nit, but the other non-signal-prefixed types are exported at the opentelemetry
root, could be more consistent to expose this as opentelemetry::AttributeSet;
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved AttributeSet
reference to opentelemetry
root
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me, could update the examples to remove the extra reference when constructing the attribute sets to clean them up, but not strictly necessary.
@@ -131,7 +132,7 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> { | |||
|
|||
span.add_event("Sub span event", vec![]); | |||
|
|||
histogram.record(1.3, &[]); | |||
histogram.record(1.3, AttributeSet::default()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not in this PR, but just realized that this is somewhat awkward having to pass empty/default when no dimensions!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of an artifact of the original state of the PR being backward incompatible. In the current state of the code &[]
now works again.
//! histogram.record(100, &[KeyValue::new("key", "value")]); | ||
//! let attributes = AttributeSet::from(&[KeyValue::new("key", "value")]); | ||
//! | ||
//! counter.add(100, attributes.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am not sure how common is it to have 2 instruments used with same attributes.. maybe better to stick with existing examples only.
(Also counter's functionality is already provided by histogram, so this example of showing both could be confusing! But that is unrelated to this PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this example uses a histogram
, and histograms couldn't be updated to take a Into<AttributeSet>
either, the precreated attribute set is required here for at least that one.
@@ -46,7 +47,8 @@ use std::sync::{Arc, Mutex}; | |||
/// // Create and record metrics using the MeterProvider | |||
/// let meter = meter_provider.meter(std::borrow::Cow::Borrowed("example")); | |||
/// let counter = meter.u64_counter("my_counter").init(); | |||
/// counter.add(1, &[KeyValue::new("key", "value")]); | |||
/// let attributes = AttributeSet::from(&[KeyValue::new("key", "value")]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am not sure if we should change all examples to use AttributeSet.. maybe show it only where it gives some perf benefits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These examples were changed prior to a backward compatible solution being found, thus I had to update all the doctests to use precreated attributes for CI to pass.
let attrs2 = vec![ | ||
counter.add(5.0, attrs.clone()); | ||
counter.add(10.3, attrs.clone()); | ||
counter.add(9.0, attrs.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - do we need to clone() here?
let attrs2 = vec![ | ||
counter.add(5.0, attrs.clone()); | ||
counter.add(10.3, attrs.clone()); | ||
counter.add(9.0, attrs.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - same as earlier, we can remove clone() here? Also few more places in the integration test.
@@ -96,7 +91,7 @@ impl<T: Number<T>> AggregateBuilder<T> { | |||
let filter = self.filter.as_ref().map(Arc::clone); | |||
move |n, mut attrs: AttributeSet| { | |||
if let Some(filter) = &filter { | |||
attrs.retain(filter.as_ref()); | |||
attrs = attrs.clone_with(filter.as_ref()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will clone every HashKeyValue that passes the filter. Unfortunately, I don't think this would be easy to avoid. Not blocking this PR, maybe something we can look in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I wasn't happy about this part of the change, but the only way precreated attribute sets really worked is by making them immutable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR. Nicely done. Please update the ChangeLog to reflect breaking changes for Observables.
Co-authored-by: Cijo Thomas <[email protected]>
This caused some unintended side-effects, opened a separate issue to discussion options : #1508 |
Fixes #1387
Design discussion issue (if applicable) #
Changes
This PR creates a backwards incompatible change to the public metric interfaces to allow pre-created and cached attribute sets to be passed into
Counter
,Histogram
, andObserver
APIs instead of references toKeyValue
slices.The first benefit of this change is that there is no more hidden string cloning/allocations occurring. The
AttributeSet
that is passed in now contains anArc<T>
to it's internal data. This allows each measurement used by the instrument to share its internalKeyValue
pairs instead of cloning them for each instrument. Likewise, the caller of OpenTelemetry-rust can also pas the same attribute set into multiple instrument measurement calls without performing any cloning.A second benefit is that if consumers have a stable set of
KeyValue
pairs for a set of counters, they can create anAttributeSet
once and only pay the sorting + deduplication + hash calculation costs a single time instead of every measurement call.This should increase performance when metrics are used with common attributes, while having minimal impact when using dynamic attributes. The one case where performance regression might be encountered are instruments that heavily utilize the
AttributeSet::retain()
function, as we now have to clone the underlyingKeyValue
pairs and create a newAttributeSet
for the retained version now thatAttributeSet
s are internally immutable.This is a breaking API change, however it now requires a previous call of
cntr.add(1, &[KeyValue::new("K", i)]);
to be changed tocntr.add(1, [KeyValue::new("K", i)].into());
. Specifically, a reference is no longer passed in and it has to be converted into anAttributeSet
, which can be done with the.into()
clause.The
AttributeSet
type had to be raised to the rootopentelemetry
trait to be visible to all types.Metrics Stress Tests
Likewise, a new stress test was added to show throughput with premade attribute sets
Metric_Counter Benchmark
(note that the last benchmark is new)
Metrics Benchmark
main: metric.main.txt
pr: metric.pr.txt
Merge requirement checklist
CHANGELOG.md
files updated for non-trivial, user-facing changes