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

com.nike.wingtips.zipkin2.util.WingtipsToZipkinSpanConverterDefaultImpl Maven / Gradle / Ivy

package com.nike.wingtips.zipkin2.util;

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

import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import zipkin2.Endpoint;

/**
 * Default implementation of {@link WingtipsToZipkinSpanConverter} that knows how to convert a Wingtips span to a
 * Zipkin span.
 *
 * 

NOTE: Although Wingtips encourages conforming to the * Zipkin B3 specification for IDs (16 character/64 bit * lowercase hexadecimal values, with an option for 32 char/128 bit lowerhex for trace IDs), Wingtips itself treats * IDs as strings - you can pass any string for IDs and Wingtips will happily handle it. There's no enforcement of * any specific format in Wingtips. This works ok as long as you don't need to integrate with another system that * has more strict requirements on ID formats, i.e. when you want to send Wingtips span data to Zipkin. But if you do * need to integrate with Zipkin for visualization of your traces, *and* you can't force your callers to use the proper * ID format, then you'd be in trouble. This class handles this situation by giving you an option to "sanitize" IDs * if necessary. You can enable sanitization by using the alternate {@link * WingtipsToZipkinSpanConverterDefaultImpl#WingtipsToZipkinSpanConverterDefaultImpl(boolean)} constructor and passing * true. If you enable sanitization and this class sees a badly formatted ID, then it will convert it to the proper * lowerhex format, add a {@code invalid.[trace/span/parent]_id} tag with a value of the original ID to the Zipkin span, * and add a {@code sanitized_[trace/span/parent]_id} tag with the sanitized ID value to the Wingtips span. The * sanitization is done in a deterministic way so that the same original ID input will always be sanitized into the same * output. * * @author Nic Munroe */ @SuppressWarnings("WeakerAccess") public class WingtipsToZipkinSpanConverterDefaultImpl implements WingtipsToZipkinSpanConverter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); protected final boolean enableIdSanitization; public WingtipsToZipkinSpanConverterDefaultImpl() { // Disable ID sanitization by default - this should be something that users consciously opt-in for. this(false); } public WingtipsToZipkinSpanConverterDefaultImpl(boolean enableIdSanitization) { this.enableIdSanitization = enableIdSanitization; } @Override public zipkin2.Span convertWingtipsSpanToZipkinSpan(Span wingtipsSpan, Endpoint zipkinEndpoint) { long durationMicros = TimeUnit.NANOSECONDS.toMicros(wingtipsSpan.getDurationNanos()); String spanId = sanitizeIdIfNecessary(wingtipsSpan.getSpanId(), false); String traceId = sanitizeIdIfNecessary(wingtipsSpan.getTraceId(), true); String parentId = sanitizeIdIfNecessary(wingtipsSpan.getParentSpanId(), false); final zipkin2.Span.Builder spanBuilder = zipkin2.Span .newBuilder() .id(spanId) .name(wingtipsSpan.getSpanName()) .parentId(parentId) .traceId(traceId) .timestamp(wingtipsSpan.getSpanStartTimeEpochMicros()) .duration(durationMicros) .localEndpoint(zipkinEndpoint) .kind(determineZipkinKind(wingtipsSpan)); // Iterate over existing wingtips tags and add them to the zipkin builder. for (Map.Entry tagEntry : wingtipsSpan.getTags().entrySet()) { spanBuilder.putTag(tagEntry.getKey(), tagEntry.getValue()); } if (!spanId.equals(wingtipsSpan.getSpanId())) { spanBuilder.putTag("invalid.span_id", wingtipsSpan.getSpanId()); wingtipsSpan.putTag("sanitized_span_id", spanId); } if (!traceId.equals(wingtipsSpan.getTraceId())) { spanBuilder.putTag("invalid.trace_id", wingtipsSpan.getTraceId()); wingtipsSpan.putTag("sanitized_trace_id", traceId); } if (parentId != null && !parentId.equals(wingtipsSpan.getParentSpanId())) { spanBuilder.putTag("invalid.parent_id", wingtipsSpan.getParentSpanId()); wingtipsSpan.putTag("sanitized_parent_id", parentId); } // Iterate over existing wingtips annotations and add them to the zipkin builder. for (TimestampedAnnotation wingtipsAnnotation : wingtipsSpan.getTimestampedAnnotations()) { spanBuilder.addAnnotation(wingtipsAnnotation.getTimestampEpochMicros(), wingtipsAnnotation.getValue()); } return spanBuilder.build(); } protected zipkin2.Span.Kind determineZipkinKind(Span wingtipsSpan) { SpanPurpose wtsp = wingtipsSpan.getSpanPurpose(); // Clunky if checks necessary to avoid code coverage gaps with a switch statement // due to unreachable default case. :( if (SpanPurpose.SERVER == wtsp) { return zipkin2.Span.Kind.SERVER; } else if (SpanPurpose.CLIENT == wtsp) { return zipkin2.Span.Kind.CLIENT; } else if (SpanPurpose.LOCAL_ONLY == wtsp || SpanPurpose.UNKNOWN == wtsp) { // No Zipkin Kind associated with these SpanPurposes. return null; } else { // This case should technically be impossible, but in case it happens we'll log a warning and default to // no Zipkin kind. logger.warn("Unhandled SpanPurpose type: {}", wtsp); return null; } } protected String sanitizeIdIfNecessary(final String originalId, final boolean allow128Bit) { if (!enableIdSanitization) { return originalId; } if (originalId == null) { return null; } if (isAllowedNumChars(originalId, allow128Bit)) { if (isLowerHex(originalId)) { // Already lowerhex with correct number of chars, no modifications needed. return originalId; } else if (isHex(originalId, true)) { // It wasn't lowerhex, but it is hex and it is the correct number of chars. // We can trivially convert to valid lowerhex by lowercasing the ID. return originalId.toLowerCase(); } } // If the originalId can be parsed as a long, then its sanitized ID is the lowerhex representation of that long. Long originalIdAsRawLong = attemptToConvertToLong(originalId); if (originalIdAsRawLong != null) { return TraceAndSpanIdGenerator.longToUnsignedLowerHexString(originalIdAsRawLong); } // If the originalId can be parsed as a UUID and is allowed to be 128 bit, // then its sanitized ID is that UUID with the dashes ripped out and forced lowercase. if (allow128Bit) { String sanitizedId = attemptToSanitizeAsUuid(originalId); if (sanitizedId != null) { return sanitizedId; } } // No convenient/sensible conversion to a valid lowerhex ID was found. // Do a SHA256 hash of the original ID to get a (deterministic) valid sanitized lowerhex ID that can be // converted to a long, but only take the number of characters we're allowed to take. Truncation // of a SHA digest like this is specifically allowed by the SHA algorithm - see Section 7 // ("TRUNCATION OF A MESSAGE DIGEST") here: // https://csrc.nist.gov/csrc/media/publications/fips/180/4/final/documents/fips180-4-draft-aug2014.pdf int allowedNumChars = allow128Bit ? 32 : 16; return DigestUtils.sha256Hex(originalId).toLowerCase().substring(0, allowedNumChars); } protected boolean isLowerHex(String id) { return isHex(id, false); } /** * Copied from {@link zipkin2.Span#validateHex(String)} and slightly modified. * * @param id The ID to check for hexadecimal conformity. * @param allowUppercase Pass true to allow uppercase A-F letters, false to force lowercase-hexadecimal check * (only a-f letters allowed). * * @return true if the given id is hexadecimal, false if there are any characters that are not hexadecimal, with * the {@code allowUppercase} parameter determining whether uppercase hex characters are allowed. */ protected boolean isHex(String id, boolean allowUppercase) { for (int i = 0, length = id.length(); i < length; i++) { char c = id.charAt(i); if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { // Not 0-9, and not a-f. So it's not lowerhex. If we don't allow uppercase then we can return false. if (!allowUppercase) { return false; } else if (c < 'A' || c > 'F') { // Uppercase is allowed but it's not A-F either, so we still have to return false. return false; } // If we reach here inside this if-block, then it's an uppercase A-F and allowUppercase is true, so // do nothing and move onto the next character. } } return true; } protected boolean isAllowedNumChars(final String id, final boolean allow128Bit) { if (allow128Bit) { return id.length() <= 16 || id.length() == 32; } else { return id.length() <= 16; } } private static final char[] MAX_LONG_AS_CHAR_ARRY = String.valueOf(Long.MAX_VALUE).toCharArray(); private static final int MIN_OR_MAX_LONG_NUM_DIGITS = MAX_LONG_AS_CHAR_ARRY.length; private static final char[] ABS_MIN_LONG_AS_CHAR_ARRY = String.valueOf(Long.MIN_VALUE).substring(1).toCharArray(); static { // Sanity check that MAX_LONG_AS_CHAR_ARRY and ABS_MIN_LONG_AS_CHAR_ARRY have the same number of chars. assert MAX_LONG_AS_CHAR_ARRY.length == ABS_MIN_LONG_AS_CHAR_ARRY.length; } protected Long attemptToConvertToLong(final String id) { if (id == null) { return null; } // Only try if all chars in the ID are digits (the first char is allowed to be a dash to indicate negative). int numDigits = 0; boolean firstCharIsDash = false; for (int i = 0; i < id.length(); i++) { char nextChar = id.charAt(i); boolean isDigit = (nextChar >= '0' && nextChar <= '9'); if (isDigit) { numDigits++; } else { // The first char is allowed to be a dash. if (i == 0 && nextChar == '-') { // This is allowed. firstCharIsDash = true; } else { // Not a digit, and not a first-char-dash. So id cannot be a long. Return null. return null; } } } // All chars are digits (or negative sign followed by digits). Next we need to make sure they are in the // valid range of a java long. if (!isWithinRangeOfJavaLongMinAndMax(id, numDigits, firstCharIsDash)) { // Too many digits, or the value was too high. Can't be converted to java long, so return null. return null; } try { // This *should* be convertible to a java long at this point. return Long.parseLong(id); } catch (final NumberFormatException nfe) { // This should never happen, but if it does, return null as it can't be converted to a long. logger.warn( "Found digits-based-ID that reached Long.parseLong(id) and failed. " + "This should never happen - please report this to the Wingtips maintainers. " + "invalid_id={}, NumberFormatException={}", id, nfe.toString() ); return null; } } protected boolean isWithinRangeOfJavaLongMinAndMax(String longAsString, int numDigits, boolean firstCharIsDash) { if (numDigits < MIN_OR_MAX_LONG_NUM_DIGITS) { // Fewer number of digits than max java long, so it is in the valid min/max java long range. return true; } if (numDigits > MIN_OR_MAX_LONG_NUM_DIGITS) { // Too many digits, so it is outside the valid min/max java long range. return false; } // At this point, we know the ID has the same number of digits as the max java long value. The ID may or may // not be within the range of a java long. We could use a BigInteger to compare, but that's too slow. // So we'll root through digit by digit. // Adjust the string we're checking to get rid of the negative sign (dash) if necessary. String absLongAsString = (firstCharIsDash) ? longAsString.substring(1) : longAsString; // Choose the correct min/max value to compare against. char[] comparisonValue = (firstCharIsDash) ? ABS_MIN_LONG_AS_CHAR_ARRY : MAX_LONG_AS_CHAR_ARRY; for (int i = 0; i < comparisonValue.length; i++) { char nextCharAtIndex = absLongAsString.charAt(i); char maxLongCharAtIndex = comparisonValue[i]; if (nextCharAtIndex > maxLongCharAtIndex) { // Up to this index, the values were equal, but *at* this index the value is greater than max java long, // so it's outside the java long range. return false; } else if (nextCharAtIndex < maxLongCharAtIndex) { // Up to this index, the values were equal, but *at* this index the value is less than max java long, // so it's within the java long range. return true; } // If neither of the comparisons above were triggered, then this char matches the max long char. Move on // to the next char. } // If we reach here, then longAsString has the same number of digits as max java long, and all digits were // equal. So this is exactly min or max java long, and therefore within range. return true; } protected String attemptToSanitizeAsUuid(String originalId) { if (originalId == null) { return null; } if (originalId.length() == 36) { // 36 chars - might be a UUID. Rip out the dashes and check to see if it's a valid 128 bit ID. String noDashesAndLowercase = stripDashesAndConvertToLowercase(originalId); if (noDashesAndLowercase.length() == 32 && isLowerHex(noDashesAndLowercase)) { // 32 chars and lowerhex - it's now a valid 128 bit ID. return noDashesAndLowercase; } } // It wasn't a UUID, so return null. return null; } protected String stripDashesAndConvertToLowercase(String orig) { if (orig == null) { return null; } StringBuilder sb = new StringBuilder(orig.length()); for (int i = 0; i < orig.length(); i++) { char nextChar = orig.charAt(i); if (nextChar != '-') { sb.append(Character.toLowerCase(nextChar)); } } return sb.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy