Skip to content

Commit bc5e6ce

Browse files
authored
InstrumentationScope to include attributes in hash and eq check (#2701)
1 parent 27d364b commit bc5e6ce

File tree

5 files changed

+211
-121
lines changed

5 files changed

+211
-121
lines changed

opentelemetry-sdk/src/metrics/meter_provider.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,25 @@ mod tests {
562562
assert_eq!(provider.inner.meters.lock().unwrap().len(), 5);
563563
}
564564

565+
#[test]
566+
fn same_meter_reused_same_scope_attributes() {
567+
let meter_provider = super::SdkMeterProvider::builder().build();
568+
let make_scope = |attributes| {
569+
InstrumentationScope::builder("test.meter")
570+
.with_version("v0.1.0")
571+
.with_schema_url("http://example.com")
572+
.with_attributes(attributes)
573+
.build()
574+
};
575+
576+
let _meter1 =
577+
meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
578+
let _meter2 =
579+
meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
580+
581+
assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 1);
582+
}
583+
565584
#[test]
566585
fn with_resource_multiple_calls_ensure_additive() {
567586
let builder = SdkMeterProvider::builder()

opentelemetry-sdk/src/metrics/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ mod tests {
782782
.build();
783783

784784
// Act
785-
// Meters are identical except for scope attributes, but scope attributes are not an identifying property.
785+
// Meters are identical.
786786
// Hence there should be a single metric stream output for this test.
787787
let make_scope = |attributes| {
788788
InstrumentationScope::builder("test.meter")
@@ -795,7 +795,7 @@ mod tests {
795795
let meter1 =
796796
meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
797797
let meter2 =
798-
meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value2")]));
798+
meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
799799

800800
let counter1 = meter1
801801
.u64_counter("my_counter")

opentelemetry/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- *Breaking* Moved `TraceError` enum from `opentelemetry::trace::TraceError` to `opentelemetry_sdk::trace::TraceError`
77
- *Breaking* Moved `TraceResult` type alias from `opentelemetry::trace::TraceResult` to `opentelemetry_sdk::trace::TraceResult`
88
- {PLACEHOLDER} - Remove the above completely. // TODO fill this when changes are actually in.
9+
- Bug Fix: `InstrumentationScope` implementation for `PartialEq` and `Hash` fixed to include Attributes also.
910

1011
## 0.28.0
1112

opentelemetry/src/common.rs

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::borrow::{Borrow, Cow};
22
use std::sync::Arc;
33
use std::{fmt, hash};
44

5+
use std::hash::{Hash, Hasher};
6+
57
/// The key part of attribute [KeyValue] pairs.
68
///
79
/// See the [attribute naming] spec for guidelines.
@@ -399,6 +401,42 @@ impl KeyValue {
399401
}
400402
}
401403

404+
struct F64Hashable(f64);
405+
406+
impl PartialEq for F64Hashable {
407+
fn eq(&self, other: &Self) -> bool {
408+
self.0.to_bits() == other.0.to_bits()
409+
}
410+
}
411+
412+
impl Eq for F64Hashable {}
413+
414+
impl Hash for F64Hashable {
415+
fn hash<H: Hasher>(&self, state: &mut H) {
416+
self.0.to_bits().hash(state);
417+
}
418+
}
419+
420+
impl Hash for KeyValue {
421+
fn hash<H: Hasher>(&self, state: &mut H) {
422+
self.key.hash(state);
423+
match &self.value {
424+
Value::F64(f) => F64Hashable(*f).hash(state),
425+
Value::Array(a) => match a {
426+
Array::Bool(b) => b.hash(state),
427+
Array::I64(i) => i.hash(state),
428+
Array::F64(f) => f.iter().for_each(|f| F64Hashable(*f).hash(state)),
429+
Array::String(s) => s.hash(state),
430+
},
431+
Value::Bool(b) => b.hash(state),
432+
Value::I64(i) => i.hash(state),
433+
Value::String(s) => s.hash(state),
434+
};
435+
}
436+
}
437+
438+
impl Eq for KeyValue {}
439+
402440
/// Information about a library or crate providing instrumentation.
403441
///
404442
/// An instrumentation scope should be named to follow any naming conventions
@@ -427,22 +465,33 @@ pub struct InstrumentationScope {
427465
attributes: Vec<KeyValue>,
428466
}
429467

430-
// Uniqueness for InstrumentationScope does not depend on attributes
431-
impl Eq for InstrumentationScope {}
432-
433468
impl PartialEq for InstrumentationScope {
434469
fn eq(&self, other: &Self) -> bool {
435470
self.name == other.name
436471
&& self.version == other.version
437472
&& self.schema_url == other.schema_url
473+
&& {
474+
let mut self_attrs = self.attributes.clone();
475+
let mut other_attrs = other.attributes.clone();
476+
self_attrs.sort_unstable_by(|a, b| a.key.cmp(&b.key));
477+
other_attrs.sort_unstable_by(|a, b| a.key.cmp(&b.key));
478+
self_attrs == other_attrs
479+
}
438480
}
439481
}
440482

483+
impl Eq for InstrumentationScope {}
484+
441485
impl hash::Hash for InstrumentationScope {
442486
fn hash<H: hash::Hasher>(&self, state: &mut H) {
443487
self.name.hash(state);
444488
self.version.hash(state);
445489
self.schema_url.hash(state);
490+
let mut sorted_attrs = self.attributes.clone();
491+
sorted_attrs.sort_unstable_by(|a, b| a.key.cmp(&b.key));
492+
for attribute in sorted_attrs {
493+
attribute.hash(state);
494+
}
446495
}
447496
}
448497

@@ -561,3 +610,140 @@ impl InstrumentationScopeBuilder {
561610
}
562611
}
563612
}
613+
614+
#[cfg(test)]
615+
mod tests {
616+
use std::hash::{Hash, Hasher};
617+
618+
use crate::{InstrumentationScope, KeyValue};
619+
620+
use rand::random;
621+
use std::collections::hash_map::DefaultHasher;
622+
use std::f64;
623+
624+
#[test]
625+
fn kv_float_equality() {
626+
let kv1 = KeyValue::new("key", 1.0);
627+
let kv2 = KeyValue::new("key", 1.0);
628+
assert_eq!(kv1, kv2);
629+
630+
let kv1 = KeyValue::new("key", 1.0);
631+
let kv2 = KeyValue::new("key", 1.01);
632+
assert_ne!(kv1, kv2);
633+
634+
let kv1 = KeyValue::new("key", f64::NAN);
635+
let kv2 = KeyValue::new("key", f64::NAN);
636+
assert_ne!(kv1, kv2, "NAN is not equal to itself");
637+
638+
for float_val in [
639+
f64::INFINITY,
640+
f64::NEG_INFINITY,
641+
f64::MAX,
642+
f64::MIN,
643+
f64::MIN_POSITIVE,
644+
]
645+
.iter()
646+
{
647+
let kv1 = KeyValue::new("key", *float_val);
648+
let kv2 = KeyValue::new("key", *float_val);
649+
assert_eq!(kv1, kv2);
650+
}
651+
652+
for _ in 0..100 {
653+
let random_value = random::<f64>();
654+
let kv1 = KeyValue::new("key", random_value);
655+
let kv2 = KeyValue::new("key", random_value);
656+
assert_eq!(kv1, kv2);
657+
}
658+
}
659+
660+
#[test]
661+
fn kv_float_hash() {
662+
for float_val in [
663+
f64::NAN,
664+
f64::INFINITY,
665+
f64::NEG_INFINITY,
666+
f64::MAX,
667+
f64::MIN,
668+
f64::MIN_POSITIVE,
669+
]
670+
.iter()
671+
{
672+
let kv1 = KeyValue::new("key", *float_val);
673+
let kv2 = KeyValue::new("key", *float_val);
674+
assert_eq!(hash_helper(&kv1), hash_helper(&kv2));
675+
}
676+
677+
for _ in 0..100 {
678+
let random_value = random::<f64>();
679+
let kv1 = KeyValue::new("key", random_value);
680+
let kv2 = KeyValue::new("key", random_value);
681+
assert_eq!(hash_helper(&kv1), hash_helper(&kv2));
682+
}
683+
}
684+
685+
fn hash_helper<T: Hash>(item: &T) -> u64 {
686+
let mut hasher = DefaultHasher::new();
687+
item.hash(&mut hasher);
688+
hasher.finish()
689+
}
690+
691+
#[test]
692+
fn instrumentation_scope_equality() {
693+
let scope1 = InstrumentationScope::builder("my-crate")
694+
.with_version("v0.1.0")
695+
.with_schema_url("https://opentelemetry.io/schemas/1.17.0")
696+
.with_attributes([KeyValue::new("k", "v")])
697+
.build();
698+
let scope2 = InstrumentationScope::builder("my-crate")
699+
.with_version("v0.1.0")
700+
.with_schema_url("https://opentelemetry.io/schemas/1.17.0")
701+
.with_attributes([KeyValue::new("k", "v")])
702+
.build();
703+
assert_eq!(scope1, scope2);
704+
}
705+
706+
#[test]
707+
fn instrumentation_scope_equality_attributes_diff_order() {
708+
let scope1 = InstrumentationScope::builder("my-crate")
709+
.with_version("v0.1.0")
710+
.with_schema_url("https://opentelemetry.io/schemas/1.17.0")
711+
.with_attributes([KeyValue::new("k1", "v1"), KeyValue::new("k2", "v2")])
712+
.build();
713+
let scope2 = InstrumentationScope::builder("my-crate")
714+
.with_version("v0.1.0")
715+
.with_schema_url("https://opentelemetry.io/schemas/1.17.0")
716+
.with_attributes([KeyValue::new("k2", "v2"), KeyValue::new("k1", "v1")])
717+
.build();
718+
assert_eq!(scope1, scope2);
719+
720+
// assert hash are same for both scopes
721+
let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
722+
scope1.hash(&mut hasher1);
723+
let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
724+
scope2.hash(&mut hasher2);
725+
assert_eq!(hasher1.finish(), hasher2.finish());
726+
}
727+
728+
#[test]
729+
fn instrumentation_scope_equality_different_attributes() {
730+
let scope1 = InstrumentationScope::builder("my-crate")
731+
.with_version("v0.1.0")
732+
.with_schema_url("https://opentelemetry.io/schemas/1.17.0")
733+
.with_attributes([KeyValue::new("k1", "v1"), KeyValue::new("k2", "v2")])
734+
.build();
735+
let scope2 = InstrumentationScope::builder("my-crate")
736+
.with_version("v0.1.0")
737+
.with_schema_url("https://opentelemetry.io/schemas/1.17.0")
738+
.with_attributes([KeyValue::new("k2", "v3"), KeyValue::new("k4", "v5")])
739+
.build();
740+
assert_ne!(scope1, scope2);
741+
742+
// assert hash are same for both scopes
743+
let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
744+
scope1.hash(&mut hasher1);
745+
let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
746+
scope2.hash(&mut hasher2);
747+
assert_ne!(hasher1.finish(), hasher2.finish());
748+
}
749+
}

0 commit comments

Comments
 (0)