Skip to content

Commit df2ad56

Browse files
perf(inject): write tracestate in one go (#95)
# What does this PR do? Two optimizations: * Don't use an intermediary buffer for the datadog part of the trace context and instead truncate if we have written too much * Have a fast path for header normalization
1 parent 6468647 commit df2ad56

File tree

2 files changed

+228
-72
lines changed

2 files changed

+228
-72
lines changed

dd-trace-propagation/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,36 @@ impl DatadogCompositePropagator {
181181
}
182182
}
183183

184+
pub(crate) const fn const_append(source: &[u8], dest: &mut [u8], at: usize) {
185+
let mut i = 0;
186+
loop {
187+
if i >= source.len() {
188+
break;
189+
}
190+
dest[i + at] = source[i];
191+
i += 1;
192+
}
193+
}
194+
195+
macro_rules! const_concat {
196+
($($s:expr,)+) => {{
197+
const LEN: usize = 0 $( + $s.len())*;
198+
const CONCATENATED: [u8; LEN] = {
199+
let mut concatenated: [u8; LEN] = [0; LEN];
200+
let mut at = 0;
201+
$(
202+
let part: &str = $s;
203+
crate::const_append(part.as_bytes(), &mut concatenated, at);
204+
at += $s.len();
205+
)*
206+
let _ = at;
207+
concatenated
208+
};
209+
std::str::from_utf8(&CONCATENATED).expect("the concatenation of valid utf-8 strings is always a valid utf-8 string")
210+
}};
211+
}
212+
pub(crate) use const_concat;
213+
184214
#[cfg(test)]
185215
pub mod tests {
186216
use std::{collections::HashMap, str::FromStr, sync::LazyLock, vec};

dd-trace-propagation/src/tracecontext.rs

Lines changed: 198 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ fn replace_chars<MatchFn: Fn(u8) -> bool>(
4343
f: MatchFn,
4444
replacement_char: char,
4545
) -> Cow<'_, str> {
46+
// Fast first pass
47+
if s.as_bytes().iter().all(|c| c.is_ascii() && !f(*c)) {
48+
return Cow::Borrowed(s);
49+
}
50+
4651
let mut replaced = String::new();
4752
let mut tail = s;
4853
loop {
@@ -68,11 +73,7 @@ fn replace_chars<MatchFn: Fn(u8) -> bool>(
6873
};
6974
tail = &tail[pos + offset..];
7075
}
71-
if replaced.is_empty() {
72-
Cow::Borrowed(s)
73-
} else {
74-
Cow::Owned(replaced)
75-
}
76+
Cow::Owned(replaced)
7677
}
7778

