Skip to content

Commit 3bd8de9

Browse files
authored
[METRICS SDK] Fix hash collision in MetricAttributes (open-telemetry#3322)
1 parent 83ac2ae commit 3bd8de9

18 files changed

+218
-217
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Increment the:
1515

1616
## [Unreleased]
1717

18+
* [METRICS SDK] Fix hash collision in MetricAttributes
19+
[#3322](https://github.com/open-telemetry/opentelemetry-cpp/pull/3322)
20+
1821
* [BUILD] Fix misssing exported definition for OTLP file exporter and forceflush
1922
[#3319](https://github.com/open-telemetry/opentelemetry-cpp/pull/3319)
2023

sdk/include/opentelemetry/sdk/common/attributemap_hash.h

-25
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010
#include <utility>
1111
#include <vector>
1212

13-
#include "opentelemetry/common/attribute_value.h"
14-
#include "opentelemetry/common/key_value_iterable.h"
15-
#include "opentelemetry/nostd/function_ref.h"
16-
#include "opentelemetry/nostd/string_view.h"
1713
#include "opentelemetry/nostd/variant.h"
1814
#include "opentelemetry/sdk/common/attribute_utils.h"
1915
#include "opentelemetry/version.h"
@@ -74,27 +70,6 @@ inline size_t GetHashForAttributeMap(const OrderedAttributeMap &attribute_map)
7470
return seed;
7571
}
7672

77-
// Calculate hash of keys and values of KeyValueIterable, filtered using callback.
78-
inline size_t GetHashForAttributeMap(
79-
const opentelemetry::common::KeyValueIterable &attributes,
80-
nostd::function_ref<bool(nostd::string_view)> is_key_present_callback)
81-
{
82-
AttributeConverter converter;
83-
size_t seed = 0UL;
84-
attributes.ForEachKeyValue(
85-
[&](nostd::string_view key, opentelemetry::common::AttributeValue value) noexcept {
86-
if (!is_key_present_callback(key))
87-
{
88-
return true;
89-
}
90-
GetHash(seed, key);
91-
auto attr_val = nostd::visit(converter, value);
92-
nostd::visit(GetHashForAttributeValueVisitor(seed), attr_val);
93-
return true;
94-
});
95-
return seed;
96-
}
97-
9873
template <class T>
9974
inline size_t GetHash(T value)
10075
{

sdk/include/opentelemetry/sdk/metrics/exemplar/reservoir_cell.h

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class ReservoirCell
125125
res.erase(it);
126126
}
127127
}
128+
res.UpdateHash();
128129
return res;
129130
}
130131

sdk/include/opentelemetry/sdk/metrics/instruments.h

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ struct InstrumentDescriptor
6565
};
6666

6767
using MetricAttributes = opentelemetry::sdk::metrics::FilteredOrderedAttributeMap;
68+
using MetricAttributesHash = opentelemetry::sdk::metrics::FilteredOrderedAttributeMapHash;
6869
using AggregationTemporalitySelector = std::function<AggregationTemporality(InstrumentType)>;
6970

