Skip to content

Commit 3b2fdbe

Browse files
authored
Avoid tracestate error with sample rate provided through headers (#1808)
* allow to set identical sample rate multiple times * change check method name * make vertx version test more robust * ignore 2cnd value + log in debug * fail on double set + warn on double header * fix tests * early skip if sample rate is already set * revert micrometer plugin hack * update changelog * fix javadoc
1 parent 38aef91 commit 3b2fdbe

File tree

4 files changed

+188
-80
lines changed

4 files changed

+188
-80
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ events. With the new `OVERRIDE` option, non-file logs can be ECS-reformatted aut
3535
===== Bug fixes
3636
* Fix another error related to instrumentation plugins loading on Windows - {pull}1785[#1785]
3737
* Load Spring AMQP plugin- {pull}1784[#1784]
38+
* Avoid `IllegalStateException` when multiple `tracestate` headers are used - {pull}1808[#1808]
3839
3940
[float]
4041
===== Refactors

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceState.java

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@
2626

2727
import co.elastic.apm.agent.configuration.converter.RoundedDoubleConverter;
2828
import co.elastic.apm.agent.objectpool.Recyclable;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
2931

3032
import javax.annotation.Nullable;
3133
import java.util.ArrayList;
3234
import java.util.List;
3335

3436
public class TraceState implements Recyclable {
3537

38+
private static final Logger log = LoggerFactory.getLogger(TraceState.class);
39+
3640
private static final int DEFAULT_SIZE_LIMIT = 4096;
3741

3842
private static final RoundedDoubleConverter DOUBLE_CONVERTER = RoundedDoubleConverter.withDefaultPrecision();
@@ -72,68 +76,109 @@ public void copyFrom(TraceState other) {
7276
rewriteBuffer.setLength(0);
7377
}
7478

75-
7679
public void addTextHeader(String headerValue) {
77-
int elasticEntryStartIndex = headerValue.indexOf(VENDOR_PREFIX);
78-
79-
if (elasticEntryStartIndex >= 0) {
80-
// parsing (and maybe fixing) current tracestate required
81-
int entriesStart = headerValue.indexOf(SAMPLE_RATE_PREFIX, elasticEntryStartIndex);
82-
if (entriesStart >= 0) {
83-
int valueStart = entriesStart + 2;
84-
int valueEnd = valueStart;
85-
if (valueEnd < headerValue.length()) {
86-
char c = headerValue.charAt(valueEnd);
87-
while (valueEnd < headerValue.length() && c != VENDOR_SEPARATOR && c != ENTRY_SEPARATOR) {
88-
c = headerValue.charAt(valueEnd++);
89-
}
90-
if (valueEnd < headerValue.length()) {
91-
// end due to separator char that needs to be trimmed
92-
valueEnd--;
93-
}
94-
}
95-
double value;
96-
try {
97-
value = Double.parseDouble(headerValue.substring(valueStart, valueEnd));
98-
if (0 <= value && value <= 1.0) {
99-
// ensure proper rounding of sample rate to minimize storage
100-
// even if configuration should not allow this, any upstream value might require rounding
101-
double rounded = DOUBLE_CONVERTER.round(value);
102-
if (rounded != value) {
103-
// value needs to be re-written due to rounding
104-
105-
rewriteBuffer.setLength(0);
106-
rewriteBuffer.append(headerValue, 0, valueStart);
107-
rewriteBuffer.append(rounded);
108-
rewriteBuffer.append(headerValue, valueEnd, headerValue.length());
109-
// we don't minimize allocation as re-writing should be an exception
110-
headerValue = rewriteBuffer.toString();
111-
}
112-
sampleRate = rounded;
113-
}
114-
} catch (NumberFormatException e) {
115-
// silently ignored
80+
int vendorStart = headerValue.indexOf(VENDOR_PREFIX);
81+
82+
if (vendorStart < 0) {
83+
// no ES entry
84+
tracestate.add(headerValue);
85+
return;
86+
}
87+
88+
int vendorEnd = headerValue.indexOf(VENDOR_SEPARATOR, vendorStart);
89+
if (vendorEnd < 0) {
90+
vendorEnd = headerValue.length();
91+
}
92+
93+
int sampleRateStart = headerValue.indexOf(SAMPLE_RATE_PREFIX, vendorStart);
94+
if (sampleRateStart < 0) {
95+
// no sample rate, rewrite
96+
log.warn("invalid header, no sample rate {}", headerValue);
97+
headerValue = rewriteRemoveInvalidHeader(headerValue, vendorStart, vendorEnd);
98+
if (headerValue.length() > 0) {
99+
tracestate.add(headerValue);
100+
}
101+
return;
102+
}
103+
104+
int valueStart = sampleRateStart + 2;
105+
int valueEnd = valueStart;
106+
if (valueEnd < headerValue.length()) {
107+
char c = headerValue.charAt(valueEnd);
108+
while (valueEnd < headerValue.length() && c != VENDOR_SEPARATOR && c != ENTRY_SEPARATOR) {
109+
c = headerValue.charAt(valueEnd++);
110+
}
111+
if (valueEnd < headerValue.length()) {
112+
// end due to separator char that needs to be trimmed
113+
valueEnd--;
114+
}
115+
}
116+
117+
String headerValueString = headerValue.substring(valueStart, valueEnd);
118+
double value = Double.NaN;
119+
try {
120+
value = Double.parseDouble(headerValueString);
121+
} catch (NumberFormatException e) {
122+
// silently ignored
123+
}
124+
125+
if (Double.isNaN(value) || value < 0 || value > 1) {
126+
log.warn("invalid sample rate header {}", headerValueString);
127+
headerValue = rewriteRemoveInvalidHeader(headerValue, vendorStart, vendorEnd);
128+
} else {
129+
if (!Double.isNaN(sampleRate)) {
130+
log.warn("sample rate already set to {}, trying to set it to {} through header will be ignored", sampleRate, value);
131+
headerValue = rewriteRemoveInvalidHeader(headerValue, vendorStart, vendorEnd);
132+
} else {
133+
// ensure proper rounding of sample rate to minimize storage
134+
// even if configuration should not allow this, any upstream value might require rounding
135+
double rounded = DOUBLE_CONVERTER.round(value);
136+
if (rounded != value) {
137+
// value needs to be re-written due to rounding
138+
headerValue = rewriteRoundedHeader(headerValue, valueStart, valueEnd, rounded);
139+
sampleRate = rounded;
140+
} else {
141+
sampleRate = value;
116142
}
117143
}
118144
}
119145

120-
tracestate.add(headerValue);
146+
if (!headerValue.isEmpty()) {
147+
tracestate.add(headerValue);
148+
}
149+
150+
}
151+
152+
private String rewriteRoundedHeader(String fullHeader, int valueStart, int valueEnd, double rounded) {
153+
// we don't minimize allocation as re-writing should be an exception
154+
rewriteBuffer.setLength(0);
155+
rewriteBuffer.append(fullHeader, 0, valueStart);
156+
rewriteBuffer.append(DOUBLE_CONVERTER.toString(rounded));
157+
rewriteBuffer.append(fullHeader, valueEnd, fullHeader.length());
158+
return rewriteBuffer.toString();
159+
}
160+
161+
private String rewriteRemoveInvalidHeader(String fullHeader, int start, int end) {
162+
rewriteBuffer.setLength(0);
163+
if (start > 0) {
164+
rewriteBuffer.append(fullHeader, 0, start);
165+
}
166+
if (end < fullHeader.length()) {
167+
rewriteBuffer.append(fullHeader, end + 1, fullHeader.length());
168+
}
169+
return rewriteBuffer.toString();
121170
}
122171

123172
/**
124173
* Sets value for trace state. Provided rate and string value are assumed to be correct and consistent
125174
*
126175
* @param rate sample rate
127176
* @param headerValue header value, as provided by a call to {@link #getHeaderValue(double)}
128-
* @throws IllegalStateException if sample rate has already been set
129-
* @throws IllegalArgumentException if rate has an invalid value
177+
* @throws IllegalStateException if sample rate has already been set
130178
*/
131179
public void set(double rate, String headerValue) {
132180
if (!Double.isNaN(sampleRate)) {
133-
// sample rate is set either explicitly from this method (for root transactions)
134-
// or through upstream header, thus there is no need to change after. This allows to only
135-
// write/rewrite headers once
136-
throw new IllegalStateException("sample rate has already been set from headers");
181+
throw new IllegalStateException(String.format("sample rate already set to %f, trying to set it to %f", sampleRate, rate));
137182
}
138183

139184
sampleRate = rate;
@@ -148,7 +193,10 @@ public void set(double rate, String headerValue) {
148193
* @return header value
149194
*/
150195
public static String getHeaderValue(double sampleRate) {
151-
return FULL_PREFIX + sampleRate;
196+
if (Double.isNaN(sampleRate) || sampleRate < 0 || sampleRate > 1) {
197+
throw new IllegalArgumentException("invalid sample rate " + sampleRate);
198+
}
199+
return FULL_PREFIX + DOUBLE_CONVERTER.toString(DOUBLE_CONVERTER.round(sampleRate));
152200
}
153201

154202
/**

apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceContextTest.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -548,19 +548,19 @@ void testNonTracedChildSpanWithoutTraceState() {
548548

549549
@ParameterizedTest
550550
@CsvSource(delimiter = '|', value = {
551-
// invalid tracestate values: just assume no sample rate is provided
552-
"|NaN",
553-
"es=|NaN",
554-
"es=s|NaN",
555-
"es=s:|NaN",
556-
"es=s:a|NaN",
551+
// invalid tracestate values: no sample rate and header fixed
552+
"|NaN|",
553+
"es=|NaN|",
554+
"es=s|NaN|",
555+
"es=s:|NaN|",
556+
"es=s:a|NaN|",
557557
// valid tracestate values with sample rate
558-
"es=s:1|1",
559-
"es=s:0.42|0.42",
558+
"es=s:1|1|es=s:1",
559+
"es=s:0.42|0.42|es=s:0.42",
560560
// other vendors entries
561-
"a=123,es=s:0.42|0.42",
561+
"a=123,es=s:0.42|0.42|a=123,es=s:0.42",
562562
})
563-
void checkExpectedSampleRate(@Nullable String traceState, double expectedRate) {
563+
void checkExpectedSampleRate(@Nullable String traceState, double expectedRate, @Nullable String expectedHeader) {
564564
Map<String, String> headers = new HashMap<>();
565565
headers.put(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01");
566566
if (null != traceState) {
@@ -577,8 +577,7 @@ void checkExpectedSampleRate(@Nullable String traceState, double expectedRate) {
577577
.isEqualTo(Double.valueOf(expectedRate));
578578

579579
assertThat(child.getTraceState().toTextHeader())
580-
.describedAs("tracestate should be kept as-is")
581-
.isEqualTo(traceState);
580+
.isEqualTo(expectedHeader);
582581

583582
}
584583

0 commit comments

Comments
 (0)