7879
pub fn inject(context: &InjectSpanContext, carrier: &mut dyn Injector) {
@@ -88,30 +89,104 @@ fn inject_traceparent(context: &InjectSpanContext, carrier: &mut dyn Injector) {
8889
// TODO: if higher trace_id 64bits are 0, we should verify _dd.p.tid is unset
8990
// if not 0, verify that `_dd.p.tid` is either unset or set to the encoded value of
9091
// the higher-order 64 bits
91-
let trace_id = format!("{:032x}", context.trace_id);
92-
let parent_id = format!("{:016x}", context.span_id);
9392

9493
let flags = context
9594
.sampling
9695
.priority
9796
.map(|priority| if priority.is_keep() { "01" } else { "00" })
9897
.unwrap_or("00");
9998

100-
let traceparent = format!("00-{trace_id}-{parent_id}-{flags}");
99+
let traceparent = format!(
100+
"00-{:032x}-{:016x}-{flags}",
101+
context.trace_id, context.span_id
102+
);
101103

102104
dd_debug!("Propagator (tracecontext): injecting traceparent: {traceparent}");
103105

104106
carrier.set(TRACEPARENT_KEY, traceparent);
105107
}
106108

109+
fn buf_appender(buf: &mut String) -> BufAppender<'_> {
110+
BufAppender {
111+
start: buf.len(),
112+
buf,
113+
}
114+
}
115+
116+
struct BufAppender<'a> {
117+
start: usize,
118+
buf: &'a mut String,
119+
}
120+
121+
impl BufAppender<'_> {
122+
fn push_str(&mut self, s: &str) {
123+
self.buf.push_str(s);
124+
}
125+
126+
fn len(&self) -> usize {
127+
self.buf.len() - self.start
128+
}
129+
130+
fn appender(&mut self) -> BufAppender<'_> {
131+
BufAppender {
132+
start: self.buf.len(),
133+
buf: self.buf,
134+
}
135+
}
136+
137+
fn truncate(&mut self, len: usize) {
138+
self.buf.truncate(self.start + len);
139+
}
140+
}
141+
142+
fn append_dd_propagation_tags(context: &InjectSpanContext, tags_buffer: &mut BufAppender) {
143+
for (key, value) in context.tags.iter() {
144+
let Some(key_suffix) = key.strip_prefix(DATADOG_PROPAGATION_TAG_PREFIX) else {
145+
continue;
146+
};
147+
148+
let t_key_suffix = replace_chars(
149+
key_suffix,
150+
|c| !matches!(c, b'!'..=b'+' | b'-'..=b'<' | b'>'..=b'~'),
151+
INVALID_CHAR_REPLACEMENT,
152+
);
153+
let encoded_value = replace_chars(
154+
value,
155+
|c| !matches!(c, b' '..=b'+' | b'-'..=b':' | b'<'..=b'}'),
156+
INVALID_CHAR_REPLACEMENT,
157+
);
158+
let encoded_value = encode_tag_value(&encoded_value);
159+
160+
let entry_size = TRACESTATE_DD_PAIR_SEPARATOR.len()
161+
+ TRACESTATE_DATADOG_PROPAGATION_TAG_PREFIX.len()
162+
+ t_key_suffix.len()
163+
+ 1
164+
+ encoded_value.len();
165+
166+
if tags_buffer.len() + entry_size > TRACESTATE_DD_KEY_MAX_LENGTH / 2 {
167+
break;
168+
}
169+
170+
tags_buffer.push_str(crate::const_concat!(
171+
TRACESTATE_DD_PAIR_SEPARATOR,
172+
TRACESTATE_DATADOG_PROPAGATION_TAG_PREFIX,
173+
));
174+
tags_buffer.push_str(&t_key_suffix);
175+
tags_buffer.push_str(":");
176+
tags_buffer.push_str(&encoded_value);
177+
}
178+
}
179+
107180
fn inject_tracestate(context: &InjectSpanContext, carrier: &mut dyn Injector) {
181+
let mut tracestate = String::with_capacity(256);
182+
tracestate.push_str("dd=");
183+
108184
// Use a single String buffer to build the entire tracestate, avoiding intermediate allocations
109-
let mut dd_parts = String::new();
185+
let mut dd_parts = buf_appender(&mut tracestate);
110186

111187
// Build sampling priority part
112188
let priority = context.sampling.priority.unwrap_or(priority::USER_KEEP);
113-
dd_parts.push_str(TRACESTATE_SAMPLING_PRIORITY_KEY);
114-
dd_parts.push(':');
189+
dd_parts.push_str(crate::const_concat!(TRACESTATE_SAMPLING_PRIORITY_KEY, ":",));
115190
dd_parts.push_str(&priority.to_string());
116191

117192
// Build origin part if present
@@ -130,9 +205,11 @@ fn inject_tracestate(context: &InjectSpanContext, carrier: &mut dyn Injector) {
130205
+ origin_encoded.len()
131206
< TRACESTATE_DD_KEY_MAX_LENGTH
132207
{
133-
dd_parts.push_str(TRACESTATE_DD_PAIR_SEPARATOR);
134-
dd_parts.push_str(TRACESTATE_ORIGIN_KEY);
135-
dd_parts.push(':');
208+
dd_parts.push_str(crate::const_concat!(
209+
TRACESTATE_DD_PAIR_SEPARATOR,
210+
TRACESTATE_ORIGIN_KEY,
211+
":",
212+
));
136213
dd_parts.push_str(&origin_encoded);
137214
}
138215
}
@@ -142,78 +219,35 @@ fn inject_tracestate(context: &InjectSpanContext, carrier: &mut dyn Injector) {
142219
dd_parts.len() + TRACESTATE_DD_PAIR_SEPARATOR.len() + TRACESTATE_LAST_PARENT_KEY.len() + 1;
143220
if last_parent_id_part_start + 16 < TRACESTATE_DD_KEY_MAX_LENGTH {
144221
// 16 chars for hex span_id
145-
dd_parts.push_str(TRACESTATE_DD_PAIR_SEPARATOR);
146-
dd_parts.push_str(TRACESTATE_LAST_PARENT_KEY);
147-
dd_parts.push(':');
222+
223+
dd_parts.push_str(crate::const_concat!(
224+
TRACESTATE_DD_PAIR_SEPARATOR,
225+
TRACESTATE_LAST_PARENT_KEY,
226+
":",
227+
));
148228

149229
if context.is_remote {
150230
if let Some(id) = context.tags.get(DATADOG_LAST_PARENT_ID_KEY) {
151231
dd_parts.push_str(id);
152232
} else {
153-
let _ = write!(&mut dd_parts, "{:016x}", context.span_id);
233+
let _ = write!(&mut dd_parts.buf, "{:016x}", context.span_id);
154234
}
155235
} else {
156-
let _ = write!(&mut dd_parts, "{:016x}", context.span_id);
236+
let _ = write!(&mut dd_parts.buf, "{:016x}", context.span_id);
157237
}
158238
}
159239

240+
let index_before_tags = dd_parts.len();
160241
// Build propagation tags part
161-
let mut tags_buffer = String::new();
162-
let mut first_tag = true;
242+
let mut tags_buffer = dd_parts.appender();
163243

164-
for (key, value) in context.tags.iter() {
165-
let Some(key_suffix) = key.strip_prefix(DATADOG_PROPAGATION_TAG_PREFIX) else {
166-
continue;
167-
};
168-
169-
let t_key_suffix = replace_chars(
170-
key_suffix,
171-
|c| !matches!(c, b'!'..=b'+' | b'-'..=b'<' | b'>'..=b'~'),
172-
INVALID_CHAR_REPLACEMENT,
173-
);
174-
let encoded_value = replace_chars(
175-
value,
176-
|c| !matches!(c, b' '..=b'+' | b'-'..=b':' | b'<'..=b'}'),
177-
INVALID_CHAR_REPLACEMENT,
178-
);
179-
let encoded_value = encode_tag_value(&encoded_value);
180-
181-
let entry_size = if first_tag {
182-
0
183-
} else {
184-
TRACESTATE_DD_PAIR_SEPARATOR.len()
185-
} + TRACESTATE_DATADOG_PROPAGATION_TAG_PREFIX.len()
186-
+ t_key_suffix.len()
187-
+ 1
188-
+ encoded_value.len();
189-
190-
if tags_buffer.len() + entry_size > TRACESTATE_DD_KEY_MAX_LENGTH / 2 {
191-
break;
192-
}
193-
194-
if !first_tag {
195-
tags_buffer.push_str(TRACESTATE_DD_PAIR_SEPARATOR);
196-
}
197-
tags_buffer.push_str(TRACESTATE_DATADOG_PROPAGATION_TAG_PREFIX);
198-
tags_buffer.push_str(&t_key_suffix);
199-
tags_buffer.push(':');
200-
tags_buffer.push_str(&encoded_value);
201-
first_tag = false;
202-
}
244+
append_dd_propagation_tags(context, &mut tags_buffer);
203245

204246
// Add tags part to dd_parts if there's room
205-
if !tags_buffer.is_empty()
206-
&& dd_parts.len() + TRACESTATE_DD_PAIR_SEPARATOR.len() + tags_buffer.len()
207-
< TRACESTATE_DD_KEY_MAX_LENGTH
208-
{
209-
dd_parts.push_str(TRACESTATE_DD_PAIR_SEPARATOR);
210-
dd_parts.push_str(&tags_buffer);
247+
if tags_buffer.len() == 0 || dd_parts.len() >= TRACESTATE_DD_KEY_MAX_LENGTH {
248+
dd_parts.truncate(index_before_tags);
211249
}
212250

213-
let mut tracestate = String::with_capacity(256);
214-
tracestate.push_str("dd=");
215-
tracestate.push_str(&dd_parts);
216-
217251
// Add additional tracestate values if present
218252
if let Some(ts) = context.tracestate {
219253
if let Some(ref additional) = ts.additional_values {
@@ -226,7 +260,10 @@ fn inject_tracestate(context: &InjectSpanContext, carrier: &mut dyn Injector) {
226260
}
227261
}
228262

229-
dd_debug!("Propagator (tracecontext): injecting tracestate: {tracestate}");
263+
dd_debug!(
264+
"Propagator (tracecontext): injecting tracestate: {}",
265+
tracestate
266+
);
230267

231268
carrier.set(TRACESTATE_KEY, tracestate);
232269
}
@@ -472,7 +509,7 @@ pub fn keys() -> &'static [String] {
472509
mod test {
473510
use dd_trace::{configuration::TracePropagationStyle, sampling::priority, Config};
474511

475-
use crate::Propagator;
512+
use crate::{context::span_context_to_inject, Propagator};
476513

477514
use super::*;
478515

@@ -787,6 +824,95 @@ mod test {
787824
assert!(carrier[TRACESTATE_KEY].ends_with("state30=value-30"));
788825
}
789826

827+
#[test]
828+
fn test_tracestate_with_tags_longer_than_limit() {
829+
let long_origin = "abcd".repeat(32);
830+
let long_tag = "abcd".repeat(30);
831+
let mut context = SpanContext {
832+
trace_id: u128::from_str_radix("1111aaaa2222bbbb3333cccc4444dddd", 16).unwrap(),
833+
span_id: u64::from_str_radix("5555eeee6666ffff", 16).unwrap(),
834+
sampling: Sampling {
835+
priority: Some(priority::USER_KEEP),
836+
mechanism: Some(mechanism::MANUAL),
837+
},
838+
origin: Some(long_origin.clone()),
839+
tags: HashMap::from([("_dd.p.foo".to_string(), long_tag.clone())]),
840+
links: vec![],
841+
is_remote: false,
842+
tracestate: None,
843+
};
844+
let mut carrier: HashMap<String, String> = HashMap::new();
845+
TracePropagationStyle::TraceContext.inject(
846+
&mut span_context_to_inject(&mut context),
847+
&mut carrier,
848+
&Config::builder().build(),
849+
);
850+
assert_eq!(
851+
carrier[TRACESTATE_KEY],
852+
format!("dd=s:2;o:{long_origin};p:5555eeee6666ffff")
853+
);
854+
}
855+
856+
#[test]
857+
fn test_tracestate_with_tags_shorter_than_limit() {
858+
#[allow(clippy::repeat_once)]
859+
let short_origin = "abcd".repeat(1);
860+
let long_tag = "abcd".repeat(30);
861+
let mut context = SpanContext {
862+
trace_id: u128::from_str_radix("1111aaaa2222bbbb3333cccc4444dddd", 16).unwrap(),
863+
span_id: u64::from_str_radix("5555eeee6666ffff", 16).unwrap(),
864+
sampling: Sampling {
865+
priority: Some(priority::USER_KEEP),
866+
mechanism: Some(mechanism::MANUAL),
867+
},
868+
origin: Some(short_origin.clone()),
869+
tags: HashMap::from([("_dd.p.foo".to_string(), long_tag.clone())]),
870+
links: vec![],
871+
is_remote: false,
872+
tracestate: None,
873+
};
874+
let mut carrier: HashMap<String, String> = HashMap::new();
875+
TracePropagationStyle::TraceContext.inject(
876+
&mut span_context_to_inject(&mut context),
877+
&mut carrier,
878+
&Config::builder().build(),
879+
);
880+
assert_eq!(
881+
carrier[TRACESTATE_KEY],
882+
format!("dd=s:2;o:{short_origin};p:5555eeee6666ffff;t.foo:{long_tag}")
883+
);
884+
}
885+
886+
#[test]
887+
fn test_tracestate_with_long_dd_tags() {
888+
#[allow(clippy::repeat_once)]
889+
let short_origin = "abcd".repeat(1);
890+
let long_tag = "abcd".repeat(32);
891+
let mut context = SpanContext {
892+
trace_id: u128::from_str_radix("1111aaaa2222bbbb3333cccc4444dddd", 16).unwrap(),
893+
span_id: u64::from_str_radix("5555eeee6666ffff", 16).unwrap(),
894+
sampling: Sampling {
895+
priority: Some(priority::USER_KEEP),
896+
mechanism: Some(mechanism::MANUAL),
897+
},
898+
origin: Some(short_origin.clone()),
899+
tags: HashMap::from([("_dd.p.foo".to_string(), long_tag.clone())]),
900+
links: vec![],
901+
is_remote: false,
902+
tracestate: None,
903+
};
904+
let mut carrier: HashMap<String, String> = HashMap::new();
905+
TracePropagationStyle::TraceContext.inject(
906+
&mut span_context_to_inject(&mut context),
907+
&mut carrier,
908+
&Config::builder().build(),
909+
);
910+
assert_eq!(
911+
carrier[TRACESTATE_KEY],
912+
format!("dd=s:2;o:{short_origin};p:5555eeee6666ffff")
913+
);
914+
}
915+
790916
#[test]
791917
fn test_replace_chars() {
792918
let tests = vec![

0 commit comments

Comments
 (0)