7071
/*class InstrumentSelector {

sdk/include/opentelemetry/sdk/metrics/state/async_metric_storage.h

+5-7
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,22 @@ class AsyncMetricStorage : public MetricStorage, public AsyncWritableMetricStora
7171

7272
auto aggr = DefaultAggregation::CreateAggregation(aggregation_type_, instrument_descriptor_);
7373
aggr->Aggregate(measurement.second);
74-
auto hash = opentelemetry::sdk::common::GetHashForAttributeMap(measurement.first);
75-
auto prev = cumulative_hash_map_->Get(hash);
74+
auto prev = cumulative_hash_map_->Get(measurement.first);
7675
if (prev)
7776
{
7877
auto delta = prev->Diff(*aggr);
7978
// store received value in cumulative map, and the diff in delta map (to pass it to temporal
8079
// storage)
81-
cumulative_hash_map_->Set(measurement.first, std::move(aggr), hash);
82-
delta_hash_map_->Set(measurement.first, std::move(delta), hash);
80+
cumulative_hash_map_->Set(measurement.first, std::move(aggr));
81+
delta_hash_map_->Set(measurement.first, std::move(delta));
8382
}
8483
else
8584
{
8685
// store received value in cumulative and delta map.
8786
cumulative_hash_map_->Set(
8887
measurement.first,
89-
DefaultAggregation::CloneAggregation(aggregation_type_, instrument_descriptor_, *aggr),
90-
hash);
91-
delta_hash_map_->Set(measurement.first, std::move(aggr), hash);
88+
DefaultAggregation::CloneAggregation(aggregation_type_, instrument_descriptor_, *aggr));
89+
delta_hash_map_->Set(measurement.first, std::move(aggr));
9290
}
9391
}
9492
}

sdk/include/opentelemetry/sdk/metrics/state/attributes_hashmap.h

+64-57
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "opentelemetry/sdk/common/attribute_utils.h"
1616
#include "opentelemetry/sdk/common/attributemap_hash.h"
1717
#include "opentelemetry/sdk/metrics/aggregation/aggregation.h"
18+
#include "opentelemetry/sdk/metrics/state/filtered_ordered_attribute_map.h"
1819
#include "opentelemetry/sdk/metrics/view/attributes_processor.h"
1920
#include "opentelemetry/version.h"
2021

@@ -29,9 +30,9 @@ using opentelemetry::sdk::common::OrderedAttributeMap;
2930
constexpr size_t kAggregationCardinalityLimit = 2000;
3031
const std::string kAttributesLimitOverflowKey = "otel.metrics.overflow";
3132
const bool kAttributesLimitOverflowValue = true;
32-
const size_t kOverflowAttributesHash = opentelemetry::sdk::common::GetHashForAttributeMap(
33-
{{kAttributesLimitOverflowKey,
34-
kAttributesLimitOverflowValue}}); // precalculated for optimization
33+
const MetricAttributes kOverflowAttributes = {
34+
{kAttributesLimitOverflowKey,
35+
kAttributesLimitOverflowValue}}; // precalculated for optimization
3536

3637
class AttributeHashGenerator
3738
{
@@ -42,18 +43,19 @@ class AttributeHashGenerator
4243
}
4344
};
4445

45-
class AttributesHashMap
46+
template <typename CustomHash = MetricAttributesHash>
47+
class AttributesHashMapWithCustomHash
4648
{
4749
public:
48-
AttributesHashMap(size_t attributes_limit = kAggregationCardinalityLimit)
50+
AttributesHashMapWithCustomHash(size_t attributes_limit = kAggregationCardinalityLimit)
4951
: attributes_limit_(attributes_limit)
5052
{}
51-
Aggregation *Get(size_t hash) const
53+
Aggregation *Get(const MetricAttributes &attributes) const
5254
{
53-
auto it = hash_map_.find(hash);
55+
auto it = hash_map_.find(attributes);
5456
if (it != hash_map_.end())
5557
{
56-
return it->second.second.get();
58+
return it->second.get();
5759
}
5860
return nullptr;
5961
}
@@ -62,7 +64,10 @@ class AttributesHashMap
6264
* @return check if key is present in hash
6365
*
6466
*/
65-
bool Has(size_t hash) const { return hash_map_.find(hash) != hash_map_.end(); }
67+
bool Has(const MetricAttributes &attributes) const
68+
{
69+
return hash_map_.find(attributes) != hash_map_.end();
70+
}
6671

