Skip to content

Commit 775f1f9

Browse files
anujnegi270cijothomaslalitb
authored
Disabling the Instrument Name Validation under a new feature flag (#2543)
Co-authored-by: Cijo Thomas <[email protected]> Co-authored-by: Lalit Kumar Bhasin <[email protected]>
1 parent fdc1151 commit 775f1f9

File tree

4 files changed

+164
-8
lines changed

4 files changed

+164
-8
lines changed

opentelemetry-sdk/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## vNext
44

5+
- *Feature*: Introduced a new feature flag, `experimental_metrics_disable_name_validation`, under the `opentelemetry-sdk`, which allows disabling the Instrument Name Validation. This is useful in scenarios where you need to use *special characters*, *Windows Perf Counter Wildcard Path*, or similar cases. For more details, check [#2543](https://github.com/open-telemetry/opentelemetry-rust/pull/2543).
6+
> **WARNING**: While this feature provides flexibility, **be cautious** when using it, as platforms like **Prometheus** impose restrictions on metric names and labels (e.g., no spaces, capital letters, or certain special characters). Using invalid characters may result in compatibility issues or incorrect behavior. Ensure that instrument names comply with the requirements of your target platform to avoid potential problems.
7+
58
- *Breaking(Affects custom metric exporter authors only)* `start_time` and `time` is moved from DataPoints to aggregations (Sum, Gauge, Histogram, ExpoHistogram) see [#2377](https://github.com/open-telemetry/opentelemetry-rust/pull/2377) and [#2411](https://github.com/open-telemetry/opentelemetry-rust/pull/2411), to reduce memory.
69

710
- *Breaking* `start_time` is no longer optional for `Sum` aggregation, see [#2367](https://github.com/open-telemetry/opentelemetry-rust/pull/2367), but is still optional for `Gauge` aggregation see [#2389](https://github.com/open-telemetry/opentelemetry-rust/pull/2389).

opentelemetry-sdk/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ experimental_metrics_periodicreader_with_async_runtime = ["metrics"]
5858
spec_unstable_metrics_views = ["metrics"]
5959
experimental_logs_batch_log_processor_with_async_runtime = ["logs"]
6060
experimental_trace_batch_span_processor_with_async_runtime = ["trace"]
61+
experimental_metrics_disable_name_validation = ["metrics"]
6162

6263

6364
[[bench]]

opentelemetry-sdk/src/metrics/meter.rs

+64-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(dead_code)]
12
use core::fmt;
23
use std::{borrow::Cow, sync::Arc};
34

@@ -23,15 +24,18 @@ use super::noop::NoopSyncInstrument;
2324
const INSTRUMENT_NAME_MAX_LENGTH: usize = 255;
2425
// maximum length of instrument unit name
2526
const INSTRUMENT_UNIT_NAME_MAX_LENGTH: usize = 63;
27+
// Characters allowed in instrument name
2628
const INSTRUMENT_NAME_ALLOWED_NON_ALPHANUMERIC_CHARS: [char; 4] = ['_', '.', '-', '/'];
2729

28-
// instrument validation error strings
30+
// instrument name validation error strings
2931
const INSTRUMENT_NAME_EMPTY: &str = "instrument name must be non-empty";
3032
const INSTRUMENT_NAME_LENGTH: &str = "instrument name must be less than 256 characters";
3133
const INSTRUMENT_NAME_INVALID_CHAR: &str =
3234
"characters in instrument name must be ASCII and belong to the alphanumeric characters, '_', '.', '-' and '/'";
3335
const INSTRUMENT_NAME_FIRST_ALPHABETIC: &str =
3436
"instrument name must start with an alphabetic character";
37+
38+
// instrument unit validation error strings
3539
const INSTRUMENT_UNIT_LENGTH: &str = "instrument unit must be less than 64 characters";
3640
const INSTRUMENT_UNIT_INVALID_CHAR: &str = "characters in instrument unit must be ASCII";
3741

@@ -572,6 +576,13 @@ fn validate_bucket_boundaries(boundaries: &[f64]) -> MetricResult<()> {
572576
Ok(())
573577
}
574578

579+
#[cfg(feature = "experimental_metrics_disable_name_validation")]
580+
fn validate_instrument_name(_name: &str) -> MetricResult<()> {
581+
// No name restrictions when name validation is disabled
582+
Ok(())
583+
}
584+
585+
#[cfg(not(feature = "experimental_metrics_disable_name_validation"))]
575586
fn validate_instrument_name(name: &str) -> MetricResult<()> {
576587
if name.is_empty() {
577588
return Err(MetricError::InvalidInstrumentConfiguration(
@@ -583,6 +594,7 @@ fn validate_instrument_name(name: &str) -> MetricResult<()> {
583594
INSTRUMENT_NAME_LENGTH,
584595
));
585596
}
597+
586598
if name.starts_with(|c: char| !c.is_ascii_alphabetic()) {
587599
return Err(MetricError::InvalidInstrumentConfiguration(
588600
INSTRUMENT_NAME_FIRST_ALPHABETIC,
@@ -669,19 +681,21 @@ where
669681
}
670682
}
671683

684+
#[allow(unused_imports)]
672685
#[cfg(test)]
673686
mod tests {
674687
use std::borrow::Cow;
675688

676689
use crate::metrics::MetricError;
677690

678691
use super::{
679-
validate_instrument_name, validate_instrument_unit, INSTRUMENT_NAME_FIRST_ALPHABETIC,
680-
INSTRUMENT_NAME_INVALID_CHAR, INSTRUMENT_NAME_LENGTH, INSTRUMENT_UNIT_INVALID_CHAR,
681-
INSTRUMENT_UNIT_LENGTH,
692+
validate_instrument_name, validate_instrument_unit, INSTRUMENT_NAME_EMPTY,
693+
INSTRUMENT_NAME_FIRST_ALPHABETIC, INSTRUMENT_NAME_INVALID_CHAR, INSTRUMENT_NAME_LENGTH,
694+
INSTRUMENT_UNIT_INVALID_CHAR, INSTRUMENT_UNIT_LENGTH,
682695
};
683696

684697
#[test]
698+
#[cfg(not(feature = "experimental_metrics_disable_name_validation"))]
685699
fn instrument_name_validation() {
686700
// (name, expected error)
687701
let instrument_name_test_cases = vec![
@@ -694,6 +708,52 @@ mod tests {
694708
("allow/slash", ""),
695709
("allow_under_score", ""),
696710
("allow.dots.ok", ""),
711+
("", INSTRUMENT_NAME_EMPTY),
712+
("\\allow\\slash /sec", INSTRUMENT_NAME_FIRST_ALPHABETIC),
713+
("\\allow\\$$slash /sec", INSTRUMENT_NAME_FIRST_ALPHABETIC),
714+
("Total $ Count", INSTRUMENT_NAME_INVALID_CHAR),
715+
(
716+
"\\test\\UsagePercent(Total) > 80%",
717+
INSTRUMENT_NAME_FIRST_ALPHABETIC,
718+
),
719+
("/not / allowed", INSTRUMENT_NAME_FIRST_ALPHABETIC),
720+
];
721+
for (name, expected_error) in instrument_name_test_cases {
722+
let assert = |result: Result<_, MetricError>| {
723+
if expected_error.is_empty() {
724+
assert!(result.is_ok());
725+
} else {
726+
assert!(matches!(
727+
result.unwrap_err(),
728+
MetricError::InvalidInstrumentConfiguration(msg) if msg == expected_error
729+
));
730+
}
731+
};
732+
733+
assert(validate_instrument_name(name).map(|_| ()));
734+
}
735+
}
736+
737+
#[test]
738+
#[cfg(feature = "experimental_metrics_disable_name_validation")]
739+
fn instrument_name_validation_disabled() {
740+
// (name, expected error)
741+
let instrument_name_test_cases = vec![
742+
("validateName", ""),
743+
("_startWithNoneAlphabet", ""),
744+
("utf8char锈", ""),
745+
("a".repeat(255).leak(), ""),
746+
("a".repeat(256).leak(), ""),
747+
("invalid name", ""),
748+
("allow/slash", ""),
749+
("allow_under_score", ""),
750+
("allow.dots.ok", ""),
751+
("", ""),
752+
("\\allow\\slash /sec", ""),
753+
("\\allow\\$$slash /sec", ""),
754+
("Total $ Count", ""),
755+
("\\test\\UsagePercent(Total) > 80%", ""),
756+
("/not / allowed", ""),
697757
];
698758
for (name, expected_error) in instrument_name_test_cases {
699759
let assert = |result: Result<_, MetricError>| {

opentelemetry-sdk/src/metrics/mod.rs

+96-4
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,16 @@ mod tests {
132132
use std::time::Duration;
133133

134134
// Run all tests in this mod
135-
// cargo test metrics::tests --features=testing
135+
// cargo test metrics::tests --features=testing,spec_unstable_metrics_views
136136
// Note for all tests from this point onwards in this mod:
137137
// "multi_thread" tokio flavor must be used else flush won't
138138
// be able to make progress!
139139

140140
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
141+
#[cfg(not(feature = "experimental_metrics_disable_name_validation"))]
141142
async fn invalid_instrument_config_noops() {
142143
// Run this test with stdout enabled to see output.
143-
// cargo test invalid_instrument_config_noops --features=testing -- --nocapture
144+
// cargo test invalid_instrument_config_noops --features=testing,spec_unstable_metrics_views -- --nocapture
144145
let invalid_instrument_names = vec![
145146
"_startWithNoneAlphabet",
146147
"utf8char锈",
@@ -209,8 +210,99 @@ mod tests {
209210
histogram.record(1.9, &[]);
210211
test_context.flush_metrics();
211212

212-
// As bucket boundaires provided via advisory params are invalid, no
213-
// metrics should be exported
213+
// As bucket boundaries provided via advisory params are invalid,
214+
// no metrics should be exported
215+
test_context.check_no_metrics();
216+
}
217+
}
218+
219+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
220+
#[cfg(feature = "experimental_metrics_disable_name_validation")]
221+
async fn valid_instrument_config_with_feature_experimental_metrics_disable_name_validation() {
222+
// Run this test with stdout enabled to see output.
223+
// cargo test valid_instrument_config_with_feature_experimental_metrics_disable_name_validation --all-features -- --nocapture
224+
let invalid_instrument_names = vec![
225+
"_startWithNoneAlphabet",
226+
"utf8char锈",
227+
"",
228+
"a".repeat(256).leak(),
229+
"\\allow\\slash /sec",
230+
"\\allow\\$$slash /sec",
231+
"Total $ Count",
232+
"\\test\\UsagePercent(Total) > 80%",
233+
"invalid name",
234+
];
235+
for name in invalid_instrument_names {
236+
let test_context = TestContext::new(Temporality::Cumulative);
237+
let counter = test_context.meter().u64_counter(name).build();
238+
counter.add(1, &[]);
239+
240+
let up_down_counter = test_context.meter().i64_up_down_counter(name).build();
241+
up_down_counter.add(1, &[]);
242+
243+
let gauge = test_context.meter().f64_gauge(name).build();
244+
gauge.record(1.9, &[]);
245+
246+
let histogram = test_context.meter().f64_histogram(name).build();
247+
histogram.record(1.0, &[]);
248+
249+
let _observable_counter = test_context
250+
.meter()
251+
.u64_observable_counter(name)
252+
.with_callback(move |observer| {
253+
observer.observe(1, &[]);
254+
})
255+
.build();
256+
257+
let _observable_gauge = test_context
258+
.meter()
259+
.f64_observable_gauge(name)
260+
.with_callback(move |observer| {
261+
observer.observe(1.0, &[]);
262+
})
263+
.build();
264+
265+
let _observable_up_down_counter = test_context
266+
.meter()
267+
.i64_observable_up_down_counter(name)
268+
.with_callback(move |observer| {
269+
observer.observe(1, &[]);
270+
})
271+
.build();
272+
273+
test_context.flush_metrics();
274+
275+
// As instrument name are valid because of the feature flag, metrics should be exported
276+
let resource_metrics = test_context
277+
.exporter
278+
.get_finished_metrics()
279+
.expect("metrics expected to be exported");
280+
281+
assert!(!resource_metrics.is_empty(), "metrics should be exported");
282+
}
283+
284+
// Ensuring that the Histograms with invalid bucket boundaries are not exported
285+
// when using the feature flag
286+
let invalid_bucket_boundaries = vec![
287+
vec![1.0, 1.0], // duplicate boundaries
288+
vec![1.0, 2.0, 3.0, 2.0], // duplicate non consequent boundaries
289+
vec![1.0, 2.0, 3.0, 4.0, 2.5], // unsorted boundaries
290+
vec![1.0, 2.0, 3.0, f64::INFINITY, 4.0], // boundaries with positive infinity
291+
vec![1.0, 2.0, 3.0, f64::NAN], // boundaries with NaNs
292+
vec![f64::NEG_INFINITY, 2.0, 3.0], // boundaries with negative infinity
293+
];
294+
for bucket_boundaries in invalid_bucket_boundaries {
295+
let test_context = TestContext::new(Temporality::Cumulative);
296+
let histogram = test_context
297+
.meter()
298+
.f64_histogram("test")
299+
.with_boundaries(bucket_boundaries)
300+
.build();
301+
histogram.record(1.9, &[]);
302+
test_context.flush_metrics();
303+
304+
// As bucket boundaries provided via advisory params are invalid,
305+
// no metrics should be exported
214306
test_context.check_no_metrics();
215307
}
216308
}

0 commit comments

Comments
 (0)