Skip to content

Commit 92dd28c

Browse files
dbarkerThomsonTanmarcalfflalitb
authored
[SDK] support aggregation of identical instruments (open-telemetry#3358)
* use the existing storage for sync or async instruments of the same name. add tests * add hash and name case insensitive hash for InstrumentDescriptor. Update storage registry to use the hash and equality structs. Add tests. * don't allocate heap in the hash. fix some ci failures * fix a few more ci failures. * move the instrument descriptor ostream operator to meter.cc to not leak the ostream header and keep the meter instrument creation warning implementation in the same file * adds instrument descriptor tests * add comments * Move case-insensitive equals method and IsDuplicate method into InstrumentDescriptorUtils struct. Add log streamable wrappers for scopes and instrument descriptors. Add tests for correcitve views for name and description duplicates * fix iwyu errors * duplicate instrument log message improvements to match spec. minor test additions/cleanup * changelog entry * address review feedback. Add Ascii to the name of the instrument util case-insensitive equal function * address feedback: short circuit the instrument descriptor CaseInsensitiveAsciiEquals and IsDuplicate checks * fix comments --------- Co-authored-by: Tom Tan <[email protected]> Co-authored-by: Marc Alff <[email protected]> Co-authored-by: Lalit Kumar Bhasin <[email protected]>
1 parent 4e4d8de commit 92dd28c

File tree

7 files changed

+965
-21
lines changed

7 files changed

+965
-21
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ Increment the:
3939
* [SDK] Optimize PeriodicExportingMetricReader thread usage
4040
[#3383](https://github.com/open-telemetry/opentelemetry-cpp/pull/3383)
4141

42+
* [SDK] Aggregate identical metrics instruments and detect duplicates
43+
[#3358](https://github.com/open-telemetry/opentelemetry-cpp/pull/3358)
44+
4245
## [1.20 2025-04-01]
4346

4447
* [BUILD] Update opentelemetry-proto version

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

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#pragma once
55

6+
#include <algorithm>
7+
#include <cctype>
68
#include <functional>
79
#include <string>
810

@@ -65,6 +67,152 @@ struct InstrumentDescriptor
6567
InstrumentValueType value_type_;
6668
};
6769

70+
struct InstrumentDescriptorUtil
71+
{
72+
// Case-insensitive comparison of two ASCII strings used to evaluate equality of instrument names
73+
static bool CaseInsensitiveAsciiEquals(const std::string &lhs, const std::string &rhs) noexcept
74+
{
75+
return lhs.size() == rhs.size() &&
76+
std::equal(lhs.begin(), lhs.end(), rhs.begin(), [](char a, char b) {
77+
return std::tolower(static_cast<unsigned char>(a)) ==
78+
std::tolower(static_cast<unsigned char>(b));
79+
});
80+
}
81+
82+
// Implementation of the specification requirements on duplicate instruments
83+
// An instrument is a duplicate if it has the same name (case-insensitive) as another instrument,
84+
// but different instrument kind, unit, or description.
85+
// https://github.com/open-telemetry/opentelemetry-specification/blob/9c8c30631b0e288de93df7452f91ed47f6fba330/specification/metrics/sdk.md?plain=1#L869
86+
static bool IsDuplicate(const InstrumentDescriptor &lhs, const InstrumentDescriptor &rhs) noexcept
87+
{
88+
// Not a duplicate if case-insensitive names are not equal
89+
if (!InstrumentDescriptorUtil::CaseInsensitiveAsciiEquals(lhs.name_, rhs.name_))
90+
{
91+
return false;
92+
}
93+
94+
// Duplicate if names equal and kinds (Type and ValueType) are not equal
95+
if (lhs.type_ != rhs.type_ || lhs.value_type_ != rhs.value_type_)
96+
{
97+
return true;
98+
}
99+
100+
// Duplicate if names equal and units (case-sensitive) are not equal
101+
if (lhs.unit_ != rhs.unit_)
102+
{
103+
return true;
104+
}
105+
106+
// Duplicate if names equal and descriptions (case-sensitive) are not equal
107+
if (lhs.description_ != rhs.description_)
108+
{
109+
return true;
110+
}
111+
112+
// All identifying fields are equal
113+
// These are identical instruments or only have a name case conflict
114+
return false;
115+
}
116+
117+
static opentelemetry::nostd::string_view GetInstrumentTypeString(InstrumentType type) noexcept
118+
{
119+
switch (type)
120+
{
121+
case InstrumentType::kCounter:
122+
return "Counter";
123+
case InstrumentType::kUpDownCounter:
124+
return "UpDownCounter";
125+
case InstrumentType::kHistogram:
126+
return "Histogram";
127+
case InstrumentType::kObservableCounter:
128+
return "ObservableCounter";
129+
case InstrumentType::kObservableUpDownCounter:
130+
return "ObservableUpDownCounter";
131+
case InstrumentType::kObservableGauge:
132+
return "ObservableGauge";
133+
case InstrumentType::kGauge:
134+
return "Gauge";
135+
default:
136+
return "Unknown";
137+
}
138+
}
139+
140+
static opentelemetry::nostd::string_view GetInstrumentValueTypeString(
141+
InstrumentValueType value_type) noexcept
142+
{
143+
switch (value_type)
144+
{
145+
case InstrumentValueType::kInt:
146+
return "Int";
147+
case InstrumentValueType::kLong:
148+
return "Long";
149+
case InstrumentValueType::kFloat:
150+
return "Float";
151+
case InstrumentValueType::kDouble:
152+
return "Double";
153+
default:
154+
return "Unknown";
155+
}
156+
}
157+
};
158+
159+
struct InstrumentEqualNameCaseInsensitive
160+
{
161+
bool operator()(const InstrumentDescriptor &lhs, const InstrumentDescriptor &rhs) const noexcept
162+
{
163+
// Names (case-insensitive)
164+
if (!InstrumentDescriptorUtil::CaseInsensitiveAsciiEquals(lhs.name_, rhs.name_))
165+
{
166+
return false;
167+
}
168+
169+
// Kinds (Type and ValueType)
170+
if (lhs.type_ != rhs.type_ || lhs.value_type_ != rhs.value_type_)
171+
{
172+
return false;
173+
}
174+
175+
// Units (case-sensitive)
176+
if (lhs.unit_ != rhs.unit_)
177+
{
178+
return false;
179+
}
180+
181+
// Descriptions (case-sensitive)
182+
if (lhs.description_ != rhs.description_)
183+
{
184+
return false;
185+
}
186+
187+
// All identifying fields are equal
188+
return true;
189+
}
190+
};
191+
192+
// Hash for InstrumentDescriptor
193+
// Identical instruments must have the same hash value
194+
// Two instruments are identical when all identifying fields (case-insensitive name , kind,
195+
// description, unit) are equal.
196+
struct InstrumentDescriptorHash
197+
{
198+
std::size_t operator()(const InstrumentDescriptor &instrument_descriptor) const noexcept
199+
{
200+
std::size_t hashcode{};
201+
202+
for (char c : instrument_descriptor.name_)
203+
{
204+
sdk::common::GetHash(hashcode,
205+
static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
206+
}
207+
208+
sdk::common::GetHash(hashcode, instrument_descriptor.description_);
209+
sdk::common::GetHash(hashcode, instrument_descriptor.unit_);
210+
sdk::common::GetHash(hashcode, static_cast<uint32_t>(instrument_descriptor.type_));
211+
sdk::common::GetHash(hashcode, static_cast<uint32_t>(instrument_descriptor.value_type_));
212+
return hashcode;
213+
}
214+
};
215+
68216
using MetricAttributes = opentelemetry::sdk::metrics::FilteredOrderedAttributeMap;
69217
using MetricAttributesHash = opentelemetry::sdk::metrics::FilteredOrderedAttributeMapHash;
70218
using AggregationTemporalitySelector = std::function<AggregationTemporality(InstrumentType)>;

sdk/include/opentelemetry/sdk/metrics/meter.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@ class Meter final : public opentelemetry::metrics::Meter
136136
// meter-context.
137137
std::unique_ptr<sdk::instrumentationscope::InstrumentationScope> scope_;
138138
std::weak_ptr<sdk::metrics::MeterContext> meter_context_;
139-
// Mapping between instrument-name and Aggregation Storage.
140-
std::unordered_map<std::string, std::shared_ptr<MetricStorage>> storage_registry_;
139+
// Mapping between instrument descriptor and Aggregation Storage.
140+
using MetricStorageMap = std::unordered_map<InstrumentDescriptor,
141+
std::shared_ptr<MetricStorage>,
142+
InstrumentDescriptorHash,
143+
InstrumentEqualNameCaseInsensitive>;
144+
MetricStorageMap storage_registry_;
141145
std::shared_ptr<ObservableRegistry> observable_registry_;
142146
MeterConfig meter_config_;
143147
std::unique_ptr<SyncWritableMetricStorage> RegisterSyncMetricStorage(
@@ -164,6 +168,19 @@ class Meter final : public opentelemetry::metrics::Meter
164168
return instrument_validator.ValidateName(name) && instrument_validator.ValidateUnit(unit) &&
165169
instrument_validator.ValidateDescription(description);
166170
}
171+
172+
// This function checks if the instrument is a duplicate of an existing one
173+
// and emits a warning through the internal logger.
174+
static void WarnOnDuplicateInstrument(
175+
const sdk::instrumentationscope::InstrumentationScope *scope,
176+
const MetricStorageMap &storage_registry,
177+
const InstrumentDescriptor &new_instrument);
178+
179+
// This function checks if the instrument has a name case conflict with an existing one
180+
// and emits a warning through the internal logger.
181+
static void WarnOnNameCaseConflict(const sdk::instrumentationscope::InstrumentationScope *scope,
182+
const InstrumentDescriptor &existing_instrument,
183+
const InstrumentDescriptor &new_instrument);
167184
};
168185
} // namespace metrics
169186
} // namespace sdk

0 commit comments

Comments
 (0)