6772
/**
6873
* @return the pointer to value for given key if present.
@@ -71,108 +76,103 @@ class AttributesHashMap
7176
*/
7277
Aggregation *GetOrSetDefault(const opentelemetry::common::KeyValueIterable &attributes,
7378
const AttributesProcessor *attributes_processor,
74-
std::function<std::unique_ptr<Aggregation>()> aggregation_callback,
75-
size_t hash)
79+
std::function<std::unique_ptr<Aggregation>()> aggregation_callback)
7680
{
77-
auto it = hash_map_.find(hash);
81+
// TODO: avoid constructing MetricAttributes from KeyValueIterable for
82+
// hash_map_.find which is a heavy operation
83+
MetricAttributes attr{attributes, attributes_processor};
84+
85+
auto it = hash_map_.find(attr);
7886
if (it != hash_map_.end())
7987
{
80-
return it->second.second.get();
88+
return it->second.get();
8189
}
8290

8391
if (IsOverflowAttributes())
8492
{
8593
return GetOrSetOveflowAttributes(aggregation_callback);
8694
}
8795

88-
MetricAttributes attr{attributes, attributes_processor};
89-
90-
hash_map_[hash] = {attr, aggregation_callback()};
91-
return hash_map_[hash].second.get();
96+
auto result = hash_map_.emplace(std::move(attr), aggregation_callback());
97+
return result.first->second.get();
9298
}
9399

94-
Aggregation *GetOrSetDefault(std::function<std::unique_ptr<Aggregation>()> aggregation_callback,
95-
size_t hash)
100+
Aggregation *GetOrSetDefault(const MetricAttributes &attributes,
101+
std::function<std::unique_ptr<Aggregation>()> aggregation_callback)
96102
{
97-
auto it = hash_map_.find(hash);
103+
auto it = hash_map_.find(attributes);
98104
if (it != hash_map_.end())
99105
{
100-
return it->second.second.get();
106+
return it->second.get();
101107
}
102108

103109
if (IsOverflowAttributes())
104110
{
105111
return GetOrSetOveflowAttributes(aggregation_callback);
106112
}
107113

108-
MetricAttributes attr{};
109-
hash_map_[hash] = {attr, aggregation_callback()};
110-
return hash_map_[hash].second.get();
114+
hash_map_[attributes] = aggregation_callback();
115+
return hash_map_[attributes].get();
111116
}
112117

113-
Aggregation *GetOrSetDefault(const MetricAttributes &attributes,
114-
std::function<std::unique_ptr<Aggregation>()> aggregation_callback,
115-
size_t hash)
118+
Aggregation *GetOrSetDefault(MetricAttributes &&attributes,
119+
std::function<std::unique_ptr<Aggregation>()> aggregation_callback)
116120
{
117-
auto it = hash_map_.find(hash);
121+
auto it = hash_map_.find(attributes);
118122
if (it != hash_map_.end())
119123
{
120-
return it->second.second.get();
124+
return it->second.get();
121125
}
122126

123127
if (IsOverflowAttributes())
124128
{
125129
return GetOrSetOveflowAttributes(aggregation_callback);
126130
}
127131

128-
MetricAttributes attr{attributes};
129-
130-
hash_map_[hash] = {attr, aggregation_callback()};
131-
return hash_map_[hash].second.get();
132+
auto result = hash_map_.emplace(std::move(attributes), aggregation_callback());
133+
return result.first->second.get();
132134
}
133-
134135
/**
135136
* Set the value for given key, overwriting the value if already present
136137
*/
137138
void Set(const opentelemetry::common::KeyValueIterable &attributes,
138139
const AttributesProcessor *attributes_processor,
139-
std::unique_ptr<Aggregation> aggr,
140-
size_t hash)
140+
std::unique_ptr<Aggregation> aggr)
141+
{
142+
Set(MetricAttributes{attributes, attributes_processor}, std::move(aggr));
143+
}
144+
145+
void Set(const MetricAttributes &attributes, std::unique_ptr<Aggregation> aggr)
141146
{
142-
auto it = hash_map_.find(hash);
147+
auto it = hash_map_.find(attributes);
143148
if (it != hash_map_.end())
144149
{
145-
it->second.second = std::move(aggr);
150+
it->second = std::move(aggr);
146151
}
147152
else if (IsOverflowAttributes())
148153
{
149-
hash_map_[kOverflowAttributesHash] = {
150-
MetricAttributes{{kAttributesLimitOverflowKey, kAttributesLimitOverflowValue}},
151-
std::move(aggr)};
154+
hash_map_[kOverflowAttributes] = std::move(aggr);
152155
}
153156
else
154157
{
155-
MetricAttributes attr{attributes, attributes_processor};
156-
hash_map_[hash] = {attr, std::move(aggr)};
158+
hash_map_[attributes] = std::move(aggr);
157159
}
158160
}
159161

