Skip to content

Commit

Permalink
add instrumentation for servlet-5.0 (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
shashank11p authored May 14, 2024
1 parent 281b70f commit cbf3c2f
Show file tree
Hide file tree
Showing 26 changed files with 2,943 additions and 1 deletion.
1 change: 1 addition & 0 deletions instrumentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
dependencies{
implementation(project(":instrumentation:servlet:servlet-rw"))
implementation(project(":instrumentation:servlet:servlet-3.0"))
implementation(project(":instrumentation:servlet:servlet-5.0"))
implementation(project(":instrumentation:spark-2.3"))
implementation(project(":instrumentation:grpc-1.6"))
implementation(project(":instrumentation:grpc-shaded-netty-1.9"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
import javax.servlet.http.HttpSession;
import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes;
import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair;
import org.hypertrace.agent.core.instrumentation.buffer.*;
import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream;
import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter;
import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair;
import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair;
import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair;

public class Utils {

Expand Down
38 changes: 38 additions & 0 deletions instrumentation/servlet/servlet-5.0/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
`java-library`
id("net.bytebuddy.byte-buddy")
id("io.opentelemetry.instrumentation.auto-instrumentation")
muzzle
}
evaluationDependsOn(":javaagent-tooling")

muzzle {
pass {
group = "jakarta.servlet"
module = "jakarta.servlet-api"
versions = "[5.0.0,)"
}
}

afterEvaluate{
io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project,
sourceSets.main.get(),
io.opentelemetry.javaagent.tooling.muzzle.generation.MuzzleCodeGenerationPlugin::class.java.name,
files(project(":javaagent-tooling").configurations["instrumentationMuzzle"], configurations.runtimeClasspath)
).configure()
}

val versions: Map<String, String> by extra

dependencies {
implementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-common:${versions["opentelemetry_java_agent"]}")
implementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-5.0:${versions["opentelemetry_java_agent"]}") // Servlet5Accessor
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap:${versions["opentelemetry_java_agent"]}")
compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0")
muzzleBootstrap("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-common-bootstrap:${versions["opentelemetry_java_agent"]}")

testImplementation(project(":testing-common", "shadow"))
testCompileOnly("com.squareup.okhttp3:okhttp:4.9.0")
testImplementation("org.eclipse.jetty:jetty-server:11.0.0")
testImplementation("org.eclipse.jetty:jetty-servlet:11.0.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* Copyright The Hypertrace Authors
*
* 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.
*/

package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.hypertrace.agent.core.config.InstrumentationConfig;
import org.hypertrace.agent.core.filter.FilterResult;
import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap;
import org.hypertrace.agent.core.instrumentation.HypertraceEvaluationException;
import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes;
import org.hypertrace.agent.core.instrumentation.SpanAndObjectPair;
import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream;
import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter;
import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair;
import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair;
import org.hypertrace.agent.core.instrumentation.buffer.StringMapSpanPair;
import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils;
import org.hypertrace.agent.filter.FilterRegistry;

public class Servlet50AndFilterInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("jakarta.servlet.Filter");
}

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return hasSuperType(namedOneOf("jakarta.servlet.Filter", "jakarta.servlet.Servlet"));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
namedOneOf("doFilter", "service")
.and(takesArgument(0, named("jakarta.servlet.ServletRequest")))
.and(takesArgument(1, named("jakarta.servlet.ServletResponse")))
.and(isPublic()),
Servlet50AndFilterInstrumentation.class.getName() + "$ServletAdvice");
}

public static class ServletAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class)
public static boolean start(
@Advice.Argument(value = 0) ServletRequest request,
@Advice.Argument(value = 1) ServletResponse response,
@Advice.Local("currentSpan") Span currentSpan) {

int callDepth =
HypertraceCallDepthThreadLocalMap.incrementCallDepth(Servlet50InstrumentationName.class);
if (callDepth > 0) {
return false;
}
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
return false;
}

HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
currentSpan = Java8BytecodeBridge.currentSpan();

InstrumentationConfig instrumentationConfig = InstrumentationConfig.ConfigProvider.get();

Utils.addSessionId(currentSpan, httpRequest);

// set request headers
Enumeration<String> headerNames = httpRequest.getHeaderNames();
Map<String, String> headers = new HashMap<>();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = httpRequest.getHeader(headerName);
AttributeKey<String> attributeKey =
HypertraceSemanticAttributes.httpRequestHeader(headerName);

if (instrumentationConfig.httpHeaders().request()) {
currentSpan.setAttribute(attributeKey, headerValue);
}
headers.put(attributeKey.getKey(), headerValue);
}

FilterResult filterResult =
FilterRegistry.getFilter().evaluateRequestHeaders(currentSpan, headers);
if (filterResult.shouldBlock()) {
try {
httpResponse.getWriter().write(filterResult.getBlockingMsg());
} catch (IOException ignored) {
}
httpResponse.setStatus(filterResult.getBlockingStatusCode());
// skip execution of the user code
return true;
}

