diff --git a/agent-module/agent-testweb/ktor-plugin-testweb/pom.xml b/agent-module/agent-testweb/ktor-plugin-testweb/pom.xml new file mode 100644 index 000000000000..a70b3e1794f9 --- /dev/null +++ b/agent-module/agent-testweb/ktor-plugin-testweb/pom.xml @@ -0,0 +1,111 @@ + + + + 4.0.0 + + com.navercorp.pinpoint + pinpoint-agent-testweb + 3.0.1-SNAPSHOT + + + pinpoint-ktor-plugin-testweb + + jar + + + + ${pinpoint.agent.default.jvmargument} + + 2.0.0 + + + + + io.ktor + ktor-server-status-pages + 2.3.12 + runtime + + + io.ktor + ktor-server-core-jvm + 2.3.12 + + + io.ktor + ktor-server-netty-jvm + 2.3.12 + + + io.ktor + ktor-server-config-yaml-jvm + 2.3.12 + runtime + + + io.ktor + ktor-server-tests-jvm + 2.3.12 + + + + io.ktor + ktor-client-core-jvm + 2.3.12 + + + + io.ktor + ktor-client-cio-jvm + 2.3.12 + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + 1.8 + + + + + \ No newline at end of file diff --git a/agent-module/agent-testweb/ktor-plugin-testweb/src/main/java/com/pinpoint/test/plugin/Application.kt b/agent-module/agent-testweb/ktor-plugin-testweb/src/main/java/com/pinpoint/test/plugin/Application.kt new file mode 100644 index 000000000000..d479132dac46 --- /dev/null +++ b/agent-module/agent-testweb/ktor-plugin-testweb/src/main/java/com/pinpoint/test/plugin/Application.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.pinpoint.test.plugin + +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* + +fun main() { + embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) + .start(wait = true) +} + +fun Application.module() { + configureRouting() +} \ No newline at end of file diff --git a/agent-module/agent-testweb/ktor-plugin-testweb/src/main/java/com/pinpoint/test/plugin/Routing.kt b/agent-module/agent-testweb/ktor-plugin-testweb/src/main/java/com/pinpoint/test/plugin/Routing.kt new file mode 100644 index 000000000000..71a0350ec2e0 --- /dev/null +++ b/agent-module/agent-testweb/ktor-plugin-testweb/src/main/java/com/pinpoint/test/plugin/Routing.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.pinpoint.test.plugin + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.http.content.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureRouting() { + install(StatusPages) { + exception { call, cause -> + call.respondText("App in illegal state as ${cause.message}") + } + } + routing { + staticResources("/content", "mycontent") + + get("/") { + call.respondText("Hello World!") + } + + get("/test1") { + val text = "

Hello From Ktor

" + val type = ContentType.parse("text/html") + call.respondText(text, type) + } + get("/client") { + val client = HttpClient(CIO) + val response: HttpResponse = client.get("https://ktor.io/") + println(response.status) + client.close() + call.respondText(response.toString()) + } + + get("/error-test") { + throw IllegalStateException("Too Busy") + } + } +} \ No newline at end of file diff --git a/agent-module/agent-testweb/pom.xml b/agent-module/agent-testweb/pom.xml index 5c172c7b0ddb..6e21ff0ad1d6 100644 --- a/agent-module/agent-testweb/pom.xml +++ b/agent-module/agent-testweb/pom.xml @@ -88,6 +88,7 @@ okhttp-plugin-testweb grpc-plugin-testweb resilience4j-plugin-testweb + ktor-plugin-testweb closed-module-testweb closed-module-testlib diff --git a/agent-module/agent/src/main/resources/profiles/local/pinpoint.config b/agent-module/agent/src/main/resources/profiles/local/pinpoint.config index b5de7faa5caf..8d3ce67548fb 100644 --- a/agent-module/agent/src/main/resources/profiles/local/pinpoint.config +++ b/agent-module/agent/src/main/resources/profiles/local/pinpoint.config @@ -623,6 +623,34 @@ profiler.reactor-netty.client.mark.error.transport.error=false profiler.reactor-netty.client.trace.http.error=true profiler.reactor-netty.client.mark.error.http.error=false +########################################################### +# Ktor +########################################################### +profiler.ktor.enable=true + +# Server +# Classes for detecting application server type. Comma separated list of fully qualified class names. Wildcard not supported. +profiler.ktor.server.bootstrap.main= +# trace param in request ,default value is true +profiler.ktor.server.tracerequestparam=true +# URLs to exclude from tracing. +# Support ant style pattern. e.g. /aa/*.html, /??/exclude.html +profiler.ktor.server.excludeurl= +# profiler.ktor.server.trace.excludemethod= +# HTTP Request methods to exclude from tracing +#profiler.ktor.server.excludemethod= + +# original IP address header +# https://en.wikipedia.org/wiki/X-Forwarded-For +#profiler.ktor.server.realipheader=X-Forwarded-For +# nginx real ip header +#profiler.ktor.server.realipheader=X-Real-IP +# optional parameter, If the header value is ${profiler.ktor.realipemptyvalue}, Ignore header value. +#profiler.ktor.server.realipemptyvalue=unknown + +# Retransform +profiler.ktor.http.server.retransform.configure-routing=true + ########################################################### # JSP # ########################################################### diff --git a/agent-module/agent/src/main/resources/profiles/release/pinpoint.config b/agent-module/agent/src/main/resources/profiles/release/pinpoint.config index 9783fc91c6f9..19ea5ec07048 100644 --- a/agent-module/agent/src/main/resources/profiles/release/pinpoint.config +++ b/agent-module/agent/src/main/resources/profiles/release/pinpoint.config @@ -620,6 +620,34 @@ profiler.reactor-netty.client.mark.error.transport.error=false profiler.reactor-netty.client.trace.http.error=false profiler.reactor-netty.client.mark.error.http.error=false +########################################################### +# Ktor +########################################################### +profiler.ktor.enable=false + +# Server +# Classes for detecting application server type. Comma separated list of fully qualified class names. Wildcard not supported. +profiler.ktor.server.bootstrap.main= +# trace param in request ,default value is true +profiler.ktor.server.tracerequestparam=true +# URLs to exclude from tracing. +# Support ant style pattern. e.g. /aa/*.html, /??/exclude.html +profiler.ktor.server.excludeurl= +# profiler.ktor.server.trace.excludemethod= +# HTTP Request methods to exclude from tracing +#profiler.ktor.server.excludemethod= + +# original IP address header +# https://en.wikipedia.org/wiki/X-Forwarded-For +#profiler.ktor.server.realipheader=X-Forwarded-For +# nginx real ip header +#profiler.ktor.server.realipheader=X-Real-IP +# optional parameter, If the header value is ${profiler.ktor.realipemptyvalue}, Ignore header value. +#profiler.ktor.server.realipemptyvalue=unknown + +# Retransform +profiler.ktor.http.server.retransform.configure-routing=true + ########################################################### # JSP # ########################################################### diff --git a/agent-module/plugins/assembly/pom.xml b/agent-module/plugins/assembly/pom.xml index 28c5d9076b7f..8056159ff674 100644 --- a/agent-module/plugins/assembly/pom.xml +++ b/agent-module/plugins/assembly/pom.xml @@ -432,6 +432,11 @@ pinpoint-spring-cloud-sleuth-plugin ${project.version} + + com.navercorp.pinpoint + pinpoint-ktor-plugin + ${project.version} + diff --git a/agent-module/plugins/ktor/README.md b/agent-module/plugins/ktor/README.md new file mode 100644 index 000000000000..cca74f2472a0 --- /dev/null +++ b/agent-module/plugins/ktor/README.md @@ -0,0 +1,38 @@ +## Ktor +* Since: Pinpoint 3.0.1 +* See: https://ktor.io/ + +### Pinpoint Configuration +pinpoint.config + +#### Set enable options. +~~~ +########################################################### +# Ktor +########################################################### +profiler.ktor.enable=false + +# Server +# Classes for detecting application server type. Comma separated list of fully qualified class names. Wildcard not supported. +profiler.ktor.server.bootstrap.main= +# trace param in request ,default value is true +profiler.ktor.server.tracerequestparam=true +# URLs to exclude from tracing. +# Support ant style pattern. e.g. /aa/*.html, /??/exclude.html +profiler.ktor.server.excludeurl= +# profiler.ktor.server.trace.excludemethod= +# HTTP Request methods to exclude from tracing +#profiler.ktor.server.excludemethod= + +# original IP address header +# https://en.wikipedia.org/wiki/X-Forwarded-For +#profiler.ktor.server.realipheader=X-Forwarded-For +# nginx real ip header +#profiler.ktor.server.realipheader=X-Real-IP +# optional parameter, If the header value is ${profiler.ktor.realipemptyvalue}, Ignore header value. +#profiler.ktor.server.realipemptyvalue=unknown + +# Retransform +profiler.ktor.http.server.retransform.configure-routing=true + +~~~ diff --git a/agent-module/plugins/ktor/pom.xml b/agent-module/plugins/ktor/pom.xml new file mode 100644 index 000000000000..7cb5411a184b --- /dev/null +++ b/agent-module/plugins/ktor/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.navercorp.pinpoint + pinpoint-plugins + 3.0.1-SNAPSHOT + + + pinpoint-ktor-plugin + pinpoint-ktor-plugin + jar + + + + + + + com.navercorp.pinpoint + pinpoint-bootstrap-core + provided + + + com.navercorp.pinpoint + pinpoint-common-servlet + ${project.version} + provided + + + + io.ktor + ktor-server-core-jvm + 2.3.12 + provided + + + io.ktor + ktor-server-netty-jvm + 2.3.12 + provided + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + com/navercorp/**/* + META-INF/**/* + + + + + + diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/CoroutineContextGetter.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/CoroutineContextGetter.java new file mode 100644 index 000000000000..13e1af430812 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/CoroutineContextGetter.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor; + +import kotlin.coroutines.CoroutineContext; + +public interface CoroutineContextGetter { + CoroutineContext _$PINPOINT$_getCoroutineContext(); +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorConstants.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorConstants.java new file mode 100644 index 000000000000..a89f47f33cf9 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorConstants.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor; + +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.common.trace.ServiceTypeProvider; + +public class KtorConstants { + + public static final ServiceType KTOR = ServiceTypeProvider.getByName("KTOR"); + public static final ServiceType KTOR_INTERNAL = ServiceTypeProvider.getByName("KTOR_INTERNAL"); + +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorDetector.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorDetector.java new file mode 100644 index 000000000000..afa3dad92e28 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorDetector.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor; + +import com.navercorp.pinpoint.bootstrap.resolver.condition.MainClassCondition; +import com.navercorp.pinpoint.common.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; + +public class KtorDetector { + private static final String DEFAULT_EXPECTED_MAIN_CLASS = "io.ktor.server.engine.ApplicationEngine"; + + private final List expectedMainClasses; + + public KtorDetector(List expectedMainClasses) { + if (CollectionUtils.isEmpty(expectedMainClasses)) { + this.expectedMainClasses = Collections.singletonList(DEFAULT_EXPECTED_MAIN_CLASS); + } else { + this.expectedMainClasses = expectedMainClasses; + } + } + + public boolean detect() { + String bootstrapMainClass = MainClassCondition.INSTANCE.getValue(); + boolean isExpectedMainClass = expectedMainClasses.contains(bootstrapMainClass); + if (!isExpectedMainClass) { + return false; + } + return true; + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorPlugin.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorPlugin.java new file mode 100644 index 000000000000..68ce4447b56a --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorPlugin.java @@ -0,0 +1,155 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor; + +import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessor; +import com.navercorp.pinpoint.bootstrap.instrument.InstrumentClass; +import com.navercorp.pinpoint.bootstrap.instrument.InstrumentException; +import com.navercorp.pinpoint.bootstrap.instrument.InstrumentMethod; +import com.navercorp.pinpoint.bootstrap.instrument.Instrumentor; +import com.navercorp.pinpoint.bootstrap.instrument.MethodFilters; +import com.navercorp.pinpoint.bootstrap.instrument.transformer.MatchableTransformTemplate; +import com.navercorp.pinpoint.bootstrap.instrument.transformer.MatchableTransformTemplateAware; +import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformCallback; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogManager; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogger; +import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin; +import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPluginSetupContext; +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.plugin.ktor.interceptor.ConfigureRoutingFactoryInterceptor; +import com.navercorp.pinpoint.plugin.ktor.interceptor.NettyApplicationCallHandlerInterceptor; +import com.navercorp.pinpoint.plugin.ktor.interceptor.NettyHttp1HandlerHandleRequestInterceptor; +import com.navercorp.pinpoint.plugin.ktor.interceptor.NettyHttp1HandlerPrepareCallFromRequestInterceptor; +import com.navercorp.pinpoint.plugin.ktor.interceptor.SuspendFunctionGunInterceptor; + +import java.security.ProtectionDomain; + +import static com.navercorp.pinpoint.common.util.VarArgs.va; + +public class KtorPlugin implements ProfilerPlugin, MatchableTransformTemplateAware { + private final PluginLogger logger = PluginLogManager.getLogger(getClass()); + private MatchableTransformTemplate transformTemplate; + + @Override + public void setup(ProfilerPluginSetupContext context) { + KtorPluginConfig config = new KtorPluginConfig(context.getConfig()); + if (!config.isEnable()) { + logger.info("{} disabled", this.getClass().getSimpleName()); + return; + } + logger.info("{} config:{}", this.getClass().getSimpleName(), config); + + if (ServiceType.UNDEFINED.equals(context.getConfiguredApplicationType())) { + final KtorDetector detector = new KtorDetector(config.getBootstrapMains()); + if (detector.detect()) { + logger.info("Detected application type : {}", KtorConstants.KTOR); + if (!context.registerApplicationType(KtorConstants.KTOR)) { + logger.info("Application type [{}] already set, skipping [{}] registration.", context.getApplicationType(), KtorConstants.KTOR); + } + } + } + + // Server + transformTemplate.transform("io.ktor.server.netty.http1.NettyHttp1Handler", NettyHttp1HandlerTransform.class); + transformTemplate.transform("io.ktor.server.netty.http1.NettyHttp1ApplicationCall", NettyHttp1ApplicationCallTransform.class); + transformTemplate.transform("io.ktor.server.netty.NettyApplicationCallHandler", NettyApplicationCallHandlerTransform.class); + transformTemplate.transform("io.ktor.util.pipeline.SuspendFunctionGun", SuspendFunctionGunTransform.class); + if (config.isRetransformConfigureRouting()) { + transformTemplate.transform("io.ktor.server.routing.Route", RouteTransform.class); + } + } + + @Override + public void setTransformTemplate(MatchableTransformTemplate transformTemplate) { + this.transformTemplate = transformTemplate; + } + + public static class NettyHttp1HandlerTransform implements TransformCallback { + @Override + public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { + final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer); + // Async Object + target.addField(AsyncContextAccessor.class); + + InstrumentMethod handleRequestMethod = target.getDeclaredMethod("handleRequest", "io.netty.channel.ChannelHandlerContext", "io.netty.handler.codec.http.HttpRequest"); + if (handleRequestMethod != null) { + handleRequestMethod.addInterceptor(NettyHttp1HandlerHandleRequestInterceptor.class); + } + + InstrumentMethod prepareCallFromRequestMethod = target.getDeclaredMethod("prepareCallFromRequest", "io.netty.channel.ChannelHandlerContext", "io.netty.handler.codec.http.HttpRequest"); + if (prepareCallFromRequestMethod != null) { + prepareCallFromRequestMethod.addInterceptor(NettyHttp1HandlerPrepareCallFromRequestInterceptor.class); + } + + return target.toBytecode(); + } + } + + public static class NettyHttp1ApplicationCallTransform implements TransformCallback { + @Override + public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { + final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer); + // Async Object + target.addField(AsyncContextAccessor.class); + + return target.toBytecode(); + } + } + + public static class NettyApplicationCallHandlerTransform implements TransformCallback { + @Override + public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { + final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer); + target.addGetter(CoroutineContextGetter.class, "coroutineContext"); + + for (InstrumentMethod method : target.getDeclaredMethods(MethodFilters.name("handleRequest"))) { + if (method != null) { + method.addInterceptor(NettyApplicationCallHandlerInterceptor.class); + } + } + + return target.toBytecode(); + } + } + + public static class SuspendFunctionGunTransform implements TransformCallback { + @Override + public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { + final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer); + + for (InstrumentMethod method : target.getDeclaredMethods(MethodFilters.name("loop"))) { + method.addInterceptor(SuspendFunctionGunInterceptor.class); + } + + return target.toBytecode(); + } + } + + public static class RouteTransform implements TransformCallback { + @Override + public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { + final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer); + + final RouteMethodTransformer routeMethodTransformer = new RouteMethodTransformer(Boolean.FALSE); + for (InstrumentMethod method : target.getDeclaredMethods(MethodFilters.name("handle"))) { + method.addInterceptor(ConfigureRoutingFactoryInterceptor.class, va(routeMethodTransformer)); + } + + return target.toBytecode(); + } + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorPluginConfig.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorPluginConfig.java new file mode 100644 index 000000000000..83648b6075d6 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/KtorPluginConfig.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor; + +import com.navercorp.pinpoint.bootstrap.config.Filter; +import com.navercorp.pinpoint.bootstrap.config.ProfilerConfig; +import com.navercorp.pinpoint.bootstrap.config.ServerConfig; + +import java.util.List; +import java.util.Objects; + +public class KtorPluginConfig { + + private final boolean enable; + private final List bootstrapMains; + private final boolean enableAsyncEndPoint; + private final boolean traceRequestParam; + private final Filter excludeUrlFilter; + private final Filter traceExcludeMethodFilter; + private final String realIpHeader; + private final String realIpEmptyValue; + private final Filter excludeProfileMethodFilter; + + private final boolean retransformConfigureRouting; + + public KtorPluginConfig(ProfilerConfig config) { + Objects.requireNonNull(config, "config"); + + // plugin + this.enable = config.readBoolean("profiler.ktor.enable", Boolean.TRUE); + // Server + final ServerConfig serverConfig = new ServerConfig(config); + this.bootstrapMains = config.readList("profiler.ktor.http.server.bootstrap.main"); + this.enableAsyncEndPoint = config.readBoolean("profiler.ktor.http.server.end-point.async.enable", true); + this.traceRequestParam = serverConfig.isTraceRequestParam("profiler.ktor.http.server.tracerequestparam"); + this.excludeUrlFilter = serverConfig.getExcludeUrlFilter("profiler.ktor.http.server.excludeurl"); + this.traceExcludeMethodFilter = serverConfig.getTraceExcludeMethodFilter("profiler.ktor.http.server.trace.excludemethod"); + this.realIpHeader = serverConfig.getRealIpHeader("profiler.ktor.http.server.realipheader"); + this.realIpEmptyValue = serverConfig.getRealIpEmptyValue("profiler.ktor.http.server.realipemptyvalue"); + this.excludeProfileMethodFilter = serverConfig.getExcludeMethodFilter("profiler.ktor.http.server.excludemethod"); + + this.retransformConfigureRouting = config.readBoolean("profiler.ktor.http.server.retransform.configure-routing", Boolean.TRUE); + } + + public boolean isEnable() { + return enable; + } + + public List getBootstrapMains() { + return bootstrapMains; + } + + public boolean isEnableAsyncEndPoint() { + return enableAsyncEndPoint; + } + + public boolean isTraceRequestParam() { + return traceRequestParam; + } + + public Filter getExcludeUrlFilter() { + return excludeUrlFilter; + } + + public Filter getTraceExcludeMethodFilter() { + return traceExcludeMethodFilter; + } + + public String getRealIpHeader() { + return realIpHeader; + } + + public String getRealIpEmptyValue() { + return realIpEmptyValue; + } + + public Filter getExcludeProfileMethodFilter() { + return excludeProfileMethodFilter; + } + + public boolean isRetransformConfigureRouting() { + return retransformConfigureRouting; + } + + @Override + public String toString() { + return "KtorPluginConfig{" + + "enable=" + enable + + ", bootstrapMains=" + bootstrapMains + + ", enableAsyncEndPoint=" + enableAsyncEndPoint + + ", traceRequestParam=" + traceRequestParam + + ", excludeUrlFilter=" + excludeUrlFilter + + ", traceExcludeMethodFilter=" + traceExcludeMethodFilter + + ", realIpHeader='" + realIpHeader + '\'' + + ", realIpEmptyValue='" + realIpEmptyValue + '\'' + + ", excludeProfileMethodFilter=" + excludeProfileMethodFilter + + ", retransformConfigureRouting=" + retransformConfigureRouting + + '}'; + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/RouteMethodTransformer.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/RouteMethodTransformer.java new file mode 100644 index 000000000000..41975dd78f1e --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/RouteMethodTransformer.java @@ -0,0 +1,93 @@ +/* + * Copyright 2014 NAVER Corp. + * 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 com.navercorp.pinpoint.plugin.ktor; + +import com.navercorp.pinpoint.bootstrap.instrument.InstrumentClass; +import com.navercorp.pinpoint.bootstrap.instrument.InstrumentException; +import com.navercorp.pinpoint.bootstrap.instrument.InstrumentMethod; +import com.navercorp.pinpoint.bootstrap.instrument.Instrumentor; +import com.navercorp.pinpoint.bootstrap.instrument.MethodFilter; +import com.navercorp.pinpoint.bootstrap.instrument.MethodFilters; +import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformCallback; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogManager; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogger; +import com.navercorp.pinpoint.plugin.ktor.interceptor.RouteMethodInterceptor; + +import java.lang.reflect.Modifier; +import java.security.ProtectionDomain; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.navercorp.pinpoint.common.util.VarArgs.va; + +public class RouteMethodTransformer implements TransformCallback { + private static final int REQUIRED_ACCESS_FLAG = Modifier.PUBLIC; + private static final int REJECTED_ACCESS_FLAG = Modifier.ABSTRACT | Modifier.NATIVE | Modifier.STATIC; + private static final MethodFilter METHOD_FILTER = MethodFilters.modifier(REQUIRED_ACCESS_FLAG, REJECTED_ACCESS_FLAG); + private final PluginLogger logger = PluginLogManager.getLogger(getClass()); + + private final Object lock = new Object(); + private final AtomicInteger interceptorId = new AtomicInteger(-1); + + private final boolean markError; + + public RouteMethodTransformer(boolean markError) { + this.markError = markError; + } + + @Override + public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { + try { + final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer); + if (!target.isInterceptable()) { + return null; + } + + final List methodList = target.getDeclaredMethods(METHOD_FILTER); + for (InstrumentMethod method : methodList) { + addInterceptor(method); + } + + return target.toBytecode(); + } catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to transform", e); + } + + return null; + } + } + + private void addInterceptor(InstrumentMethod targetMethod) throws InstrumentException { + int id = interceptorId.get(); + + if (id != -1) { + targetMethod.addInterceptor(id); + return; + } + + synchronized (lock) { + id = interceptorId.get(); + if (id != -1) { + targetMethod.addInterceptor(id); + return; + } + + id = targetMethod.addInterceptor(RouteMethodInterceptor.class, va(markError)); + interceptorId.set(id); + } + } +} \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/ConfigureRoutingFactoryInterceptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/ConfigureRoutingFactoryInterceptor.java new file mode 100644 index 000000000000..fee103d3b354 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/ConfigureRoutingFactoryInterceptor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.instrument.Instrumentor; +import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformCallback; +import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogManager; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogger; +import com.navercorp.pinpoint.common.util.ArrayArgumentUtils; + +public class ConfigureRoutingFactoryInterceptor implements AroundInterceptor { + private final PluginLogger logger = PluginLogManager.getLogger(getClass()); + private final boolean isDebug = logger.isDebugEnabled(); + + private final Instrumentor instrumentor; + private final TransformCallback transformer; + + public ConfigureRoutingFactoryInterceptor(Instrumentor instrumentor, TransformCallback transformer) { + this.instrumentor = instrumentor; + this.transformer = transformer; + } + + @Override + public void before(Object target, Object[] args) { + } + + @Override + public void after(Object target, Object[] args, Object result, Throwable throwable) { + if (throwable != null) { + return; + } + + Object object = findHandleObject(args); + if (object == null) { + return; + } + + processBean(object); + } + + Object findHandleObject(Object[] args) { + return ArrayArgumentUtils.getArgument(args, 0, Object.class); + } + + public final void processBean(Object bean) { + Class clazz = bean.getClass(); + + // If you want to trace inherited methods, you have to retranform super classes, too. + instrumentor.retransform(clazz, transformer); + if (isDebug) { + logger.debug("Retransform class=" + clazz.getName()); + } + } +} \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestAdaptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestAdaptor.java new file mode 100644 index 000000000000..c029c672f4e8 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestAdaptor.java @@ -0,0 +1,135 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.plugin.request.RequestAdaptor; +import com.navercorp.pinpoint.common.plugin.util.HostAndPort; +import com.navercorp.pinpoint.common.util.CollectionUtils; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpHeaders; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class HttpRequestAdaptor implements RequestAdaptor { + + @Override + public String getHeader(HttpRequestAndContext httpRequestAndContext, String name) { + try { + final HttpHeaders httpHeaders = httpRequestAndContext.getHttpRequest().headers(); + if (httpHeaders == null) { + return null; + } + final String values = httpHeaders.get(name); + if (values != null) { + return values; + } + } catch (Exception ignored) { + } + + return null; + } + + @Override + public Collection getHeaderNames(HttpRequestAndContext httpRequestAndContext) { + try { + final HttpHeaders httpHeaders = httpRequestAndContext.getHttpRequest().headers(); + if (httpHeaders == null) { + return Collections.emptyList(); + } + final Set headerNames = httpHeaders.names(); + if (CollectionUtils.isEmpty(headerNames)) { + return Collections.emptyList(); + } + Set values = new HashSet<>(headerNames.size()); + for (String headerName : headerNames) { + values.add(headerName); + } + return values; + } catch (Exception ignored) { + } + return Collections.emptyList(); + } + + @Override + public String getRpcName(HttpRequestAndContext httpRequestAndContext) { + try { + return UriUtils.path(httpRequestAndContext.getHttpRequest().uri()); + } catch (Exception ignored) { + } + return null; + } + + @Override + public String getMethodName(HttpRequestAndContext httpRequestAndContext) { + try { + return httpRequestAndContext.getHttpRequest().method().name(); + } catch (Exception ignored) { + } + return null; + } + + @Override + public String getEndPoint(HttpRequestAndContext httpRequestAndContext) { + try { + Channel ch = httpRequestAndContext.getContext().channel(); + if (ch != null) { + return getHost((InetSocketAddress) ch.localAddress()); + } + } catch (Exception ignored) { + } + return null; + } + + @Override + public String getRemoteAddress(HttpRequestAndContext httpRequestAndContext) { + try { + Channel ch = httpRequestAndContext.getContext().channel(); + if (ch != null) { + return getHost((InetSocketAddress) ch.remoteAddress()); + } + } catch (Exception ignored) { + } + return null; + } + + @Override + public String getAcceptorHost(HttpRequestAndContext httpRequestAndContext) { + try { + Channel ch = httpRequestAndContext.getContext().channel(); + if (ch != null) { + return getHost((InetSocketAddress) ch.localAddress()); + } + } catch (Exception ignored) { + } + return null; + } + + private String getHost(InetSocketAddress inetSocketAddress) { + if (inetSocketAddress != null) { + final InetAddress address = inetSocketAddress.getAddress(); + if (address != null) { + return HostAndPort.toHostAndPortString(address.getHostAddress(), inetSocketAddress.getPort()); + } + } + return null; + } +} \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestAndContext.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestAndContext.java new file mode 100644 index 000000000000..1662b368cbcf --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestAndContext.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; + +public class HttpRequestAndContext { + private HttpRequest httpRequest; + private ChannelHandlerContext context; + + public HttpRequestAndContext(HttpRequest httpRequest, ChannelHandlerContext context) { + this.httpRequest = httpRequest; + this.context = context; + } + + public HttpRequest getHttpRequest() { + return httpRequest; + } + + public ChannelHandlerContext getContext() { + return context; + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestParameterExtractor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestParameterExtractor.java new file mode 100644 index 000000000000..ca4db2b7d578 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/HttpRequestParameterExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.plugin.request.util.ParameterExtractor; + +public class HttpRequestParameterExtractor implements ParameterExtractor { + private int eachLimit; + private int totalLimit; + + public HttpRequestParameterExtractor(int eachLimit, int totalLimit) { + this.eachLimit = eachLimit; + this.totalLimit = totalLimit; + } + + @Override + public String extractParameter(HttpRequestAndContext request) { + return ""; + } +} \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/MethodFilterExtractor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/MethodFilterExtractor.java new file mode 100644 index 000000000000..990d9d3a672a --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/MethodFilterExtractor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.config.Filter; +import com.navercorp.pinpoint.bootstrap.plugin.request.util.ParameterExtractor; + +public class MethodFilterExtractor implements ParameterExtractor { + + private final Filter excludeProfileMethodFilter; + + private final ParameterExtractor delegate; + + public MethodFilterExtractor(Filter excludeProfileMethodFilter, ParameterExtractor delegate) { + this.excludeProfileMethodFilter = excludeProfileMethodFilter; + this.delegate = delegate; + } + + @Override + public String extractParameter(HttpRequestAndContext httpRequestAndContext) { + if (excludeProfileMethodFilter.filter(httpRequestAndContext.getHttpRequest().method().name())) { + return null; + } + return delegate.extractParameter(httpRequestAndContext); + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyApplicationCallHandlerInterceptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyApplicationCallHandlerInterceptor.java new file mode 100644 index 000000000000..a7de79a05b2f --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyApplicationCallHandlerInterceptor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessorUtils; +import com.navercorp.pinpoint.bootstrap.context.AsyncContext; +import com.navercorp.pinpoint.bootstrap.context.TraceContext; +import com.navercorp.pinpoint.bootstrap.interceptor.ApiIdAwareAroundInterceptor; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogManager; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogger; +import com.navercorp.pinpoint.plugin.ktor.CoroutineContextGetter; +import kotlin.coroutines.CoroutineContext; + +public class NettyApplicationCallHandlerInterceptor implements ApiIdAwareAroundInterceptor { + private final PluginLogger logger = PluginLogManager.getLogger(this.getClass()); + private final boolean isDebug = logger.isDebugEnabled(); + private final TraceContext traceContext; + + public NettyApplicationCallHandlerInterceptor(TraceContext traceContext) { + this.traceContext = traceContext; + } + + @Override + public void before(Object target, int apiId, Object[] args) { + if (isDebug) { + logger.beforeInterceptor(target, args); + } + try { + final AsyncContext asyncContext = AsyncContextAccessorUtils.getAsyncContext(args, 1); + if (asyncContext != null) { + if (target instanceof CoroutineContextGetter) { + CoroutineContext coroutineContext = ((CoroutineContextGetter) target)._$PINPOINT$_getCoroutineContext(); + AsyncContextAccessorUtils.setAsyncContext(asyncContext, coroutineContext); + } + } + } catch (Throwable t) { + logger.info("Failed to request event handle.", t); + } + } + + @Override + public void after(Object target, int apiId, Object[] args, Object result, Throwable throwable) { + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyHttp1HandlerHandleRequestInterceptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyHttp1HandlerHandleRequestInterceptor.java new file mode 100644 index 000000000000..5536b6e1f634 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyHttp1HandlerHandleRequestInterceptor.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessorUtils; +import com.navercorp.pinpoint.bootstrap.config.ProfilerConfig; +import com.navercorp.pinpoint.bootstrap.context.AsyncContext; +import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor; +import com.navercorp.pinpoint.bootstrap.context.MethodDescriptorHelper; +import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder; +import com.navercorp.pinpoint.bootstrap.context.Trace; +import com.navercorp.pinpoint.bootstrap.context.TraceContext; +import com.navercorp.pinpoint.bootstrap.interceptor.ApiIdAwareAroundInterceptor; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogManager; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogger; +import com.navercorp.pinpoint.bootstrap.plugin.RequestRecorderFactory; +import com.navercorp.pinpoint.bootstrap.plugin.request.RequestAdaptor; +import com.navercorp.pinpoint.bootstrap.plugin.request.ServerCookieRecorder; +import com.navercorp.pinpoint.bootstrap.plugin.request.ServerHeaderRecorder; +import com.navercorp.pinpoint.bootstrap.plugin.request.ServletRequestListener; +import com.navercorp.pinpoint.bootstrap.plugin.request.ServletRequestListenerBuilder; +import com.navercorp.pinpoint.bootstrap.plugin.request.util.ParameterRecorder; +import com.navercorp.pinpoint.common.util.ArrayArgumentUtils; +import com.navercorp.pinpoint.plugin.ktor.KtorConstants; +import com.navercorp.pinpoint.plugin.ktor.KtorPluginConfig; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; + + +public class NettyHttp1HandlerHandleRequestInterceptor implements ApiIdAwareAroundInterceptor { + private final PluginLogger logger = PluginLogManager.getLogger(this.getClass()); + private final boolean isDebug = logger.isDebugEnabled(); + private final TraceContext traceContext; + private final ServletRequestListener servletRequestListener; + + + public NettyHttp1HandlerHandleRequestInterceptor(TraceContext traceContext, RequestRecorderFactory requestRecorderFactory) { + this.traceContext = traceContext; + final KtorPluginConfig config = new KtorPluginConfig(traceContext.getProfilerConfig()); + RequestAdaptor requestAdaptor = new HttpRequestAdaptor(); + ParameterRecorder parameterRecorder = ParameterRecorderFactory.newParameterRecorderFactory(config.getExcludeProfileMethodFilter(), config.isTraceRequestParam()); + + ServletRequestListenerBuilder reqBuilder = new ServletRequestListenerBuilder<>(KtorConstants.KTOR, traceContext, requestAdaptor); + reqBuilder.setExcludeURLFilter(config.getExcludeUrlFilter()); + reqBuilder.setTraceExcludeMethodFilter(config.getTraceExcludeMethodFilter()); + reqBuilder.setParameterRecorder(parameterRecorder); + reqBuilder.setRequestRecorderFactory(requestRecorderFactory); + + final ProfilerConfig profilerConfig = traceContext.getProfilerConfig(); + reqBuilder.setRealIpSupport(config.getRealIpHeader(), config.getRealIpEmptyValue()); + reqBuilder.setHttpStatusCodeRecorder(profilerConfig.getHttpStatusCodeErrors()); + reqBuilder.setServerHeaderRecorder(profilerConfig.readList(ServerHeaderRecorder.CONFIG_KEY_RECORD_REQ_HEADERS)); + reqBuilder.setServerCookieRecorder(profilerConfig.readList(ServerCookieRecorder.CONFIG_KEY_RECORD_REQ_COOKIES)); + + this.servletRequestListener = reqBuilder.build(); + } + + @Override + public void before(Object target, int apiId, Object[] args) { + if (isDebug) { + logger.beforeInterceptor(target, args); + } + try { + final ChannelHandlerContext ctx = ArrayArgumentUtils.getArgument(args, 0, ChannelHandlerContext.class); + final HttpRequest request = ArrayArgumentUtils.getArgument(args, 1, HttpRequest.class); + if (ctx == null || request == null) { + return; + } + + MethodDescriptor methodDescriptor = MethodDescriptorHelper.apiId(apiId); + final HttpRequestAndContext httpRequestAndContext = new HttpRequestAndContext(request, ctx); + this.servletRequestListener.initialized(httpRequestAndContext, KtorConstants.KTOR_INTERNAL, methodDescriptor); + + // Set end-point + final Trace trace = this.traceContext.currentRawTraceObject(); + if (trace == null) { + return; + } + + final SpanEventRecorder recorder = trace.currentSpanEventRecorder(); + if (recorder != null) { + // make asynchronous trace-id + final AsyncContext asyncContext = recorder.recordNextAsyncContext(); + // HttpRequest + AsyncContextAccessorUtils.setAsyncContext(asyncContext, args, 1); + if (isDebug) { + logger.debug("Set asyncContext to args[2]. asyncContext={}", asyncContext); + } + } + } catch (Throwable t) { + logger.info("Failed to request event handle.", t); + } + } + + @Override + public void after(Object target, int apiId, Object[] args, Object result, Throwable throwable) { + if (isDebug) { + logger.afterInterceptor(target, args, result, throwable); + } + + try { + final ChannelHandlerContext ctx = ArrayArgumentUtils.getArgument(args, 0, ChannelHandlerContext.class); + final HttpRequest request = ArrayArgumentUtils.getArgument(args, 1, HttpRequest.class); + if (ctx == null || request == null) { + return; + } + + final int statusCode = getStatusCode(request); + final HttpRequestAndContext httpRequestAndContext = new HttpRequestAndContext(request, ctx); + this.servletRequestListener.destroyed(httpRequestAndContext, throwable, statusCode); + } catch (Throwable t) { + logger.info("Failed to request event handle.", t); + } + } + + private int getStatusCode(final HttpRequest response) { + return 0; + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyHttp1HandlerPrepareCallFromRequestInterceptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyHttp1HandlerPrepareCallFromRequestInterceptor.java new file mode 100644 index 000000000000..bf5f135cffdd --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/NettyHttp1HandlerPrepareCallFromRequestInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessorUtils; +import com.navercorp.pinpoint.bootstrap.context.AsyncContext; +import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder; +import com.navercorp.pinpoint.bootstrap.context.TraceContext; +import com.navercorp.pinpoint.bootstrap.interceptor.SpanEventApiIdAwareAroundInterceptorForPlugin; +import com.navercorp.pinpoint.plugin.ktor.KtorConstants; + +public class NettyHttp1HandlerPrepareCallFromRequestInterceptor extends SpanEventApiIdAwareAroundInterceptorForPlugin { + public NettyHttp1HandlerPrepareCallFromRequestInterceptor(TraceContext traceContext) { + super(traceContext); + } + + @Override + public void doInBeforeTrace(SpanEventRecorder recorder, Object target, int apiId, Object[] args) throws Exception { + } + + @Override + public void doInAfterTrace(SpanEventRecorder recorder, Object target, int apiId, Object[] args, Object result, Throwable throwable) throws Exception { + recorder.recordServiceType(KtorConstants.KTOR_INTERNAL); + recorder.recordException(throwable); + recorder.recordApiId(apiId); + + if (throwable == null) { + AsyncContext asyncContext = recorder.recordNextAsyncContext(); + AsyncContextAccessorUtils.setAsyncContext(asyncContext, result); + } + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/ParameterRecorderFactory.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/ParameterRecorderFactory.java new file mode 100644 index 000000000000..a2482aa7fd24 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/ParameterRecorderFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.config.Filter; +import com.navercorp.pinpoint.bootstrap.plugin.request.util.DisableParameterRecorder; +import com.navercorp.pinpoint.bootstrap.plugin.request.util.HttpParameterRecorder; +import com.navercorp.pinpoint.bootstrap.plugin.request.util.ParameterExtractor; +import com.navercorp.pinpoint.bootstrap.plugin.request.util.ParameterRecorder; + +public class ParameterRecorderFactory { + public static ParameterRecorder newParameterRecorderFactory(Filter excludeProfileMethodFilter, boolean traceRequestParam) { + if (!traceRequestParam) { + return new DisableParameterRecorder<>(); + } + ParameterExtractor parameterExtractor = new HttpRequestParameterExtractor(64, 512); + ParameterExtractor methodFilterExtractor = new MethodFilterExtractor(excludeProfileMethodFilter, parameterExtractor); + return new HttpParameterRecorder<>(methodFilterExtractor); + } +} diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/RouteMethodInterceptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/RouteMethodInterceptor.java new file mode 100644 index 000000000000..bd235925a128 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/RouteMethodInterceptor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder; +import com.navercorp.pinpoint.bootstrap.context.Trace; +import com.navercorp.pinpoint.bootstrap.context.TraceContext; +import com.navercorp.pinpoint.bootstrap.interceptor.ApiIdAwareAroundInterceptor; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogManager; +import com.navercorp.pinpoint.bootstrap.logging.PluginLogger; +import com.navercorp.pinpoint.plugin.ktor.KtorConstants; + +public class RouteMethodInterceptor implements ApiIdAwareAroundInterceptor { + private final PluginLogger logger = PluginLogManager.getLogger(RouteMethodInterceptor.class); + private final boolean isDebug = logger.isDebugEnabled(); + + private final TraceContext traceContext; + private final boolean markError; + + public RouteMethodInterceptor(TraceContext traceContext, boolean markError) { + this.traceContext = traceContext; + this.markError = markError; + } + + @Override + public void before(Object target, int apiId, Object[] args) { + if (isDebug) { + logger.beforeInterceptor(target, args); + } + + final Trace trace = traceContext.currentTraceObject(); + if (trace == null) { + return; + } + + final SpanEventRecorder recorder = trace.traceBlockBegin(); + recorder.recordServiceType(KtorConstants.KTOR_INTERNAL); + } + + @Override + public void after(Object target, int apiId, Object[] args, Object result, Throwable throwable) { + if (isDebug) { + logger.afterInterceptor(target, args); + } + + final Trace trace = traceContext.currentTraceObject(); + if (trace == null) { + return; + } + + try { + final SpanEventRecorder recorder = trace.currentSpanEventRecorder(); + recorder.recordApiId(apiId); + recorder.recordException(markError, throwable); + } finally { + trace.traceBlockEnd(); + } + } +} \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/SuspendFunctionGunInterceptor.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/SuspendFunctionGunInterceptor.java new file mode 100644 index 000000000000..4db5975d8167 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/SuspendFunctionGunInterceptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessor; +import com.navercorp.pinpoint.bootstrap.async.AsyncContextAccessorUtils; +import com.navercorp.pinpoint.bootstrap.context.AsyncContext; +import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor; +import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder; +import com.navercorp.pinpoint.bootstrap.context.TraceContext; +import com.navercorp.pinpoint.bootstrap.interceptor.AsyncContextSpanEventSimpleAroundInterceptor; +import com.navercorp.pinpoint.plugin.ktor.KtorConstants; +import io.ktor.util.pipeline.PipelineContext; +import kotlin.coroutines.CoroutineContext; +import kotlinx.coroutines.CoroutineScope; + +public class SuspendFunctionGunInterceptor extends AsyncContextSpanEventSimpleAroundInterceptor { + + public SuspendFunctionGunInterceptor(TraceContext traceContext, MethodDescriptor descriptor) { + super(traceContext, descriptor); + } + + @Override + public AsyncContext getAsyncContext(Object target, Object[] args, Object result, Throwable throwable) { + return getAsyncContext(target); + } + + @Override + public AsyncContext getAsyncContext(Object target, Object[] args) { + return getAsyncContext(target); + } + + private AsyncContext getAsyncContext(Object object) { + if (object instanceof PipelineContext) { + final PipelineContext pipelineContext = (PipelineContext) object; + final Object context = pipelineContext.getContext(); + if (context instanceof AsyncContextAccessor) { + return AsyncContextAccessorUtils.getAsyncContext(context); + } + if (context instanceof CoroutineScope) { + final CoroutineScope continuation = (CoroutineScope) context; + final CoroutineContext coroutineContext = continuation.getCoroutineContext(); + if (coroutineContext instanceof AsyncContextAccessor) { + return AsyncContextAccessorUtils.getAsyncContext(coroutineContext); + } + } + } + + return null; + } + + @Override + public void doInBeforeTrace(SpanEventRecorder recorder, AsyncContext asyncContext, Object target, Object[] args) { + } + + @Override + public void doInAfterTrace(SpanEventRecorder recorder, Object target, Object[] args, Object result, Throwable throwable) { + recorder.recordApi(methodDescriptor); + recorder.recordServiceType(KtorConstants.KTOR_INTERNAL); + recorder.recordException(throwable); + } +} \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/UriUtils.java b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/UriUtils.java new file mode 100644 index 000000000000..9d5b850f1182 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/java/com/navercorp/pinpoint/plugin/ktor/interceptor/UriUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 NAVER Corp. + * + * 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 com.navercorp.pinpoint.plugin.ktor.interceptor; + +import com.navercorp.pinpoint.common.plugin.util.HostAndPort; + +import java.net.URI; +import java.net.URISyntaxException; + +public class UriUtils { + + public static String path(final String uri) { + if (uri == null) { + return null; + } + + String path = uri; + int index = path.indexOf('?'); + if (index > -1) { + path = path.substring(0, index); + } + index = path.indexOf('#'); + if (index > -1) { + path = path.substring(0, index); + } + + return path; + } + + public static String host(String uri) { + if (uri == null) { + return null; + } + try { + URI u = new URI(uri); + String host = u.getHost(); + int port = u.getPort(); + return HostAndPort.toHostAndPortString(host, port); + } catch (URISyntaxException e) { + return null; + } + } + + public static String params(final String uri) { + if (uri == null) { + return null; + } + + String params = uri; + int index = params.indexOf('?'); + if (index == -1) { + return null; + } + params = params.substring(index + 1); + index = params.indexOf('#'); + if (index > -1) { + params = params.substring(0, index); + } + + return params; + } +} diff --git a/agent-module/plugins/ktor/src/main/resources/META-INF/pinpoint/type-provider.yml b/agent-module/plugins/ktor/src/main/resources/META-INF/pinpoint/type-provider.yml new file mode 100644 index 000000000000..37248afff1ab --- /dev/null +++ b/agent-module/plugins/ktor/src/main/resources/META-INF/pinpoint/type-provider.yml @@ -0,0 +1,8 @@ +serviceTypes: + - code: 1160 + name: 'KTOR' + property: + recordStatistics: true + - code: 1161 + name: 'KTOR_INTERNAL' + desc: 'KTOR' \ No newline at end of file diff --git a/agent-module/plugins/ktor/src/main/resources/META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin b/agent-module/plugins/ktor/src/main/resources/META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin new file mode 100644 index 000000000000..e866b1d96820 --- /dev/null +++ b/agent-module/plugins/ktor/src/main/resources/META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin @@ -0,0 +1 @@ +com.navercorp.pinpoint.plugin.ktor.KtorPlugin \ No newline at end of file diff --git a/agent-module/plugins/pom.xml b/agent-module/plugins/pom.xml index fc3fb20c2329..f852412dfb79 100644 --- a/agent-module/plugins/pom.xml +++ b/agent-module/plugins/pom.xml @@ -117,6 +117,7 @@ clickhouse-jdbc spring-cloud-sleuth spring-stub + ktor diff --git a/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java b/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java index fb9676649d45..0b6646a79cf1 100644 --- a/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java +++ b/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java @@ -73,6 +73,8 @@ * 1141REACTOR_NETTY_INTERNAL * 1150ARMERIA * 1151ARMERIA_INTERNAL + * 1160KSTOR + * 1161KSTOR_INTERNAL * * 1300C_CPP * 1301C_CPP_METHOD