diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 6062e6eebf..99c9032d4f 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -43,6 +43,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes: * Add support for Elasticsearch client 8.9 - {pull}3283[#3283] * Added `baggage_to_attach` config option to allow automatic lifting of baggage into transaction, span and error attributes - {pull}3288[#3288], {pull}3289[#3289] * Exclude elasticsearch 8.10 and newer clients from instrumentation because they natively support OpenTelemetry - {pull}3303[#3303] +* Switched to OpenTelemetry compatible context propagation for Kafka - {pull}3300[#3300] [[release-notes-1.x]] === Java Agent version 1.x diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java index 254bd3a42d..92b5c9631a 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java @@ -30,6 +30,7 @@ import co.elastic.apm.agent.context.ClosableLifecycleListenerAdapter; import co.elastic.apm.agent.context.LifecycleListener; import co.elastic.apm.agent.impl.baggage.Baggage; +import co.elastic.apm.agent.impl.baggage.W3CBaggagePropagation; import co.elastic.apm.agent.impl.error.ErrorCapture; import co.elastic.apm.agent.impl.metadata.MetaDataFuture; import co.elastic.apm.agent.impl.sampling.ProbabilitySampler; @@ -55,8 +56,7 @@ import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap; import co.elastic.apm.agent.tracer.GlobalTracer; import co.elastic.apm.agent.tracer.Scope; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; import co.elastic.apm.agent.tracer.reference.ReferenceCounted; import co.elastic.apm.agent.tracer.reference.ReferenceCountedMap; import co.elastic.apm.agent.util.DependencyInjectingServiceLoader; @@ -69,7 +69,9 @@ import java.io.Closeable; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -91,6 +93,14 @@ public class ElasticApmTracer implements Tracer { private static final WeakMap serviceInfoByClassLoader = WeakConcurrent.buildMap(); private static final Map, Class> configs = new HashMap<>(); + public static final Set TRACE_HEADER_NAMES; + + static { + Set headerNames = new HashSet<>(); + headerNames.addAll(TraceContext.TRACE_TEXTUAL_HEADERS); + headerNames.add(W3CBaggagePropagation.BAGGAGE_HEADER_NAME); + TRACE_HEADER_NAMES = Collections.unmodifiableSet(headerNames); + } private static volatile boolean classloaderCheckOk = false; @@ -277,49 +287,29 @@ public Transaction startRootTransaction(Sampler sampler, long epochMicros, Bagga @Override @Nullable - public Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { - return startChildTransaction(headerCarrier, textHeadersGetter, sampler, -1, initiatingClassLoader); - } - - @Override - @Nullable - public Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader, Baggage baseBaggage, long epochMicros) { - return startChildTransaction(headerCarrier, textHeadersGetter, sampler, epochMicros, initiatingClassLoader); + public Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, @Nullable ClassLoader initiatingClassLoader) { + return startChildTransaction(headerCarrier, headersGetter, sampler, -1, initiatingClassLoader); } @Override @Nullable - public Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, Sampler sampler, - long epochMicros, @Nullable ClassLoader initiatingClassLoader) { - return startChildTransaction(headerCarrier, textHeadersGetter, sampler, epochMicros, currentContext().getBaggage(), initiatingClassLoader); - } - - private Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, Sampler sampler, - long epochMicros, Baggage baseBaggage, @Nullable ClassLoader initiatingClassLoader) { - Transaction transaction = null; - if (isRunning()) { - transaction = createTransaction().start(TraceContext.getFromTraceContextTextHeaders(), headerCarrier, - textHeadersGetter, epochMicros, sampler, baseBaggage); - afterTransactionStart(initiatingClassLoader, transaction); - } - return transaction; + public Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, @Nullable ClassLoader initiatingClassLoader, Baggage baseBaggage, long epochMicros) { + return startChildTransaction(headerCarrier, headersGetter, sampler, epochMicros, initiatingClassLoader); } @Override @Nullable - public Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { - return startChildTransaction(headerCarrier, binaryHeadersGetter, sampler, -1, initiatingClassLoader); + public Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, Sampler sampler, + long epochMicros, @Nullable ClassLoader initiatingClassLoader) { + return startChildTransaction(headerCarrier, headersGetter, sampler, epochMicros, currentContext().getBaggage(), initiatingClassLoader); } - @Override - @Nullable - public Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, - Sampler sampler, long epochMicros, @Nullable ClassLoader initiatingClassLoader) { + private Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, Sampler sampler, + long epochMicros, Baggage baseBaggage, @Nullable ClassLoader initiatingClassLoader) { Transaction transaction = null; if (isRunning()) { - Baggage baseBaggage = currentContext().getBaggage(); - transaction = createTransaction().start(TraceContext.getFromTraceContextBinaryHeaders(), headerCarrier, - binaryHeadersGetter, epochMicros, sampler, baseBaggage); + transaction = createTransaction().start(headerCarrier, + headersGetter, epochMicros, sampler, baseBaggage); afterTransactionStart(initiatingClassLoader, transaction); } return transaction; @@ -969,6 +959,6 @@ public T require(Class type) { @Override public Set getTraceHeaderNames() { - return TraceContext.TRACE_TEXTUAL_HEADERS; + return TRACE_HEADER_NAMES; } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/Tracer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/Tracer.java index 9b3629b799..febb3c6645 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/Tracer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/Tracer.java @@ -26,9 +26,7 @@ import co.elastic.apm.agent.impl.transaction.ElasticContext; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; import javax.annotation.Nullable; @@ -59,10 +57,10 @@ public interface Tracer extends co.elastic.apm.agent.tracer.Tracer { @Override @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader); + Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, @Nullable ClassLoader initiatingClassLoader); @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader, Baggage baseBaggage, long epochMicros); + Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, @Nullable ClassLoader initiatingClassLoader, Baggage baseBaggage, long epochMicros); /** * Starts a transaction as a child of the context headers obtained through the provided {@link HeaderGetter}. @@ -70,7 +68,7 @@ public interface Tracer extends co.elastic.apm.agent.tracer.Tracer { * available), then it will be started as the root transaction of the trace. * * @param headerCarrier the Object from which context headers can be obtained, typically a request or a message - * @param textHeadersGetter provides the trace context headers required in order to create a child transaction + * @param headersGetter provides the trace context headers required in order to create a child transaction * @param sampler the {@link Sampler} instance which is responsible for determining the sampling decision if this is a root transaction * @param epochMicros the start timestamp * @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction. @@ -79,30 +77,9 @@ public interface Tracer extends co.elastic.apm.agent.tracer.Tracer { * @return a transaction which is a child of the provided parent if the agent is currently RUNNING; null otherwise */ @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, Sampler sampler, - long epochMicros, @Nullable ClassLoader initiatingClassLoader); + Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter headersGetter, Sampler sampler, + long epochMicros, @Nullable ClassLoader initiatingClassLoader); - @Override - @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, @Nullable ClassLoader initiatingClassLoader); - - /** - * Starts a transaction as a child of the context headers obtained through the provided {@link HeaderGetter}. - * If the created transaction cannot be started as a child transaction (for example - if no parent context header is - * available), then it will be started as the root transaction of the trace. - * - * @param headerCarrier the Object from which context headers can be obtained, typically a request or a message - * @param binaryHeadersGetter provides the trace context headers required in order to create a child transaction - * @param sampler the {@link Sampler} instance which is responsible for determining the sampling decision if this is a root transaction - * @param epochMicros the start timestamp - * @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction. - * Used to determine the service name and to load application-scoped classes like the {@link org.slf4j.MDC}, - * for log correlation. - * @return a transaction which is a child of the provided parent if the agent is currently RUNNING; null otherwise - */ - @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, - Sampler sampler, long epochMicros, @Nullable ClassLoader initiatingClassLoader); @Override ElasticContext currentContext(); diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/Baggage.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/Baggage.java index cafe963e08..cecfe45f9e 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/Baggage.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/Baggage.java @@ -23,6 +23,7 @@ import co.elastic.apm.agent.impl.transaction.AbstractSpan; import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -61,6 +62,11 @@ public class Baggage implements co.elastic.apm.agent.tracer.Baggage { */ private volatile String cachedSerializedW3CHeader = null; + /** + * Cached UTF8-representation of {@link #cachedSerializedW3CHeader}. + */ + byte[] cachedSerializedW3CHeaderUtf8 = null; + private Baggage(Map baggage, Map baggageMetadata) { this.baggage = baggage; this.baggageMetadata = baggageMetadata; @@ -102,6 +108,20 @@ String getCachedSerializedW3CHeader() { return this.cachedSerializedW3CHeader; } + + byte[] getCachedSerializedW3CHeaderUtf8() { + if (cachedSerializedW3CHeader == null) { + throw new IllegalStateException("cached string header must be set first"); + } + if (cachedSerializedW3CHeader.isEmpty()) { + return null; + } + if (cachedSerializedW3CHeaderUtf8 == null) { + cachedSerializedW3CHeaderUtf8 = cachedSerializedW3CHeader.getBytes(StandardCharsets.UTF_8); + } + return this.cachedSerializedW3CHeaderUtf8; + } + public void storeBaggageInAttributes(AbstractSpan span, List keyFilter) { if (baggage.isEmpty() || keyFilter.isEmpty()) { // early out to prevent unnecessarily allocating an iterator @@ -143,6 +163,24 @@ private String getKeyWithAttributePrefix(String key) { return keyWithPrefix; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Baggage baggage1 = (Baggage) o; + + if (!baggage.equals(baggage1.baggage)) return false; + return baggageMetadata.equals(baggage1.baggageMetadata); + } + + @Override + public int hashCode() { + int result = baggage.hashCode(); + result = 31 * result + baggageMetadata.hashCode(); + return result; + } + public static class Builder { public Builder(Baggage parent) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagation.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagation.java index fadac3a08e..6a52a1712d 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagation.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagation.java @@ -21,12 +21,16 @@ import co.elastic.apm.agent.impl.baggage.otel.Parser; import co.elastic.apm.agent.impl.baggage.otel.PercentEscaper; import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderSetter; import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderSetter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; public class W3CBaggagePropagation { @@ -36,7 +40,7 @@ public class W3CBaggagePropagation { public static final String BAGGAGE_HEADER_NAME = "baggage"; - private static final HeaderGetter.HeaderConsumer PARSING_CONSUMER = new HeaderGetter.HeaderConsumer() { + private static final HeaderGetter.HeaderConsumer STRING_PARSING_CONSUMER = new HeaderGetter.HeaderConsumer() { @Override public void accept(@Nullable String headerValue, Baggage.Builder state) { if (headerValue != null) { @@ -49,22 +53,71 @@ public void accept(@Nullable String headerValue, Baggage.Builder state) { } }; - public static void parse(C carrier, TextHeaderGetter headerGetter, Baggage.Builder into) { - headerGetter.forEach(BAGGAGE_HEADER_NAME, carrier, into, PARSING_CONSUMER); + + private static final HeaderGetter.HeaderConsumer UTF8_BYTES_PARSING_CONSUMER = new HeaderGetter.HeaderConsumer() { + @Override + public void accept(@Nullable byte[] headerValue, Baggage.Builder state) { + if (headerValue != null) { + try { + STRING_PARSING_CONSUMER.accept(new String(headerValue, StandardCharsets.UTF_8), state); + } catch (Exception e) { + logger.error("Failed to decode baggage header bytes as UTF8", e); + } + } + } + }; + + @SuppressWarnings("unchecked") + public static void parse(C carrier, HeaderGetter headerGetter, Baggage.Builder into) { + HeaderGetter.HeaderConsumer consumer; + if (headerGetter instanceof TextHeaderGetter) { + consumer = (HeaderGetter.HeaderConsumer) STRING_PARSING_CONSUMER; + } else if (headerGetter instanceof UTF8ByteHeaderGetter) { + consumer = (HeaderGetter.HeaderConsumer) UTF8_BYTES_PARSING_CONSUMER; + } else { + throw new IllegalArgumentException("HeaderGetter must be either a TextHeaderGetter or UTF8ByteHeaderGetter: " + headerGetter.getClass().getName()); + } + headerGetter.forEach(BAGGAGE_HEADER_NAME, carrier, into, consumer); } - public static void propagate(Baggage baggage, C carrier, TextHeaderSetter setter) { + public static void propagate(Baggage baggage, C carrier, HeaderSetter setter) { if (baggage.isEmpty()) { return; } + T header = getTextHeader(baggage, setter); + if (header != null) { + setter.setHeader(BAGGAGE_HEADER_NAME, header, carrier); + } + } + + @SuppressWarnings("unchecked") + private static T getTextHeader(Baggage baggage, HeaderSetter setter) { + if (setter instanceof TextHeaderSetter) { + return (T) getTextHeaderString(baggage); + } else if (setter instanceof UTF8ByteHeaderSetter) { + return (T) getTextHeaderUtf8Bytes(baggage); + } else { + throw new IllegalArgumentException("HeaderSetter must be either a TextHeaderSetter or UTF8ByteHeaderSetter: " + setter.getClass().getName()); + } + } + + @Nullable + private static byte[] getTextHeaderUtf8Bytes(Baggage baggage) { + getTextHeaderString(baggage); //ensures that the string-header is computed and cached + return baggage.getCachedSerializedW3CHeaderUtf8(); + } + + @Nullable + private static String getTextHeaderString(Baggage baggage) { String header = baggage.getCachedSerializedW3CHeader(); if (header == null) { header = encodeToHeader(baggage); baggage.setCachedSerializedW3CHeader(header); } - if (!header.isEmpty()) { - setter.setHeader(BAGGAGE_HEADER_NAME, encodeToHeader(baggage), carrier); + if (header.isEmpty()) { + return null; } + return header; } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java index 173507cc47..96de90c4c0 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java @@ -29,9 +29,7 @@ import co.elastic.apm.agent.sdk.logging.Logger; import co.elastic.apm.agent.sdk.logging.LoggerFactory; import co.elastic.apm.agent.tracer.Outcome; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; import co.elastic.apm.agent.tracer.pooling.Recyclable; import javax.annotation.Nullable; @@ -367,7 +365,6 @@ private boolean canAddSpanLink() { * Adds a span link based on the tracecontext header retrieved from the provided {@code carrier} through the provided {@code * headerGetter}. * - * @param childContextCreator the proper tracecontext inference implementation, corresponding on the header and types * @param headerGetter the proper header extractor, corresponding the header and carrier types * @param carrier the object from which the tracecontext header is to be retrieved * @param the tracecontext header type - either binary ({@code byte[]}) or textual ({@code String}) @@ -375,7 +372,6 @@ private boolean canAddSpanLink() { * @return {@code true} if added, {@code false} otherwise */ public boolean addSpanLink( - TraceContext.HeaderChildContextCreator childContextCreator, HeaderGetter headerGetter, @Nullable C carrier ) { @@ -385,7 +381,7 @@ public boolean addSpanLink( boolean added = false; try { TraceContext childTraceContext = tracer.createSpanLink(); - if (childContextCreator.asChildOf(childTraceContext, carrier, headerGetter)) { + if (childTraceContext.asChildOf(carrier, headerGetter, false)) { added = spanLinks.add(childTraceContext); } if (!added) { @@ -793,16 +789,7 @@ private String normalizeType(@Nullable String type) { } @Override - public boolean addLink(BinaryHeaderGetter headerGetter, @Nullable C carrier) { - return addSpanLink(TraceContext.getFromTraceContextBinaryHeaders(), - headerGetter, - carrier); - } - - @Override - public boolean addLink(TextHeaderGetter headerGetter, @Nullable C carrier) { - return addSpanLink(TraceContext.getFromTraceContextTextHeaders(), - headerGetter, - carrier); + public boolean addLink(HeaderGetter headerGetter, @Nullable C carrier) { + return addSpanLink(headerGetter, carrier); } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/CharAccessor.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/CharAccessor.java new file mode 100644 index 0000000000..3c90853b22 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/CharAccessor.java @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.agent.impl.transaction; + +import co.elastic.apm.agent.tracer.util.HexUtils; + +import java.nio.charset.StandardCharsets; + +abstract class CharAccessor { + + abstract int length(T text); + + abstract char charAt(T text, int index); + + abstract void readHex(T text, int offset, byte[] into); + + abstract byte readHexByte(T text, int offset); + + abstract String asString(T text); + + int getLeadingWhitespaceCount(T text) { + int count = 0; + int len = length(text); + while (count < len && Character.isWhitespace(charAt(text, count))) { + count++; + } + return count; + } + + int getTrailingWhitespaceCount(T text) { + int count = 0; + int len = length(text); + while (count < len && Character.isWhitespace(charAt(text, len - 1 - count))) { + count++; + } + return count; + } + + public boolean containsAtOffset(T text, int offset, CharSequence substringToCheck) { + int len = length(text); + if (offset < 0 || offset > len) { + throw new IllegalArgumentException("Bad offset: " + offset); + } + int subStrLen = substringToCheck.length(); + if (offset + subStrLen > len) { + return false; + } + + for (int i = 0; i < subStrLen; i++) { + if (charAt(text, i + offset) != substringToCheck.charAt(i)) { + return false; + } + } + return true; + } + + public static CharAccessor forCharSequence() { + return CharSeq.INSTANCE; + } + + public static CharAccessor forAsciiBytes() { + return AsciiBytes.INSTANCE; + } + + + private static class CharSeq extends CharAccessor { + + static final CharSeq INSTANCE = new CharSeq(); + + @Override + int length(CharSequence text) { + return text.length(); + } + + @Override + char charAt(CharSequence text, int index) { + return text.charAt(index); + } + + @Override + void readHex(CharSequence text, int offset, byte[] into) { + HexUtils.nextBytes(text, offset, into); + } + + @Override + byte readHexByte(CharSequence text, int offset) { + return HexUtils.getNextByte(text, offset); + } + + @Override + String asString(CharSequence text) { + return text.toString(); + } + } + + private static class AsciiBytes extends CharAccessor { + + static final AsciiBytes INSTANCE = new AsciiBytes(); + + @Override + int length(byte[] text) { + return text.length; + } + + @Override + char charAt(byte[] text, int index) { + return (char) text[index]; + } + + @Override + void readHex(byte[] text, int offset, byte[] into) { + HexUtils.nextBytesAscii(text, offset, into); + } + + @Override + byte readHexByte(byte[] text, int offset) { + return HexUtils.getNextByteAscii(text, offset); + } + + @Override + String asString(byte[] text) { + return new String(text, StandardCharsets.UTF_8); + } + } + +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/ElasticContext.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/ElasticContext.java index 6545b24513..a59d74dfcc 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/ElasticContext.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/ElasticContext.java @@ -23,10 +23,9 @@ import co.elastic.apm.agent.impl.baggage.BaggageContext; import co.elastic.apm.agent.impl.baggage.W3CBaggagePropagation; import co.elastic.apm.agent.tracer.Scope; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderSetter; import co.elastic.apm.agent.tracer.dispatch.HeaderUtils; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; import javax.annotation.Nullable; @@ -103,24 +102,13 @@ public boolean isEmpty() { return getSpan() == null && getBaggage().isEmpty(); } - - @Override - public final boolean propagateContext(C carrier, BinaryHeaderSetter headerSetter) { - AbstractSpan contextSpan = getSpan(); - if (contextSpan != null) { - contextSpan.setNonDiscardable(); - return contextSpan.getTraceContext().propagateTraceContext(carrier, headerSetter); - } - return false; - } - @Override - public final void propagateContext(C carrier, TextHeaderSetter headerSetter, @Nullable TextHeaderGetter headerGetter) { + public final void propagateContext(C carrier, HeaderSetter headerSetter, @Nullable HeaderGetter headerGetter) { propagateContext(carrier, headerSetter, carrier, headerGetter); } @Override - public void propagateContext(C1 carrier, TextHeaderSetter headerSetter, @Nullable C2 carrier2, @Nullable TextHeaderGetter headerGetter) { + public void propagateContext(C1 carrier, HeaderSetter headerSetter, @Nullable C2 carrier2, @Nullable HeaderGetter headerGetter) { AbstractSpan contextSpan = getSpan(); if (contextSpan != null) { if (headerGetter == null || carrier2 == null || !HeaderUtils.containsAny(TraceContext.TRACE_TEXTUAL_HEADERS, carrier2, headerGetter)) { @@ -137,7 +125,7 @@ public void propagateContext(C1 carrier, TextHeaderSetter headerSet } @Override - public boolean isPropagationRequired(C carrier, TextHeaderGetter headerGetter) { + public boolean isPropagationRequired(C carrier, HeaderGetter headerGetter) { AbstractSpan contextSpan = getSpan(); boolean traceContextPropagationRequired = contextSpan != null && !HeaderUtils.containsAny(TraceContext.TRACE_TEXTUAL_HEADERS, carrier, headerGetter); boolean baggagePropagationRequired = !getBaggage().isEmpty() && headerGetter.getFirstHeader(W3CBaggagePropagation.BAGGAGE_HEADER_NAME, carrier) == null; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Id.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Id.java index e76ab5513d..27aa8289b7 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Id.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Id.java @@ -18,8 +18,9 @@ */ package co.elastic.apm.agent.impl.transaction; -import co.elastic.apm.agent.util.HexUtils; +import co.elastic.apm.agent.report.serialize.HexSerializationUtils; import co.elastic.apm.agent.tracer.pooling.Recyclable; +import co.elastic.apm.agent.tracer.util.HexUtils; import com.dslplatform.json.JsonWriter; import javax.annotation.Nullable; @@ -65,10 +66,15 @@ public void fromHexString(String hexEncodedString, int offset) { onMutation(); } + public void fromHexString(T hexEncodedString, int offset, CharAccessor accessor) { + accessor.readHex(hexEncodedString, offset, data); + onMutation(); + } + /** * Sets the id based on a byte array * - * @param bytes the byte array used to fill this id's {@link #data} + * @param bytes the byte array used to fill this id's {@link #data} * @param offset the offset in the byte array * @return the number of read bytes which is equivalent to {@link #getLength()} */ @@ -162,7 +168,7 @@ private static boolean isAllZeros(byte[] bytes) { } public void writeAsHex(JsonWriter jw) { - HexUtils.writeBytesAsHex(data, jw); + HexSerializationUtils.writeBytesAsHex(data, jw); } public void writeAsHex(StringBuilder sb) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java index 7252a29737..f8854468f0 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java @@ -24,14 +24,16 @@ import co.elastic.apm.agent.impl.sampling.Sampler; import co.elastic.apm.agent.sdk.logging.Logger; import co.elastic.apm.agent.sdk.logging.LoggerFactory; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; import co.elastic.apm.agent.tracer.dispatch.HeaderRemover; +import co.elastic.apm.agent.tracer.dispatch.HeaderSetter; import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderSetter; import co.elastic.apm.agent.tracer.pooling.Recyclable; +import co.elastic.apm.agent.tracer.util.HexUtils; import co.elastic.apm.agent.util.ByteUtils; -import co.elastic.apm.agent.util.HexUtils; import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; @@ -56,23 +58,6 @@ * Header name Version Trace-Id Span-Id Flags * *

- * Binary representation (e.g. 0.11.0.0+ Kafka record header), based on - * https://github.com/elastic/apm/blob/main/docs/agent-development.md#binary-fields: - *

- *      traceparent     = version version_format
- *      version         = 1BYTE                   ; version is 0 in the current spec
- *      version_format  = "{ 0x0 }" trace-id "{ 0x1 }" parent-id "{ 0x2 }" trace-flags
- *      trace-id        = 16BYTES
- *      parent-id       = 8BYTES
- *      trace-flags     = 1BYTE  ; only the least significant bit is used
- * 
- * For example: - *
- * elasticapmparent:   [0,
- *                      0, 75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 0, 14, 71, 54,
- *                      1, 52, 240, 103, 170, 11, 169, 2, 183,
- *                      2, 1]
- * 
*/ public class TraceContext implements Recyclable, co.elastic.apm.agent.tracer.TraceContext { @@ -84,18 +69,6 @@ public class TraceContext implements Recyclable, co.elastic.apm.agent.tracer.Tra private static final int TEXT_HEADER_TRACE_ID_OFFSET = 3; private static final int TEXT_HEADER_PARENT_ID_OFFSET = 36; private static final int TEXT_HEADER_FLAGS_OFFSET = 53; - - public static final int BINARY_FORMAT_EXPECTED_LENGTH = 29; - private static final byte BINARY_FORMAT_CURRENT_VERSION = (byte) 0b0000_0000; - // one byte for the trace-id field id (0x00), followed by 16 bytes of the actual ID - private static final int BINARY_FORMAT_TRACE_ID_OFFSET = 1; - private static final byte BINARY_FORMAT_TRACE_ID_FIELD_ID = (byte) 0b0000_0000; - // one byte for the parent-id field id (0x01), followed by 8 bytes of the actual ID - private static final int BINARY_FORMAT_PARENT_ID_OFFSET = 18; - private static final byte BINARY_FORMAT_PARENT_ID_FIELD_ID = (byte) 0b0000_0001; - // one byte for the flags field id (0x02), followed by two bytes of flags contents - private static final int BINARY_FORMAT_FLAGS_OFFSET = 27; - private static final byte BINARY_FORMAT_FLAGS_FIELD_ID = (byte) 0b0000_0010; private static final Logger logger = LoggerFactory.getLogger(TraceContext.class); private static final Double SAMPLE_RATE_ZERO = 0d; @@ -124,66 +97,66 @@ public boolean asChildOf(TraceContext child, AbstractSpan parent) { return true; } }; - private static final HeaderGetter.HeaderConsumer TRACESTATE_HEADER_CONSUMER = new HeaderGetter.HeaderConsumer() { + + private static final HeaderGetter.HeaderConsumer STRING_TRACESTATE_HEADER_CONSUMER = new HeaderGetter.HeaderConsumer() { @Override public void accept(@Nullable String tracestateHeaderValue, TraceContext state) { - if (tracestateHeaderValue != null) { - state.traceState.addTextHeader(tracestateHeaderValue); - } + state.addTraceStateHeader(tracestateHeaderValue, CharAccessor.forCharSequence()); } }; - private static final HeaderChildContextCreator FROM_TRACE_CONTEXT_TEXT_HEADERS = - new HeaderChildContextCreator() { - @Override - public boolean asChildOf(TraceContext child, @Nullable Object carrier, HeaderGetter traceContextHeaderGetter) { - if (carrier == null) { - return false; - } - boolean isValid = false; - String traceparent = traceContextHeaderGetter.getFirstHeader(W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, carrier); - if (traceparent != null) { - isValid = child.asChildOf(traceparent); - } - - if (!isValid) { - // Look for the legacy Elastic traceparent header (in case this comes from an older agent) - traceparent = traceContextHeaderGetter.getFirstHeader(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, carrier); - if (traceparent != null) { - isValid = child.asChildOf(traceparent); - } - } + private static final HeaderGetter.HeaderConsumer UTF8_BYTES_TRACESTATE_HEADER_CONSUMER = new HeaderGetter.HeaderConsumer() { + @Override + public void accept(@Nullable byte[] tracestateHeaderValue, TraceContext state) { + state.addTraceStateHeader(tracestateHeaderValue, CharAccessor.forAsciiBytes()); + } + }; - if (isValid) { - // as per spec, the tracestate header can be multi-valued - traceContextHeaderGetter.forEach(TRACESTATE_HEADER_NAME, carrier, child, TRACESTATE_HEADER_CONSUMER); - } + public boolean asChildOf(@Nullable C carrier, HeaderGetter headerGetter) { + return asChildOf(carrier, headerGetter, true); + } - return isValid; - } - }; - private static final HeaderChildContextCreator FROM_TRACE_CONTEXT_BINARY_HEADERS = - new HeaderChildContextCreator() { - @Override - public boolean asChildOf(TraceContext child, @Nullable Object carrier, HeaderGetter traceContextHeaderGetter) { - if (carrier == null) { - return false; - } - byte[] traceparent = traceContextHeaderGetter.getFirstHeader(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, carrier); - if (traceparent != null) { - return child.asChildOf(traceparent); - } - return false; - } - }; + @SuppressWarnings("unchecked") + public boolean asChildOf(@Nullable C carrier, HeaderGetter headerGetter, boolean parseTraceState) { + CharAccessor headerValueAccessor; + HeaderGetter.HeaderConsumer traceStateConsumer; + if (headerGetter instanceof TextHeaderGetter) { + headerValueAccessor = (CharAccessor) CharAccessor.forCharSequence(); + traceStateConsumer = (HeaderGetter.HeaderConsumer) STRING_TRACESTATE_HEADER_CONSUMER; + } else if (headerGetter instanceof UTF8ByteHeaderGetter) { + headerValueAccessor = (CharAccessor) CharAccessor.forAsciiBytes(); + traceStateConsumer = (HeaderGetter.HeaderConsumer) UTF8_BYTES_TRACESTATE_HEADER_CONSUMER; + } else { + throw new IllegalArgumentException("HeaderGetter must be either a TextHeaderGetter or UTF8ByteHeaderGetter: " + headerGetter.getClass().getName()); + } + boolean valid = extractTraceParentFromHeaders(carrier, headerGetter, headerValueAccessor); + if (valid && parseTraceState) { + headerGetter.forEach(TRACESTATE_HEADER_NAME, carrier, this, traceStateConsumer); + } + return valid; + } - private static final ChildContextCreator AS_ROOT = new ChildContextCreator() { - @Override - public boolean asChildOf(TraceContext child, Object ignore) { + private boolean extractTraceParentFromHeaders(@Nullable C carrier, HeaderGetter traceContextHeaderGetter, CharAccessor charAccessor) { + if (carrier == null) { return false; } - }; + boolean isValid = false; + T traceparent = traceContextHeaderGetter.getFirstHeader(W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, carrier); + if (traceparent != null) { + isValid = asChildOf(traceparent, charAccessor); + } + + if (!isValid) { + // Look for the legacy Elastic traceparent header (in case this comes from an older agent) + traceparent = traceContextHeaderGetter.getFirstHeader(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, carrier); + if (traceparent != null) { + isValid = asChildOf(traceparent, charAccessor); + } + } + + return isValid; + } public static boolean containsTraceContextTextHeaders(C carrier, TextHeaderGetter headerGetter) { // We assume that this header is always present if we found any of the other headers. @@ -271,16 +244,6 @@ public static TraceContext with128BitId(ElasticApmTracer tracer) { return new TraceContext(tracer, Id.new128BitId()); } - @SuppressWarnings("unchecked") - public static HeaderChildContextCreator getFromTraceContextTextHeaders() { - return (HeaderChildContextCreator) FROM_TRACE_CONTEXT_TEXT_HEADERS; - } - - @SuppressWarnings("unchecked") - public static HeaderChildContextCreator getFromTraceContextBinaryHeaders() { - return (HeaderChildContextCreator) FROM_TRACE_CONTEXT_BINARY_HEADERS; - } - public static ChildContextCreator fromParentContext() { return FROM_PARENT_CONTEXT; } @@ -289,8 +252,8 @@ public static ChildContextCreator> fromParent() { return FROM_PARENT; } - public static ChildContextCreator asRoot() { - return AS_ROOT; + boolean asChildOf(String traceParentHeader) { + return asChildOf(traceParentHeader, CharAccessor.forCharSequence()); } /** @@ -299,38 +262,50 @@ public static ChildContextCreator asRoot() { * @param traceParentHeader traceparent text header value * @return {@literal true} if header value is valid, {@literal false} otherwise */ - boolean asChildOf(String traceParentHeader) { - traceParentHeader = traceParentHeader.trim(); + boolean asChildOf(T traceParentHeader, CharAccessor charAccessor) { + + int leadingWs = charAccessor.getLeadingWhitespaceCount(traceParentHeader); + int trailingWs = charAccessor.getTrailingWhitespaceCount(traceParentHeader); + try { - if (traceParentHeader.length() < TEXT_HEADER_EXPECTED_LENGTH) { - logger.warn("The traceparent header has to be at least 55 chars long, but was '{}'", traceParentHeader); + int trimmedLen = Math.max(0, charAccessor.length(traceParentHeader) - leadingWs - trailingWs); + if (trimmedLen < TEXT_HEADER_EXPECTED_LENGTH) { + logger.warn("The traceparent header has to be at least 55 chars long, but was '{}'", trimmedLen); return false; } - if (noDashAtPosition(traceParentHeader, TEXT_HEADER_TRACE_ID_OFFSET - 1) - || noDashAtPosition(traceParentHeader, TEXT_HEADER_PARENT_ID_OFFSET - 1) - || noDashAtPosition(traceParentHeader, TEXT_HEADER_FLAGS_OFFSET - 1)) { - logger.warn("The traceparent header has an invalid format: '{}'", traceParentHeader); + if (noDashAtPosition(traceParentHeader, leadingWs + TEXT_HEADER_TRACE_ID_OFFSET - 1, charAccessor) + || noDashAtPosition(traceParentHeader, leadingWs + TEXT_HEADER_PARENT_ID_OFFSET - 1, charAccessor) + || noDashAtPosition(traceParentHeader, leadingWs + TEXT_HEADER_FLAGS_OFFSET - 1, charAccessor)) { + if (logger.isWarnEnabled()) { + logger.warn("The traceparent header has an invalid format: '{}'", charAccessor.asString(traceParentHeader)); + } return false; } - if (traceParentHeader.length() > TEXT_HEADER_EXPECTED_LENGTH - && noDashAtPosition(traceParentHeader, TEXT_HEADER_EXPECTED_LENGTH)) { - logger.warn("The traceparent header has an invalid format: '{}'", traceParentHeader); + if (trimmedLen > TEXT_HEADER_EXPECTED_LENGTH + && noDashAtPosition(traceParentHeader, leadingWs + TEXT_HEADER_EXPECTED_LENGTH, charAccessor)) { + if (logger.isWarnEnabled()) { + logger.warn("The traceparent header has an invalid format: '{}'", charAccessor.asString(traceParentHeader)); + } return false; } - if (traceParentHeader.startsWith("ff")) { - logger.warn("Version ff is not supported"); + if (charAccessor.containsAtOffset(traceParentHeader, leadingWs, "ff")) { + if (logger.isWarnEnabled()) { + logger.warn("Version ff is not supported"); + } return false; } - byte version = HexUtils.getNextByte(traceParentHeader, 0); - if (version == 0 && traceParentHeader.length() > TEXT_HEADER_EXPECTED_LENGTH) { - logger.warn("The traceparent header has to be exactly 55 chars long for version 00, but was '{}'", traceParentHeader); + byte version = charAccessor.readHexByte(traceParentHeader, leadingWs); + if (version == 0 && trimmedLen > TEXT_HEADER_EXPECTED_LENGTH) { + if (logger.isWarnEnabled()) { + logger.warn("The traceparent header has to be exactly 55 chars long for version 00, but was '{}'", charAccessor.asString(traceParentHeader)); + } return false; } - traceId.fromHexString(traceParentHeader, TEXT_HEADER_TRACE_ID_OFFSET); + traceId.fromHexString(traceParentHeader, leadingWs + TEXT_HEADER_TRACE_ID_OFFSET, charAccessor); if (traceId.isEmpty()) { return false; } - parentId.fromHexString(traceParentHeader, TEXT_HEADER_PARENT_ID_OFFSET); + parentId.fromHexString(traceParentHeader, leadingWs + TEXT_HEADER_PARENT_ID_OFFSET, charAccessor); if (parentId.isEmpty()) { return false; } @@ -339,7 +314,7 @@ && noDashAtPosition(traceParentHeader, TEXT_HEADER_EXPECTED_LENGTH)) { // TODO don't blindly trust the flags from the caller // consider implement rate limiting and/or having a list of trusted sources // trace the request if it's either requested or if the parent has recorded it - flags = HexUtils.getNextByte(traceParentHeader, TEXT_HEADER_FLAGS_OFFSET); + flags = charAccessor.readHexByte(traceParentHeader, TEXT_HEADER_FLAGS_OFFSET + leadingWs); clock.init(); return true; } catch (IllegalArgumentException e) { @@ -350,66 +325,14 @@ && noDashAtPosition(traceParentHeader, TEXT_HEADER_EXPECTED_LENGTH)) { } } - /** - * Tries to set trace context identifiers (Id, parent, ...) from traceparent binary header value - * - * @param traceParentHeader traceparent binary header value - * @return {@literal true} if header value is valid, {@literal false} otherwise - */ - boolean asChildOf(byte[] traceParentHeader) { - if (logger.isTraceEnabled()) { - logger.trace("Binary header content UTF-8-decoded: {}", new String(traceParentHeader, StandardCharsets.UTF_8)); - } - try { - if (traceParentHeader.length < BINARY_FORMAT_EXPECTED_LENGTH) { - logger.warn("The traceparent header has to be at least 29 bytes long, but is not"); - return false; - } - // Current spec says: "Note, that parsing should not treat any additional bytes in the end of the buffer - // as an invalid status. Those fields can be added for padding purposes.", which means there is no upper - // limit. In addition, no version is specified as erroneous, so version is non-informative. - - byte fieldId = traceParentHeader[BINARY_FORMAT_TRACE_ID_OFFSET]; - if (fieldId != BINARY_FORMAT_TRACE_ID_FIELD_ID) { - logger.warn("Wrong trace-id field identifier: {}", fieldId); - return false; - } - traceId.fromBytes(traceParentHeader, BINARY_FORMAT_TRACE_ID_OFFSET + 1); - if (traceId.isEmpty()) { - return false; - } - fieldId = traceParentHeader[BINARY_FORMAT_PARENT_ID_OFFSET]; - if (fieldId != BINARY_FORMAT_PARENT_ID_FIELD_ID) { - logger.warn("Wrong parent-id field identifier: {}", fieldId); - return false; - } - parentId.fromBytes(traceParentHeader, BINARY_FORMAT_PARENT_ID_OFFSET + 1); - if (parentId.isEmpty()) { - return false; - } - id.setToRandomValue(); - transactionId.copyFrom(id); - fieldId = traceParentHeader[BINARY_FORMAT_FLAGS_OFFSET]; - if (fieldId != BINARY_FORMAT_FLAGS_FIELD_ID) { - logger.warn("Wrong flags field identifier: {}", fieldId); - return false; - } - // TODO don't blindly trust the flags from the caller - // consider implement rate limiting and/or having a list of trusted sources - // trace the request if it's either requested or if the parent has recorded it - flags = traceParentHeader[BINARY_FORMAT_FLAGS_OFFSET + 1]; - clock.init(); - return true; - } catch (IllegalArgumentException e) { - logger.warn(e.getMessage()); - return false; - } finally { - onMutation(); - } + private boolean noDashAtPosition(T traceParentHeader, int index, CharAccessor accessor) { + return accessor.charAt(traceParentHeader, index) != '-'; } - private boolean noDashAtPosition(String traceParentHeader, int index) { - return traceParentHeader.charAt(index) != '-'; + private void addTraceStateHeader(@Nullable T tracestateHeaderValue, CharAccessor charAccessor) { + if (tracestateHeaderValue != null) { + traceState.addTextHeader(charAccessor.asString(tracestateHeaderValue)); + } } public void asRootSpan(Sampler sampler) { @@ -541,54 +464,63 @@ String getIncomingTraceParentHeader() { * @param headerSetter a setter implementing the actual addition of headers to the headers carrier * @param the header carrier type, for example - an HTTP request */ - void propagateTraceContext(C carrier, TextHeaderSetter headerSetter) { + void propagateTraceContext(C carrier, HeaderSetter headerSetter) { if (coreConfiguration.isOutgoingTraceContextHeadersInjectionDisabled()) { logger.debug("Outgoing TraceContext header injection is disabled"); return; } - String outgoingTraceParent = getOutgoingTraceParentTextHeader().toString(); + T outgoingTraceParent = getOutgoingTraceParentTextHeader(headerSetter); headerSetter.setHeader(W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, outgoingTraceParent, carrier); if (coreConfiguration.isElasticTraceparentHeaderEnabled()) { headerSetter.setHeader(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, outgoingTraceParent, carrier); } - String outgoingTraceState = traceState.toTextHeader(); + T outgoingTraceState = getOutgoingTraceStateHeader(headerSetter); if (outgoingTraceState != null) { headerSetter.setHeader(TRACESTATE_HEADER_NAME, outgoingTraceState, carrier); } logger.trace("Trace context headers added to {}", carrier); } - /** - * Sets Trace context binary headers, using this context as parent, on the provided carrier using the provided setter - * - * @param carrier the binary headers carrier - * @param headerSetter a setter implementing the actual addition of headers to the headers carrier - * @param the header carrier type, for example - a Kafka record - * @return true if Trace Context headers were set; false otherwise - */ - boolean propagateTraceContext(C carrier, BinaryHeaderSetter headerSetter) { - if (coreConfiguration.isOutgoingTraceContextHeadersInjectionDisabled()) { - logger.debug("Outgoing TraceContext header injection is disabled"); - return false; + T getOutgoingTraceParentTextHeader(HeaderSetter headerSetter) { + StringBuilder outgoingTraceParentTextHeader = getOutgoingTraceParentTextHeader(); + if (headerSetter instanceof TextHeaderSetter) { + return (T) outgoingTraceParentTextHeader.toString(); + } else if (headerSetter instanceof UTF8ByteHeaderSetter) { + int length = outgoingTraceParentTextHeader.length(); + byte[] result = new byte[length]; + for (int i = 0; i < length; i++) { + char c = outgoingTraceParentTextHeader.charAt(i); + if (c > 127) { + throw new IllegalStateException("Expected traceparent header to be ascii only"); + } + result[i] = (byte) c; + } + return (T) result; + } else { + throw new IllegalArgumentException("HeaderSetter must be either a TextHeaderSetter or UTF8ByteHeaderSetter: " + headerSetter.getClass().getName()); } - byte[] buffer = headerSetter.getFixedLengthByteArray(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, BINARY_FORMAT_EXPECTED_LENGTH); - if (buffer == null || buffer.length != BINARY_FORMAT_EXPECTED_LENGTH) { - logger.warn("Header setter {} failed to provide a byte buffer with the proper length. Allocating a buffer for each header.", - headerSetter.getClass().getName()); - buffer = new byte[BINARY_FORMAT_EXPECTED_LENGTH]; + } + + @Nullable + T getOutgoingTraceStateHeader(HeaderSetter headerSetter) { + String outgoingTraceState = traceState.toTextHeader(); + if (outgoingTraceState == null) { + return null; } - boolean headerBufferFilled = fillOutgoingTraceParentBinaryHeader(buffer); - if (headerBufferFilled) { - headerSetter.setHeader(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, buffer, carrier); + if (headerSetter instanceof TextHeaderSetter) { + return (T) outgoingTraceState; + } else if (headerSetter instanceof UTF8ByteHeaderSetter) { + return (T) outgoingTraceState.getBytes(StandardCharsets.UTF_8); + } else { + throw new IllegalArgumentException("HeaderSetter must be either a TextHeaderSetter or UTF8ByteHeaderSetter: " + headerSetter.getClass().getName()); } - return headerBufferFilled; } /** - * @return the value of the {@code traceparent} header for downstream services. + * @return the value of the {@code traceparent} header for downstream services. */ StringBuilder getOutgoingTraceParentTextHeader() { if (outgoingTextHeader.length() == 0) { @@ -613,31 +545,6 @@ private void fillTraceParentHeader(StringBuilder sb, Id spanId) { HexUtils.writeByteAsHex(flags, sb); } - /** - * Fills the given byte array with a binary representation of the {@code traceparent} header for downstream services. - * - * @param buffer buffer to fill - * @return true if buffer was filled, false otherwise - */ - private boolean fillOutgoingTraceParentBinaryHeader(byte[] buffer) { - if (buffer.length < BINARY_FORMAT_EXPECTED_LENGTH) { - logger.warn("Given byte array does not have the minimal required length - {}", BINARY_FORMAT_EXPECTED_LENGTH); - return false; - } - buffer[0] = BINARY_FORMAT_CURRENT_VERSION; - buffer[BINARY_FORMAT_TRACE_ID_OFFSET] = BINARY_FORMAT_TRACE_ID_FIELD_ID; - traceId.toBytes(buffer, BINARY_FORMAT_TRACE_ID_OFFSET + 1); - buffer[BINARY_FORMAT_PARENT_ID_OFFSET] = BINARY_FORMAT_PARENT_ID_FIELD_ID; - // for unsampled traces, propagate the ID of the transaction in calls to downstream services - // such that the parentID of those transactions point to a transaction that exists - // remember that we do report unsampled transactions - Id parentId = isSampled() ? id : transactionId; - parentId.toBytes(buffer, BINARY_FORMAT_PARENT_ID_OFFSET + 1); - buffer[BINARY_FORMAT_FLAGS_OFFSET] = BINARY_FORMAT_FLAGS_FIELD_ID; - buffer[BINARY_FORMAT_FLAGS_OFFSET + 1] = flags; - return true; - } - public boolean isChildOf(TraceContext other) { return other.getTraceId().equals(traceId) && other.getId().equals(parentId); } @@ -748,20 +655,6 @@ public void serialize(byte[] buffer) { ByteUtils.putLong(buffer, offset, clock.getOffset()); } - private void asChildOf(byte[] buffer, @Nullable String serviceName, @Nullable String serviceVersion) { - int offset = 0; - offset += traceId.fromBytes(buffer, offset); - offset += parentId.fromBytes(buffer, offset); - offset += transactionId.fromBytes(buffer, offset); - id.setToRandomValue(); - flags = buffer[offset++]; - discardable = buffer[offset++] == (byte) 1; - clock.init(ByteUtils.getLong(buffer, offset)); - this.serviceName = serviceName; - this.serviceVersion = serviceVersion; - onMutation(); - } - public void deserialize(byte[] buffer, @Nullable String serviceName, @Nullable String serviceVersion) { int offset = 0; offset += traceId.fromBytes(buffer, offset); @@ -775,10 +668,6 @@ public void deserialize(byte[] buffer, @Nullable String serviceName, @Nullable S onMutation(); } - public static void deserializeSpanId(Id id, byte[] buffer) { - id.fromBytes(buffer, 16); - } - public static long getSpanId(byte[] serializedTraceContext) { return ByteUtils.getLong(serializedTraceContext, 16); } @@ -795,10 +684,6 @@ public interface ChildContextCreator { boolean asChildOf(TraceContext child, T parent); } - public interface HeaderChildContextCreator { - boolean asChildOf(TraceContext child, @Nullable C carrier, HeaderGetter headerGetter); - } - public TraceContext copy() { final TraceContext copy; final int idLength = id.getLength(); diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java index b907f26b9d..aabac69aed 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java @@ -32,7 +32,6 @@ import co.elastic.apm.agent.metrics.Timer; import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; import co.elastic.apm.agent.tracer.util.ResultUtil; import co.elastic.apm.agent.util.KeyListConcurrentHashMap; import org.HdrHistogram.WriterReaderPhaser; @@ -133,7 +132,6 @@ public Transaction startRoot(long epochMicros, Sampler sampler, Baggage bagg } public Transaction start( - TraceContext.HeaderChildContextCreator childContextCreator, @Nullable C parent, HeaderGetter headerGetter, long epochMicros, @@ -143,13 +141,9 @@ public Transaction start( if (parent == null) { return startRoot(epochMicros, sampler, baseBaggage); } - if (headerGetter instanceof TextHeaderGetter) { - Baggage.Builder baggageBuilder = baseBaggage.toBuilder(); - W3CBaggagePropagation.parse(parent, (TextHeaderGetter) headerGetter, baggageBuilder); - this.baggage = baggageBuilder.build(); - } else { - this.baggage = baseBaggage; - } + Baggage.Builder baggageBuilder = baseBaggage.toBuilder(); + W3CBaggagePropagation.parse(parent, headerGetter, baggageBuilder); + this.baggage = baggageBuilder.build(); CoreConfiguration.TraceContinuationStrategy traceContinuationStrategy = coreConfig.getTraceContinuationStrategy(); boolean restartTrace = false; if (traceContinuationStrategy.equals(RESTART)) { @@ -159,10 +153,10 @@ public Transaction start( } if (restartTrace) { // need to add a span link - addSpanLink(childContextCreator, headerGetter, parent); + addSpanLink(headerGetter, parent); traceContext.asRootSpan(sampler); } else { - boolean valid = childContextCreator.asChildOf(traceContext, parent, headerGetter); + boolean valid = traceContext.asChildOf(parent, headerGetter); if (!valid) { traceContext.asRootSpan(sampler); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java index d944cc72da..6ec3676c4b 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java @@ -66,7 +66,6 @@ import co.elastic.apm.agent.sdk.logging.LoggerFactory; import co.elastic.apm.agent.tracer.metadata.PotentiallyMultiValuedMap; import co.elastic.apm.agent.tracer.pooling.Recyclable; -import co.elastic.apm.agent.util.HexUtils; import com.dslplatform.json.BoolConverter; import com.dslplatform.json.DslJson; import com.dslplatform.json.JsonWriter; @@ -1867,7 +1866,7 @@ private void writeHexArray(String fieldName, @Nullable LongList longList) { jw.writeByte(COMMA); } jw.writeByte(QUOTE); - HexUtils.writeAsHex(longList.get(i), jw); + HexSerializationUtils.writeAsHex(longList.get(i), jw); jw.writeByte(QUOTE); } jw.writeByte(ARRAY_END); diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/HexSerializationUtils.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/HexSerializationUtils.java new file mode 100644 index 0000000000..40378bc7cf --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/HexSerializationUtils.java @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.agent.report.serialize; + +import co.elastic.apm.agent.tracer.util.HexUtils; +import com.dslplatform.json.JsonWriter; + +public class HexSerializationUtils { + public static void writeBytesAsHex(byte[] bytes, JsonWriter jw) { + for (int i = 0; i < bytes.length; i++) { + writeHexByte(jw, bytes[i]); + } + } + + private static void writeHexByte(JsonWriter jw, byte b) { + int v = b & 0xFF; + jw.writeByte((byte) HexUtils.HEX_CHARS[v >>> 4]); + jw.writeByte((byte) HexUtils.HEX_CHARS[v & 0x0F]); + } + + public static void writeAsHex(long l, JsonWriter jw) { + writeHexByte(jw, (byte) (l >> 56)); + writeHexByte(jw, (byte) (l >> 48)); + writeHexByte(jw, (byte) (l >> 40)); + writeHexByte(jw, (byte) (l >> 32)); + writeHexByte(jw, (byte) (l >> 24)); + writeHexByte(jw, (byte) (l >> 16)); + writeHexByte(jw, (byte) (l >> 8)); + writeHexByte(jw, (byte) l); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/BinaryHeaderMapAccessor.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/BinaryHeaderMapAccessor.java deleted file mode 100644 index 29c6a5fb5e..0000000000 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/BinaryHeaderMapAccessor.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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. - */ -package co.elastic.apm.agent.impl; - -import co.elastic.apm.agent.impl.transaction.TraceContext; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; -import co.elastic.apm.agent.tracer.dispatch.HeaderRemover; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; - -public class BinaryHeaderMapAccessor implements BinaryHeaderGetter>, - BinaryHeaderSetter>, HeaderRemover> { - - public static final BinaryHeaderMapAccessor INSTANCE = new BinaryHeaderMapAccessor(); - - private final Map headerCache = new HashMap<>(); - - private BinaryHeaderMapAccessor() { - } - - @Nullable - @Override - public byte[] getFirstHeader(String headerName, Map headerMap) { - return headerMap.get(headerName); - } - - @Override - public void forEach(String headerName, Map carrier, S state, HeaderConsumer consumer) { - byte[] headerValue = carrier.get(headerName); - if (headerValue != null) { - consumer.accept(headerValue, state); - } - } - - @Nullable - @Override - public byte[] getFixedLengthByteArray(String headerName, int length) { - headerCache.computeIfAbsent(headerName, k -> new byte[TraceContext.BINARY_FORMAT_EXPECTED_LENGTH]); - return headerCache.get(headerName); - } - - @Override - public void setHeader(String headerName, byte[] headerValue, Map headerMap) { - headerMap.put(headerName, headerValue); - } - - @Override - public void remove(String headerName, Map headerMap) { - headerMap.remove(headerName); - } -} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/Utf8HeaderMapAccessor.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/Utf8HeaderMapAccessor.java new file mode 100644 index 0000000000..1c8bbd09d6 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/Utf8HeaderMapAccessor.java @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.agent.impl; + +import co.elastic.apm.agent.tracer.dispatch.AbstractHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderRemover; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderSetter; + +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class Utf8HeaderMapAccessor extends AbstractHeaderGetter> implements + UTF8ByteHeaderGetter>, UTF8ByteHeaderSetter>, HeaderRemover> { + + public static final Utf8HeaderMapAccessor INSTANCE = new Utf8HeaderMapAccessor(); + + private Utf8HeaderMapAccessor() { + } + + @Nullable + @Override + public byte[] getFirstHeader(String headerName, Map headerMap) { + String result = headerMap.get(headerName); + return result == null ? null : result.getBytes(StandardCharsets.UTF_8); + } + + @Override + public void setHeader(String headerName, byte[] headerValue, Map headerMap) { + headerMap.put(headerName, new String(headerValue, StandardCharsets.UTF_8)); + } + + @Override + public void remove(String headerName, Map carrier) { + carrier.remove(headerName); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagationTest.java index ddac6ba382..13490f3c5b 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagationTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/baggage/W3CBaggagePropagationTest.java @@ -19,11 +19,15 @@ package co.elastic.apm.agent.impl.baggage; import co.elastic.apm.agent.impl.TextHeaderMapAccessor; +import co.elastic.apm.agent.impl.Utf8HeaderMapAccessor; import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderGetter; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -43,20 +47,30 @@ public void testComplexValue() { .put("my_key!", "hello, world!?%ä=", "metadata=blub") .build(); - Map resultHeaders = new HashMap<>(); - W3CBaggagePropagation.propagate(baggage, resultHeaders, TextHeaderMapAccessor.INSTANCE); + Map resultHeaders = doPropagate(baggage); assertThat(resultHeaders) .hasSize(1) .containsEntry("baggage", "foo=bar,my_key!=hello%2C%20world!%3F%25%C3%A4%3D;metadata=blub"); } + @NotNull + private Map doPropagate(Baggage baggage) { + Map resultHeaders = new HashMap<>(); + W3CBaggagePropagation.propagate(baggage, resultHeaders, TextHeaderMapAccessor.INSTANCE); + + Map utf8Headers = new HashMap<>(); + W3CBaggagePropagation.propagate(baggage, utf8Headers, Utf8HeaderMapAccessor.INSTANCE); + assertThat(resultHeaders).isEqualTo(utf8Headers); + + return resultHeaders; + } + @Test public void testEmptyBaggage() { Baggage baggage = Baggage.builder().build(); - Map resultHeaders = new HashMap<>(); - W3CBaggagePropagation.propagate(baggage, resultHeaders, TextHeaderMapAccessor.INSTANCE); + Map resultHeaders = doPropagate(baggage); assertThat(resultHeaders).hasSize(0); } @@ -68,8 +82,7 @@ public void testInvalidKeysIgnored() { .put("bad,key", "42") .build(); - Map resultHeaders = new HashMap<>(); - W3CBaggagePropagation.propagate(baggage, resultHeaders, TextHeaderMapAccessor.INSTANCE); + Map resultHeaders = doPropagate(baggage); assertThat(resultHeaders) .hasSize(1) @@ -83,8 +96,7 @@ public void testOnlyInvalidKeys() { .put("bad,key", "42") .build(); - Map resultHeaders = new HashMap<>(); - W3CBaggagePropagation.propagate(baggage, resultHeaders, TextHeaderMapAccessor.INSTANCE); + Map resultHeaders = doPropagate(baggage); assertThat(resultHeaders).hasSize(0); } @@ -95,49 +107,79 @@ public void testOnlyInvalidKeys() { public class Parsing { @Test - public void testComplexValue() { + public void testComplexValueString() { String[] baggageHeaders = { "foo=bar,my_key!=hello%2C%20world!%3F%25%C3%A4%3D;metadata=blub,,bar=baz;override=me,foo=bar2;overridden=meta", "bar=baz2" }; - TextHeaderGetter headerGetter = new TextHeaderGetter() { - @Nullable - @Override - public String getFirstHeader(String headerName, String[] baggageHeaders) { - if (headerName.equals("baggage")) { - return baggageHeaders[0]; - } - return null; - } - - @Override - public void forEach(String headerName, String[] baggageHeaders, S state, HeaderConsumer consumer) { - if (headerName.equals("baggage")) { - for (String val : baggageHeaders) { - consumer.accept(val, state); - } - } - } - }; Baggage.Builder resultBuilder = Baggage.builder(); - W3CBaggagePropagation.parse(baggageHeaders, headerGetter, resultBuilder); + W3CBaggagePropagation.parse(baggageHeaders, new TextBaggageheaderGetter(), resultBuilder); assertThat(resultBuilder.build()) .hasSize(3) .containsEntry("my_key!", "hello, world!?%ä=", "metadata=blub") .containsEntry("bar", "baz2", null) .containsEntry("foo", "bar2", "overridden=meta"); + + Baggage.Builder utf8ResultBuilder = Baggage.builder(); + W3CBaggagePropagation.parse(baggageHeaders, new UTF8BaggageheaderGetter(), utf8ResultBuilder); + assertThat(utf8ResultBuilder.build()).isEqualTo(resultBuilder.build()); } + @Test public void testNoValue() { - Baggage.Builder resultBuilder = Baggage.builder(); - W3CBaggagePropagation.parse(Collections.emptyMap(), TextHeaderMapAccessor.INSTANCE, resultBuilder); + Baggage.Builder textResultBuilder = Baggage.builder(); + W3CBaggagePropagation.parse(Collections.emptyMap(), TextHeaderMapAccessor.INSTANCE, textResultBuilder); + assertThat(textResultBuilder.build()).hasSize(0); - assertThat(resultBuilder.build()).hasSize(0); + Baggage.Builder utf8ResultBuilder = Baggage.builder(); + W3CBaggagePropagation.parse(Collections.emptyMap(), Utf8HeaderMapAccessor.INSTANCE, utf8ResultBuilder); + assertThat(textResultBuilder.build()).hasSize(0); } } + private static class TextBaggageheaderGetter implements TextHeaderGetter { + @Nullable + @Override + public String getFirstHeader(String headerName, String[] baggageHeaders) { + if (headerName.equals("baggage")) { + return baggageHeaders[0]; + } + return null; + } + + @Override + public void forEach(String headerName, String[] baggageHeaders, S state, HeaderConsumer consumer) { + if (headerName.equals("baggage")) { + for (String val : baggageHeaders) { + consumer.accept(val, state); + } + } + } + } + + + private static class UTF8BaggageheaderGetter implements UTF8ByteHeaderGetter { + @Nullable + @Override + public byte[] getFirstHeader(String headerName, String[] baggageHeaders) { + if (headerName.equals("baggage")) { + return baggageHeaders[0].getBytes(StandardCharsets.UTF_8); + } + return null; + } + + @Override + public void forEach(String headerName, String[] baggageHeaders, S state, HeaderConsumer consumer) { + if (headerName.equals("baggage")) { + for (String val : baggageHeaders) { + consumer.accept(val.getBytes(StandardCharsets.UTF_8), state); + } + } + } + } + } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/AbstractCompressionStrategyTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/AbstractCompressionStrategyTest.java index b37021c739..99c576f68b 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/AbstractCompressionStrategyTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/AbstractCompressionStrategyTest.java @@ -25,12 +25,14 @@ import co.elastic.apm.agent.impl.context.ServiceTarget; import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThat; @@ -195,7 +197,7 @@ void testContextPropagationStopsRegularCompression() { runInTransactionScope(t -> { startExitSpan(t).end(); Span span = startExitSpan(t); - span.propagateContext(new HashMap(), (h, v, c) -> { + span.propagateContext(new HashMap(), (TextHeaderSetter>) (h, v, c) -> { c.put(h, v); }, null); span.end(); @@ -217,7 +219,7 @@ void testContextPropagationStopsCompositeCompression() { startExitSpan(t).end(); startExitSpan(t).end(); Span span = startExitSpan(t); - span.propagateContext(new HashMap(), (h, v, c) -> { + span.propagateContext(new HashMap(), (TextHeaderSetter>) (h, v, c) -> { c.put(h, v); }, null); span.end(); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/CharAccessorTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/CharAccessorTest.java new file mode 100644 index 0000000000..2cb886a729 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/CharAccessorTest.java @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.agent.impl.transaction; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; +import java.util.stream.Stream; + +import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CharAccessorTest { + + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedTest(name = "{index} {0}") + @MethodSource("testArguments") + @interface TestForAllAccessors { + } + + private static Stream testArguments() { + return Stream.of( + Arguments.of(Named.of("CharSequence", CharAccessor.forCharSequence()), (Function) (input) -> input), + Arguments.of(Named.of("ascii byte[]", CharAccessor.forAsciiBytes()), (Function) (input) -> input.getBytes(StandardCharsets.US_ASCII)) + ); + } + + @TestForAllAccessors + void testLength(CharAccessor accessor, Function inputConverter) { + assertThat(accessor.length(inputConverter.apply(""))).isEqualTo(0); + assertThat(accessor.length(inputConverter.apply("abc"))).isEqualTo(3); + } + + @TestForAllAccessors + void testCharAt(CharAccessor accessor, Function inputConverter) { + T input = inputConverter.apply("abcde"); + assertThat(accessor.charAt(input, 0)).isEqualTo('a'); + assertThat(accessor.charAt(input, 2)).isEqualTo('c'); + assertThat(accessor.charAt(input, 4)).isEqualTo('e'); + assertThatThrownBy(() -> accessor.charAt(input, -1)); + assertThatThrownBy(() -> accessor.charAt(input, 5)); + } + + @TestForAllAccessors + void testAsString(CharAccessor accessor, Function inputConverter) { + assertThat(accessor.asString(inputConverter.apply(""))).isEqualTo(""); + assertThat(accessor.asString(inputConverter.apply("abc"))).isEqualTo("abc"); + } + + @TestForAllAccessors + void testHexByteDecode(CharAccessor accessor, Function inputConverter) { + assertThat(accessor.readHexByte(inputConverter.apply("0a"), 0)).isEqualTo((byte) 0x0a); + assertThat(accessor.readHexByte(inputConverter.apply("ff"), 0)).isEqualTo((byte) 0xFF); + assertThat(accessor.readHexByte(inputConverter.apply("zz23"), 2)).isEqualTo((byte) 0x23); + + assertThatThrownBy(() -> accessor.readHexByte(inputConverter.apply("ab"), -1)); + assertThatThrownBy(() -> accessor.readHexByte(inputConverter.apply("a"), 0)); + assertThatThrownBy(() -> accessor.readHexByte(inputConverter.apply("ab"), 1)); + assertThatThrownBy(() -> accessor.readHexByte(inputConverter.apply("1g"), 0)); + } + + @TestForAllAccessors + void testHexByteArrayDecode(CharAccessor accessor, Function inputConverter) { + + T input = inputConverter.apply("0123456789abcdef"); + + byte[] readFull = new byte[8]; + accessor.readHex(input, 0, readFull); + assertThat(readFull).containsExactly(0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF); + + byte[] readPartial = new byte[2]; + accessor.readHex(input, 3, readPartial); + assertThat(readPartial).containsExactly(0x34, 0x56); + + assertThatThrownBy(() -> accessor.readHex(input, 1, new byte[8])); + assertThatThrownBy(() -> accessor.readHex(input, -1, new byte[8])); + } + + @TestForAllAccessors + void testLeadingWhitespaceCounting(CharAccessor accessor, Function inputConverter) { + assertThat(accessor.getLeadingWhitespaceCount(inputConverter.apply(""))).isEqualTo(0); + assertThat(accessor.getLeadingWhitespaceCount(inputConverter.apply(" \t\r\nabcd"))).isEqualTo(4); + assertThat(accessor.getLeadingWhitespaceCount(inputConverter.apply(" a\t\r\n"))).isEqualTo(1); + assertThat(accessor.getLeadingWhitespaceCount(inputConverter.apply("a\t\r\n"))).isEqualTo(0); + } + + @TestForAllAccessors + void testTrailingWhitespaceCounting(CharAccessor accessor, Function inputConverter) { + assertThat(accessor.getTrailingWhitespaceCount(inputConverter.apply(""))).isEqualTo(0); + assertThat(accessor.getTrailingWhitespaceCount(inputConverter.apply("abcd \t\r\n"))).isEqualTo(4); + assertThat(accessor.getTrailingWhitespaceCount(inputConverter.apply("\t\r\na "))).isEqualTo(1); + assertThat(accessor.getTrailingWhitespaceCount(inputConverter.apply("\t\r\na"))).isEqualTo(0); + } + + @TestForAllAccessors + void testContainsAtOffset(CharAccessor accessor, Function inputConverter) { + + assertThat(accessor.containsAtOffset(inputConverter.apply(""), 0, "")).isTrue(); + + T abc = inputConverter.apply("abc"); + assertThat(accessor.containsAtOffset(abc, 0, "abc")).isTrue(); + assertThat(accessor.containsAtOffset(abc, 1, "bc")).isTrue(); + assertThat(accessor.containsAtOffset(abc, 0, "ab")).isTrue(); + assertThat(accessor.containsAtOffset(abc, 1, "b")).isTrue(); + + assertThat(accessor.containsAtOffset(abc, 1, "a")).isFalse(); + assertThat(accessor.containsAtOffset(abc, 1, "bcd")).isFalse(); + assertThat(accessor.containsAtOffset(abc, 0, "A")).isFalse(); + assertThat(accessor.containsAtOffset(abc, 0, "abcd")).isFalse(); + + assertThatThrownBy(() -> accessor.containsAtOffset(inputConverter.apply("a"), 2, "")); + assertThatThrownBy(() -> accessor.containsAtOffset(inputConverter.apply("a"), -1, "")); + } + +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/SpanTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/SpanTest.java index 3fea10b4c3..7b7bbae19b 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/SpanTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/SpanTest.java @@ -19,9 +19,9 @@ package co.elastic.apm.agent.impl.transaction; import co.elastic.apm.agent.MockTracer; -import co.elastic.apm.agent.impl.BinaryHeaderMapAccessor; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.TextHeaderMapAccessor; +import co.elastic.apm.agent.impl.Utf8HeaderMapAccessor; import co.elastic.apm.agent.impl.baggage.Baggage; import co.elastic.apm.agent.impl.sampling.ConstantSampler; import co.elastic.apm.agent.objectpool.TestObjectPoolFactory; @@ -138,7 +138,6 @@ void testSpanLinks() { Map textTraceContextCarrier = new HashMap<>(); parent1.propagateContext(textTraceContextCarrier, TextHeaderMapAccessor.INSTANCE, null); assertThat(testSpan.addSpanLink( - TraceContext.getFromTraceContextTextHeaders(), TextHeaderMapAccessor.INSTANCE, textTraceContextCarrier) ).isTrue(); @@ -146,12 +145,11 @@ void testSpanLinks() { assertThat(objectPoolFactory.getSpanLinksPool().getRequestedObjectCount()).isEqualTo(1); assertThat(testSpan.getSpanLinks()).hasSize(1); Span parent2 = transaction.createSpan(); - Map binaryTraceContextCarrier = new HashMap<>(); - parent2.propagateContext(binaryTraceContextCarrier, BinaryHeaderMapAccessor.INSTANCE); + Map utfTraceContextCarrier = new HashMap<>(); + parent2.propagateContext(utfTraceContextCarrier, Utf8HeaderMapAccessor.INSTANCE, null); assertThat(testSpan.addSpanLink( - TraceContext.getFromTraceContextBinaryHeaders(), - BinaryHeaderMapAccessor.INSTANCE, - binaryTraceContextCarrier) + TextHeaderMapAccessor.INSTANCE, + utfTraceContextCarrier) ).isTrue(); assertThat(objectPoolFactory.getSpanLinksPool().getObjectsInPool()).isEqualTo(0); assertThat(objectPoolFactory.getSpanLinksPool().getRequestedObjectCount()).isEqualTo(2); @@ -175,13 +173,11 @@ void testSpanLinksUniqueness() { Map textTraceContextCarrier = new HashMap<>(); parent1.propagateContext(textTraceContextCarrier, TextHeaderMapAccessor.INSTANCE, null); assertThat(testSpan.addSpanLink( - TraceContext.getFromTraceContextTextHeaders(), TextHeaderMapAccessor.INSTANCE, textTraceContextCarrier) ).isTrue(); assertThat(testSpan.getSpanLinks()).hasSize(1); assertThat(testSpan.addSpanLink( - TraceContext.getFromTraceContextTextHeaders(), TextHeaderMapAccessor.INSTANCE, textTraceContextCarrier) ).isFalse(); @@ -192,7 +188,6 @@ void testSpanLinksUniqueness() { // verifying that uniqueness cache is cleared properly as well assertThat(testSpan.addSpanLink( - TraceContext.getFromTraceContextTextHeaders(), TextHeaderMapAccessor.INSTANCE, textTraceContextCarrier) ).isTrue(); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceContextTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceContextTest.java index 2c75ccc6bc..e1f5c0a1d1 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceContextTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceContextTest.java @@ -22,16 +22,16 @@ import co.elastic.apm.agent.MockTracer; import co.elastic.apm.agent.configuration.CoreConfiguration; import co.elastic.apm.agent.configuration.SpyConfiguration; -import co.elastic.apm.agent.impl.BinaryHeaderMapAccessor; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.ElasticApmTracerBuilder; import co.elastic.apm.agent.impl.TextHeaderMapAccessor; +import co.elastic.apm.agent.impl.Utf8HeaderMapAccessor; import co.elastic.apm.agent.impl.sampling.ConstantSampler; import co.elastic.apm.agent.impl.sampling.Sampler; import co.elastic.apm.agent.objectpool.TestObjectPoolFactory; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderSetter; import co.elastic.apm.agent.tracer.metadata.PotentiallyMultiValuedMap; -import co.elastic.apm.agent.util.HexUtils; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -39,9 +39,9 @@ import org.stagemonitor.configuration.ConfigurationRegistry; import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -67,8 +67,8 @@ public void setup() { * Test flow: * 1. create a parent context from a fixed string * 2. create a child based on the string header - test {@link TraceContext#asChildOf(String)} - * 3. create a grandchild based on binary header - test {@link TraceContext#propagateTraceContext(Object, BinaryHeaderSetter)} - * and {@link TraceContext#asChildOf(byte[])} + * 3. create a grandchild based on byte[] utf8 header - test {@link TraceContext#propagateTraceContext(Object, HeaderSetter)} + * and {@link TraceContext#asChildOf(Object, HeaderGetter, boolean)} with utf8 encoded byte[]s * 4. create a second grandchild based on text header - test both {@link TraceContext#getOutgoingTraceParentTextHeader()} * and {@link TraceContext#asChildOf(String)} * @@ -78,7 +78,7 @@ public void setup() { private void mixTextAndBinaryParsingAndContextPropagation(String flagsValue, boolean isSampled) { Map textHeaderMap = Map.of(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-" + flagsValue); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); assertThat(child.getTraceId().toString()).isEqualTo("0af7651916cd43dd8448eb211c80319c"); assertThat(child.getParentId().toString()).isEqualTo("b9c7c989f97918e1"); assertThat(child.getId()).isNotEqualTo(child.getParentId()); @@ -86,9 +86,9 @@ private void mixTextAndBinaryParsingAndContextPropagation(String flagsValue, boo // create a grandchild to ensure proper regenerated trace context final TraceContext grandchild1 = TraceContext.with64BitId(tracer); - final Map binaryHeaderMap = new HashMap<>(); - assertThat(child.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); - assertThat(TraceContext.>getFromTraceContextBinaryHeaders().asChildOf(grandchild1, binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); + final Map binaryHeaderMap = new HashMap<>(); + child.propagateTraceContext(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE); + assertThat(grandchild1.asChildOf(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE)).isTrue(); assertThat(grandchild1.getTraceId().toString()).isEqualTo("0af7651916cd43dd8448eb211c80319c"); assertThat(grandchild1.getParentId().toString()).isEqualTo(child.getId().toString()); assertThat(grandchild1.getId()).isNotEqualTo(child.getId()); @@ -123,7 +123,7 @@ void parseFromTraceParentHeaderUnsupportedFlag() { void testChildOfElasticTraceparentHeader() { Map textHeaderMap = Map.of(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); assertThat(child.getTraceId().toString()).isEqualTo("0af7651916cd43dd8448eb211c80319c"); assertThat(child.getParentId().toString()).isEqualTo("b9c7c989f97918e1"); assertThat(child.getId()).isNotEqualTo(child.getParentId()); @@ -137,7 +137,7 @@ void testW3CTraceparentHeaderPrecedence() { TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-dd8448eb211c80319c0af7651916cd43-f97918e1b9c7c989-01" ); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); assertThat(child.getTraceId().toString()).isEqualTo("0af7651916cd43dd8448eb211c80319c"); assertThat(child.getParentId().toString()).isEqualTo("b9c7c989f97918e1"); assertThat(child.getId()).isNotEqualTo(child.getParentId()); @@ -152,7 +152,7 @@ void testInvalidElasticTraceparentHeader() { TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-d8448eb211c80319c0af7651916cd43-f97918e1b9c7c989-00" ); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); // we should fallback to try the W3C header assertThat(child.getTraceId().toString()).isEqualTo("0af7651916cd43dd8448eb211c80319c"); assertThat(child.getParentId().toString()).isEqualTo("b9c7c989f97918e1"); @@ -164,7 +164,7 @@ void testInvalidElasticTraceparentHeader() { void testElasticTraceparentHeaderDisabled() { Map textHeaderMap = Map.of(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); Map outgoingHeaders = new HashMap<>(); doReturn(false).when(config.getConfig(CoreConfiguration.class)).isElasticTraceparentHeaderEnabled(); child.propagateTraceContext(outgoingHeaders, TextHeaderMapAccessor.INSTANCE); @@ -204,7 +204,7 @@ void testTracestateHeader() { incomingHeaders.add("tracestate", "foo=bar"); incomingHeaders.add("tracestate", "baz=qux,quux=quuz"); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.getFromTraceContextTextHeaders().asChildOf(child, incomingHeaders, MultiValueMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(incomingHeaders, MultiValueMapAccessor.INSTANCE)).isTrue(); assertThat(child.getTraceId().toString()).isEqualTo("0af7651916cd43dd8448eb211c80319c"); assertThat(child.getParentId().toString()).isEqualTo("b9c7c989f97918e1"); assertThat(child.getId()).isNotEqualTo(child.getParentId()); @@ -225,7 +225,7 @@ void testTracestateHeaderSizeLimit() { incomingHeaders.add("tracestate", "foo=bar"); incomingHeaders.add("tracestate", "baz=qux,quux=quuz"); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.getFromTraceContextTextHeaders().asChildOf(child, incomingHeaders, MultiValueMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(incomingHeaders, MultiValueMapAccessor.INSTANCE)).isTrue(); PotentiallyMultiValuedMap outgoingHeaders = new PotentiallyMultiValuedMap(); child.propagateTraceContext(outgoingHeaders, MultiValueMapAccessor.INSTANCE); assertThat(outgoingHeaders.size()).isEqualTo(3); @@ -242,7 +242,7 @@ void testNoTracestateWhenInvalidTraceparentHeader() { TraceContext.TRACESTATE_HEADER_NAME, "foo=bar,baz=qux" ); final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isFalse(); + assertThat(child.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isFalse(); assertThat(child.isRecorded()).isFalse(); @@ -251,61 +251,6 @@ void testNoTracestateWhenInvalidTraceparentHeader() { assertThat(outgoingHeaders.get(TraceContext.TRACESTATE_HEADER_NAME)).isNull(); } - @Test - void testBinaryHeaderSizeEnforcement() { - final Map headerMap = Map.of(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"); - final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, headerMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); - final byte[] outgoingBinaryHeader = new byte[TraceContext.BINARY_FORMAT_EXPECTED_LENGTH - 1]; - assertThat(child.propagateTraceContext(new HashMap<>(), new BinaryHeaderSetter>() { - @Override - public byte[] getFixedLengthByteArray(String headerName, int length) { - return outgoingBinaryHeader; - } - - @Override - public void setHeader(String headerName, byte[] headerValue, Map headerMap) { - // assert that the original byte array was not used due to its size limitation - assertThat(headerValue).isNotEqualTo(outgoingBinaryHeader); - } - })).isTrue(); - } - - @Test - void testBinaryHeaderCaching() { - final Map headerMap = Map.of(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"); - final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, headerMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); - HashMap binaryHeaderMap = new HashMap<>(); - assertThat(child.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); - byte[] outgoingHeader = binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME); - assertThat(child.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); - assertThat(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)).isSameAs(outgoingHeader); - } - - @Test - void testBinaryHeader_CachingDisabled() { - final Map headerMap = Map.of(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"); - final TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, headerMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); - BinaryHeaderSetter> headerSetter = new BinaryHeaderSetter<>() { - @Override - public byte[] getFixedLengthByteArray(String headerName, int length) { - return null; - } - - @Override - public void setHeader(String headerName, byte[] headerValue, Map headerMap) { - headerMap.put(headerName, headerValue); - } - }; - HashMap binaryHeaderMap = new HashMap<>(); - assertThat(child.propagateTraceContext(binaryHeaderMap, headerSetter)).isTrue(); - byte[] outgoingHeader = binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME); - assertThat(child.propagateTraceContext(binaryHeaderMap, headerSetter)).isTrue(); - assertThat(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)).isNotSameAs(outgoingHeader); - } - private void verifyTraceContextContents(String traceContext, String expectedTraceId, String expectedParentId, String expectedVersion, String expectedFlags) { String[] parts = traceContext.split("-"); @@ -315,21 +260,6 @@ private void verifyTraceContextContents(String traceContext, String expectedTrac assertThat(parts[3]).isEqualTo(expectedFlags); } - private void verifyTraceContextContents(byte[] traceContext, String expectedTraceId, String expectedParentId, - byte expectedVersion, byte expectedFlags) { - assertThat(traceContext[0]).isEqualTo(expectedVersion); - assertThat(traceContext[1]).isEqualTo((byte) 0b0000_0000); - StringBuilder sb = new StringBuilder(); - HexUtils.writeBytesAsHex(traceContext, 2, 16, sb); - assertThat(sb.toString()).isEqualTo(expectedTraceId); - assertThat(traceContext[18]).isEqualTo((byte) 0b0000_0001); - sb.setLength(0); - HexUtils.writeBytesAsHex(traceContext, 19, 8, sb); - assertThat(sb.toString()).isEqualTo(expectedParentId); - assertThat(traceContext[27]).isEqualTo((byte) 0b0000_0010); - assertThat(traceContext[28]).isEqualTo(expectedFlags); - } - @Test void outgoingHeader() { final TraceContext traceContext = TraceContext.with64BitId(tracer); @@ -338,10 +268,10 @@ void outgoingHeader() { String parentId = traceContext.getId().toString(); verifyTraceContextContents(traceContext.getOutgoingTraceParentTextHeader().toString(), "0af7651916cd43dd8448eb211c80319c", parentId, "00", "03"); - Map headerMap = new HashMap<>(); - assertThat(traceContext.propagateTraceContext(headerMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); + Map headerMap = new HashMap<>(); + traceContext.propagateTraceContext(headerMap, Utf8HeaderMapAccessor.INSTANCE); verifyTraceContextContents(headerMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME), - "0af7651916cd43dd8448eb211c80319c", parentId, (byte) 0x00, (byte) 0x03); + "0af7651916cd43dd8448eb211c80319c", parentId, "00", "03"); } @Test @@ -353,10 +283,10 @@ void outgoingHeaderRootSpan() { assertThat(outgoingStringHeader).hasSize(55); verifyTraceContextContents(outgoingStringHeader, traceContext.getTraceId().toString(), traceContext.getId().toString(), "00", "01"); - Map headerMap = new HashMap<>(); - assertThat(traceContext.propagateTraceContext(headerMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); + Map headerMap = new HashMap<>(); + traceContext.propagateTraceContext(headerMap, Utf8HeaderMapAccessor.INSTANCE); verifyTraceContextContents(headerMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME), traceContext.getTraceId().toString(), - traceContext.getId().toString(), (byte) 0x00, (byte) 0x01); + traceContext.getId().toString(), "00", "01"); } @Test @@ -384,23 +314,6 @@ void testResetOutgoingTextHeader() { assertThat(traceContext.getOutgoingTraceParentTextHeader().toString()).isNotEqualTo(traceParentHeader); } - @Test - void testResetOutgoingBinaryHeader() { - final TraceContext traceContext = TraceContext.with64BitId(tracer); - Map headerMap = new HashMap<>(); - assertThat(traceContext.propagateTraceContext(headerMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); - byte[] outgoingByteHeader = headerMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME); - byte[] tmp = new byte[outgoingByteHeader.length]; - System.arraycopy(outgoingByteHeader, 0, tmp, 0, outgoingByteHeader.length); - traceContext.asChildOf("00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-00"); - assertThat(traceContext.propagateTraceContext(headerMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); - // relies on the byte array caching in BinaryHeaderMapAccessor - assertThat(outgoingByteHeader).isNotEqualTo(tmp); - traceContext.resetState(); - assertThat(traceContext.propagateTraceContext(headerMap, BinaryHeaderMapAccessor.INSTANCE)).isTrue(); - assertThat(outgoingByteHeader).isEqualTo(tmp); - } - @Test void testCopyFrom() { Map textHeaderMap = Map.of( @@ -408,23 +321,20 @@ void testCopyFrom() { TraceContext.TRACESTATE_HEADER_NAME, "foo=bar,baz=qux" ); final TraceContext first = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(first, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(first.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); textHeaderMap = Map.of(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME, "00-8448ebb9c7c989f97918e11916cd43dd-211c80319c0af765-00"); final TraceContext second = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(second, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(second.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); assertThat(first.getTraceId()).isNotEqualTo(second.getTraceId()); assertThat(first.getParentId()).isNotEqualTo(second.getParentId()); assertThat(first.isSampled()).isNotEqualTo(second.isSampled()); assertThat(first.getOutgoingTraceParentTextHeader()).isNotEqualTo(second.getOutgoingTraceParentTextHeader()); - Map binaryHeaderMap = new HashMap<>(); - first.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); - byte[] outgoingHeader = binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME); - // We must copy because of the byte array caching in BinaryHeaderMapAccessor - byte[] firstOutgoingHeader = new byte[outgoingHeader.length]; - System.arraycopy(outgoingHeader, 0, firstOutgoingHeader, 0, outgoingHeader.length); - second.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); + Map binaryHeaderMap = new HashMap<>(); + first.propagateTraceContext(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE); + String firstOutgoingHeader = binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME); + second.propagateTraceContext(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE); assertThat(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)).isNotEqualTo(firstOutgoingHeader); second.copyFrom(first); @@ -432,7 +342,7 @@ void testCopyFrom() { assertThat(first.getParentId()).isEqualTo(second.getParentId()); assertThat(first.isSampled()).isEqualTo(second.isSampled()); assertThat(first.getOutgoingTraceParentTextHeader().toString()).isEqualTo(second.getOutgoingTraceParentTextHeader().toString()); - second.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); + second.propagateTraceContext(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE); assertThat(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)).isEqualTo(firstOutgoingHeader); } @@ -443,7 +353,7 @@ void testAsChildOfHeaders() { TraceContext.TRACESTATE_HEADER_NAME, "foo=bar,baz=qux" ); final TraceContext first = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(first, textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(first.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE)).isTrue(); final TraceContext second = TraceContext.with64BitId(tracer); second.asChildOf(first); @@ -452,6 +362,15 @@ void testAsChildOfHeaders() { second.propagateTraceContext(textHeaders, TextHeaderMapAccessor.INSTANCE); assertThat(textHeaders.get(TraceContext.TRACESTATE_HEADER_NAME)).isEqualTo("foo=bar,baz=qux"); assertThat(textHeaders.get(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME)).startsWith("00-0af7651916cd43dd8448eb211c80319c-"); + + + final TraceContext firstUtf8 = TraceContext.with64BitId(tracer); + assertThat(firstUtf8.asChildOf(textHeaderMap, Utf8HeaderMapAccessor.INSTANCE)).isTrue(); + + HashMap utf8Headers = new HashMap<>(); + firstUtf8.propagateTraceContext(utf8Headers, Utf8HeaderMapAccessor.INSTANCE); + assertThat(utf8Headers.get(TraceContext.TRACESTATE_HEADER_NAME)).isEqualTo("foo=bar,baz=qux"); + assertThat(utf8Headers.get(TraceContext.W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME)).startsWith("00-0af7651916cd43dd8448eb211c80319c-"); } @Test @@ -568,7 +487,7 @@ void checkExpectedSampleRate(@Nullable String traceState, double expectedRate, @ private TraceContext createChildSpanFromHeaders(Map inHeaders) { TraceContext child = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(child, inHeaders, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(child.asChildOf(inHeaders, TextHeaderMapAccessor.INSTANCE)).isTrue(); return child; } @@ -582,10 +501,10 @@ void testPropagateTransactionIdForUnsampledSpan() { verifyTraceContextContents(childContext.getOutgoingTraceParentTextHeader().toString(), childContext.getTraceId().toString(), rootContext.getId().toString(), "00", "00"); - Map binaryHeaderMap = new HashMap<>(); - childContext.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); + Map binaryHeaderMap = new HashMap<>(); + childContext.propagateTraceContext(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE); verifyTraceContextContents(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME), - childContext.getTraceId().toString(), rootContext.getId().toString(), (byte) 0x00, (byte) 0x00); + childContext.getTraceId().toString(), rootContext.getId().toString(), "00", "00"); } @Test @@ -598,10 +517,10 @@ void testPropagateSpanIdForSampledSpan() { verifyTraceContextContents(childContext.getOutgoingTraceParentTextHeader().toString(), childContext.getTraceId().toString(), childContext.getId().toString(), "00", "01"); - Map binaryHeaderMap = new HashMap<>(); - childContext.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); + Map binaryHeaderMap = new HashMap<>(); + childContext.propagateTraceContext(binaryHeaderMap, Utf8HeaderMapAccessor.INSTANCE); verifyTraceContextContents(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME), - childContext.getTraceId().toString(), childContext.getId().toString(), (byte) 0x00, (byte) 0x01); + childContext.getTraceId().toString(), childContext.getId().toString(), "00", "01"); } @Test @@ -621,21 +540,12 @@ void testRootContextSampleRateFromSampler() { void testUnknownVersion() { String testTextHeader = "42-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"; assertValid(testTextHeader); - assertValid(convertToBinary(testTextHeader)); } @Test void testUnknownExtraStuff() { String testTextHeader = "42-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01-unknown-extra-stuff"; assertValid(testTextHeader); - - byte[] header = convertToBinary(testTextHeader); - byte[] withExtra = new byte[40]; - for (int i = header.length; i < withExtra.length; i++) { - new Random().nextBytes(withExtra); - } - System.arraycopy(header, 0, withExtra, 0, header.length); - assertValid(withExtra); } // If a traceparent header is invalid, ignore it and create a new root context @@ -644,14 +554,12 @@ void testUnknownExtraStuff() { void testInvalidHeader_traceIdAllZeroes() { String testTextHeader = "00-00000000000000000000000000000000-b9c7c989f97918e1-00"; assertInvalid(testTextHeader); - assertInvalid(convertToBinary(testTextHeader)); } @Test void testInvalidHeader_spanIdAllZeroes() { String testTextHeader = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-00"; assertInvalid(testTextHeader); - assertInvalid(convertToBinary(testTextHeader)); } @Test @@ -677,39 +585,19 @@ void testInvalidHeader_invalidTotalLength() { private void assertInvalid(String s) { final TraceContext traceContext = TraceContext.with64BitId(tracer); assertThat(traceContext.asChildOf(s)).isFalse(); - } - - private void assertInvalid(byte[] binaryHeader) { - final TraceContext traceContext = TraceContext.with64BitId(tracer); - assertThat(traceContext.asChildOf(binaryHeader)).isFalse(); + assertThat(traceContext.asChildOf(s.getBytes(StandardCharsets.UTF_8), CharAccessor.forAsciiBytes())).isFalse(); } private void assertValid(String s) { - final TraceContext traceContext = TraceContext.with64BitId(tracer); - assertThat(traceContext.asChildOf(s)).isTrue(); - verifyTraceContextContents(traceContext.getOutgoingTraceParentTextHeader().toString(), - traceContext.getTraceId().toString(), traceContext.getId().toString(), "00", s.substring(53, 55)); - } - - private void assertValid(byte[] binaryHeader) { - final TraceContext traceContext = TraceContext.with64BitId(tracer); - assertThat(traceContext.asChildOf(binaryHeader)).isTrue(); - Map binaryHeaderMap = new HashMap<>(); - traceContext.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); - verifyTraceContextContents(binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME), - traceContext.getTraceId().toString(), traceContext.getId().toString(), (byte) 0x00, binaryHeader[28]); - } - - private byte[] convertToBinary(String textHeader) { - final TraceContext traceContext = TraceContext.with64BitId(tracer); - traceContext.asChildOf(textHeader); - Map binaryHeaderMap = new HashMap<>(); - traceContext.propagateTraceContext(binaryHeaderMap, BinaryHeaderMapAccessor.INSTANCE); - byte[] binaryHeader = binaryHeaderMap.get(TraceContext.ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME); - // replace the version and parent ID - HexUtils.decode(textHeader, 0, 2, binaryHeader, 0); - HexUtils.decode(textHeader, 36, 16, binaryHeader, 19); - return binaryHeader; + TraceContext textTraceContext = TraceContext.with64BitId(tracer); + assertThat(textTraceContext.asChildOf(s)).isTrue(); + verifyTraceContextContents(textTraceContext.getOutgoingTraceParentTextHeader().toString(), + textTraceContext.getTraceId().toString(), textTraceContext.getId().toString(), "00", s.substring(53, 55)); + + TraceContext utf8TraceContext = TraceContext.with64BitId(tracer); + assertThat(utf8TraceContext.asChildOf(s.getBytes(StandardCharsets.UTF_8), CharAccessor.forAsciiBytes())).isTrue(); + verifyTraceContextContents(utf8TraceContext.getOutgoingTraceParentTextHeader().toString(), + utf8TraceContext.getTraceId().toString(), utf8TraceContext.getId().toString(), "00", s.substring(53, 55)); } @Test diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java index c5608ec0dd..86def7f803 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java @@ -23,7 +23,6 @@ import co.elastic.apm.agent.configuration.CoreConfiguration; import co.elastic.apm.agent.configuration.ServerlessConfiguration; import co.elastic.apm.agent.configuration.SpyConfiguration; -import co.elastic.apm.agent.impl.BinaryHeaderMapAccessor; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.TextHeaderMapAccessor; import co.elastic.apm.agent.impl.Tracer; @@ -1565,11 +1564,11 @@ void testSpanLinksSerialization() { Span parent1 = Objects.requireNonNull(transaction).createSpan(); Map textTraceContextCarrier = new HashMap<>(); parent1.propagateContext(textTraceContextCarrier, TextHeaderMapAccessor.INSTANCE, null); - transaction.addSpanLink(TraceContext.getFromTraceContextTextHeaders(), TextHeaderMapAccessor.INSTANCE, textTraceContextCarrier); + transaction.addSpanLink(TextHeaderMapAccessor.INSTANCE, textTraceContextCarrier); Span parent2 = transaction.createSpan(); - Map binaryTraceContextCarrier = new HashMap<>(); - parent2.propagateContext(binaryTraceContextCarrier, BinaryHeaderMapAccessor.INSTANCE); - transaction.addSpanLink(TraceContext.getFromTraceContextBinaryHeaders(), BinaryHeaderMapAccessor.INSTANCE, binaryTraceContextCarrier); + Map binaryTraceContextCarrier = new HashMap<>(); + parent2.propagateContext(binaryTraceContextCarrier, TextHeaderMapAccessor.INSTANCE, null); + transaction.addSpanLink(TextHeaderMapAccessor.INSTANCE, binaryTraceContextCarrier); JsonNode transactionJson = readJsonString(writer.toJsonString(transaction)); JsonNode spanLinks = transactionJson.get("links"); assertThat(spanLinks).isNotNull(); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/HexSerializationUtilsTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/HexSerializationUtilsTest.java new file mode 100644 index 0000000000..f32fed39c4 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/HexSerializationUtilsTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.agent.report.serialize; + +import co.elastic.apm.agent.tracer.util.HexUtils; +import co.elastic.apm.agent.util.ByteUtils; +import com.dslplatform.json.DslJson; +import com.dslplatform.json.JsonWriter; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HexSerializationUtilsTest { + + @Test + void testLongToHex() { + byte[] bytes = new byte[8]; + HexUtils.nextBytes("09c2572177fdae24", 0, bytes); + long l = ByteUtils.getLong(bytes, 0); + JsonWriter jw = new DslJson<>().newWriter(); + HexSerializationUtils.writeAsHex(l, jw); + assertThat(jw.toString()).isEqualTo("09c2572177fdae24"); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/tracemethods/TraceMethodInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/tracemethods/TraceMethodInstrumentationTest.java index 4b7a49168e..393e2cce56 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/tracemethods/TraceMethodInstrumentationTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/tracemethods/TraceMethodInstrumentationTest.java @@ -30,6 +30,7 @@ import co.elastic.apm.agent.matcher.MethodMatcher; import co.elastic.apm.agent.objectpool.TestObjectPoolFactory; import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; import net.bytebuddy.agent.ByteBuddyAgent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -352,7 +353,7 @@ private void manuallyTraced() { AbstractSpan active = tracer.getActive(); if (active != null) { Span span = active.createSpan(); - span.propagateContext(new HashMap<>(), (k, v, m) -> m.put(k, v), null); + span.propagateContext(new HashMap<>(), (TextHeaderSetter>) (k, v, m) -> m.put(k, v), null); span.end(); } } diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIterableWrapper.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIterableWrapper.java index 4666d79f64..aa5521676b 100644 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIterableWrapper.java +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIterableWrapper.java @@ -22,22 +22,19 @@ import org.apache.kafka.clients.consumer.ConsumerRecord; import java.util.Iterator; -import java.util.Set; class ConsumerRecordsIterableWrapper implements Iterable> { private final Iterable> delegate; private final Tracer tracer; - private final Set binaryTraceHeaders; - public ConsumerRecordsIterableWrapper(Iterable> delegate, Tracer tracer, Set binaryTraceHeaders) { + public ConsumerRecordsIterableWrapper(Iterable> delegate, Tracer tracer) { this.delegate = delegate; this.tracer = tracer; - this.binaryTraceHeaders = binaryTraceHeaders; } @Override public Iterator> iterator() { - return new ConsumerRecordsIteratorWrapper(delegate.iterator(), tracer, binaryTraceHeaders); + return new ConsumerRecordsIteratorWrapper(delegate.iterator(), tracer); } } diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIteratorWrapper.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIteratorWrapper.java index 7ecf0452e4..e577b61626 100644 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIteratorWrapper.java +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsIteratorWrapper.java @@ -18,21 +18,20 @@ */ package co.elastic.apm.agent.kafka.helper; -import co.elastic.apm.agent.tracer.configuration.CoreConfiguration; -import co.elastic.apm.agent.tracer.configuration.MessagingConfiguration; +import co.elastic.apm.agent.common.util.WildcardMatcher; +import co.elastic.apm.agent.sdk.internal.util.PrivilegedActionUtils; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; import co.elastic.apm.agent.tracer.Tracer; import co.elastic.apm.agent.tracer.Transaction; -import co.elastic.apm.agent.common.util.WildcardMatcher; +import co.elastic.apm.agent.tracer.configuration.CoreConfiguration; +import co.elastic.apm.agent.tracer.configuration.MessagingConfiguration; import co.elastic.apm.agent.tracer.metadata.Message; -import co.elastic.apm.agent.sdk.internal.util.PrivilegedActionUtils; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.common.header.Header; import org.apache.kafka.common.record.TimestampType; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; import java.util.Iterator; -import java.util.Set; class ConsumerRecordsIteratorWrapper implements Iterator> { @@ -41,14 +40,12 @@ class ConsumerRecordsIteratorWrapper implements Iterator> { private final Iterator> delegate; private final Tracer tracer; - private final Set binaryTraceHeaders; private final CoreConfiguration coreConfiguration; private final MessagingConfiguration messagingConfiguration; - public ConsumerRecordsIteratorWrapper(Iterator> delegate, Tracer tracer, Set binaryTraceHeaders) { + public ConsumerRecordsIteratorWrapper(Iterator> delegate, Tracer tracer) { this.delegate = delegate; this.tracer = tracer; - this.binaryTraceHeaders = binaryTraceHeaders; coreConfiguration = tracer.getConfig(CoreConfiguration.class); messagingConfiguration = tracer.getConfig(MessagingConfiguration.class); } @@ -91,7 +88,7 @@ public void endCurrentTransaction() { if (transaction.isSampled() && coreConfiguration.isCaptureHeaders()) { for (Header header : record.headers()) { String key = header.key(); - if (!binaryTraceHeaders.contains(key) && + if (!tracer.getTraceHeaderNames().contains(key) && WildcardMatcher.anyMatch(coreConfiguration.getSanitizeFieldNames(), key) == null) { message.addHeader(key, header.value()); } diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsListWrapper.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsListWrapper.java index 9723e40c4d..357de0f3da 100644 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsListWrapper.java +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ConsumerRecordsListWrapper.java @@ -25,18 +25,15 @@ import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import java.util.Set; class ConsumerRecordsListWrapper implements List> { private final List> delegate; private final Tracer tracer; - private final Set binaryTraceHeaders; - public ConsumerRecordsListWrapper(List> delegate, Tracer tracer, Set binaryTraceHeaders) { + public ConsumerRecordsListWrapper(List> delegate, Tracer tracer) { this.delegate = delegate; this.tracer = tracer; - this.binaryTraceHeaders = binaryTraceHeaders; } @Override @@ -56,7 +53,7 @@ public boolean contains(Object o) { @Override public Iterator> iterator() { - return new ConsumerRecordsIteratorWrapper(delegate.iterator(), tracer, binaryTraceHeaders); + return new ConsumerRecordsIteratorWrapper(delegate.iterator(), tracer); } @Override @@ -161,6 +158,6 @@ public int lastIndexOf(Object o) { @Override public List> subList(int fromIndex, int toIndex) { - return new ConsumerRecordsListWrapper(delegate.subList(fromIndex, toIndex), tracer, binaryTraceHeaders); + return new ConsumerRecordsListWrapper(delegate.subList(fromIndex, toIndex), tracer); } } diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ElasticHeaderImpl.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ElasticHeaderImpl.java deleted file mode 100644 index 171c40f2c1..0000000000 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/ElasticHeaderImpl.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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. - */ -package co.elastic.apm.agent.kafka.helper; - -import org.apache.kafka.common.header.Header; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; - -import javax.annotation.Nullable; - -/** - * An implementation of the Kafka record {@link Header} interface, meant for reducing memory allocations by reusing. - * This implementation assumes that the thread asking for the {@link ElasticHeaderImpl#value()} is the same one setting - * it. If that's not the case, distributed tracing through Kafka may be impaired, therefore a warning is logged and - * the returned value is null. - */ -class ElasticHeaderImpl implements Header { - - public static final Logger logger = LoggerFactory.getLogger(ElasticHeaderImpl.class); - - private final String key; - @Nullable - byte[] value; - - private long settingThreadId; - - public ElasticHeaderImpl(String key, int headerLength) { - this.key = key; - value = new byte[headerLength]; - } - - @Override - public String key() { - return key; - } - - /** - * Used when the value is required in order to be set - * - * @return the byte array representing the value - */ - @Nullable - public byte[] valueForSetting() { - settingThreadId = Thread.currentThread().getId(); - return value; - } - - /** - * The actual {@link Header#value()} implementation - typically used by producers during serialization - * - * @return the set value if same thread set it; null otherwise - */ - @Override - @Nullable - public byte[] value() { - if (Thread.currentThread().getId() != settingThreadId && value != null) { - // Our assumption that the same thread setting the value is the one serializing the header is invalid. - // We log this once and set the value of this header to null. Distributed tracing will still work but will - // allocate a byte array for every record - logger.warn("The assumption of same thread setting and serializing the header is invalid."); - value = null; - } - return value; - } -} diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaInstrumentationHeadersHelper.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaInstrumentationHeadersHelper.java index 0019bc2e9f..14cc72b372 100644 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaInstrumentationHeadersHelper.java +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaInstrumentationHeadersHelper.java @@ -30,13 +30,8 @@ import org.apache.kafka.clients.producer.ProducerRecord; import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; public class KafkaInstrumentationHeadersHelper { @@ -51,8 +46,6 @@ protected Boolean initialValue() { }; private final Tracer tracer; - private final Set binaryTraceHeaders = new HashSet<>(); - private final Map translatedTraceHeaders = new HashMap<>(); public static KafkaInstrumentationHeadersHelper get() { return INSTANCE; @@ -60,25 +53,11 @@ public static KafkaInstrumentationHeadersHelper get() { public KafkaInstrumentationHeadersHelper(Tracer tracer) { this.tracer = tracer; - Pattern pattern = Pattern.compile("[^a-zA-Z0-9]"); - Set traceHeaders = tracer.getTraceHeaderNames(); - for (String traceHeader : traceHeaders) { - String binaryTraceHeader = pattern.matcher(traceHeader).replaceAll(""); - if (!binaryTraceHeaders.add(binaryTraceHeader)) { - throw new IllegalStateException("Ambiguous translation of trace headers into binary format: " + traceHeaders); - } - translatedTraceHeaders.put(traceHeader, binaryTraceHeader); - } - } - - public String resolvePossibleTraceHeader(String header) { - String translation = translatedTraceHeaders.get(header); - return translation == null ? header : translation; } public Iterator> wrapConsumerRecordIterator(Iterator> consumerRecordIterator) { try { - return new ConsumerRecordsIteratorWrapper(consumerRecordIterator, tracer, binaryTraceHeaders); + return new ConsumerRecordsIteratorWrapper(consumerRecordIterator, tracer); } catch (Throwable throwable) { logger.debug("Failed to wrap Kafka ConsumerRecords iterator", throwable); return consumerRecordIterator; @@ -87,7 +66,7 @@ public String resolvePossibleTraceHeader(String header) { public Iterable> wrapConsumerRecordIterable(Iterable> consumerRecordIterable) { try { - return new ConsumerRecordsIterableWrapper(consumerRecordIterable, tracer, binaryTraceHeaders); + return new ConsumerRecordsIterableWrapper(consumerRecordIterable, tracer); } catch (Throwable throwable) { logger.debug("Failed to wrap Kafka ConsumerRecords", throwable); return consumerRecordIterable; @@ -96,7 +75,7 @@ public String resolvePossibleTraceHeader(String header) { public List> wrapConsumerRecordList(List> consumerRecordList) { try { - return new ConsumerRecordsListWrapper(consumerRecordList, tracer, binaryTraceHeaders); + return new ConsumerRecordsListWrapper(consumerRecordList, tracer); } catch (Throwable throwable) { logger.debug("Failed to wrap Kafka ConsumerRecords list", throwable); return consumerRecordList; @@ -138,10 +117,10 @@ public void addSpanLinks(@Nullable ConsumerRecords records, AbstractSpan toPropagate, ProducerRecord producerRecord) { - toPropagate.propagateContext(producerRecord, KafkaRecordHeaderAccessor.instance()); + toPropagate.propagateContext(producerRecord, KafkaRecordHeaderAccessor.instance(), null); } public void removeTraceContextHeader(ProducerRecord producerRecord) { - HeaderUtils.remove(binaryTraceHeaders, producerRecord, KafkaRecordHeaderAccessor.instance()); + HeaderUtils.remove(tracer.getTraceHeaderNames(), producerRecord, KafkaRecordHeaderAccessor.instance()); } } diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaRecordHeaderAccessor.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaRecordHeaderAccessor.java index 4ea45c0fc2..44085f2547 100644 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaRecordHeaderAccessor.java +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/main/java/co/elastic/apm/agent/kafka/helper/KafkaRecordHeaderAccessor.java @@ -18,92 +18,148 @@ */ package co.elastic.apm.agent.kafka.helper; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; import co.elastic.apm.agent.tracer.dispatch.HeaderRemover; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.UTF8ByteHeaderSetter; +import co.elastic.apm.agent.tracer.util.HexUtils; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.header.Header; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; +/** + * This header accessor implements backwards compatibility for the legacy binary "elasticapmtraceparent" header. + * This is done by translating the textual "elastic-apm-traceparent" representation on the fly to the legacy binary representation. + * The legacy binary header format is as follows: + *
+ *      traceparent     = version version_format
+ *      version         = 1BYTE                   ; version is 0 in the current spec
+ *      version_format  = "{ 0x0 }" trace-id "{ 0x1 }" parent-id "{ 0x2 }" trace-flags
+ *      trace-id        = 16BYTES
+ *      parent-id       = 8BYTES
+ *      trace-flags     = 1BYTE  ; only the least significant bit is used
+ * 
+ * For example: + *
+ * elasticapmtraceparent:   [0,
+ *                           0, 75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 0, 14, 71, 54,
+ *                           1, 52, 240, 103, 170, 11, 169, 2, 183,
+ *                           2, 1]
+ * 
+ */ @SuppressWarnings("rawtypes") -public class KafkaRecordHeaderAccessor implements BinaryHeaderGetter, BinaryHeaderSetter, +public class KafkaRecordHeaderAccessor implements UTF8ByteHeaderGetter, UTF8ByteHeaderSetter, HeaderRemover { public static final Logger logger = LoggerFactory.getLogger(KafkaRecordHeaderAccessor.class); private static final KafkaRecordHeaderAccessor INSTANCE = new KafkaRecordHeaderAccessor(); - - private static final ThreadLocal> threadLocalHeaderMap = new ThreadLocal<>(); + public static final String ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME = "elastic-apm-traceparent"; + public static final String LEGACY_BINARY_TRACEPARENT = "elasticapmtraceparent"; public static KafkaRecordHeaderAccessor instance() { return INSTANCE; } - private final KafkaInstrumentationHeadersHelper helper = KafkaInstrumentationHeadersHelper.get(); - @Nullable @Override public byte[] getFirstHeader(String headerName, ConsumerRecord record) { - headerName = helper.resolvePossibleTraceHeader(headerName); - Header traceParentHeader = record.headers().lastHeader(headerName); - if (traceParentHeader != null) { - return traceParentHeader.value(); + if (headerName.equals(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)) { + Header header = record.headers().lastHeader(LEGACY_BINARY_TRACEPARENT); + if (header != null) { + return convertLegacyBinaryTraceparentToTextHeader(header.value()); + } + } else { + Header header = record.headers().lastHeader(headerName); + if (header != null) { + return header.value(); + } } return null; } + @Override public void forEach(String headerName, ConsumerRecord carrier, S state, HeaderConsumer consumer) { - headerName = helper.resolvePossibleTraceHeader(headerName); - for (Header header : carrier.headers().headers(headerName)) { - consumer.accept(header.value(), state); + if (headerName.equals(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)) { + for (Header header : carrier.headers().headers(LEGACY_BINARY_TRACEPARENT)) { + byte[] convertedHeader = convertLegacyBinaryTraceparentToTextHeader(header.value()); + if (convertedHeader != null) { + consumer.accept(convertedHeader, state); + } + } + } else { + for (Header header : carrier.headers().headers(headerName)) { + consumer.accept(header.value(), state); + } } } - @Override - @Nullable - public byte[] getFixedLengthByteArray(String headerName, int length) { - headerName = helper.resolvePossibleTraceHeader(headerName); - Map headerMap = threadLocalHeaderMap.get(); - if (headerMap == null) { - headerMap = new HashMap<>(); - threadLocalHeaderMap.set(headerMap); - } - ElasticHeaderImpl header = headerMap.get(headerName); - if (header == null) { - header = new ElasticHeaderImpl(headerName, length); - headerMap.put(headerName, header); - } - return header.valueForSetting(); - } @Override public void setHeader(String headerName, byte[] headerValue, ProducerRecord record) { - headerName = helper.resolvePossibleTraceHeader(headerName); - ElasticHeaderImpl header = null; - Map headerMap = threadLocalHeaderMap.get(); - if (headerMap != null) { - header = headerMap.get(headerName); - } - // Not accessing the value through the method, as it checks the thread - if (header == null || header.value == null) { - logger.warn("No header cached for {}, allocating byte array for each record", headerName); - record.headers().add(headerName, headerValue); + // TODO: this currently allocates! Prior to the removal of binary propagation, + // custom thread-local cached headers instances with cached byte arrays were used. + // we can't use ThreadLocals due to loom, but we could use a bounded LRU cache instead + remove(headerName, record); + if (headerName.equals(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)) { + record.headers().add(LEGACY_BINARY_TRACEPARENT, convertTextHeaderToLegacyBinaryTraceparent(headerValue)); } else { - record.headers().add(header); + record.headers().add(headerName, headerValue); } } @Override public void remove(String headerName, ProducerRecord carrier) { - headerName = helper.resolvePossibleTraceHeader(headerName); - carrier.headers().remove(headerName); + if (headerName.equals(ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME)) { + carrier.headers().remove(LEGACY_BINARY_TRACEPARENT); + } else { + carrier.headers().remove(headerName); + } + } + + + private byte[] convertTextHeaderToLegacyBinaryTraceparent(byte[] asciiTextHeaderValue) { + //input is guaranteed to be a valid w3c header, no need to validate + byte[] buffer = new byte[29]; + buffer[0] = 0; //version + buffer[1] = 0; //trace-id field-id + HexUtils.decodeAscii(asciiTextHeaderValue, 3, 32, buffer, 2); //read 16 byte traceid + buffer[18] = 1; //parent-id field-id + HexUtils.decodeAscii(asciiTextHeaderValue, 36, 16, buffer, 19); //read 8 byte parentid + buffer[27] = 2; //flags field-id + buffer[28] = HexUtils.getNextByteAscii(asciiTextHeaderValue, 53); //flags + return buffer; } + @Nullable + private byte[] convertLegacyBinaryTraceparentToTextHeader(byte[] binaryHeader) { + if (binaryHeader.length < 29) { + logger.warn("The elasticapmtraceparent header has to be at least 29 bytes long, but is not"); + return null; + } + try { + byte[] asciiTextHeader = { + '0', '0', '-', + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //traceId + '-', + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //parentId + '-', + '0', '0' //flags + }; + HexUtils.writeBytesAsHexAscii(binaryHeader, 2, 16, asciiTextHeader, 3); + HexUtils.writeBytesAsHexAscii(binaryHeader, 19, 8, asciiTextHeader, 36); + byte flags = binaryHeader[28]; + HexUtils.writeBytesAsHexAscii(flags, asciiTextHeader, 53); + return asciiTextHeader; + } catch (Exception e) { + logger.warn("Failed to parse legacy elasticapmtraceparent header", e); + } + return null; + } + + } diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaIT.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaIT.java index 5d530111b8..48508ca34c 100644 --- a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaIT.java +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaIT.java @@ -19,9 +19,10 @@ package co.elastic.apm.agent.kafka; import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.common.util.WildcardMatcher; import co.elastic.apm.agent.configuration.CoreConfiguration; -import co.elastic.apm.agent.tracer.configuration.MessagingConfiguration; import co.elastic.apm.agent.impl.TracerInternalApiUtils; +import co.elastic.apm.agent.impl.baggage.BaggageContext; import co.elastic.apm.agent.impl.context.Destination; import co.elastic.apm.agent.impl.context.Headers; import co.elastic.apm.agent.impl.context.Message; @@ -29,12 +30,12 @@ import co.elastic.apm.agent.impl.context.TransactionContext; import co.elastic.apm.agent.impl.sampling.ConstantSampler; import co.elastic.apm.agent.impl.sampling.Sampler; -import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.common.util.WildcardMatcher; import co.elastic.apm.agent.testutils.TestContainersUtils; +import co.elastic.apm.agent.tracer.Outcome; +import co.elastic.apm.agent.tracer.configuration.MessagingConfiguration; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; @@ -297,14 +298,19 @@ public void testIgnoreTopic() { @Test public void testTransactionCreationWithoutContext() { - testScenario = TestScenario.NO_CONTEXT_PROPAGATION; + testScenario = TestScenario.NO_TRACE_CONTEXT_PROPAGATION; consumerThread.setIterationMode(RecordIterationMode.ITERABLE_FOR); //noinspection ConstantConditions tracer.currentTransaction().deactivate().end(); reporter.reset(); - // Send without context + // Send without trace context, but verify baggage is still propagated + BaggageContext baggageContext = tracer.currentContext().withUpdatedBaggage() + .put("test_baggage", "baggage_val") + .buildContext() + .activate(); sendTwoRecordsAndConsumeReplies(); + baggageContext.deactivate(); // We expect two transactions from records read from the request topic, each creating a send span as well. // In addition, we expect two transactions from the main test thread, iterating over reply messages. @@ -320,6 +326,10 @@ public void testTransactionCreationWithoutContext() { verifyKafkaTransactionContents(transactions.get(1), null, null, REQUEST_TOPIC); verifyKafkaTransactionContents(transactions.get(2), sendSpan1, null, REPLY_TOPIC); verifyKafkaTransactionContents(transactions.get(3), sendSpan2, null, REPLY_TOPIC); + assertThat(transactions).allSatisfy(tx -> assertThat(tx.getBaggage()) + .hasSize(1) + .containsEntry("test_baggage", "baggage_val") + ); } @Test @@ -355,7 +365,7 @@ private void sendTwoRecordsAndConsumeReplies() { if (testScenario != TestScenario.IGNORE_REQUEST_TOPIC && testScenario != TestScenario.AGENT_PAUSED && testScenario != TestScenario.BATCH_PROCESSING) { await().atMost(2000, MILLISECONDS).until(() -> reporter.getTransactions().size() == 2); if (testScenario != TestScenario.NON_SAMPLED_TRANSACTION) { - int expectedSpans = (testScenario == TestScenario.NO_CONTEXT_PROPAGATION) ? 2 : 4; + int expectedSpans = (testScenario == TestScenario.NO_TRACE_CONTEXT_PROPAGATION) ? 2 : 4; await().atMost(500, MILLISECONDS).until(() -> reporter.getSpans().size() == expectedSpans); } } @@ -610,7 +620,7 @@ enum TestScenario { SANITIZED_HEADER, IGNORE_REQUEST_TOPIC, AGENT_PAUSED, - NO_CONTEXT_PROPAGATION, + NO_TRACE_CONTEXT_PROPAGATION, TOPIC_ADDRESS_COLLECTION_DISABLED, NON_SAMPLED_TRANSACTION, BATCH_PROCESSING diff --git a/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaRecordHeaderAccessorTest.java b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaRecordHeaderAccessorTest.java new file mode 100644 index 0000000000..91deb8a387 --- /dev/null +++ b/apm-agent-plugins/apm-kafka-plugin/apm-kafka-headers-plugin/src/test/java/co/elastic/apm/agent/kafka/KafkaRecordHeaderAccessorTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.agent.kafka; + +import co.elastic.apm.agent.kafka.helper.KafkaRecordHeaderAccessor; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.apache.kafka.common.record.TimestampType; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KafkaRecordHeaderAccessorTest { + + @Test + public void testLegacyHeaderSetterTranslation() { + String W3C_HEADER = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"; + byte[] W3C_HEADER_BYTES = W3C_HEADER.getBytes(StandardCharsets.UTF_8); + byte[] binary_header = { + 0, //version + 0, //trace-id field-id + 0x0a, (byte) 0xf7, 0x65, 0x19, 0x16, (byte) 0xcd, 0x43, (byte) 0xdd, + (byte) 0x84, 0x48, (byte) 0xeb, 0x21, 0x1c, (byte) 0x80, 0x31, (byte) 0x9c, + 1, //parent-id field-id + 0x00, (byte) 0xf0, 0x67, (byte) 0xaa, 0x0b, (byte) 0xa9, 0x02, (byte) 0xb7, + 2, //flags field-id + 0x01, + }; + ProducerRecord dummyRecord = new ProducerRecord("", 0, "", ""); + //set twice to ensure it is not added twice + KafkaRecordHeaderAccessor.instance().setHeader("elastic-apm-traceparent", W3C_HEADER_BYTES, dummyRecord); + KafkaRecordHeaderAccessor.instance().setHeader("elastic-apm-traceparent", W3C_HEADER_BYTES, dummyRecord); + + assertThat(dummyRecord.headers()).hasSize(1); + assertThat(dummyRecord.headers().lastHeader("elasticapmtraceparent").value()) + .isEqualTo(binary_header); + + KafkaRecordHeaderAccessor.instance().remove("elastic-apm-traceparent", dummyRecord); + assertThat(dummyRecord.headers()).hasSize(0); + } + + + @Test + public void testLegacyHeaderGetterTranslation() { + String W3C_HEADER = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"; + byte[] binary_header = { + 0, //version + 0, //trace-id field-id + 0x0a, (byte) 0xf7, 0x65, 0x19, 0x16, (byte) 0xcd, 0x43, (byte) 0xdd, + (byte) 0x84, 0x48, (byte) 0xeb, 0x21, 0x1c, (byte) 0x80, 0x31, (byte) 0x9c, + 1, //parent-id field-id + 0x00, (byte) 0xf0, 0x67, (byte) 0xaa, 0x0b, (byte) 0xa9, 0x02, (byte) 0xb7, + 2, //flags field-id + 0x01, + }; + Headers headers = new RecordHeaders().add("elasticapmtraceparent", binary_header); + ConsumerRecord dummyRecord = new ConsumerRecord("", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", "", headers, Optional.empty()); + + byte[] headerText = KafkaRecordHeaderAccessor.instance().getFirstHeader("elastic-apm-traceparent", dummyRecord); + assertThat(new String(headerText, StandardCharsets.UTF_8)).isEqualTo(W3C_HEADER); + + List allHeaders = new ArrayList<>(); + KafkaRecordHeaderAccessor.instance().forEach("elastic-apm-traceparent", dummyRecord, null, + (val, state) -> allHeaders.add(new String(val, StandardCharsets.UTF_8))); + assertThat(allHeaders).containsExactly(W3C_HEADER); + } + + + @Test + public void testInvalidLegacyHeaderGetterTranslation() { + byte[] binary_header = {42}; + Headers headers = new RecordHeaders().add("elasticapmtraceparent", binary_header); + ConsumerRecord dummyRecord = new ConsumerRecord("", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", "", headers, Optional.empty()); + + byte[] headerText = KafkaRecordHeaderAccessor.instance().getFirstHeader("elastic-apm-traceparent", dummyRecord); + assertThat(headerText).isNull(); + + List allHeaders = new ArrayList<>(); + KafkaRecordHeaderAccessor.instance().forEach("elastic-apm-traceparent", dummyRecord, null, + (val, state) -> allHeaders.add(new String(val, StandardCharsets.UTF_8))); + assertThat(allHeaders).isEmpty(); + } + + + @Test + public void testW3cHeaderSetter() { + String W3C_HEADER = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"; + byte[] W3C_HEADER_BYTES = W3C_HEADER.getBytes(StandardCharsets.UTF_8); + ProducerRecord dummyRecord = new ProducerRecord("", 0, "", ""); + //set twice to ensure it is not added twice + KafkaRecordHeaderAccessor.instance().setHeader("traceparent", W3C_HEADER_BYTES, dummyRecord); + KafkaRecordHeaderAccessor.instance().setHeader("traceparent", W3C_HEADER_BYTES, dummyRecord); + + assertThat(dummyRecord.headers()).hasSize(1); + assertThat(dummyRecord.headers().lastHeader("traceparent").value()) + .isEqualTo(W3C_HEADER.getBytes(StandardCharsets.UTF_8)); + + KafkaRecordHeaderAccessor.instance().remove("traceparent", dummyRecord); + assertThat(dummyRecord.headers()).hasSize(0); + } + + + @Test + public void testW3CHeaderGetter() { + String W3C_HEADER = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"; + Headers headers = new RecordHeaders().add("traceparent", W3C_HEADER.getBytes(StandardCharsets.UTF_8)); + ConsumerRecord dummyRecord = new ConsumerRecord("", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", "", headers, Optional.empty()); + + byte[] headerText = KafkaRecordHeaderAccessor.instance().getFirstHeader("traceparent", dummyRecord); + assertThat(new String(headerText, StandardCharsets.UTF_8)).isEqualTo(W3C_HEADER); + + List allHeaders = new ArrayList<>(); + KafkaRecordHeaderAccessor.instance().forEach("traceparent", dummyRecord, null, + (val, state) -> allHeaders.add(new String(val, StandardCharsets.UTF_8))); + assertThat(allHeaders).containsExactly(W3C_HEADER); + } + +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLogManagerCorrelationInstrumentationTest.java b/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLogManagerCorrelationInstrumentationTest.java index 61357f37f0..081905a8de 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLogManagerCorrelationInstrumentationTest.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLogManagerCorrelationInstrumentationTest.java @@ -116,7 +116,7 @@ protected void doPublish(ExtLogRecord record) { assertThat(traceParent).isNotNull(); Map textHeaderMap = Map.of(W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, traceParent); TraceContext childTraceContext = TraceContext.with64BitId(tracer); - TraceContext.>getFromTraceContextTextHeaders().asChildOf(childTraceContext, textHeaderMap, TextHeaderMapAccessor.INSTANCE); + childTraceContext.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE); System.out.println("childTraceContext = " + childTraceContext); assertThat(childTraceContext.getTraceId().toString()).isEqualTo(traceId.toString()); assertThat(childTraceContext.getParentId().toString()).isEqualTo(transactionId.toString()); diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLoggingCorrelationInstrumentationTest.java b/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLoggingCorrelationInstrumentationTest.java index 97407028de..af869e005f 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLoggingCorrelationInstrumentationTest.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-jboss-logging-plugin/src/test/java/co/elastic/apm/agent/jbosslogging/correlation/JBossLoggingCorrelationInstrumentationTest.java @@ -120,7 +120,7 @@ public void publish(LogRecord record) { assertThat(traceParent).isNotNull(); Map textHeaderMap = Map.of(W3C_TRACE_PARENT_TEXTUAL_HEADER_NAME, traceParent); TraceContext childTraceContext = TraceContext.with64BitId(tracer); - TraceContext.>getFromTraceContextTextHeaders().asChildOf(childTraceContext, textHeaderMap, TextHeaderMapAccessor.INSTANCE); + childTraceContext.asChildOf(textHeaderMap, TextHeaderMapAccessor.INSTANCE); System.out.println("childTraceContext = " + childTraceContext); assertThat(childTraceContext.getTraceId().toString()).isEqualTo(traceId.toString()); assertThat(childTraceContext.getParentId().toString()).isEqualTo(transactionId.toString()); diff --git a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/src/test/java/co/elastic/apm/agent/opentelemetry/tracing/ElasticOpenTelemetryTest.java b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/src/test/java/co/elastic/apm/agent/opentelemetry/tracing/ElasticOpenTelemetryTest.java index c5a4a35608..aa2084f246 100644 --- a/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/src/test/java/co/elastic/apm/agent/opentelemetry/tracing/ElasticOpenTelemetryTest.java +++ b/apm-agent-plugins/apm-opentelemetry/apm-opentelemetry-plugin/src/test/java/co/elastic/apm/agent/opentelemetry/tracing/ElasticOpenTelemetryTest.java @@ -23,6 +23,7 @@ import co.elastic.apm.agent.impl.transaction.OTelSpanKind; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.tracer.Outcome; +import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.baggage.BaggageEntryMetadata; import io.opentelemetry.api.trace.Span; @@ -231,7 +232,7 @@ public void testTransactionInject() { HashMap elasticApmHeaders = new HashMap<>(); try (Scope scope = transaction.makeCurrent()) { openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), otelHeaders, HashMap::put); - tracer.currentContext().propagateContext(elasticApmHeaders, (k, v, m) -> m.put(k, v), null); + tracer.currentContext().propagateContext(elasticApmHeaders, (TextHeaderSetter>) (k, v, m) -> m.put(k, v), null); } finally { transaction.end(); } diff --git a/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracingimpl/ExternalSpanContextInstrumentation.java b/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracingimpl/ExternalSpanContextInstrumentation.java index 9e08b58e94..30a05603d6 100644 --- a/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracingimpl/ExternalSpanContextInstrumentation.java +++ b/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracingimpl/ExternalSpanContextInstrumentation.java @@ -119,7 +119,7 @@ public static TraceContext parseTextMap(Iterable> text ElasticApmTracer tracer = OpenTracingBridgeInstrumentation.tracer.require(ElasticApmTracer.class); if (tracer != null) { TraceContext childTraceContext = TraceContext.with64BitId(tracer); - if (TraceContext.>>getFromTraceContextTextHeaders().asChildOf(childTraceContext, textMap, OpenTracingTextMapBridge.instance())) { + if (childTraceContext.asChildOf(textMap, OpenTracingTextMapBridge.instance())) { return childTraceContext; } } diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/AbstractSpan.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/AbstractSpan.java index 481ac69c1e..8e45b76c59 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/AbstractSpan.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/AbstractSpan.java @@ -18,8 +18,7 @@ */ package co.elastic.apm.agent.tracer; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; import co.elastic.apm.agent.tracer.reference.ReferenceCounted; import javax.annotation.Nullable; @@ -76,9 +75,7 @@ public interface AbstractSpan> extends Activateable boolean isSampled(); - boolean addLink(BinaryHeaderGetter headerGetter, @Nullable C carrier); - - boolean addLink(TextHeaderGetter headerGetter, @Nullable C carrier); + boolean addLink(HeaderGetter headerGetter, @Nullable C carrier); /** * Appends a string to the name. diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/ElasticContext.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/ElasticContext.java index a36a74ec7b..ab1536f093 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/ElasticContext.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/ElasticContext.java @@ -18,9 +18,8 @@ */ package co.elastic.apm.agent.tracer; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderSetter; import co.elastic.apm.agent.tracer.reference.ReferenceCounted; import javax.annotation.Nullable; @@ -71,16 +70,6 @@ public interface ElasticContext> extends ReferenceCo */ boolean isEmpty(); - /** - * Propagates this context onto the given carrier. This includes both trace context and baggage. - * - * @param carrier the binary headers carrier - * @param headerSetter a setter implementing the actual addition of headers to the headers carrier - * @param the header carrier type, for example - a Kafka record - * @return true if Trace Context headers were set; false otherwise - */ - boolean propagateContext(C carrier, BinaryHeaderSetter headerSetter); - /** * Propagates this context onto the given carrier. This includes both trace context and baggage. * This method ensures that if trace-context headers are already present, they will not be overridden. @@ -91,10 +80,10 @@ public interface ElasticContext> extends ReferenceCo * If not provided, no such check will be performed. * @param the header carrier type, for example - an HTTP request */ - void propagateContext(C carrier, TextHeaderSetter headerSetter, @Nullable TextHeaderGetter headerGetter); + void propagateContext(C carrier, HeaderSetter headerSetter, @Nullable HeaderGetter headerGetter); /** - * Same as {@link #propagateContext(Object, TextHeaderSetter, TextHeaderGetter)}, except that different types can be used + * Same as {@link #propagateContext(Object, HeaderSetter, HeaderGetter)}, except that different types can be used * for the getter and setter carriers (e.g. builder vs request). * * @param carrier the text headers carrier for setting header @@ -105,10 +94,10 @@ public interface ElasticContext> extends ReferenceCo * @param the header carrier type for writing headers * @param the header carrier type for reading headers */ - void propagateContext(C1 carrier, TextHeaderSetter headerSetter, @Nullable C2 carrier2, @Nullable TextHeaderGetter headerGetter); + void propagateContext(C1 carrier, HeaderSetter headerSetter, @Nullable C2 carrier2, @Nullable HeaderGetter headerGetter); /** - * Checks if a call to {@link #propagateContext(Object, TextHeaderSetter, TextHeaderGetter)} would modify the headers of this carrier. + * Checks if a call to {@link #propagateContext(Object, HeaderSetter, HeaderGetter)} would modify the headers of this carrier. * In other words, this method can be used as a precheck to see whether a propagation is required. *

* This allows the delay and avoidance of creating costly resources, e.g. builder. @@ -118,7 +107,7 @@ public interface ElasticContext> extends ReferenceCo * @param the carrier type * @return true, if a call to propagateContext would modify the headers of the carrier */ - boolean isPropagationRequired(C carrier, TextHeaderGetter headerGetter); + boolean isPropagationRequired(C carrier, HeaderGetter headerGetter); /** * @return {@literal true} when span limit is reached and the caller can optimize and not create a span. The caller diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/GlobalTracer.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/GlobalTracer.java index 7dbb91ab95..8f011fd7a3 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/GlobalTracer.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/GlobalTracer.java @@ -18,8 +18,7 @@ */ package co.elastic.apm.agent.tracer; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; import co.elastic.apm.agent.tracer.pooling.ObjectPoolFactory; import co.elastic.apm.agent.tracer.reference.ReferenceCounted; import co.elastic.apm.agent.tracer.reference.ReferenceCountedMap; @@ -119,13 +118,8 @@ public Transaction startRootTransaction(@Nullable ClassLoader initiatingClass @Nullable @Override - public Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { + public Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { return tracer.startChildTransaction(headerCarrier, textHeadersGetter, initiatingClassLoader); } - @Nullable - @Override - public Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { - return tracer.startChildTransaction(headerCarrier, binaryHeadersGetter, initiatingClassLoader); - } } diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopElasticContext.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopElasticContext.java index 563f40dcf2..1f662c7328 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopElasticContext.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopElasticContext.java @@ -18,9 +18,8 @@ */ package co.elastic.apm.agent.tracer; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderSetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderSetter; import javax.annotation.Nullable; @@ -87,22 +86,17 @@ public boolean isEmpty() { } @Override - public boolean propagateContext(C carrier, BinaryHeaderSetter headerSetter) { - return false; - } - - @Override - public void propagateContext(C carrier, TextHeaderSetter headerSetter, TextHeaderGetter headerGetter) { + public void propagateContext(C carrier, HeaderSetter headerSetter, HeaderGetter headerGetter) { } @Override - public void propagateContext(C1 carrier, TextHeaderSetter headerSetter, @Nullable C2 carrier2, @Nullable TextHeaderGetter headerGetter) { + public void propagateContext(C1 carrier, HeaderSetter headerSetter, @Nullable C2 carrier2, @Nullable HeaderGetter headerGetter) { } @Override - public boolean isPropagationRequired(C carrier, TextHeaderGetter headerGetter) { + public boolean isPropagationRequired(C carrier, HeaderGetter headerGetter) { return false; } diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopTracer.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopTracer.java index 6b8490cbe4..1556ead3dc 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopTracer.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopTracer.java @@ -18,8 +18,7 @@ */ package co.elastic.apm.agent.tracer; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; import co.elastic.apm.agent.tracer.pooling.ObjectPoolFactory; import co.elastic.apm.agent.tracer.reference.ReferenceCounted; import co.elastic.apm.agent.tracer.reference.ReferenceCountedMap; @@ -96,13 +95,8 @@ public Transaction startRootTransaction(@Nullable ClassLoader initiatingClass @Nullable @Override - public Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { + public Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { return null; } - @Nullable - @Override - public Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, @Nullable ClassLoader initiatingClassLoader) { - return null; - } } diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/Tracer.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/Tracer.java index 36fe7a695c..e2eaf77bd6 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/Tracer.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/Tracer.java @@ -18,9 +18,7 @@ */ package co.elastic.apm.agent.tracer; -import co.elastic.apm.agent.tracer.dispatch.BinaryHeaderGetter; import co.elastic.apm.agent.tracer.dispatch.HeaderGetter; -import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; import co.elastic.apm.agent.tracer.pooling.ObjectPoolFactory; import co.elastic.apm.agent.tracer.reference.ReferenceCounted; import co.elastic.apm.agent.tracer.reference.ReferenceCountedMap; @@ -76,19 +74,6 @@ public interface Tracer { * @return a transaction which is a child of the provided parent if the agent is currently RUNNING; null otherwise */ @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, TextHeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader); + Transaction startChildTransaction(@Nullable C headerCarrier, HeaderGetter textHeadersGetter, @Nullable ClassLoader initiatingClassLoader); - /** - * Starts a transaction as a child of the context headers obtained through the provided {@link HeaderGetter}. - * If the created transaction cannot be started as a child transaction (for example - if no parent context header is - * available), then it will be started as the root transaction of the trace. - * - * @param headerCarrier the Object from which context headers can be obtained, typically a request or a message - * @param binaryHeadersGetter provides the trace context headers required in order to create a child transaction - * @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction. - * Used to determine the service name. - * @return a transaction which is a child of the provided parent if the agent is currently RUNNING; null otherwise - */ - @Nullable - Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHeaderGetter binaryHeadersGetter, @Nullable ClassLoader initiatingClassLoader); } diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/HeaderUtils.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/HeaderUtils.java index 051cae999b..cc0f4e305b 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/HeaderUtils.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/HeaderUtils.java @@ -25,7 +25,7 @@ public class HeaderUtils { private HeaderUtils() { } - public static boolean containsAny(Set headerNames, C carrier, TextHeaderGetter headerGetter) { + public static boolean containsAny(Set headerNames, C carrier, HeaderGetter headerGetter) { for (String headerName : headerNames) { if (headerGetter.getFirstHeader(headerName, carrier) != null) { return true; diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/BinaryHeaderGetter.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/UTF8ByteHeaderGetter.java similarity index 91% rename from apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/BinaryHeaderGetter.java rename to apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/UTF8ByteHeaderGetter.java index f0ce2b3392..c5631626c6 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/BinaryHeaderGetter.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/UTF8ByteHeaderGetter.java @@ -18,5 +18,5 @@ */ package co.elastic.apm.agent.tracer.dispatch; -public interface BinaryHeaderGetter extends HeaderGetter { +public interface UTF8ByteHeaderGetter extends HeaderGetter { } diff --git a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/BinaryHeaderSetter.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/UTF8ByteHeaderSetter.java similarity index 81% rename from apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/BinaryHeaderSetter.java rename to apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/UTF8ByteHeaderSetter.java index d98ba2841c..de6251e2c4 100644 --- a/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/BinaryHeaderSetter.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/dispatch/UTF8ByteHeaderSetter.java @@ -18,10 +18,5 @@ */ package co.elastic.apm.agent.tracer.dispatch; -import javax.annotation.Nullable; - -public interface BinaryHeaderSetter extends HeaderSetter { - - @Nullable - byte[] getFixedLengthByteArray(String headerName, int length); +public interface UTF8ByteHeaderSetter extends HeaderSetter { } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/util/HexUtils.java b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/util/HexUtils.java similarity index 57% rename from apm-agent-core/src/main/java/co/elastic/apm/agent/util/HexUtils.java rename to apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/util/HexUtils.java index 29e05d5c22..4bf5a507cc 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/util/HexUtils.java +++ b/apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/util/HexUtils.java @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.util; +package co.elastic.apm.agent.tracer.util; -import com.dslplatform.json.JsonWriter; +import java.nio.charset.StandardCharsets; public class HexUtils { - private final static char[] hexArray = "0123456789abcdef".toCharArray(); + public static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); private HexUtils() { // only static utility methods, don't instantiate @@ -43,18 +43,6 @@ public static String bytesToHex(byte[] bytes) { return sb.toString(); } - public static void writeBytesAsHex(byte[] bytes, JsonWriter jw) { - for (int i = 0; i < bytes.length; i++) { - writeHexByte(jw, bytes[i]); - } - } - - private static void writeHexByte(JsonWriter jw, byte b) { - int v = b & 0xFF; - jw.writeByte((byte) hexArray[v >>> 4]); - jw.writeByte((byte) hexArray[v & 0x0F]); - } - public static void writeBytesAsHex(byte[] bytes, StringBuilder sb) { writeBytesAsHex(bytes, 0, bytes.length, sb); } @@ -65,13 +53,29 @@ public static void writeBytesAsHex(byte[] bytes, int offset, int length, StringB } } + + public static void writeBytesAsHexAscii(byte[] input, int inputOffset, int inputLength, byte[] outputAscii, int outputOffset) { + if (outputAscii.length - outputOffset < inputLength * 2) { + throw new IllegalArgumentException("Output buffer is not big enough!"); + } + for (int i = 0; i < inputLength; i++) { + writeBytesAsHexAscii(input[inputOffset + i], outputAscii, outputOffset + i * 2); + } + } + + public static void writeBytesAsHexAscii(byte input, byte[] outputAscii, int outputOffset) { + int v = input & 0xFF; + outputAscii[outputOffset] = (byte) HEX_CHARS[v >>> 4]; + outputAscii[outputOffset + 1] = (byte) HEX_CHARS[v & 0x0F]; + } + public static void writeByteAsHex(byte b, StringBuilder sb) { int v = b & 0xFF; - sb.append(hexArray[v >>> 4]); - sb.append(hexArray[v & 0x0F]); + sb.append(HEX_CHARS[v >>> 4]); + sb.append(HEX_CHARS[v & 0x0F]); } - public static byte getNextByte(String hexEncodedString, int offset) { + public static byte getNextByte(CharSequence hexEncodedString, int offset) { final int hi = hexCharToBinary(hexEncodedString.charAt(offset)); final int lo = hexCharToBinary(hexEncodedString.charAt(offset + 1)); if (hi == -1 || lo == -1) { @@ -80,6 +84,15 @@ public static byte getNextByte(String hexEncodedString, int offset) { return (byte) ((hi << 4) + lo); } + public static byte getNextByteAscii(byte[] asciiText, int offset) { + final int hi = hexCharToBinary((char) asciiText[offset]); + final int lo = hexCharToBinary((char) asciiText[offset + 1]); + if (hi == -1 || lo == -1) { + throw new IllegalArgumentException("Not a hex encoded string"); + } + return (byte) ((hi << 4) + lo); + } + private static int hexCharToBinary(char ch) { if ('0' <= ch && ch <= '9') { return ch - '0'; @@ -93,7 +106,7 @@ private static int hexCharToBinary(char ch) { return -1; } - public static void nextBytes(String hexEncodedString, int offset, byte[] bytes) { + public static void nextBytes(CharSequence hexEncodedString, int offset, byte[] bytes) { final int charsToRead = bytes.length * 2; if (hexEncodedString.length() < offset + charsToRead) { throw new IllegalArgumentException(String.format("Can't read %d bytes from string %s with offset %d", bytes.length, hexEncodedString, offset)); @@ -103,6 +116,25 @@ public static void nextBytes(String hexEncodedString, int offset, byte[] bytes) } } + public static void nextBytesAscii(byte[] asciiText, int offset, byte[] bytes) { + final int charsToRead = bytes.length * 2; + if (asciiText.length < offset + charsToRead) { + throw new IllegalArgumentException(String.format("Can't read %d bytes from byte array with length %d with offset %d", bytes.length, asciiText.length, offset)); + } + for (int i = 0; i < charsToRead; i += 2) { + bytes[i / 2] = getNextByteAscii(asciiText, offset + i); + } + } + + public static void decodeAscii(byte[] asciiHexEncodedString, int srcOffset, int srcLength, byte[] bytes, int destOffset) { + if (asciiHexEncodedString.length < srcOffset + srcLength) { + throw new IllegalArgumentException(String.format("Can't read %d chars from string %s with offset %d", srcLength, new String(asciiHexEncodedString, StandardCharsets.UTF_8), srcOffset)); + } + for (int i = 0; i < srcLength; i += 2) { + bytes[destOffset + (i / 2)] = getNextByteAscii(asciiHexEncodedString, srcOffset + i); + } + } + public static void decode(String hexEncodedString, int srcOffset, int srcLength, byte[] bytes, int destOffset) { if (hexEncodedString.length() < srcOffset + srcLength) { throw new IllegalArgumentException(String.format("Can't read %d chars from string %s with offset %d", srcLength, hexEncodedString, srcOffset)); @@ -112,14 +144,4 @@ public static void decode(String hexEncodedString, int srcOffset, int srcLength, } } - public static void writeAsHex(long l, JsonWriter jw) { - writeHexByte(jw, (byte) (l >> 56)); - writeHexByte(jw, (byte) (l >> 48)); - writeHexByte(jw, (byte) (l >> 40)); - writeHexByte(jw, (byte) (l >> 32)); - writeHexByte(jw, (byte) (l >> 24)); - writeHexByte(jw, (byte) (l >> 16)); - writeHexByte(jw, (byte) (l >> 8)); - writeHexByte(jw, (byte) l); - } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/util/HexUtilsTest.java b/apm-agent-tracer/src/test/java/co/elastic/apm/agent/tracer/util/HexUtilsTest.java similarity index 60% rename from apm-agent-core/src/test/java/co/elastic/apm/agent/util/HexUtilsTest.java rename to apm-agent-tracer/src/test/java/co/elastic/apm/agent/tracer/util/HexUtilsTest.java index 82a467e250..ca6a7f1d35 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/util/HexUtilsTest.java +++ b/apm-agent-tracer/src/test/java/co/elastic/apm/agent/tracer/util/HexUtilsTest.java @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.util; +package co.elastic.apm.agent.tracer.util; -import com.dslplatform.json.DslJson; -import com.dslplatform.json.JsonWriter; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -32,8 +31,15 @@ class HexUtilsTest { @Test void hexConversionRoundTrip() throws IOException { byte[] bytes = new byte[8]; - HexUtils.nextBytes("09c2572177fdae24", 0, bytes); - assertThat(HexUtils.bytesToHex(bytes)).isEqualTo("09c2572177fdae24"); + String hexEncodedString = "09c2572177fdae24"; + HexUtils.nextBytes(hexEncodedString, 0, bytes); + assertThat(HexUtils.bytesToHex(bytes)).isEqualTo(hexEncodedString); + + bytes = new byte[8]; + HexUtils.nextBytesAscii(hexEncodedString.getBytes(StandardCharsets.US_ASCII), 0, bytes); + byte[] outputAscii = new byte[16]; + HexUtils.writeBytesAsHexAscii(bytes, 0, 8, outputAscii, 0); + assertThat(new String(outputAscii, StandardCharsets.US_ASCII)).isEqualTo(hexEncodedString); } @Test @@ -41,6 +47,10 @@ void testInvalidHex() { assertThatThrownBy(() -> HexUtils.getNextByte("0$", 0)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Not a hex encoded string: 0$ at offset 0"); + + assertThatThrownBy(() -> HexUtils.getNextByteAscii("0$".getBytes(StandardCharsets.US_ASCII), 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Not a hex encoded string"); } @Test @@ -48,23 +58,21 @@ void testStringTooSmall() { assertThatThrownBy(() -> HexUtils.nextBytes("00", 0, new byte[2])) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Can't read 2 bytes from string 00 with offset 0"); + + assertThatThrownBy(() -> HexUtils.nextBytesAscii("00".getBytes(StandardCharsets.US_ASCII), 0, new byte[2])) + .isInstanceOf(IllegalArgumentException.class); } @Test void testUnevenLength() { - final byte[] bytes = new byte[1]; + byte[] bytes = new byte[1]; // reads the first two chars and converts "0a" to (byte) 10 HexUtils.nextBytes("0a0", 0, bytes); assertThat(bytes).isEqualTo(new byte[]{10}); - } - @Test - void testLongToHex() { - byte[] bytes = new byte[8]; - HexUtils.nextBytes("09c2572177fdae24", 0, bytes); - long l = ByteUtils.getLong(bytes, 0); - JsonWriter jw = new DslJson<>().newWriter(); - HexUtils.writeAsHex(l, jw); - assertThat(jw.toString()).isEqualTo("09c2572177fdae24"); + bytes = new byte[1]; + HexUtils.nextBytesAscii("0a0".getBytes(StandardCharsets.US_ASCII), 0, bytes); + assertThat(bytes).isEqualTo(new byte[]{10}); } + } diff --git a/apm-opentracing/src/test/java/co/elastic/apm/opentracing/OpenTracingBridgeTest.java b/apm-opentracing/src/test/java/co/elastic/apm/opentracing/OpenTracingBridgeTest.java index b72cb09194..23d5831870 100644 --- a/apm-opentracing/src/test/java/co/elastic/apm/opentracing/OpenTracingBridgeTest.java +++ b/apm-opentracing/src/test/java/co/elastic/apm/opentracing/OpenTracingBridgeTest.java @@ -22,11 +22,11 @@ import co.elastic.apm.agent.impl.TextHeaderMapAccessor; import co.elastic.apm.agent.impl.TracerInternalApiUtils; import co.elastic.apm.agent.impl.context.Http; -import co.elastic.apm.agent.tracer.util.ResultUtil; import co.elastic.apm.agent.impl.transaction.Id; import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.tracer.Outcome; +import co.elastic.apm.agent.tracer.util.ResultUtil; import io.opentracing.Scope; import io.opentracing.Span; import io.opentracing.SpanContext; @@ -576,7 +576,7 @@ void testInjectExtract() { final HashMap map = new HashMap<>(); apmTracer.inject(otSpan.context(), Format.Builtin.TEXT_MAP, new TextMapAdapter(map)); final TraceContext injectedContext = TraceContext.with64BitId(tracer); - assertThat(TraceContext.>getFromTraceContextTextHeaders().asChildOf(injectedContext, map, TextHeaderMapAccessor.INSTANCE)).isTrue(); + assertThat(injectedContext.asChildOf(map, TextHeaderMapAccessor.INSTANCE)).isTrue(); assertThat(injectedContext.getTraceId().toString()).isEqualTo(traceId); assertThat(injectedContext.getParentId()).isEqualTo(transaction.getTraceContext().getId()); assertThat(injectedContext.isSampled()).isTrue();