if (instrumentationConfig.httpBody().request()
&& ContentTypeUtils.shouldCapture(httpRequest.getContentType())) {
// The HttpServletRequest instrumentation uses this to
// enable the instrumentation
VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class)
.set(
httpRequest,
new SpanAndObjectPair(currentSpan, Collections.unmodifiableMap(headers)));
}
return false;
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exit(
@Advice.Argument(0) ServletRequest request,
@Advice.Argument(1) ServletResponse response,
@Advice.Thrown(readOnly = false) Throwable throwable,
@Advice.Local("currentSpan") Span currentSpan) {
int callDepth =
HypertraceCallDepthThreadLocalMap.decrementCallDepth(Servlet50InstrumentationName.class);
if (callDepth > 0) {
return;
}
// we are in the most outermost level of Servlet instrumentation

if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
return;
}

HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
InstrumentationConfig instrumentationConfig = InstrumentationConfig.ConfigProvider.get();

try {
// response context to capture body and clear the context
VirtualField<HttpServletResponse, SpanAndObjectPair> responseContextStore =
VirtualField.find(HttpServletResponse.class, SpanAndObjectPair.class);
VirtualField<ServletOutputStream, BoundedByteArrayOutputStream> outputStreamContextStore =
VirtualField.find(ServletOutputStream.class, BoundedByteArrayOutputStream.class);
VirtualField<PrintWriter, BoundedCharArrayWriter> writerContextStore =
VirtualField.find(PrintWriter.class, BoundedCharArrayWriter.class);

// request context to clear body buffer
VirtualField<HttpServletRequest, SpanAndObjectPair> requestContextStore =
VirtualField.find(HttpServletRequest.class, SpanAndObjectPair.class);
VirtualField<ServletInputStream, ByteBufferSpanPair> inputStreamContextStore =
VirtualField.find(ServletInputStream.class, ByteBufferSpanPair.class);
VirtualField<BufferedReader, CharBufferSpanPair> readerContextStore =
VirtualField.find(BufferedReader.class, CharBufferSpanPair.class);
VirtualField<HttpServletRequest, StringMapSpanPair> urlEncodedMapContextStore =
VirtualField.find(HttpServletRequest.class, StringMapSpanPair.class);

if (!request.isAsyncStarted()) {
if (instrumentationConfig.httpHeaders().response()) {
for (String headerName : httpResponse.getHeaderNames()) {
String headerValue = httpResponse.getHeader(headerName);
currentSpan.setAttribute(
HypertraceSemanticAttributes.httpResponseHeader(headerName), headerValue);
}
}

// capture response body
if (instrumentationConfig.httpBody().response()
&& ContentTypeUtils.shouldCapture(httpResponse.getContentType())) {
Utils.captureResponseBody(
currentSpan,
httpResponse,
responseContextStore,
outputStreamContextStore,
writerContextStore);
}

// remove request body buffers from context stores, otherwise they might get reused
if (instrumentationConfig.httpBody().request()
&& ContentTypeUtils.shouldCapture(httpRequest.getContentType())) {
Utils.resetRequestBodyBuffers(
httpRequest,
requestContextStore,
inputStreamContextStore,
readerContextStore,
urlEncodedMapContextStore);
}
}
} finally {
Throwable tmp = throwable;
while (tmp != null) { // loop in case our exception is nested (eg. springframework)
if (tmp instanceof HypertraceEvaluationException) {
FilterResult filterResult = ((HypertraceEvaluationException) tmp).getFilterResult();
try {
httpResponse.getWriter().write(filterResult.getBlockingMsg());
} catch (IOException ignored) {
}
httpResponse.setStatus(filterResult.getBlockingStatusCode());
// bytebuddy treats the reassignment of this variable to null as an instruction to
// suppress this exception, which is what we want
throwable = null;
break;
}
tmp = tmp.getCause();
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The Hypertrace Authors
*
* 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.
*/

package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.async.Servlet50AsyncInstrumentation;
import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request.ServletInputStreamInstrumentation;
import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.request.ServletRequestInstrumentation;
import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.response.ServletOutputStreamInstrumentation;
import io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0.response.ServletResponseInstrumentation;
import java.util.Arrays;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class Servlet50InstrumentationModule extends InstrumentationModule {

public Servlet50InstrumentationModule() {
super(Servlet50InstrumentationName.PRIMARY, Servlet50InstrumentationName.OTHER);
}

@Override
public int order() {
return 1;
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Arrays.asList(
new Servlet50AndFilterInstrumentation(),
new ServletRequestInstrumentation(),
new ServletInputStreamInstrumentation(),
new ServletResponseInstrumentation(),
new ServletOutputStreamInstrumentation(),
new Servlet50AsyncInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright The Hypertrace Authors
*
* 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.
*/

package io.opentelemetry.javaagent.instrumentation.hypertrace.servlet.v5_0;

public class Servlet50InstrumentationName {
public static final String PRIMARY = "servlet";
public static final String[] OTHER = {"servlet-5", "ht", "servlet-ht", "servlet-5-ht"};
}
Loading

0 comments on commit cbf3c2f

Please sign in to comment.