Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add APIs for manual inter process context propagation #396

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package co.elastic.apm.api;

import javax.annotation.Nonnull;
import java.lang.invoke.MethodHandle;

public abstract class AbstractSpanImpl implements Span {
@Nonnull
Expand Down Expand Up @@ -89,4 +90,13 @@ public boolean isSampled() {
// co.elastic.apm.agent.plugin.api.AbstractSpanInstrumentation.IsSampledInstrumentation
return false;
}

@Override
public void injectTraceHeaders(HeaderInjector headerInjector) {
doInjectTraceHeaders(ApiMethodHandles.ADD_HEADER, headerInjector);
}

private void doInjectTraceHeaders(MethodHandle addHeader, HeaderInjector headerInjector) {
// co.elastic.apm.agent.plugin.api.AbstractSpanInstrumentation.InjectTraceHeadersInstrumentation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 Elastic and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package co.elastic.apm.api;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

class ApiMethodHandles {
/**
* MethodHandle for {@link HeaderExtractor#getFirstHeader(String)}
*/
static final MethodHandle GET_FIRST_HEADER;
/**
* MethodHandle for {@link HeadersExtractor#getAllHeaders(String)}
*/
static final MethodHandle GET_ALL_HEADERS;
/**
* MethodHandle for {@link HeaderInjector#addHeader(String, String)}
*/
static final MethodHandle ADD_HEADER;

static {
try {
GET_FIRST_HEADER = MethodHandles.lookup()
.findVirtual(HeaderExtractor.class, "getFirstHeader", MethodType.methodType(String.class, String.class));
GET_ALL_HEADERS = MethodHandles.lookup()
.findVirtual(HeadersExtractor.class, "getAllHeaders", MethodType.methodType(Iterable.class, String.class));
ADD_HEADER = MethodHandles.lookup()
.findVirtual(HeaderInjector.class, "addHeader", MethodType.methodType(void.class, String.class, String.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
}
58 changes: 57 additions & 1 deletion apm-agent-api/src/main/java/co/elastic/apm/api/ElasticApm.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.invoke.MethodHandle;

/**
* This class is the main entry point of the public API for the Elastic APM agent.
Expand All @@ -37,7 +38,7 @@
*/
public class ElasticApm {

private ElasticApm() {
ElasticApm() {
// do not instantiate
}

Expand Down Expand Up @@ -83,6 +84,60 @@ private static Object doStartTransaction() {
return null;
}

/**
* Similar to {@link ElasticApm#startTransaction()} but creates this transaction as the child of a remote parent.
*
* <p>
* Example:
* </p>
* <pre>
* Transaction transaction = ElasticApm.startTransactionWithRemoteParent(request::getHeader);
* </pre>
* <p>
* Note: If the protocol supports multi-value headers, use {@link #startTransactionWithRemoteParent(HeaderExtractor, HeadersExtractor)}
* </p>
*
* @param headerExtractor a function which receives a header name and returns the fist header with that name
* @return the started transaction
* @since 1.3.0
*/
@Nonnull
public static Transaction startTransactionWithRemoteParent(final HeaderExtractor headerExtractor) {
return startTransactionWithRemoteParent(headerExtractor, null);
}

/**
* Similar to {@link ElasticApm#startTransaction()} but creates this transaction as the child of a remote parent.
*
* <p>
* Example:
* </p>
* <pre>
* Transaction transaction = ElasticApm.startTransactionWithRemoteParent(request::getHeader, request::getHeaders);
* </pre>
* <p>
* Note: If the protocol does not support multi-value headers, use {@link #startTransactionWithRemoteParent(HeaderExtractor)}
* </p>
* <p>
*
* @param headerExtractor a function which receives a header name and returns the fist header with that name
* @param headersExtractor a function which receives a header name and returns all headers with that name
* @return the started transaction
* @since 1.3.0
*/
@Nonnull
public static Transaction startTransactionWithRemoteParent(HeaderExtractor headerExtractor, HeadersExtractor headersExtractor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either replace this one to only accept HeadersExtractor or add a third API method that accepts only that and uses the same private static method with null as the first argument

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even for protocols which do support multiple headers, we want to have the ability to get a single value for a header in case we know it can only have a single value, like traceparent.

Object transaction = doStartTransactionWithRemoteParentFunction(ApiMethodHandles.GET_FIRST_HEADER, headerExtractor,
ApiMethodHandles.GET_ALL_HEADERS, headersExtractor);
return transaction != null ? new TransactionImpl(transaction) : NoopTransaction.INSTANCE;
}

private static Object doStartTransactionWithRemoteParentFunction(MethodHandle getFirstHeader, HeaderExtractor headerExtractor,
MethodHandle getAllHeaders, HeadersExtractor headersExtractor) {
// co.elastic.apm.agent.plugin.api.ElasticApmApiInstrumentation.StartTransactionWithRemoteParentInstrumentation
return null;
}

/**
* Returns the currently running transaction.
* <p>
Expand Down Expand Up @@ -122,6 +177,7 @@ private static Object doGetCurrentTransaction() {
* NOTE: Transactions created via {@link Span#createSpan()} can not be retrieved by calling this method.
* See {@link Span#activate()} on how to achieve that.
* </p>
*
* @return The currently active span, or transaction, or a noop span (never {@code null}).
*/
@Nonnull
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 Elastic and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package co.elastic.apm.api;

/**
* Used to extract the first header with a specific header name.
* <p>
* Can be implemented as a lambda in Java 8 and as an anonymous inner class in Java 7.
* </p>
*/
public interface HeaderExtractor {

/**
* Returns the value of the provided header name
*
* @param headerName the name of the header to extract
* @return the value of the provided header name
*/
String getFirstHeader(String headerName);
}
37 changes: 37 additions & 0 deletions apm-agent-api/src/main/java/co/elastic/apm/api/HeaderInjector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 Elastic and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package co.elastic.apm.api;

/**
* Used to inject a header into a networking library's underlying outgoing request object.
* <p>
* Can be implemented as a lambda in Java 8 and as an anonymous inner class in Java 7.
* </p>
*/
public interface HeaderInjector {

/**
* Injects a header key and value into the underlying request object of an networking library
*
* @param headerName the name of the header
* @param headerValue the value of the header
*/
void addHeader(String headerName, String headerValue);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 Elastic and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package co.elastic.apm.api;

/**
* Used to extract all header values with a specific header name.
* <p>
* Useful for protocols such as HTTP which allow that a header can have multiple values.
* </p>
* <p>
* Can be implemented as a lambda in Java 8 and as an anonymous inner class in Java 7.
* </p>
*/
public interface HeadersExtractor {

/**
* Returns all values of the provided header name
*
* @param headerName the name of the header to extract
* @return all values of the provided header name
*/
Iterable<String> getAllHeaders(String headerName);
}
5 changes: 5 additions & 0 deletions apm-agent-api/src/main/java/co/elastic/apm/api/NoopSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ public boolean isSampled() {
public Span createSpan() {
return INSTANCE;
}

@Override
public void injectTraceHeaders(HeaderInjector headerInjector) {
// noop
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,9 @@ public boolean isSampled() {
public Span createSpan() {
return NoopSpan.INSTANCE;
}

@Override
public void injectTraceHeaders(HeaderInjector headerInjector) {
// noop
}
}
21 changes: 20 additions & 1 deletion apm-agent-api/src/main/java/co/elastic/apm/api/Span.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ public interface Span {
void end();

/**
* Captures an exception and reports it to the APM server.
*
* @param throwable
* @param throwable the exception to report
*/
void captureException(Throwable throwable);

Expand Down Expand Up @@ -192,4 +193,22 @@ public interface Span {
*/
boolean isSampled();

/**
* Allows for manual propagation of the tracing headers.
* <p>
* If you want to manually instrument an RPC framework which is not already supported by the auto-instrumentation capabilities of the agent,
* you can use this method to inject the required tracing headers into the header section of that framework's request object.
* </p>
* <p>
* Example:
* </p>
* <pre>
* span.injectTraceHeaders(request::addHeader);
* </pre>
*
* @param headerInjector tells the agent how to inject a header into the request object
* @since 1.3.0
*/
void injectTraceHeaders(HeaderInjector headerInjector);

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import co.elastic.apm.agent.bci.bytebuddy.AnnotationValueOffsetMappingFactory;
import co.elastic.apm.agent.bci.bytebuddy.ErrorLoggingListener;
import co.elastic.apm.agent.bci.bytebuddy.FailSafeDeclaredMethodsCompiler;
import co.elastic.apm.agent.bci.bytebuddy.MatcherTimer;
import co.elastic.apm.agent.bci.bytebuddy.MinimumClassFileVersionValidator;
import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory;
Expand All @@ -36,7 +37,6 @@
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.pool.TypePool;
Expand Down Expand Up @@ -107,7 +107,7 @@ public void run() {
ElasticApmAgent.instrumentation = instrumentation;
final ByteBuddy byteBuddy = new ByteBuddy()
.with(TypeValidation.of(logger.isDebugEnabled()))
.with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE);
.with(FailSafeDeclaredMethodsCompiler.INSTANCE);
AgentBuilder agentBuilder = getAgentBuilder(byteBuddy, coreConfiguration);
int numberOfAdvices = 0;
for (final ElasticApmInstrumentation advice : instrumentations) {
Expand Down Expand Up @@ -161,7 +161,7 @@ public boolean matches(TypeDescription typeDescription, ClassLoader classLoader,
try {
typeMatches = advice.getTypeMatcher().matches(typeDescription);
} catch (Exception ignored) {
// happens for example on WebSphere, not sure why ¯\_(ツ)_/¯
// could be because of a missing type
typeMatches = false;
}
if (typeMatches) {
Expand All @@ -186,7 +186,13 @@ public boolean matches(TypeDescription typeDescription, ClassLoader classLoader,
public boolean matches(MethodDescription target) {
long start = System.nanoTime();
try {
final boolean matches = advice.getMethodMatcher().matches(target);
boolean matches;
try {
matches = advice.getMethodMatcher().matches(target);
} catch (Exception ignored) {
// could be because of a missing type
matches = false;
}
if (matches) {
logger.debug("Method match for advice {}: {} matches {}",
advice.getClass().getSimpleName(), advice.getMethodMatcher(), target);
Expand Down
Loading