diff --git a/NOTICE.txt b/NOTICE.txt index 729bbcf0..6d3144cb 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -29,6 +29,15 @@ The logo used by this project was created by Greg Reinmuth: * license/LICENSE.wingtips_logo.txt (Creative Commons Attribution-ShareAlike 4.0 International) +This product contains a modified portion of 'Zipkin', a Java Distributed Tracing +implementation, which can be obtained at: + + * LICENSE: + * license/LICENSE.zipkin.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/openzipkin/zipkin/ + + This product contains a modified portion of 'brave', a Java Distributed Tracing implementation compatible with Zipkin, which can be obtained at: diff --git a/README.md b/README.md index ca5169c4..366863f2 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ You can use the sub-span ability to create parent-child span relationships withi The pattern for passing traces across network or application boundaries is that the calling service includes its "current span" information when calling the downstream service. The downstream service uses that information to generate its overall request span with the caller's span info as its parent span. This causes the downstream service's request span to contain the same Trace ID and sets up the correct parent-child relationship between the spans. -For HTTP requests it is assumed that you will pass the caller's span information to the downstream system using the request headers defined in `TraceHeaders`. All headers defined in that class should be included in the downstream request, as well as any application specific user ID header if you want to take advantage of the optional user ID functionality of spans. +For HTTP requests it is assumed that you will pass the caller's span information to the downstream system using the request headers defined in `TraceHeaders`. All headers defined in that class should be included in the downstream request, as well as any application specific user ID header if you want to take advantage of the optional user ID functionality of spans. Note that to be [properly B3 compatible](http://zipkin.io/pages/instrumenting.html) you should send the `X-B3-Sampled` header value as `"0"` for `false` and `"1"` for `true`. All the other header values should be correct if you send the appropriate `Span` property as-is, e.g. send `Span.getTraceId()` for the `X-B3-TraceId` header as it should already be in the correct B3 format. ### Adjusting Behavior and Execution Options diff --git a/license/LICENSE.zipkin.txt b/license/LICENSE.zipkin.txt new file mode 100644 index 00000000..8f71f43f --- /dev/null +++ b/license/LICENSE.zipkin.txt @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/wingtips-core/src/main/java/com/nike/wingtips/Span.java b/wingtips-core/src/main/java/com/nike/wingtips/Span.java index f3c83de3..6de35ebd 100644 --- a/wingtips-core/src/main/java/com/nike/wingtips/Span.java +++ b/wingtips-core/src/main/java/com/nike/wingtips/Span.java @@ -211,7 +211,8 @@ public static Builder newBuilder(Span copy) { /** * @return The ID associated with the overall distributed trace - a.k.a. the trace tree ID. All spans in a distributed trace will share the same trace ID. * Don't confuse this with {@link #getSpanId()}, which is the ID for an individual span of work as part of the larger distributed trace. This will never - * be null. + * be null. NOTE: By convention this will likely be a 16 character lowercase hex-encoded 64-bit long-integer value + * (see {@link TraceAndSpanIdGenerator#generateId()} for details). */ public String getTraceId() { return traceId; @@ -220,7 +221,8 @@ public String getTraceId() { /** * @return The ID for this span of work in the distributed trace. Don't confuse this with the {@link #getTraceId()}, which is the ID associated with the overall * distributed trace and is the same for all spans in a trace. Also don't confuse this with {@link #getParentSpanId()}, which is the ID of the span that spawned - * this span instance (the logical "parent" of this span). This will never be null. + * this span instance (the logical "parent" of this span). This will never be null. NOTE: By convention this will likely be a 16 character + * lowercase hex-encoded 64-bit long-integer value (see {@link TraceAndSpanIdGenerator#generateId()} for details). */ public String getSpanId() { return spanId; @@ -229,6 +231,8 @@ public String getSpanId() { /** * @return The ID of the span that spawned this span instance (the logical "parent" of this span), or null if no such parent exists. If this returns null then this * span is the "root span" for the distributed trace - the ultimate ancestor of all other spans in the trace tree. + * NOTE: By convention this will likely be a 16 character lowercase hex-encoded 64-bit long-integer value + * (see {@link TraceAndSpanIdGenerator#generateId()} for details). */ public String getParentSpanId() { return parentSpanId; diff --git a/wingtips-core/src/main/java/com/nike/wingtips/TraceAndSpanIdGenerator.java b/wingtips-core/src/main/java/com/nike/wingtips/TraceAndSpanIdGenerator.java index 6d89fd27..1b5cc291 100644 --- a/wingtips-core/src/main/java/com/nike/wingtips/TraceAndSpanIdGenerator.java +++ b/wingtips-core/src/main/java/com/nike/wingtips/TraceAndSpanIdGenerator.java @@ -8,9 +8,13 @@ import java.util.Random; /** - * ID generation class for use with trace IDs, span IDs, and parent span IDs for the {@link Span} class. Call the static {@link #generateId()} method whenever you need to create - * a new ID. These IDs are returned as strings but they can be parsed by {@link Long#parseLong(String)} into long values. We use longs to conform to the Google Dapper paper and - * Twitter ZipKin distributed tracing implementation. + * ID generation class for use with trace IDs, span IDs, and parent span IDs for the {@link Span} class. Call the static {@link #generateId()} method whenever you + * need to create a new ID. These IDs are fundamentally 64-bit longs, but they are returned by {@link #generateId()} encoded as unsigned and in hexadecimal format. + * To turn those strings back into Java long primitives you can call {@link #unsignedLowerHexStringToLong(String)}. We use 64-bit longs to conform to the + * Google Dapper paper + * (see http://static.googleusercontent.com/media/research.google.com/en/us/pubs/archive/36356.pdf) + * and unsigned hex encoding to conform to the ZipKin distributed tracing B3 implementation + * (see http://zipkin.io/pages/instrumenting.html). * * @author Nic Munroe */ @@ -34,15 +38,47 @@ private TraceAndSpanIdGenerator() { } /** - * @return A newly-generated random 64-bit long as a String. + * @return A newly-generated random 64-bit long encoded as a String UNSIGNED AND IN HEX FORMAT - intended for use as a trace or span ID. + * The returned string will have a length of 16 characters (zeroes will be prepended as padding if necessary to + * reach a string length of 16). The hex format is necessary to be fully B3 compatible and therefore fully Zipkin compatible + * (see http://zipkin.io/pages/instrumenting.html). If you simply want a full 64-bit + * random normal *signed* Java long encoded in *decimal* format for other reasons then you can call {@link String#valueOf(long)} and pass in a + * long created by {@link #generate64BitRandomLong()}. + *

You can convert the unsigned hex-encoded longs returned by this method back into normal Java long primitives by passing them to + * {@link #unsignedLowerHexStringToLong(String)}. */ public static String generateId() { + return longToUnsignedLowerHexString(generate64BitRandomLong()); + } + + /** + * @return A random long pulled from the full 64-bit random search space (as opposed to the 48 bits of randomness you get from + * {@link java.util.Random#nextLong()}). + */ + public static long generate64BitRandomLong() { byte[] random8Bytes = new byte[8]; random.nextBytes(random8Bytes); - long randomLong = convertBytesToLong(random8Bytes); + return convertBytesToLong(random8Bytes); + } + + /** + * @param primitiveLong The long value that should be converted to an unsigned long encoded in a hexadecimal string. + * @return The given long value converted to an unsigned hex encoded string of length 16 (zeroes will be prepended as padding if necessary to + * reach a string length of 16). You can convert back to a Java long primitive by passing the result into + * {@link #unsignedLowerHexStringToLong(String)}. + */ + public static String longToUnsignedLowerHexString(long primitiveLong) { + return ZipkinHexHelpers.toLowerHex(primitiveLong); + } - return String.valueOf(randomLong); + /** + * @param hexString The lowercase hexadecimal string representing an unsigned 64-bit long that you want to convert to a Java long primitive. + * @return The Java long primitive represented by the given lowercase hex string. If the string isn't lowercase hexadecimal encoded or if + * the resulting long wouldn't fit inside 64 bits then a {@link NumberFormatException} will be thrown. + */ + public static long unsignedLowerHexStringToLong(String hexString) { + return ZipkinHexHelpers.lowerHexToUnsignedLong(hexString); } /** @@ -77,4 +113,70 @@ protected static Random getRandomInstance(String desiredSecureRandomImplementati return randomToUse; } + + /** + *

+ * The code in this class came from the Zipkin repository v1.7.0 + * (https://github.com/openzipkin/zipkin/blob/master/zipkin/src/main/java/zipkin/internal/Util.java) + * and licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0). + *

+ */ + protected static class ZipkinHexHelpers { + + private ZipkinHexHelpers() { + // Do nothing + } + + /** Parses a 1 to 16 character lower-hex string with no prefix int an unsigned long. */ + static long lowerHexToUnsignedLong(String lowerHex) { + char[] array = lowerHex.toCharArray(); + if (array.length < 1 || array.length > 16) { + throw isntLowerHexLong(lowerHex); + } + long result = 0; + for (char c : array) { + result <<= 4; + if (c >= '0' && c <= '9') { + result |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + result |= c - 'a' + 10; + } else { + throw isntLowerHexLong(lowerHex); + } + } + return result; + } + + static NumberFormatException isntLowerHexLong(String lowerHex) { + return new NumberFormatException( + lowerHex + " should be a 1 to 16 character lower-hex string with no prefix"); + } + + /** Inspired by {@code okio.Buffer.writeLong} */ + static String toLowerHex(long v) { + char[] data = new char[16]; + writeHexLong(data, 0, v); + return new String(data); + } + + /** Inspired by {@code okio.Buffer.writeLong} */ + static void writeHexLong(char[] data, int pos, long v) { + writeHexByte(data, pos + 0, (byte) ((v >>> 56L) & 0xff)); + writeHexByte(data, pos + 2, (byte) ((v >>> 48L) & 0xff)); + writeHexByte(data, pos + 4, (byte) ((v >>> 40L) & 0xff)); + writeHexByte(data, pos + 6, (byte) ((v >>> 32L) & 0xff)); + writeHexByte(data, pos + 8, (byte) ((v >>> 24L) & 0xff)); + writeHexByte(data, pos + 10, (byte) ((v >>> 16L) & 0xff)); + writeHexByte(data, pos + 12, (byte) ((v >>> 8L) & 0xff)); + writeHexByte(data, pos + 14, (byte) (v & 0xff)); + } + + static final char[] HEX_DIGITS = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + static void writeHexByte(char[] data, int pos, byte b) { + data[pos + 0] = HEX_DIGITS[(b >> 4) & 0xf]; + data[pos + 1] = HEX_DIGITS[b & 0xf]; + } + } } diff --git a/wingtips-core/src/main/java/com/nike/wingtips/TraceHeaders.java b/wingtips-core/src/main/java/com/nike/wingtips/TraceHeaders.java index 520bfab5..a3ace8cd 100644 --- a/wingtips-core/src/main/java/com/nike/wingtips/TraceHeaders.java +++ b/wingtips-core/src/main/java/com/nike/wingtips/TraceHeaders.java @@ -8,17 +8,20 @@ public interface TraceHeaders { /** - * The root id of the distributed trace. + * The root id of the distributed trace. For Zipkin/B3 compatibility this header value should be an unsigned 64 bit long-integer encoded in lowercase hex + * (see {@link TraceAndSpanIdGenerator#generateId()} for details). */ String TRACE_ID = "X-B3-TraceId"; /** - * The span id of the current span. + * The span id of the current span. For Zipkin/B3 compatibility this header value should be an unsigned 64 bit long-integer encoded in lowercase hex + * (see {@link TraceAndSpanIdGenerator#generateId()} for details). */ String SPAN_ID = "X-B3-SpanId"; /** - * The span id of the parent span. + * The span id of the parent span. For Zipkin/B3 compatibility this header value should be an unsigned 64 bit long-integer encoded in lowercase hex + * (see {@link TraceAndSpanIdGenerator#generateId()} for details). */ String PARENT_SPAN_ID = "X-B3-ParentSpanId"; @@ -28,7 +31,9 @@ public interface TraceHeaders { String SPAN_NAME = "X-B3-SpanName"; /** - * Indicates if the trace's spans should be sampled or not. + * Indicates if the trace's spans should be sampled or not. For Zipkin/B3 compatibility this header value should be set to "0" for false and "1" + * for true when making calls, although it's advised that servers receiving calls should be more lenient and check if the caller sent "true" or + * "false" values and honor those as well. */ String TRACE_SAMPLED = "X-B3-Sampled"; diff --git a/wingtips-core/src/main/java/com/nike/wingtips/http/HttpRequestTracingUtils.java b/wingtips-core/src/main/java/com/nike/wingtips/http/HttpRequestTracingUtils.java index a49dd4c3..12d655fe 100644 --- a/wingtips-core/src/main/java/com/nike/wingtips/http/HttpRequestTracingUtils.java +++ b/wingtips-core/src/main/java/com/nike/wingtips/http/HttpRequestTracingUtils.java @@ -99,15 +99,16 @@ public static String getUserIdFromRequestWithHeaders(RequestWithHeaders request, /** * Extracts the {@link TraceHeaders#TRACE_SAMPLED} boolean value from the given request's headers or attributes if available, and defaults to true if the request doesn't contain - * that header/attribute. + * that header/attribute or if it's an invalid value. In other words, request values of "0" or "false" (ignoring case) will return false from this method, + * everything else will return true. */ protected static boolean getSpanSampleableFlag(RequestWithHeaders request) { String spanSampleableHeaderStr = getHeaderWithAttributeAsBackup(request, TraceHeaders.TRACE_SAMPLED); // Default to true (enabling trace sampling for requests that don't explicitly exclude it) boolean result = true; - if (spanSampleableHeaderStr != null && spanSampleableHeaderStr.length() > 0) - result = Boolean.parseBoolean(spanSampleableHeaderStr); + if ("0".equals(spanSampleableHeaderStr) || "false".equalsIgnoreCase(spanSampleableHeaderStr)) + result = false; return result; } diff --git a/wingtips-core/src/test/java/com/nike/wingtips/TraceAndSpanIdGeneratorTest.java b/wingtips-core/src/test/java/com/nike/wingtips/TraceAndSpanIdGeneratorTest.java index 86ec6ad7..b50da952 100644 --- a/wingtips-core/src/test/java/com/nike/wingtips/TraceAndSpanIdGeneratorTest.java +++ b/wingtips-core/src/test/java/com/nike/wingtips/TraceAndSpanIdGeneratorTest.java @@ -1,9 +1,15 @@ package com.nike.wingtips; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; + +import org.assertj.core.api.ThrowableAssert; import org.junit.Test; +import org.junit.runner.RunWith; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.Provider; import java.security.SecureRandom; @@ -15,11 +21,13 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assertions.fail; /** * Tests the functionality of {@link TraceAndSpanIdGenerator} */ +@RunWith(DataProviderRunner.class) public class TraceAndSpanIdGeneratorTest { @Test @@ -42,6 +50,26 @@ public void constructor_is_private() throws NoSuchMethodException, IllegalAccess assertThat(instance).isNotNull(); } + @Test + public void constructor_for_ZipkinHexHelpers_is_private() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Constructor defaultConstructor = TraceAndSpanIdGenerator.ZipkinHexHelpers.class.getDeclaredConstructor(); + Exception caughtException = null; + try { + defaultConstructor.newInstance(); + } + catch (Exception ex) { + caughtException = ex; + } + + assertThat(caughtException).isNotNull(); + assertThat(caughtException).isInstanceOf(IllegalAccessException.class); + + // Set the constructor to accessible and create one. Why? Code coverage. :p + defaultConstructor.setAccessible(true); + TraceAndSpanIdGenerator.ZipkinHexHelpers instance = defaultConstructor.newInstance(); + assertThat(instance).isNotNull(); + } + @Test public void getRandomInstance_should_return_requested_instance_if_available() { // given: all available SecureRandom algorithm providers @@ -118,15 +146,31 @@ public void convertBytesToLong_should_explode_if_byte_array_is_more_than_8_bytes } @Test - public void generateId_should_return_string_that_can_be_parsed_into_a_long() { - // given: String ID value generated by generateId() - String idVal = TraceAndSpanIdGenerator.generateId(); - - // when: that ID is parsed into a long - Long idAsLong = Long.parseLong(idVal); + public void generateId_should_return_16_char_length_string_that_can_be_parsed_into_a_long_when_interpreted_as_a_64_bit_unsigned_hex_long() { + Set charactersFromIds = new HashSet<>(); + + for (int i = 0; i < 10000; i++) { + // given: String ID value generated by generateId() + final String idVal = TraceAndSpanIdGenerator.generateId(); + + // then: that ID has 16 characters and can be interpreted as a 64 bit unsigned hex long and parsed into a Java long primitive + assertThat(idVal).hasSize(16); + Throwable parseException = catchThrowable(new ThrowableAssert.ThrowingCallable() { + @Override + public void call() throws Throwable { + new BigInteger(idVal, 16).longValue(); + } + }); + assertThat(parseException).isNull(); + + // Store the characters we see in this ID in a set so we can verify later that we've only ever seen hex-compatible characters. + for (char c : idVal.toCharArray()) { + charactersFromIds.add(c); + } + } - // then: it doesn't explode - assertThat(idAsLong).isNotNull(); + // We should have only run into lowercase hex characters, and given how many IDs we generated we should have hit all the lowercase hex characters. + assertThat(charactersFromIds).containsOnly('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); } @Test @@ -143,4 +187,68 @@ public void generateId_should_not_generate_duplicate_ids_over_reasonable_number_ // then assertThat(ids.size()).isEqualTo(numAttempts); } + + @Test + public void generate64BitRandomLong_should_not_generate_duplicate_ids_over_reasonable_number_of_attempts() throws Exception { + // given + Set randomLongs = new HashSet<>(); + int numAttempts = 1000000; + + // when + for (int i = 0; i < numAttempts; i++) { + randomLongs.add(TraceAndSpanIdGenerator.generate64BitRandomLong()); + } + + // then + assertThat(randomLongs.size()).isEqualTo(numAttempts); + } + + @DataProvider(value = { + "0000000000000000 | 0", + "0000000000000001 | 1", + "ffffffffffffffff | 18446744073709551615", + "fffffffffffffffe | 18446744073709551614", + "7fae59489091369a | 9200389256962455194", + "eb5e7aaefeb92b4f | 16960128138740312911", + "d2153abe4c047408 | 15138070311469347848", + "9041ee0d07d6c72c | 10394851154681317164", + "6470a5ce0e9262f4 | 7237466905610707700", + "000003c8a251fb93 | 4160251624339", + }, splitBy = "\\|") + @Test + public void longToUnsignedLowerHexString_and_unsignedLowerHexStringToLong_work_as_expected_for_known_values(String actualHexValue, String actualUnsignedDecimalValue) { + // given + long actualSignedPrimitive = new BigInteger(actualUnsignedDecimalValue).longValue(); + + // when + String calculatedHexValue = TraceAndSpanIdGenerator.longToUnsignedLowerHexString(actualSignedPrimitive); + long calculatedPrimitiveValue = TraceAndSpanIdGenerator.unsignedLowerHexStringToLong(actualHexValue); + + // then + assertThat(calculatedHexValue).isEqualTo(actualHexValue); + assertThat(calculatedPrimitiveValue).isEqualTo(actualSignedPrimitive); + } + + @DataProvider(value = { + " ", // less than 16 chars + "12345678901234567 ", // longer than 16 chars + "/ ", // before '0' char + ": ", // after '9' char + "` ", // before 'a' char + "g ", // after 'f' char + "ABCDEF " // uppercase hex chars + }, splitBy = "\\|") + @Test + public void unsignedLowerHexStringToLong_throws_NumberFormatException_for_illegal_args(final String badHexString) { + // when + Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { + @Override + public void call() throws Throwable { + TraceAndSpanIdGenerator.unsignedLowerHexStringToLong(badHexString); + } + }); + + // then + assertThat(ex).isInstanceOf(NumberFormatException.class); + } } diff --git a/wingtips-core/src/test/java/com/nike/wingtips/http/HttpRequestTracingUtilsTest.java b/wingtips-core/src/test/java/com/nike/wingtips/http/HttpRequestTracingUtilsTest.java index 76b66afa..d4ca1452 100644 --- a/wingtips-core/src/test/java/com/nike/wingtips/http/HttpRequestTracingUtilsTest.java +++ b/wingtips-core/src/test/java/com/nike/wingtips/http/HttpRequestTracingUtilsTest.java @@ -260,6 +260,51 @@ public void fromRequestWithHeaders_sets_sampleable_to_attribute_value_if_sampled assertThat(newSpan.isSampleable()).isFalse(); } + @DataProvider(value = { + "0 | false", + "1 | true", + "2 | true", + "42 | true", + "false | false", + "FALSE | false", + "FaLsE | false", + "true | true", + "TRUE | true", + "TrUe | true", + "true | true", + "bad | true", + }, splitBy = "\\|") + @Test + public void fromRequestWithHeaders_extracts_sampleable_as_expected(String receivedValue, boolean expectedSampleableResult) { + // Verify via headers + { + // given + given(request.getHeader(TraceHeaders.TRACE_ID)).willReturn(sampleTraceID); + given(request.getHeader(TraceHeaders.TRACE_SAMPLED)).willReturn(receivedValue); + given(request.getAttribute(TraceHeaders.TRACE_SAMPLED)).willReturn(null); + + // when + Span newSpan = HttpRequestTracingUtils.fromRequestWithHeaders(request, USER_ID_HEADER_KEYS); + + // then + assertThat(newSpan.isSampleable()).isEqualTo(expectedSampleableResult); + } + + // Verify via attribute + { + // given + given(request.getHeader(TraceHeaders.TRACE_ID)).willReturn(sampleTraceID); + given(request.getHeader(TraceHeaders.TRACE_SAMPLED)).willReturn(null); + given(request.getAttribute(TraceHeaders.TRACE_SAMPLED)).willReturn(receivedValue); + + // when + Span newSpan = HttpRequestTracingUtils.fromRequestWithHeaders(request, USER_ID_HEADER_KEYS); + + // then + assertThat(newSpan.isSampleable()).isEqualTo(expectedSampleableResult); + } + } + @Test public void fromRequestWithHeaders_uses_headers_first_even_if_attributes_exist() { // given: request that has both headers and attributes set diff --git a/wingtips-servlet-api/src/main/java/com/nike/wingtips/servlet/HttpSpanFactory.java b/wingtips-servlet-api/src/main/java/com/nike/wingtips/servlet/HttpSpanFactory.java index 33240e59..4ea3202e 100644 --- a/wingtips-servlet-api/src/main/java/com/nike/wingtips/servlet/HttpSpanFactory.java +++ b/wingtips-servlet-api/src/main/java/com/nike/wingtips/servlet/HttpSpanFactory.java @@ -1,6 +1,7 @@ package com.nike.wingtips.servlet; import com.nike.wingtips.Span; +import com.nike.wingtips.Span.SpanPurpose; import com.nike.wingtips.http.HttpRequestTracingUtils; import java.util.List; @@ -46,14 +47,14 @@ public static Span fromHttpServletRequest(HttpServletRequest servletRequest, Lis * @return A {@link Span} object created from the headers if they exist (see {@link #fromHttpServletRequest(HttpServletRequest, List)} for details), or if the headers don't * have enough information then this will return a new root span with a span name based on the results of {@link #getSpanName(HttpServletRequest)} and user ID based * on the result of {@link #getUserIdFromHttpServletRequest(HttpServletRequest, List)}. Since this method is for a server receiving a request - * the returned span's {@link Span#getSpanPurpose()} will be {@link Span.SpanPurpose#SERVER}. + * the returned span's {@link Span#getSpanPurpose()} will be {@link SpanPurpose#SERVER}. */ public static Span fromHttpServletRequestOrCreateRootSpan(HttpServletRequest servletRequest, List userIdHeaderKeys) { Span span = fromHttpServletRequest(servletRequest, userIdHeaderKeys); if (span == null) { span = Span - .generateRootSpanForNewTrace(getSpanName(servletRequest), Span.SpanPurpose.SERVER) + .generateRootSpanForNewTrace(getSpanName(servletRequest), SpanPurpose.SERVER) .withUserId(getUserIdFromHttpServletRequest(servletRequest, userIdHeaderKeys)) .build(); } diff --git a/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListener.java b/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListener.java index 29c85af6..c8a46a7c 100644 --- a/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListener.java +++ b/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListener.java @@ -7,6 +7,12 @@ import com.nike.wingtips.zipkin.util.ZipkinSpanSender; import com.nike.wingtips.zipkin.util.ZipkinSpanSenderDefaultHttpImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + import zipkin.Endpoint; /** @@ -32,12 +38,18 @@ @SuppressWarnings("WeakerAccess") public class WingtipsToZipkinLifecycleListener implements SpanLifecycleListener { + private final Logger zipkinConversionOrReportingErrorLogger = LoggerFactory.getLogger("ZIPKIN_SPAN_CONVERSION_OR_HANDLING_ERROR"); + protected final String serviceName; protected final String localComponentNamespace; protected final Endpoint zipkinEndpoint; protected final WingtipsToZipkinSpanConverter zipkinSpanConverter; protected final ZipkinSpanSender zipkinSpanSender; + protected final AtomicLong spanHandlingErrorCounter = new AtomicLong(0); + protected long lastSpanHandlingErrorLogTimeEpochMillis = 0; + protected static final long MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS = TimeUnit.SECONDS.toMillis(60); + /** * Kitchen-sink constructor that lets you set all the options. * @@ -94,7 +106,27 @@ public void spanSampled(Span span) { @Override public void spanCompleted(Span span) { - zipkin.Span zipkinSpan = zipkinSpanConverter.convertWingtipsSpanToZipkinSpan(span, zipkinEndpoint, localComponentNamespace); - zipkinSpanSender.handleSpan(zipkinSpan); + try { + zipkin.Span zipkinSpan = zipkinSpanConverter.convertWingtipsSpanToZipkinSpan(span, zipkinEndpoint, localComponentNamespace); + zipkinSpanSender.handleSpan(zipkinSpan); + } + catch(Throwable ex) { + long currentBadSpanCount = spanHandlingErrorCounter.incrementAndGet(); + + // Only log once every MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS time interval to prevent log spam from a malicious (or broken) caller. + long currentTimeMillis = System.currentTimeMillis(); + long timeSinceLastLogMsgMillis = currentTimeMillis - lastSpanHandlingErrorLogTimeEpochMillis; + if (timeSinceLastLogMsgMillis >= MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS) { + // We're not synchronizing the read and write to lastSpanHandlingErrorLogTimeEpochMillis, and that's ok. If we get a few extra + // log messages due to a race condition it's not the end of the world - we're still satisfying the goal of not allowing a + // malicious caller to endlessly spam the logs. + lastSpanHandlingErrorLogTimeEpochMillis = currentTimeMillis; + + zipkinConversionOrReportingErrorLogger.warn( + "There have been {} spans that were not zipkin compatible, or that experienced an error during span handling. Latest example: " + + "wingtips_span_with_error=\"{}\", conversion_or_handling_error=\"{}\"", + currentBadSpanCount, span.toKeyValueString(), ex.toString()); + } + } } } \ No newline at end of file diff --git a/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImpl.java b/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImpl.java index 352ebb7e..bf537850 100644 --- a/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImpl.java +++ b/wingtips-zipkin/src/main/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImpl.java @@ -1,6 +1,7 @@ package com.nike.wingtips.zipkin.util; import com.nike.wingtips.Span; +import com.nike.wingtips.TraceAndSpanIdGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,11 +66,11 @@ protected zipkin.Span.Builder createNewZipkinSpanBuilderWithSpanPurposeAnnotatio return zsb; } - protected Long nullSafeLong(String str) { - if (str == null) + protected Long nullSafeLong(String lowerHexStr) { + if (lowerHexStr == null) return null; - return Long.parseLong(str); + return TraceAndSpanIdGenerator.unsignedLowerHexStringToLong(lowerHexStr); } } diff --git a/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListenerTest.java b/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListenerTest.java index 6fc768cc..053b7c02 100644 --- a/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListenerTest.java +++ b/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/WingtipsToZipkinLifecycleListenerTest.java @@ -6,9 +6,11 @@ import com.nike.wingtips.zipkin.util.ZipkinSpanSender; import com.nike.wingtips.zipkin.util.ZipkinSpanSenderDefaultHttpImpl; +import org.assertj.core.api.ThrowableAssert; import org.junit.Before; import org.junit.Test; import org.mockito.internal.util.reflection.Whitebox; +import org.slf4j.Logger; import java.net.MalformedURLException; import java.net.URL; @@ -16,9 +18,14 @@ import zipkin.Endpoint; +import static com.nike.wingtips.zipkin.WingtipsToZipkinLifecycleListener.MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -103,13 +110,87 @@ public void spanCompleted_converts_to_zipkin_span_and_passes_it_to_zipkinSpanSen // given zipkin.Span zipkinSpan = zipkin.Span.builder().traceId(42).id(4242).name("foo").build(); doReturn(zipkinSpan).when(spanConverterMock).convertWingtipsSpanToZipkinSpan(any(Span.class), any(Endpoint.class), any(String.class)); - Endpoint zipkinEndpoint = listener.zipkinEndpoint; // when listener.spanCompleted(spanMock); // then - verify(spanConverterMock).convertWingtipsSpanToZipkinSpan(spanMock, zipkinEndpoint, localComponentNamespace); + verify(spanConverterMock).convertWingtipsSpanToZipkinSpan(spanMock, listener.zipkinEndpoint, localComponentNamespace); verify(spanSenderMock).handleSpan(zipkinSpan); } + + @Test + public void spanCompleted_does_not_propagate_exceptions_generated_by_span_converter() { + // given + doThrow(new RuntimeException("kaboom")).when(spanConverterMock).convertWingtipsSpanToZipkinSpan(any(Span.class), any(Endpoint.class), any(String.class)); + + // when + Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { + @Override + public void call() throws Throwable { + listener.spanCompleted(spanMock); + } + }); + + // then + verify(spanConverterMock).convertWingtipsSpanToZipkinSpan(spanMock, listener.zipkinEndpoint, localComponentNamespace); + verifyZeroInteractions(spanSenderMock); + assertThat(ex).isNull(); + } + + @Test + public void spanCompleted_does_not_propagate_exceptions_generated_by_span_sender() { + // given + doThrow(new RuntimeException("kaboom")).when(spanSenderMock).handleSpan(any(zipkin.Span.class)); + + // when + Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { + @Override + public void call() throws Throwable { + listener.spanCompleted(spanMock); + } + }); + + // then + verify(spanSenderMock).handleSpan(any(zipkin.Span.class)); + assertThat(ex).isNull(); + } + + @Test + public void spanCompleted_logs_error_during_handling_if_time_since_lastSpanHandlingErrorLogTimeEpochMillis_is_greater_than_MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS() throws InterruptedException { + // given + Logger loggerMock = mock(Logger.class); + Whitebox.setInternalState(listener, "zipkinConversionOrReportingErrorLogger", loggerMock); + long lastLogTimeToSet = System.currentTimeMillis() - (MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS + 10); + Whitebox.setInternalState(listener, "lastSpanHandlingErrorLogTimeEpochMillis", lastLogTimeToSet); + doThrow(new RuntimeException("kaboom")).when(spanSenderMock).handleSpan(any(zipkin.Span.class)); + + // when + long before = System.currentTimeMillis(); + listener.spanCompleted(spanMock); + long after = System.currentTimeMillis(); + + // then + verify(loggerMock).warn(anyString(), anyLong(), anyString(), anyString()); + // Also verify that the lastSpanHandlingErrorLogTimeEpochMillis value got updated. + assertThat((long)Whitebox.getInternalState(listener, "lastSpanHandlingErrorLogTimeEpochMillis")).isBetween(before, after); + } + + @Test + public void spanCompleted_does_not_log_an_error_during_handling_if_time_since_lastSpanHandlingErrorLogTimeEpochMillis_is_less_than_MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS() throws InterruptedException { + // given + Logger loggerMock = mock(Logger.class); + Whitebox.setInternalState(listener, "zipkinConversionOrReportingErrorLogger", loggerMock); + long lastLogTimeToSet = System.currentTimeMillis() - (MIN_SPAN_HANDLING_ERROR_LOG_INTERVAL_MILLIS - 1000); + Whitebox.setInternalState(listener, "lastSpanHandlingErrorLogTimeEpochMillis", lastLogTimeToSet); + doThrow(new RuntimeException("kaboom")).when(spanSenderMock).handleSpan(any(zipkin.Span.class)); + + // when + listener.spanCompleted(spanMock); + + // then + verifyZeroInteractions(loggerMock); + // Also verify that the lastSpanHandlingErrorLogTimeEpochMillis value was *not* updated. + assertThat((long)Whitebox.getInternalState(listener, "lastSpanHandlingErrorLogTimeEpochMillis")).isEqualTo(lastLogTimeToSet); + } } \ No newline at end of file diff --git a/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImplTest.java b/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImplTest.java index 3059207a..483e89e3 100644 --- a/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImplTest.java +++ b/wingtips-zipkin/src/test/java/com/nike/wingtips/zipkin/util/WingtipsToZipkinSpanConverterDefaultImplTest.java @@ -17,6 +17,8 @@ import zipkin.Constants; import zipkin.Endpoint; +import static com.nike.wingtips.TraceAndSpanIdGenerator.generateId; +import static com.nike.wingtips.TraceAndSpanIdGenerator.unsignedLowerHexStringToLong; import static org.assertj.core.api.Assertions.assertThat; /** @@ -75,9 +77,9 @@ private void verifySpanPurposeRelatedStuff(zipkin.Span zipkinSpan, Span wingtips public void convertWingtipsSpanToZipkinSpan_works_as_expected_for_all_non_null_info(Span.SpanPurpose spanPurpose) { // given String spanName = UUID.randomUUID().toString(); - String traceId = String.valueOf(random.nextLong()); - String spanId = String.valueOf(random.nextLong()); - String parentId = String.valueOf(random.nextLong()); + String traceId = generateId(); + String spanId = generateId(); + String parentId = generateId(); long startTimeEpochMicros = Math.abs(random.nextLong()); long durationNanos = Math.abs(random.nextLong()); long durationMicros = TimeUnit.NANOSECONDS.toMicros(durationNanos); @@ -89,11 +91,11 @@ public void convertWingtipsSpanToZipkinSpan_works_as_expected_for_all_non_null_i zipkin.Span zipkinSpan = impl.convertWingtipsSpanToZipkinSpan(wingtipsSpan, zipkinEndpoint, localComponentNamespace); // then - assertThat(zipkinSpan.id).isEqualTo(Long.valueOf(wingtipsSpan.getSpanId())); + assertThat(zipkinSpan.id).isEqualTo(unsignedLowerHexStringToLong(wingtipsSpan.getSpanId())); assertThat(zipkinSpan.name).isEqualTo(wingtipsSpan.getSpanName()); - assertThat(zipkinSpan.parentId).isEqualTo(Long.valueOf(wingtipsSpan.getParentSpanId())); + assertThat(zipkinSpan.parentId).isEqualTo(unsignedLowerHexStringToLong(wingtipsSpan.getParentSpanId())); assertThat(zipkinSpan.timestamp).isEqualTo(wingtipsSpan.getSpanStartTimeEpochMicros()); - assertThat(zipkinSpan.traceId).isEqualTo(Long.valueOf(wingtipsSpan.getTraceId())); + assertThat(zipkinSpan.traceId).isEqualTo(unsignedLowerHexStringToLong(wingtipsSpan.getTraceId())); assertThat(zipkinSpan.duration).isEqualTo(durationMicros); verifySpanPurposeRelatedStuff(zipkinSpan, wingtipsSpan, zipkinEndpoint, localComponentNamespace); @@ -110,8 +112,8 @@ public void convertWingtipsSpanToZipkinSpan_works_as_expected_for_all_nullable_i // given // Not a lot that can really be null - just parent span ID String spanName = UUID.randomUUID().toString(); - String traceId = String.valueOf(random.nextLong()); - String spanId = String.valueOf(random.nextLong()); + String traceId = generateId(); + String spanId = generateId(); long startTimeEpochMicros = Math.abs(random.nextLong()); long durationNanos = Math.abs(random.nextLong()); long durationMicros = TimeUnit.NANOSECONDS.toMicros(durationNanos); @@ -123,11 +125,11 @@ public void convertWingtipsSpanToZipkinSpan_works_as_expected_for_all_nullable_i zipkin.Span zipkinSpan = impl.convertWingtipsSpanToZipkinSpan(wingtipsSpan, zipkinEndpoint, localComponentNamespace); // then - assertThat(zipkinSpan.id).isEqualTo(Long.valueOf(wingtipsSpan.getSpanId())); + assertThat(zipkinSpan.id).isEqualTo(unsignedLowerHexStringToLong(wingtipsSpan.getSpanId())); assertThat(zipkinSpan.name).isEqualTo(wingtipsSpan.getSpanName()); assertThat(zipkinSpan.parentId).isNull(); assertThat(zipkinSpan.timestamp).isEqualTo(wingtipsSpan.getSpanStartTimeEpochMicros()); - assertThat(zipkinSpan.traceId).isEqualTo(Long.valueOf(wingtipsSpan.getTraceId())); + assertThat(zipkinSpan.traceId).isEqualTo(unsignedLowerHexStringToLong(wingtipsSpan.getTraceId())); assertThat(zipkinSpan.duration).isEqualTo(durationMicros); verifySpanPurposeRelatedStuff(zipkinSpan, wingtipsSpan, zipkinEndpoint, localComponentNamespace);