Skip to content

Commit a94fdd0

Browse files
authored
Merge branch 'main' into noop-metrics-benchmark
2 parents e4eaa40 + 4aa4827 commit a94fdd0

File tree

10 files changed

+2027
-1235
lines changed

10 files changed

+2027
-1235
lines changed

opentelemetry-otlp/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## vNext
44

5+
### Fixed
6+
7+
- URL encoded values in `OTEL_EXPORTER_OTLP_HEADERS` are now correctly decoded. [#1578](https://github.com/open-telemetry/opentelemetry-rust/pull/1578)
8+
59
### Added
610

711
- Added `DeltaTemporalitySelector` ([#1568])

opentelemetry-otlp/src/exporter/http/mod.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ impl HttpExporterBuilder {
120120
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
121121
// headers will be wrapped, so we must do some logic to unwrap first.
122122
let mut inst_headers = self.http_config.headers.unwrap_or_default();
123-
inst_headers.extend(headers);
123+
inst_headers.extend(
124+
headers
125+
.into_iter()
126+
.map(|(key, value)| (key, super::url_decode(&value).unwrap_or(value))),
127+
);
124128
self.http_config.headers = Some(inst_headers);
125129
self
126130
}
@@ -310,7 +314,7 @@ fn add_header_from_string(input: &str, headers: &mut HashMap<HeaderName, HeaderV
310314
headers.extend(parse_header_string(input).filter_map(|(key, value)| {
311315
Some((
312316
HeaderName::from_str(key).ok()?,
313-
HeaderValue::from_str(value).ok()?,
317+
HeaderValue::from_str(&value).ok()?,
314318
))
315319
}));
316320
}

opentelemetry-otlp/src/exporter/mod.rs

+70-5
Original file line numberDiff line numberDiff line change
@@ -211,18 +211,53 @@ impl<B: HasExportConfig> WithExportConfig for B {
211211
}
212212

213213
#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))]
214-
fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, &str)> {
214+
fn url_decode(value: &str) -> Option<String> {
215+
let mut result = String::with_capacity(value.len());
216+
let mut chars_to_decode = Vec::<u8>::new();
217+
let mut all_chars = value.chars();
218+
219+
loop {
220+
let ch = all_chars.next();
221+
222+
if ch.is_some() && ch.unwrap() == '%' {
223+
chars_to_decode.push(
224+
u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16)
225+
.ok()?,
226+
);
227+
continue;
228+
}
229+
230+
if !chars_to_decode.is_empty() {
231+
result.push_str(std::str::from_utf8(&chars_to_decode).ok()?);
232+
chars_to_decode.clear();
233+
}
234+
235+
if let Some(c) = ch {
236+
result.push(c);
237+
} else {
238+
return Some(result);
239+
}
240+
}
241+
}
242+
243+
#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))]
244+
fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, String)> {
215245
value
216246
.split_terminator(',')
217247
.map(str::trim)
218248
.filter_map(parse_header_key_value_string)
219249
}
220250

221251
#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))]
222-
fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, &str)> {
252+
fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> {
223253
key_value_string
224254
.split_once('=')
225-
.map(|(key, value)| (key.trim(), value.trim()))
255+
.map(|(key, value)| {
256+
(
257+
key.trim(),
258+
url_decode(value.trim()).unwrap_or(value.to_string()),
259+
)
260+
})
226261
.filter(|(key, value)| !key.is_empty() && !value.is_empty())
227262
}
228263

@@ -267,6 +302,24 @@ mod tests {
267302
);
268303
}
269304