160-
void Set(const MetricAttributes &attributes, std::unique_ptr<Aggregation> aggr, size_t hash)
162+
void Set(MetricAttributes &&attributes, std::unique_ptr<Aggregation> aggr)
161163
{
162-
auto it = hash_map_.find(hash);
164+
auto it = hash_map_.find(attributes);
163165
if (it != hash_map_.end())
164166
{
165-
it->second.second = std::move(aggr);
167+
it->second = std::move(aggr);
166168
}
167169
else if (IsOverflowAttributes())
168170
{
169-
hash_map_[kOverflowAttributesHash] = {
170-
MetricAttributes{{kAttributesLimitOverflowKey, kAttributesLimitOverflowValue}},
171-
std::move(aggr)};
171+
hash_map_[kOverflowAttributes] = std::move(aggr);
172172
}
173173
else
174174
{
175-
hash_map_[hash] = {attributes, std::move(aggr)};
175+
hash_map_[std::move(attributes)] = std::move(aggr);
176176
}
177177
}
178178

@@ -184,7 +184,7 @@ class AttributesHashMap
184184
{
185185
for (auto &kv : hash_map_)
186186
{
187-
if (!callback(kv.second.first, *(kv.second.second.get())))
187+
if (!callback(kv.first, *(kv.second.get())))
188188
{
189189
return false; // callback is not prepared to consume data
190190
}
@@ -197,8 +197,13 @@ class AttributesHashMap
197197
*/
198198
size_t Size() { return hash_map_.size(); }
199199

200+
#ifdef UNIT_TESTING
201+
size_t BucketCount() { return hash_map_.bucket_count(); }
202+
size_t BucketSize(size_t n) { return hash_map_.bucket_size(n); }
203+
#endif
204+
200205
private:
201-
std::unordered_map<size_t, std::pair<MetricAttributes, std::unique_ptr<Aggregation>>> hash_map_;
206+
std::unordered_map<MetricAttributes, std::unique_ptr<Aggregation>, CustomHash> hash_map_;
202207
size_t attributes_limit_;
203208

204209
Aggregation *GetOrSetOveflowAttributes(
@@ -210,19 +215,21 @@ class AttributesHashMap
210215

211216
Aggregation *GetOrSetOveflowAttributes(std::unique_ptr<Aggregation> agg)
212217
{
213-
auto it = hash_map_.find(kOverflowAttributesHash);
218+
auto it = hash_map_.find(kOverflowAttributes);
214219
if (it != hash_map_.end())
215220
{
216-
return it->second.second.get();
221+
return it->second.get();
217222
}
218223

219-
MetricAttributes attr{{kAttributesLimitOverflowKey, kAttributesLimitOverflowValue}};
220-
hash_map_[kOverflowAttributesHash] = {attr, std::move(agg)};
221-
return hash_map_[kOverflowAttributesHash].second.get();
224+
auto result = hash_map_.emplace(kOverflowAttributes, std::move(agg));
225+
return result.first->second.get();
222226
}
223227

224228
bool IsOverflowAttributes() const { return (hash_map_.size() + 1 >= attributes_limit_); }
225229
};
230+
231+
using AttributesHashMap = AttributesHashMapWithCustomHash<>;
232+
226233
} // namespace metrics
227234

228235
} // namespace sdk

0 commit comments

Comments
 (0)