All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nike.wingtips.util.parser.SpanParser Maven / Gradle / Ivy

The newest version!
package com.nike.wingtips.util.parser;

import com.nike.wingtips.Span;
import com.nike.wingtips.Span.SpanPurpose;
import com.nike.wingtips.Span.TimestampedAnnotation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

@SuppressWarnings("WeakerAccess")
public class SpanParser {

    private static final Logger logger = LoggerFactory.getLogger(SpanParser.class);

    // Intentionally protected - use the static methods.
    protected SpanParser() { /* do nothing */ }

    /**
     * The name of the trace ID field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getTraceId()}.
     */
    public static final String TRACE_ID_FIELD = "traceId";
    /**
     * The name of the parent span ID field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getParentSpanId()}.
     */
    public static final String PARENT_SPAN_ID_FIELD = "parentSpanId";
    /**
     * The name of the span ID field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getSpanId()}.
     */
    public static final String SPAN_ID_FIELD = "spanId";
    /**
     * The name of the span name field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getSpanName()}.
     */
    public static final String SPAN_NAME_FIELD = "spanName";
    /**
     * The name of the sampleable field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#isSampleable()}.
     */
    public static final String SAMPLEABLE_FIELD = "sampleable";
    /**
     * The name of the user ID field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getUserId()}.
     */
    public static final String USER_ID_FIELD = "userId";
    /**
     * The name of the span purpose field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getSpanPurpose()}.
     */
    public static final String SPAN_PURPOSE_FIELD = "spanPurpose";
    /**
     * The name of the start-time-in-epoch-micros field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getSpanStartTimeNanos()}.
     */
    public static final String START_TIME_EPOCH_MICROS_FIELD = "startTimeEpochMicros";
    /**
     * The name of the duration-in-nanoseconds field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getDurationNanos()}.
     */
    public static final String DURATION_NANOS_FIELD = "durationNanos";
    /**
     * The name of the span tags field when serializing/deserializing to/from JSON (see {@link
     * #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to {@link Span#getTags()}.
     */
    public static final String TAGS_FIELD = "tags";
    /**
     * The name of the field for the list of timestamped annotations when serializing/deserializing to/from JSON (see
     * {@link #convertSpanToJSON(Span)} and {@link #fromJSON(String)}). Corresponds to
     * {@link Span#getTimestampedAnnotations()}.
     */
    public static final String ANNOTATIONS_LIST_FIELD = "annotations";

    /**
     * The name of the timestamp-in-epoch-micros field for the annotation JSON object when serializing/deserializing
     * a span's {@link TimestampedAnnotation} to/from JSON (see {@link #convertSpanToJSON(Span)} and
     * {@link #fromJSON(String)}). Corresponds to {@link TimestampedAnnotation#getTimestampEpochMicros()}.
     */
    public static final String ANNOTATION_SUBOBJECT_TIMESTAMP_FIELD = "timestampEpochMicros";
    /**
     * The name of the value field for the annotation JSON object when serializing/deserializing
     * a span's {@link TimestampedAnnotation} to/from JSON (see {@link #convertSpanToJSON(Span)} and
     * {@link #fromJSON(String)}). Corresponds to {@link TimestampedAnnotation#getValue()}.
     */
    public static final String ANNOTATION_SUBOBJECT_VALUE_FIELD = "value";

    /**
     * The prefix that will be added to every {@link Span#getTags()} tag key when serializing a {@link Span} to
     * key/value format. See {@link #convertSpanToKeyValueFormat(Span)}.
     */
    public static final String KEY_VALUE_TAG_PREFIX = "tag_";

    /**
     * The prefix that will be added to every {@link Span#getTimestampedAnnotations()} annotation key (the timestamp)
     * when serializing a {@link Span} to key/value format. See {@link #convertSpanToKeyValueFormat(Span)}.
     */
    public static final String KEY_VALUE_TIMESTAMPED_ANNOTATION_PREFIX = "ts_annot_";

