2626
2727import co .elastic .apm .agent .configuration .converter .RoundedDoubleConverter ;
2828import co .elastic .apm .agent .objectpool .Recyclable ;
29+ import org .slf4j .Logger ;
30+ import org .slf4j .LoggerFactory ;
2931
3032import javax .annotation .Nullable ;
3133import java .util .ArrayList ;
3234import java .util .List ;
3335
3436public 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 /**
0 commit comments