305+
#[test]
306+
fn test_url_decode() {
307+
let test_cases = vec![
308+
// Format: (encoded, expected_decoded)
309+
("v%201", Some("v 1")),
310+
("v 1", Some("v 1")),
311+
("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà§ÖabcdÄ")),
312+
("v%XX1", None),
313+
];
314+
315+
for (encoded, expected_decoded) in test_cases {
316+
assert_eq!(
317+
super::url_decode(encoded),
318+
expected_decoded.map(|v| v.to_string()),
319+
)
320+
}
321+
}
322+
270323
#[test]
271324
fn test_parse_header_string() {
272325
let test_cases = vec![
@@ -280,7 +333,10 @@ mod tests {
280333
for (input_str, expected_headers) in test_cases {
281334
assert_eq!(
282335
super::parse_header_string(input_str).collect::<Vec<_>>(),
283-
expected_headers,
336+
expected_headers
337+
.into_iter()
338+
.map(|(k, v)| (k, v.to_string()))
339+
.collect::<Vec<_>>(),
284340
)
285341
}
286342
}
@@ -290,6 +346,15 @@ mod tests {
290346
let test_cases = vec![
291347
// Format: (input_str, expected_header)
292348
("k1=v1", Some(("k1", "v1"))),
349+
(
350+
"Authentication=Basic AAA",
351+
Some(("Authentication", "Basic AAA")),
352+
),
353+
(
354+
"Authentication=Basic%20AAA",
355+
Some(("Authentication", "Basic AAA")),
356+
),
357+
("k1=%XX", Some(("k1", "%XX"))),
293358
("", None),
294359
("=v1", None),
295360
("k1=", None),
@@ -298,7 +363,7 @@ mod tests {
298363
for (input_str, expected_headers) in test_cases {
299364
assert_eq!(
300365
super::parse_header_key_value_string(input_str),
301-
expected_headers,
366+
expected_headers.map(|(k, v)| (k, v.to_string())),
302367
)
303368
}
304369
}

opentelemetry-otlp/src/exporter/tonic/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ fn parse_headers_from_env(signal_headers_var: &str) -> HeaderMap {
381381
.filter_map(|(key, value)| {
382382
Some((
383383
HeaderName::from_str(key).ok()?,
384-
HeaderValue::from_str(value).ok()?,
384+
HeaderValue::from_str(&value).ok()?,
385385
))
386386
})
387387
.collect::<HeaderMap>()

opentelemetry-sdk/src/metrics/mod.rs

+109
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,115 @@ mod tests {
590590
);
591591
}
592592

593+
// "multi_thread" tokio flavor must be used else flush won't
594+
// be able to make progress!
595+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
596+
#[ignore = "Known bug: https://github.com/open-telemetry/opentelemetry-rust/issues/1598"]
597+
async fn delta_memory_efficiency_test() {
598+
// Run this test with stdout enabled to see output.
599+
// cargo test delta_memory_efficiency_test --features=metrics,testing -- --nocapture
600+
601+
// Arrange
602+
let exporter = InMemoryMetricsExporterBuilder::new()
603+
.with_temporality_selector(DeltaTemporalitySelector())
604+
.build();
605+
let reader = PeriodicReader::builder(exporter.clone(), runtime::Tokio).build();
606+
let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
607+
608+
// Act
609+
let meter = meter_provider.meter("test");
610+
let counter = meter
611+
.u64_counter("my_counter")
612+
.with_unit(Unit::new("my_unit"))
613+
.init();
614+
counter.add(1, &[KeyValue::new("key1", "value1")]);
615+
counter.add(1, &[KeyValue::new("key1", "value1")]);
616+
counter.add(1, &[KeyValue::new("key1", "value1")]);
617+
counter.add(1, &[KeyValue::new("key1", "value1")]);
618+
counter.add(1, &[KeyValue::new("key1", "value1")]);
619+
620+
counter.add(1, &[KeyValue::new("key1", "value2")]);
621+
counter.add(1, &[KeyValue::new("key1", "value2")]);
622+
counter.add(1, &[KeyValue::new("key1", "value2")]);
623+
624+
meter_provider.force_flush().unwrap();
625+
626+
// Assert
627+
let resource_metrics = exporter
628+
.get_finished_metrics()
629+
.expect("metrics are expected to be exported.");
630+
assert!(!resource_metrics.is_empty());
631+
let metric = &resource_metrics[0].scope_metrics[0].metrics[0];
632+
assert_eq!(metric.name, "my_counter");
633+
assert_eq!(metric.unit.as_str(), "my_unit");
634+
let sum = metric
635+
.data
636+
.as_any()
637+
.downcast_ref::<data::Sum<u64>>()
638+
.expect("Sum aggregation expected for Counter instruments by default");
639+
640+
// Expecting 2 time-series.
641+
assert_eq!(sum.data_points.len(), 2);
642+
assert!(sum.is_monotonic, "Counter should produce monotonic.");
643+
assert_eq!(
644+
sum.temporality,
645+
data::Temporality::Delta,
646+
"Should produce Delta as configured"
647+
);
648+
649+
// find and validate key1=value1 datapoint
650+
let mut data_point1 = None;
651+
for datapoint in &sum.data_points {
652+
if datapoint
653+
.attributes
654+
.iter()
655+
.any(|(k, v)| k.as_str() == "key1" && v.as_str() == "value1")
656+
{
657+
data_point1 = Some(datapoint);
658+
}
659+
}
660+
assert_eq!(
661+
data_point1
662+
.expect("datapoint with key1=value1 expected")
663+
.value,
664+
5
665+
);
666+
667+
// find and validate key1=value2 datapoint
668+
let mut data_point1 = None;
669+
for datapoint in &sum.data_points {
670+
if datapoint
671+
.attributes
672+
.iter()
673+
.any(|(k, v)| k.as_str() == "key1" && v.as_str() == "value2")
674+
{
675+
data_point1 = Some(datapoint);
676+
}
677+
}
678+
assert_eq!(
679+
data_point1
680+
.expect("datapoint with key1=value2 expected")
681+
.value,
682+
3
683+
);
684+
685+
// flush again, and validate that nothing is flushed
686+
// as delta temporality.
687+
meter_provider.force_flush().unwrap();
688+
let resource_metrics = exporter
689+
.get_finished_metrics()
690+
.expect("metrics are expected to be exported.");
691+
println!("resource_metrics: {:?}", resource_metrics);
692+
assert!(resource_metrics.is_empty(), "No metrics should be exported as no new measurements were recorded since last collect.");
693+
}
694+
695+
struct DeltaTemporalitySelector();
696+
impl TemporalitySelector for DeltaTemporalitySelector {
697+
fn temporality(&self, _kind: InstrumentKind) -> Temporality {
698+
Temporality::Delta
699+
}
700+
}
701+
593702
struct TestContext {
594703
exporter: InMemoryMetricsExporter,
595704
meter_provider: SdkMeterProvider,

opentelemetry-semantic-conventions/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## vNext
44

5+
### Changed
6+
7+
- Update to [v1.24.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.24.0) of the semantic conventions.
8+
[#1596](https://github.com/open-telemetry/opentelemetry-rust/pull/1596)
9+
510
## v0.14.0
611

712
### Changed

opentelemetry-semantic-conventions/scripts/generate-consts-from-spec.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
55
CRATE_DIR="${SCRIPT_DIR}/../"
66

77
# freeze the spec version and generator version to make generation reproducible
8-
SPEC_VERSION=1.21.0
9-
SEMCOVGEN_VERSION=0.19.0
8+
SPEC_VERSION=1.24.0
9+
SEMCOVGEN_VERSION=0.23.0
1010

1111
cd "$CRATE_DIR"
1212

opentelemetry-semantic-conventions/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ pub mod trace;
2020

2121
/// The schema URL that matches the version of the semantic conventions that
2222
/// this crate defines.
23-
pub const SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.21.0";
23+
pub const SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.24.0";

0 commit comments

Comments
 (0)