    /**
     * Calculates and returns the JSON representation of this span instance. We build this manually ourselves to
     * avoid pulling in an extra dependency (e.g. Jackson) just for building a simple JSON string.
     *
     * 

NOTE: You should call {@link Span#toJSON()} directly instead of this method, as that {@link Span#toJSON()} * instance method caches the result. This can have significant performance impact in some scenarios. */ public static String convertSpanToJSON(Span span) { StringBuilder builder = new StringBuilder(); builder.append("{\"").append(TRACE_ID_FIELD).append("\":\"").append(escapeJson(span.getTraceId())).append('\"'); builder.append(",\"").append(PARENT_SPAN_ID_FIELD).append("\":\"").append(escapeJson(span.getParentSpanId())).append('\"'); builder.append(",\"").append(SPAN_ID_FIELD).append("\":\"").append(escapeJson(span.getSpanId())).append('\"'); builder.append(",\"").append(SPAN_NAME_FIELD).append("\":\"").append(escapeJson(span.getSpanName())).append('\"'); builder.append(",\"").append(SAMPLEABLE_FIELD).append("\":\"").append(span.isSampleable()).append('\"'); builder.append(",\"").append(USER_ID_FIELD).append("\":\"").append(escapeJson(span.getUserId())).append('\"'); builder.append(",\"").append(SPAN_PURPOSE_FIELD).append("\":\"").append(span.getSpanPurpose().name()).append('\"'); builder.append(",\"").append(START_TIME_EPOCH_MICROS_FIELD).append("\":\"").append(span.getSpanStartTimeEpochMicros()).append('\"'); if (span.isCompleted()) { builder.append(",\"").append(DURATION_NANOS_FIELD).append("\":\"").append(span.getDurationNanos()).append('\"'); } if(!span.getTags().isEmpty()) { // Create nested json for the tags. builder.append(",\"").append(TAGS_FIELD).append("\":{"); boolean first = true; for (Map.Entry tagEntry : span.getTags().entrySet()) { if (!first) { builder.append(','); } String escapedKey = escapeJson(tagEntry.getKey()); String escapedValue = escapeJson(tagEntry.getValue()); builder.append('\"').append(escapedKey).append("\":\"").append(escapedValue).append('\"'); first = false; } builder.append("}"); } if (!span.getTimestampedAnnotations().isEmpty()) { // Create JSON array for the annotations. builder.append(",\"").append(ANNOTATIONS_LIST_FIELD).append("\":["); boolean first = true; for (TimestampedAnnotation annotation : span.getTimestampedAnnotations()) { if (!first) { builder.append(','); } String escapedValue = escapeJson(annotation.getValue()); builder.append("{\"").append(ANNOTATION_SUBOBJECT_TIMESTAMP_FIELD) .append("\":\"").append(annotation.getTimestampEpochMicros()).append('\"'); builder.append(",\"").append(ANNOTATION_SUBOBJECT_VALUE_FIELD) .append("\":\"").append(escapedValue).append("\"}"); first = false; } builder.append("]"); } builder.append("}"); return builder.toString(); } /** * @return The {@link Span} represented by the given JSON string, or null if a proper span could not be * deserialized from the given string. * *

WARNING: This method assumes the JSON you're trying to deserialize originally came from {@link * #convertSpanToJSON(Span)}. This assumption allows it to take some shortcuts while deserializing, and allows us * to accomplish deserialization efficiently without needing to pull in a third-party dependency like Jackson. * If you try to use this method on a JSON string that didn't come from {@link #convertSpanToJSON(Span)}, * then it will likely fail. */ public static Span fromJSON(String json) { try { JsonDeserializationResult mainResult = deserializeJsonObject(json, 0); Map mainMap = mainResult.levelOneKeyValuePairs; JsonDeserializationResult tagsResult = mainResult.subObjects.get(TAGS_FIELD); Map tagsMap = (tagsResult == null) ? null : tagsResult.levelOneKeyValuePairs; List annotationsList = convertToTimestampedAnnotationsList( mainResult.arrays.get(ANNOTATIONS_LIST_FIELD) ); return fromKeyValueMap(mainMap, tagsMap, annotationsList); } catch (Exception e) { logger.error("Error extracting Span from JSON. Defaulting to null. bad_span_json={}", json, e); return null; } } protected static List convertToTimestampedAnnotationsList( List jdrList ) { if (jdrList == null || jdrList.isEmpty()) { return null; } List annotationList = new ArrayList<>(jdrList.size()); for (JsonDeserializationResult jdr : jdrList) { long timestamp = Long.parseLong(jdr.levelOneKeyValuePairs.get(ANNOTATION_SUBOBJECT_TIMESTAMP_FIELD)); String value = jdr.levelOneKeyValuePairs.get(ANNOTATION_SUBOBJECT_VALUE_FIELD); annotationList.add(new TimestampedAnnotation(timestamp, value)); } return annotationList; } protected static class JsonDeserializationResult { final Map levelOneKeyValuePairs = new LinkedHashMap<>(); final Map subObjects = new LinkedHashMap<>(); final Map> arrays = new LinkedHashMap<>(); final AtomicInteger jsonEndObjectIndex = new AtomicInteger(Integer.MAX_VALUE); } protected enum ParsingState { EXPECT_KEY_START_QUOTES, EXTRACTING_KEY, EXPECT_KEY_VALUE_DELIMITER_CHAR, EXPECT_VALUE_START_CHAR, // For JSON this might be quotes '"', or object-start '{', or array/list start '[' EXTRACTING_VALUE, EXPECT_COMMA_BEFORE_NEW_KEY } protected static JsonDeserializationResult deserializeJsonObject(String jsonStr, int deserializationStartIndex) { JsonDeserializationResult result = new JsonDeserializationResult(); // Make sure our JSON string starts with a '{' char. if (jsonStr.charAt(deserializationStartIndex) != '{') { throw new IllegalStateException( "Expected JSON string at index " + deserializationStartIndex + " to be a '{' char." ); } int i = deserializationStartIndex + 1; // Wind past the '{' char. int strLen = jsonStr.length(); int extractKeyStartIndex = i; int extractKeyEndIndex = i; int extractValueStartIndex = i; int numPrecedingBackslashes = 0; ParsingState state = ParsingState.EXPECT_KEY_START_QUOTES; while (i < strLen) { char c = jsonStr.charAt(i); if (state == ParsingState.EXPECT_KEY_START_QUOTES) { if (c == '\"') { // Found the quotes. The next character will be the first character of the key. extractKeyStartIndex = i + 1; // Switch state to extracting key. state = ParsingState.EXTRACTING_KEY; } else { throw new IllegalStateException( "Span parsing error: Expected but did not find quotes '\"' character for key-start at " + "index " + i ); } } else if (state == ParsingState.EXTRACTING_KEY) { if (isUnescapedQuotes(c, numPrecedingBackslashes)) { // We found the end of the key (a non-escaped quotes). Mark it. extractKeyEndIndex = i; // Switch state to looking for the key/value delimiter. state = ParsingState.EXPECT_KEY_VALUE_DELIMITER_CHAR; } } else if (state == ParsingState.EXPECT_KEY_VALUE_DELIMITER_CHAR) { // We expect this to be a colon character. if (c == ':') { // Found the colon. The next character should be the value-start character // (object-start '{' or value-start '"'). state = ParsingState.EXPECT_VALUE_START_CHAR; } else { throw new IllegalStateException( "Span parsing error: Expected but did not find colon ':' character for key/value delimiter " + "at index " + i ); } } else if (state == ParsingState.EXPECT_VALUE_START_CHAR) { // If the next char is an object-start '{', then we recursively call this method to extract the JSON // object. Next, we'll look for an array-start '['. Otherwise we'll expect a quotes char for // value-start. if (c == '{') { // JSON-object-start. Deserialize this subobject. JsonDeserializationResult subObject = deserializeJsonObject(jsonStr, i); // Extract the key associated with the subObject. String key = jsonStr.substring(extractKeyStartIndex, extractKeyEndIndex); key = unescapeJson(key); // Add this key->subobject info to our result. result.subObjects.put(key, subObject); // Wind the index forward to wherever the subObject stopped. i = subObject.jsonEndObjectIndex.get(); // Reset the numPrecedingBackslashes counter. numPrecedingBackslashes = 0; // Switch state to looking for the comma before the next key. state = ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY; } else if (c == '[') { // Array-start square bracket. Deserialize all the subobjects. List arrayObjects = new ArrayList<>(); int arrayParsingIndex = i + 1; if (arrayParsingIndex >= strLen) { throw new IllegalStateException( "Span parsing error: JSON string ended after array-start square bracket '[' character " + "at index " + i ); } char nextArrayParsingChar = jsonStr.charAt(arrayParsingIndex); if (nextArrayParsingChar != '{') { throw new IllegalStateException( "Span parsing error: Expected but did not find left curly brace '{' character after " + "array-start square bracket '['. Instead, found '" + nextArrayParsingChar + "' at index " + arrayParsingIndex ); } while (nextArrayParsingChar == ',' || nextArrayParsingChar == '{') { // Only do something for the object-start curly brace. If it's a comma then we do nothing, // and it'll wind forward to the next array parsing character. if (nextArrayParsingChar == '{') { // Extract the JSON object at this array parsing index, and add it to our arrayObjects list. JsonDeserializationResult arrayObject = deserializeJsonObject(jsonStr, arrayParsingIndex); arrayObjects.add(arrayObject); // Wind the array parsing index forward to wherever the JSON object parsing stopped. arrayParsingIndex = arrayObject.jsonEndObjectIndex.get(); } arrayParsingIndex++; if (arrayParsingIndex >= strLen) { throw new IllegalStateException( "Span parsing error: JSON string ended before JSON close-array square bracket ']' " + "could be found. JSON string ended at index " + arrayParsingIndex ); } nextArrayParsingChar = jsonStr.charAt(arrayParsingIndex); } // The array parsing index ran into something that wasn't a comma or start-object left curly brace. // At this point it must be the close-array right square bracket. if (nextArrayParsingChar != ']') { throw new IllegalStateException( "Span parsing error: Expected but did not find object-start left curly brace '{' or " + "comma ',' or array-close right square bracket ']' character while parsing JSON array. " + "Instead, found '" + nextArrayParsingChar + "' at index " + arrayParsingIndex ); } // The JSON array has been successfully parsed. // Extract the key associated with the array. String key = jsonStr.substring(extractKeyStartIndex, extractKeyEndIndex); key = unescapeJson(key); // Add this key->array info to our result. result.arrays.put(key, arrayObjects); // Wind the overall parsing index to the next char after the array parsing index. i = arrayParsingIndex; // Reset the numPrecedingBackslashes counter. numPrecedingBackslashes = 0; // Switch state to looking for the comma before the next key. state = ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY; } else if (c == '\"') { // Value-start quotes. The next time the loop iterates it will be pointing at the value. Mark // the index for value extraction and switch state. extractValueStartIndex = i + 1; state = ParsingState.EXTRACTING_VALUE; } else { throw new IllegalStateException( "Span parsing error: Expected but did not find left curly brace '{' or quotes '\"' character " + "for value-start at index " + i ); } } else if (state == ParsingState.EXTRACTING_VALUE) { if (isUnescapedQuotes(c, numPrecedingBackslashes)) { // We found the end of the value (a non-escaped quotes). Extract key and value, and put them in // the level one key/value map. String key = jsonStr.substring(extractKeyStartIndex, extractKeyEndIndex); String value = jsonStr.substring(extractValueStartIndex, i); key = unescapeJson(key); value = unescapeJson(value); result.levelOneKeyValuePairs.put(key, value); // We expect a comma next. state = ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY; } } else if (state == ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY) { // If the character is a close-object '}', then we're done and should return the result // we've gathered so far. if (c == '}') { // Close-object '}' curly-brace was found. Return the result after marking the // close-object index. result.jsonEndObjectIndex.set(i); return result; } else if (c == ',') { // Comma was found. Next we expect the key-start quotes. state = ParsingState.EXPECT_KEY_START_QUOTES; } else { throw new IllegalStateException( "Span parsing error: Expected but did not find right curly brace '}' or comma ',' character " + "after value-end at index " + i ); } } else { // Should never happen. throw new IllegalStateException("Unhandled state: " + state); } if (c == '\\') { numPrecedingBackslashes++; } else { numPrecedingBackslashes = 0; } i++; } // JSON should always explicitly short circuit in EXPECT_COMMA_BEFORE_NEW_KEY state due to finding // close-object '}' character. If we reach here then parsing failed. throw new IllegalStateException( "Span parsing error: JSON string did not end with a right curly brace '}'. Instead ended in state: " + state.name() ); } private static boolean isUnescapedQuotes( char charInQuestion, int numPrecedingBackslashes ) { if (charInQuestion != '\"') { // Not quotes, so can't be unescaped quotes. return false; } // The original character in question is a quotes. We know whether it's escaped or not by how many preceding // backslashes it has. return (numPrecedingBackslashes % 2) == 0; } /** * Calculates and returns the key/value representation of this span instance. Keys are not surrounded by quotes, * but values are. Both keys and values are escaped via {@link #escapeJson(String)}, with keys further being * escaped to replace equals '=' and spaces ' ' with their escaped-unicode equivalents. Tag keys will be prefixed * with {@link #KEY_VALUE_TAG_PREFIX}. Timestamped annotations will be prefixed with {@link * #KEY_VALUE_TIMESTAMPED_ANNOTATION_PREFIX}. * *

NOTE: You should call {@link Span#toKeyValueString()} directly instead of this method, as that {@link * Span#toKeyValueString()} instance method caches the result. This can have significant performance impact in some * scenarios. */ public static String convertSpanToKeyValueFormat(Span span) { StringBuilder builder = new StringBuilder(); builder.append(TRACE_ID_FIELD).append("=\"").append(escapeJson(span.getTraceId())).append('\"'); builder.append(",").append(PARENT_SPAN_ID_FIELD).append("=\"").append(escapeJson(span.getParentSpanId())).append('\"'); builder.append(",").append(SPAN_ID_FIELD).append("=\"").append(escapeJson(span.getSpanId())).append('\"'); builder.append(",").append(SPAN_NAME_FIELD).append("=\"").append(escapeJson(span.getSpanName())).append('\"'); builder.append(",").append(SAMPLEABLE_FIELD).append("=\"").append(span.isSampleable()).append('\"'); builder.append(",").append(USER_ID_FIELD).append("=\"").append(escapeJson(span.getUserId())).append('\"'); builder.append(",").append(SPAN_PURPOSE_FIELD).append("=\"").append(span.getSpanPurpose().name()).append('\"'); builder.append(",").append(START_TIME_EPOCH_MICROS_FIELD).append("=\"").append(span.getSpanStartTimeEpochMicros()).append('\"'); // Only output duration if the span is completed. if (span.isCompleted()) { builder.append(",").append(DURATION_NANOS_FIELD).append("=\"").append(span.getDurationNanos()).append('\"'); } // Output tags if we have any. for (Map.Entry tagEntry : span.getTags().entrySet()) { String sanitizedTagKey = escapeTagKeyForKeyValueFormatSerialization(tagEntry.getKey()); String escapedTagValue = escapeJson(tagEntry.getValue()); builder.append(",").append(KEY_VALUE_TAG_PREFIX).append(sanitizedTagKey) .append("=\"").append(escapedTagValue).append('\"'); } // Output timestamped annotations if we have any. for (TimestampedAnnotation annotation : span.getTimestampedAnnotations()) { String escapedAnnotationValue = escapeJson(annotation.getValue()); builder.append(",").append(KEY_VALUE_TIMESTAMPED_ANNOTATION_PREFIX) .append(annotation.getTimestampEpochMicros()) .append("=\"").append(escapedAnnotationValue).append('\"'); } return builder.toString(); } protected static String escapeTagKeyForKeyValueFormatSerialization(String key) { String escapedKey = escapeJson(key); // We also need to escape equals sign if it exists. Don't do this unless we detect an equals sign is contained // in the key, however. Most of the time it won't exist in the key, and we'll save a bunch of time // by not calling String.replace(...). if (containsChar(escapedKey, '=')) { escapedKey = escapedKey.replace("=", ESCAPED_EQUALS_SIGN); } // Same with space character. if (containsChar(escapedKey, ' ')) { escapedKey = escapedKey.replace(" ", ESCAPED_SPACE_CHAR); } // And comma character. if (containsChar(escapedKey, ',')) { escapedKey = escapedKey.replace(",", ESCAPED_COMMA_CHAR); } return escapedKey; } protected static String unescapeTagKeyForKeyValueFormatDeserialization(String escapedKey) { // Do the reverse of the escape-tag logic. if (escapedKey.contains(ESCAPED_COMMA_CHAR)) { escapedKey = escapedKey.replace(ESCAPED_COMMA_CHAR, ","); } if (escapedKey.contains(ESCAPED_SPACE_CHAR)) { escapedKey = escapedKey.replace(ESCAPED_SPACE_CHAR, " "); } if (escapedKey.contains(ESCAPED_EQUALS_SIGN)) { escapedKey = escapedKey.replace(ESCAPED_EQUALS_SIGN, "="); } return unescapeJson(escapedKey); } /** * @return The {@link Span} represented by the given key/value string, or null if a proper span could not be * deserialized from the given string. * *

WARNING: This method assumes the string you're trying to deserialize originally came from * {@link #convertSpanToKeyValueFormat(Span)}. This assumption allows it to be as fast as possible, not worry * about syntactically-correct-but-annoying-to-deal-with whitespace, not have to use a third party utility, etc. * If you try to use this method on a string that didn't come from {@link #convertSpanToKeyValueFormat(Span)}, * then it will likely fail. */ public static Span fromKeyValueString(String keyValueStr) { try { Map spanFieldsMap = new LinkedHashMap<>(); Map tagsMap = new LinkedHashMap<>(); List annotationsList = new ArrayList<>(); int i = 0; int strLen = keyValueStr.length(); int extractKeyStartIndex = 0; int extractKeyEndIndex = 0; int extractValueStartIndex = 0; int numPrecedingBackslashes = 0; // The very first character should be the start of the first key, so we'll jump straight into // extracting the key. ParsingState state = ParsingState.EXTRACTING_KEY; while (i < strLen) { char c = keyValueStr.charAt(i); if (state == ParsingState.EXTRACTING_KEY) { // Keep going until we find an equals character. if (c == '=') { // We found the end of the key. Mark it. extractKeyEndIndex = i; // Switch state to looking for the value. state = ParsingState.EXPECT_VALUE_START_CHAR; } } else if (state == ParsingState.EXPECT_VALUE_START_CHAR) { // We expect this to be a quotes character. if (c == '\"') { // Found the quotes. The next character will be the first character of the value. extractValueStartIndex = i + 1; // Switch state to extracting value. state = ParsingState.EXTRACTING_VALUE; } else { throw new IllegalStateException( "Span parsing error: Expected but did not find quotes '\"' character for value-start at " + "index " + i ); } } else if (state == ParsingState.EXTRACTING_VALUE) { if (isUnescapedQuotes(c, numPrecedingBackslashes)) { // We found the end of the value (a non-escaped quotes). Extract key and value, and put them in // the appropriate map. String key = keyValueStr.substring(extractKeyStartIndex, extractKeyEndIndex); String value = keyValueStr.substring(extractValueStartIndex, i); value = unescapeJson(value); boolean isTag = key.startsWith(KEY_VALUE_TAG_PREFIX); boolean isAnnotation = false; if (isTag) { String keyNoTagPrefix = key.substring(KEY_VALUE_TAG_PREFIX.length()); key = unescapeTagKeyForKeyValueFormatDeserialization(keyNoTagPrefix); } else { isAnnotation = key.startsWith(KEY_VALUE_TIMESTAMPED_ANNOTATION_PREFIX); if (isAnnotation) { key = key.substring(KEY_VALUE_TIMESTAMPED_ANNOTATION_PREFIX.length()); } } if (isTag) { tagsMap.put(key, value); } else if (isAnnotation) { long timestamp = Long.parseLong(key); annotationsList.add(TimestampedAnnotation.forEpochMicros(timestamp, value)); } else { spanFieldsMap.put(key, value); } // Switch state to looking for the next key. state = ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY; } } else if (state == ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY) { // We expect this to be a comma character. if (c == ',') { // Found the comma. The next character should be the first character of the key. extractKeyStartIndex = i + 1; // Switch state to extracting the key. state = ParsingState.EXTRACTING_KEY; } else { throw new IllegalStateException( "Span parsing error: Expected but did not find comma ',' character after value-end at " + "index " + i ); } } else { // Should never happen. throw new IllegalStateException("Unhandled state: " + state); } if (c == '\\') { numPrecedingBackslashes++; } else { numPrecedingBackslashes = 0; } i++; } if (state != ParsingState.EXPECT_COMMA_BEFORE_NEW_KEY) { throw new IllegalStateException( "Span parsing error: key/value string did not end after finding a value. Instead ended in state: " + state.name() ); } return fromKeyValueMap(spanFieldsMap, tagsMap, annotationsList); } catch (Exception e) { logger.error("Error extracting Span from key/value string. Defaulting to null. bad_span_key_value_string={}", keyValueStr, e); return null; } } protected static Span fromKeyValueMap( Map map, Map tags, List annotations ) { // Use the map to get the field values for the span. String traceId = nullSafeGetString(map, TRACE_ID_FIELD); String spanId = nullSafeGetString(map, SPAN_ID_FIELD); String parentSpanId = nullSafeGetString(map, PARENT_SPAN_ID_FIELD); String spanName = nullSafeGetString(map, SPAN_NAME_FIELD); Boolean sampleable = nullSafeGetBoolean(map, SAMPLEABLE_FIELD); if (sampleable == null) { throw new IllegalStateException("Unable to parse " + SAMPLEABLE_FIELD + " from serialized Span"); } String userId = nullSafeGetString(map, USER_ID_FIELD); Long startTimeEpochMicros = nullSafeGetLong(map, START_TIME_EPOCH_MICROS_FIELD); if (startTimeEpochMicros == null) { throw new IllegalStateException( "Unable to parse " + START_TIME_EPOCH_MICROS_FIELD + " from serialized Span" ); } Long durationNanos = nullSafeGetLong(map, DURATION_NANOS_FIELD); SpanPurpose spanPurpose = nullSafeGetSpanPurpose(map, SPAN_PURPOSE_FIELD); return new Span( traceId, parentSpanId, spanId, spanName, sampleable, userId, spanPurpose, startTimeEpochMicros, null, durationNanos, tags, annotations ); } protected static String nullSafeGetString(Map map, String key) { String value = map.get(key); if (value == null || value.equals("null")) return null; return value; } protected static Long nullSafeGetLong(Map map, String key) { String value = nullSafeGetString(map, key); if (value == null) return null; return Long.parseLong(value); } @SuppressWarnings("SameParameterValue") protected static Boolean nullSafeGetBoolean(Map map, String key) { String value = nullSafeGetString(map, key); if (value == null) return null; return Boolean.parseBoolean(value); } @SuppressWarnings("SameParameterValue") protected static SpanPurpose nullSafeGetSpanPurpose(Map map, String key) { String value = nullSafeGetString(map, key); if (value == null) return null; try { return SpanPurpose.valueOf(value); } catch(Exception ex) { logger.warn("Unable to parse \"{}\" to a SpanPurpose enum. Received exception: {}", value, ex.toString()); return null; } } /** * Escapes the given String using minimal JSON rules. See * RFC 7159 Section 7 for full details, but in * particular note that we are only escaping the bare minimum required characters: quotation mark, reverse solidus * (backslash), and the control characters (U+0000 through U+001F). * * @param orig The original string that needs JSON escaping. * @return The given original string, with quotation marks, backslashes, and control characters escaped so that it * is ready for use as a JSON string. */ public static String escapeJson(String orig) { if (orig == null) { return null; } StringBuilder sb = null; for (int i = 0; i < orig.length(); i++) { char nextChar = orig.charAt(i); if (needsJsonEscaping(nextChar)) { // Initialize the StringBuilder if necessary if (sb == null) { // Create it to be the original string's size, plus a few extra to accommodate escape characters. sb = new StringBuilder(orig.length() + 16); // Add everything from the beginning of the string up to, but not including, the current character. sb.append(orig, 0, i); } // Get the escaped string value associated with the given char, and append it. String escapedValue = getJsonEscapedValueForChar(nextChar); if (escapedValue == null) { logger.warn( "Somehow found a character that needs escaping, but getJsonEscapedValueForChar(char) " + "came back null. This shouldn't happen! char_int_value={}", (int)nextChar ); // Shouldn't ever reach here, but if we do, just append the character as-is. sb.append(nextChar); } else { sb.append(escapedValue); } } else if (sb != null) { // Not a char that needs escaping, but we have been forced to build an escaped string, so append // this char as-is. sb.append(nextChar); } } // If sb is null then no chars needed escaping, and we can just return the original string as-is. if (sb == null) { return orig; } return sb.toString(); } /** * Unescapes the given String using minimal JSON rules. See * RFC 7159 Section 7 for full details, but in * particular note that we are only unescaping the bare minimum required characters: quotation mark, reverse solidus * (backslash), and the control characters (U+0000 through U+001F). * *

This effectively reverses the escaping done by {@link #escapeJson(String)}. * *

NOTE: This uses {@link String#replace(CharSequence, CharSequence)} to do its work, therefore it is pretty * slow, and should not be used for general-purpose JSON unescaping. We do an optimistic check for backslash first, * though, and if no backslash is found then we immediately return without doing any of the unescape logic. So for * the common/normal case related to {@link Span} stuff this is extremely fast (it's unusual to need unescaping for * anything in a normal {@link Span}). But if your {@link Span} does have data that needs unescaping, this method * isn't particularly great. * *

TODO: Maybe someday find a more efficient way to unescape? Might never be necessary - it's unlikely this * would be used at runtime in a way that requires blistering speed. * * @param escaped The escaped string that needs JSON unescaping. * @return The given original string, with quotation marks, backslashes, and control characters escaped so that it * is ready for use as a JSON string. */ public static String unescapeJson(String escaped) { if (escaped == null) { return null; } // Most strings we run across won't actually need unescaping, so look for that optimistic case first as // it would save a bunch of time. if (!containsChar(escaped, '\\')) { return escaped; } // There's a backslash, so we need to unescape. We'll do it the slow-but-safe way of calling String.replace(...) // for every escaped mapping to turn it back into the original char. String unescaped = escaped; for (int i = 0; i < JSON_ESCAPE_CHAR_MAPPINGS.length; i++) { String escapedString = JSON_ESCAPE_CHAR_MAPPINGS[i]; if (escapedString != null) { String unescapedCharAsString = String.valueOf((char) i); unescaped = unescaped.replace(escapedString, unescapedCharAsString); } } return unescaped; } protected static boolean containsChar(String str, char c) { for (int i = 0; i < str.length(); i++) { if (str.charAt(i) == c) { return true; } } return false; } /** * This JSON_ESCAPE_CHAR_MAPPINGS array was inspired by the way Jackson does escaping - see Jackson's * {@code CharType} class. You can index into the array using the character in question, and the result you'll * get back is the escaped-string that should be used in place of that character when escaping for JSON, or null * if the character should not be escaped. */ protected static final String[] JSON_ESCAPE_CHAR_MAPPINGS; protected static final String ESCAPED_EQUALS_SIGN = "\\u003D"; protected static final String ESCAPED_SPACE_CHAR = "\\u0020"; protected static final String ESCAPED_COMMA_CHAR = "\\u002C"; static { String[] jsonEscapeCharMappings = new String[128]; // Add the control chars mappings using full unicode representation. char firstControlChar = '\u0000'; char lastControlChar = '\u001F'; for (int i = firstControlChar; i <= lastControlChar; i++) { // Generate the hex representation of the int value for the char (uppercased). String hex = Integer.toHexString(i).toUpperCase(); // Build the unicode string to represent the char. Start with the backslash-u. StringBuilder sb = new StringBuilder(6); sb.append("\\u"); // Pad with prepending zeros if necessary so that the hex representation is 4 characters long. int numPaddingNeeded = Math.max(0, (4 - hex.length())); for (int pad = 0; pad < numPaddingNeeded; pad++) { sb.append("0"); } // Add the hex value. sb.append(hex); // Set the char->escaped mapping. jsonEscapeCharMappings[i] = sb.toString(); } // Some of the JSON escapes have short versions that can be used in place of the full unicode representation. // Set the mappings for those shortened escapes. Note that forward slash is an optional escape, and we // don't do it. We only escape strictly-required characters. jsonEscapeCharMappings['"'] = "\\\""; jsonEscapeCharMappings['\\'] = "\\\\"; jsonEscapeCharMappings['\b'] = "\\b"; jsonEscapeCharMappings['\t'] = "\\t"; jsonEscapeCharMappings['\f'] = "\\f"; jsonEscapeCharMappings['\n'] = "\\n"; jsonEscapeCharMappings['\r'] = "\\r"; JSON_ESCAPE_CHAR_MAPPINGS = jsonEscapeCharMappings; } protected static String getJsonEscapedValueForChar(char c) { if ((int)c >= JSON_ESCAPE_CHAR_MAPPINGS.length) { // No need to escape. return null; } return JSON_ESCAPE_CHAR_MAPPINGS[c]; } protected static boolean needsJsonEscaping(char nextChar) { return (nextChar == '"') || (nextChar == '\\') || (nextChar < 32); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy