diff --git a/Jenkinsfile-Agent-Test b/Jenkinsfile-Agent-Test index f404cd5e6..563556c03 100755 --- a/Jenkinsfile-Agent-Test +++ b/Jenkinsfile-Agent-Test @@ -56,7 +56,7 @@ pipeline { sh './mvnw -f test/plugin/pom.xml clean package -DskipTests -Dbuild_id=wl1_${BUILD_ID} docker:build' } } - stage('Test Cases Report (135)') { + stage('Test Cases Report (139)') { steps { echo "reserve." } @@ -88,6 +88,12 @@ pipeline { sh 'bash test/plugin/run.sh --build_id=wl1_${BUILD_ID} spring-async-scenario' } } + + stage('netty-socketio 1.x (4)') { + steps { + sh 'bash test/plugin/run.sh --build_id=wl1_${BUILD_ID} netty-socketio-scenario' + } + } } } stage('Group2') { diff --git a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java index 45c494ad0..6510c4d2c 100755 --- a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java +++ b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java @@ -141,4 +141,6 @@ public class ComponentsDefine { public static final OfficialComponent PULSAR_CONSUMER = new OfficialComponent(74, "pulsar-consumer"); public static final OfficialComponent EHCACHE = new OfficialComponent(75, "Ehcache"); + + public static final OfficialComponent SOCKET_IO = new OfficialComponent(76, "SocketIO"); } diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/pom.xml new file mode 100644 index 000000000..8fb777345 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/pom.xml @@ -0,0 +1,48 @@ + + + + + + apm-sdk-plugin + org.apache.skywalking + 6.5.0-SNAPSHOT + + 4.0.0 + + apm-netty-socketio-plugin + netty-socketio-plugin + + + UTF-8 + 1.7.12 + + + + + com.corundumstudio.socketio + netty-socketio + ${netty-socketio.version} + provided + + + + + \ No newline at end of file diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOClientInfo.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOClientInfo.java new file mode 100644 index 000000000..392c38d4f --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOClientInfo.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio; + +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.namespace.Namespace; + +/** + * {@link com.corundumstudio.socketio.transport.NamespaceClient} all data + * + * @author MrPro + */ +public class NettySocketIOClientInfo { + + private final ClientHead baseClient; + private final Namespace namespace; + private final String clientAddress; + + public NettySocketIOClientInfo(ClientHead baseClient, Namespace namespace, String clientAddress) { + this.baseClient = baseClient; + this.namespace = namespace; + this.clientAddress = clientAddress; + } + + public Namespace getNamespace() { + return namespace; + } + + public ClientHead getBaseClient() { + return baseClient; + } + + public String getClientAddress() { + return clientAddress; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOConnectionInterceptor.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOConnectionInterceptor.java new file mode 100644 index 000000000..e14822385 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOConnectionInterceptor.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio; + +import com.corundumstudio.socketio.SocketIOClient; +import org.apache.skywalking.apm.agent.core.context.ContextCarrier; +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; + +import java.lang.reflect.Method; +import java.net.InetSocketAddress; + +/** + * @author MrPro + */ +public class NettySocketIOConnectionInterceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { + SocketIOClient client = (SocketIOClient) allArguments[0]; + + AbstractSpan span = ContextManager.createEntrySpan("SocketIO/" + method.getName(), new ContextCarrier()); + span.setComponent(ComponentsDefine.SOCKET_IO); + SpanLayer.asHttp(span); + + // set client addr + InetSocketAddress remoteAddress = (InetSocketAddress) client.getRemoteAddress(); + String clientAddress = remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort(); + span.tag("from", clientAddress); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) throws Throwable { + ContextManager.stopSpan(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { + if (ContextManager.isActive()) { + ContextManager.activeSpan().errorOccurred().log(t); + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOConstructorInterceptor.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOConstructorInterceptor.java new file mode 100644 index 000000000..7fe4929f1 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOConstructorInterceptor.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio; + +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.namespace.Namespace; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; + +import java.net.InetSocketAddress; + +/** + * @author MrPro + */ +public class NettySocketIOConstructorInterceptor implements InstanceConstructorInterceptor { + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { + ClientHead clientHead = (ClientHead) allArguments[0]; + Namespace namespace = (Namespace) allArguments[1]; + + // build client address + InetSocketAddress remoteAddress = (InetSocketAddress) clientHead.getRemoteAddress(); + String clientAddress = remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort(); + + objInst.setSkyWalkingDynamicField(new NettySocketIOClientInfo(clientHead, namespace, clientAddress)); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOOnEventInterceptor.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOOnEventInterceptor.java new file mode 100644 index 000000000..5b7d11136 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOOnEventInterceptor.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio; + +import org.apache.skywalking.apm.agent.core.context.ContextCarrier; +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; + +import java.lang.reflect.Method; + +/** + * @author MrPro + */ +public class NettySocketIOOnEventInterceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { + String eventName = (String) allArguments[1]; + + // no place to get context carrier + ContextCarrier contextCarrier = new ContextCarrier(); + AbstractSpan span = ContextManager.createEntrySpan("SocketIO/" + eventName + "/receive", contextCarrier); + span.setComponent(ComponentsDefine.SOCKET_IO); + SpanLayer.asHttp(span); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) throws Throwable { + ContextManager.stopSpan(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { + if (ContextManager.isActive()) { + ContextManager.activeSpan().errorOccurred().log(t); + } + } + +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIORoomInterceptor.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIORoomInterceptor.java new file mode 100644 index 000000000..5a1924de7 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIORoomInterceptor.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio; + +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; + +import java.lang.reflect.Method; + +/** + * @author MrPro + */ +public class NettySocketIORoomInterceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { + String roomName = (String) allArguments[0]; + AbstractSpan span = ContextManager.createLocalSpan("SocketIO/" + roomName + "/" + method.getName()); + span.setComponent(ComponentsDefine.SOCKET_IO); + SpanLayer.asHttp(span); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) throws Throwable { + ContextManager.stopSpan(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { + if (ContextManager.isActive()) { + ContextManager.activeSpan().errorOccurred().log(t); + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/define/NettySocketIOPluginClientInstrumentation.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/define/NettySocketIOPluginClientInstrumentation.java new file mode 100644 index 000000000..318476241 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/define/NettySocketIOPluginClientInstrumentation.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.*; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +/** + * enhance operate client + * + * @author MrPro + */ +public class NettySocketIOPluginClientInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + @Override + protected ClassMatch enhanceClass() { + return byName("com.corundumstudio.socketio.transport.NamespaceClient"); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return takesArguments(2); + } + + @Override + public String getConstructorInterceptor() { + return "org.apache.skywalking.apm.plugin.netty.socketio.NettySocketIOConstructorInterceptor"; + } + } + }; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[]{ + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("joinRoom").or(named("leaveRoom")); + } + + @Override + public String getMethodsInterceptor() { + return "org.apache.skywalking.apm.plugin.netty.socketio.NettySocketIORoomInterceptor"; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/define/NettySocketIOPluginNameSpaceInstrumentation.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/define/NettySocketIOPluginNameSpaceInstrumentation.java new file mode 100644 index 000000000..0294ed0b4 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/java/org/apache/skywalking/apm/plugin/netty/socketio/define/NettySocketIOPluginNameSpaceInstrumentation.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +/** + * enhance socketio get event + * + * @author MrPro + */ +public class NettySocketIOPluginNameSpaceInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + @Override + protected ClassMatch enhanceClass() { + return byName("com.corundumstudio.socketio.namespace.Namespace"); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[0]; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("onEvent"); + } + + @Override + public String getMethodsInterceptor() { + return "org.apache.skywalking.apm.plugin.netty.socketio.NettySocketIOOnEventInterceptor"; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + }, + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("onConnect"); + } + + @Override + public String getMethodsInterceptor() { + return "org.apache.skywalking.apm.plugin.netty.socketio.NettySocketIOConnectionInterceptor"; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + }, + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("onDisconnect"); + } + + @Override + public String getMethodsInterceptor() { + return "org.apache.skywalking.apm.plugin.netty.socketio.NettySocketIOConnectionInterceptor"; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/resources/skywalking-plugin.def new file mode 100644 index 000000000..5416d7468 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +netty-socketio=org.apache.skywalking.apm.plugin.netty.socketio.define.NettySocketIOPluginClientInstrumentation +netty-socketio=org.apache.skywalking.apm.plugin.netty.socketio.define.NettySocketIOPluginNameSpaceInstrumentation +netty-socketio=org.apache.skywalking.apm.plugin.netty.socketio.define.NettySocketIOPluginConnectionInstrumentation diff --git a/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/test/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOTest.java b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/test/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOTest.java new file mode 100644 index 000000000..81d955b30 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/netty-socketio-plugin/src/test/java/org/apache/skywalking/apm/plugin/netty/socketio/NettySocketIOTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.netty.socketio; + +import com.corundumstudio.socketio.SocketIOClient; +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.namespace.Namespace; +import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.transport.NamespaceClient; +import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.test.tools.AgentServiceRule; +import org.apache.skywalking.apm.agent.test.tools.SegmentStorage; +import org.apache.skywalking.apm.agent.test.tools.SegmentStoragePoint; +import org.apache.skywalking.apm.agent.test.tools.TracingSegmentRunner; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.powermock.reflect.Whitebox; + +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * @author MrPro + */ +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(TracingSegmentRunner.class) +public class NettySocketIOTest { + + @SegmentStoragePoint + private SegmentStorage segmentStorage; + + @Rule + public AgentServiceRule serviceRule = new AgentServiceRule(); + + private NettySocketIOConnectionInterceptor connectionInterceptor; + private NettySocketIOOnEventInterceptor onEventInterceptor; + private NettySocketIORoomInterceptor roomInterceptor; + private NettySocketIOConstructorInterceptor constructorInterceptor; + + @Mock + private SocketIOClient socketIOClient; + @Mock + private Packet sendPacket; + @Mock + private ClientHead clientHead; + @Mock + private Namespace namespace; + + private Method connectOnConnectMethod; + private Method connectOnDisConnectMethod; + private Method roomLeaveMethod; + private Method roomJoinMethod; + + private NettySocketIOClientInfo socketIOClientInfo = new NettySocketIOClientInfo(null, null, "127.0.0.1:0"); + + private EnhancedInstance enhancedInstance = new EnhancedInstance() { + @Override + public Object getSkyWalkingDynamicField() { + return socketIOClientInfo; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + + } + }; + + @Before + public void setUp() { + InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 0); + when(socketIOClient.getRemoteAddress()).thenReturn(addr); + when(sendPacket.getName()).thenReturn("test"); + when(clientHead.getRemoteAddress()).thenReturn(addr); + + connectionInterceptor = new NettySocketIOConnectionInterceptor(); + onEventInterceptor = new NettySocketIOOnEventInterceptor(); + roomInterceptor = new NettySocketIORoomInterceptor(); + constructorInterceptor = new NettySocketIOConstructorInterceptor(); + + // work for connection + connectOnConnectMethod = Whitebox.getMethods(Namespace.class, "onConnect")[0]; + connectOnDisConnectMethod = Whitebox.getMethods(Namespace.class, "onDisconnect")[0]; + + // work for room + roomJoinMethod = Whitebox.getMethods(NamespaceClient.class, "joinRoom")[0]; + roomLeaveMethod = Whitebox.getMethods(NamespaceClient.class, "leaveRoom")[0]; + } + + @Test + public void assertConnection() throws Throwable { + connectionInterceptor.beforeMethod(null, connectOnConnectMethod, new Object[]{socketIOClient}, null, null); + connectionInterceptor.afterMethod(null, connectOnConnectMethod, new Object[]{socketIOClient}, null, null); + + connectionInterceptor.beforeMethod(null, connectOnDisConnectMethod, new Object[]{socketIOClient}, null, null); + connectionInterceptor.afterMethod(null, connectOnDisConnectMethod, new Object[]{socketIOClient}, null, null); + + List traceSegments = segmentStorage.getTraceSegments(); + Assert.assertThat(traceSegments.size(), is(2)); + } + + @Test + public void assertOnEvent() throws Throwable { + onEventInterceptor.beforeMethod(null, null, new Object[]{null, "test"}, null, null); + onEventInterceptor.afterMethod(null, null, new Object[]{null, "test"}, null, null); + + List traceSegments = segmentStorage.getTraceSegments(); + Assert.assertThat(traceSegments.size(), is(1)); + } + + @Test + public void assertConstructor() throws Throwable { + constructorInterceptor.onConstruct(enhancedInstance, new Object[] {clientHead, namespace}); + } + + @Test + public void assertRoom() throws Throwable { + roomInterceptor.beforeMethod(null, roomJoinMethod, new Object[] {"test_room"}, null,null); + roomInterceptor.afterMethod(null, roomJoinMethod, new Object[] {"test_room"}, null,null); + + roomInterceptor.beforeMethod(null, roomLeaveMethod, new Object[] {"test_room"}, null,null); + roomInterceptor.afterMethod(null, roomLeaveMethod, new Object[] {"test_room"}, null,null); + + List traceSegments = segmentStorage.getTraceSegments(); + Assert.assertThat(traceSegments.size(), is(2)); + } + +} diff --git a/apm-sniffer/apm-sdk-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/pom.xml index e7c75eaa8..2c7feb750 100644 --- a/apm-sniffer/apm-sdk-plugin/pom.xml +++ b/apm-sniffer/apm-sdk-plugin/pom.xml @@ -78,6 +78,7 @@ light4j-plugins ehcache-2.x-plugin pulsar-plugin + netty-socketio-plugin pom diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md b/docs/en/setup/service-agent/java-agent/Supported-list.md index 1dc9da5fd..190452ea4 100644 --- a/docs/en/setup/service-agent/java-agent/Supported-list.md +++ b/docs/en/setup/service-agent/java-agent/Supported-list.md @@ -14,6 +14,7 @@ * [RESTEasy](https://resteasy.github.io/) 3.1.0.Final -> 3.7.0.Final * [Play Framework](https://www.playframework.com/) 2.6.x -> 2.7.x (Optional²) * [Light4J Microservices Framework](https://doc.networknt.com/) 1.6.x -> 2.x + * [Netty SocketIO](https://github.com/mrniko/netty-socketio) 1.x * HTTP Client * [Feign](https://github.com/OpenFeign/feign) 9.x * [Netflix Spring Cloud Feign](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-starter-feign) 1.1.x, 1.2.x, 1.3.x diff --git a/oap-server/server-core/src/test/resources/component-libraries.yml b/oap-server/server-core/src/test/resources/component-libraries.yml index 5a57c5fcb..7aec14683 100755 --- a/oap-server/server-core/src/test/resources/component-libraries.yml +++ b/oap-server/server-core/src/test/resources/component-libraries.yml @@ -227,6 +227,9 @@ Light4J: Ehcache: id: 75 languages: Java +SocketIO: + id: 76 + languages: Java # .NET/.NET Core components # [3000, 4000) for C#/.NET only diff --git a/oap-server/server-starter/src/main/resources/component-libraries.yml b/oap-server/server-starter/src/main/resources/component-libraries.yml index 45d0ea806..5f21c5604 100755 --- a/oap-server/server-starter/src/main/resources/component-libraries.yml +++ b/oap-server/server-starter/src/main/resources/component-libraries.yml @@ -254,6 +254,9 @@ pulsar-consumer: Ehcache: id: 75 languages: Java +SocketIO: + id: 76 + languages: Java # .NET/.NET Core components # [3000, 4000) for C#/.NET only diff --git a/test/plugin/scenarios/netty-socketio-scenario/config/expectedData.yaml b/test/plugin/scenarios/netty-socketio-scenario/config/expectedData.yaml new file mode 100644 index 000000000..5f328fd84 --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/config/expectedData.yaml @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +registryItems: + applications: + - {netty-socketio-scenario: 2} + instances: + - {netty-socketio-scenario: 1} + operationNames: + - netty-socketio-scenario: [/netty-socketio-scenario/case/netty-socketio, /socket.io/, + /netty-socketio-scenario/healthCheck, SocketIO/onConnect, SocketIO/send_data/receive] + heartbeat: [] +segmentItems: + - applicationCode: netty-socketio-scenario + segmentSize: ge 5 + segments: + - segmentId: not null + spans: + - operationName: SocketIO/onConnect + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 76 + componentName: '' + isError: false + spanType: Entry + peer: '' + peerId: 0 + tags: + - {key: from, value: not null} + - segmentId: not null + spans: + - operationName: /netty-socketio-scenario/case/netty-socketio + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 1 + componentName: '' + isError: false + spanType: Entry + peer: '' + peerId: 0 + tags: + - {key: url, value: 'http://localhost:8080/netty-socketio-scenario/case/netty-socketio'} + - {key: http.method, value: GET} + - segmentId: not null + spans: + - operationName: SocketIO/send_data/receive + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 76 + componentName: '' + isError: false + spanType: Entry + peer: '' + peerId: 0 diff --git a/test/plugin/scenarios/netty-socketio-scenario/configuration.yml b/test/plugin/scenarios/netty-socketio-scenario/configuration.yml new file mode 100644 index 000000000..b0503a5a0 --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/configuration.yml @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +type: tomcat +entryService: http://localhost:8080/netty-socketio-scenario/case/netty-socketio +healthCheck: http://localhost:8080/netty-socketio-scenario/healthCheck +framework: netty-socketio diff --git a/test/plugin/scenarios/netty-socketio-scenario/pom.xml b/test/plugin/scenarios/netty-socketio-scenario/pom.xml new file mode 100644 index 000000000..69deea55d --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + org.apache.skywalking + netty-socketio-scenario + 5.0.0 + war + + + UTF-8 + 1.8 + + + netty-socketio + 1.7.12 + + + skywalking-netty-socketio-scenario + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + org.apache.logging.log4j + log4j-api + 2.8.1 + + + org.apache.logging.log4j + log4j-core + 2.8.1 + + + com.corundumstudio.socketio + netty-socketio + ${test.framework.version} + + + io.socket + socket.io-client + 1.0.0 + + + + + netty-socketio-scenario + + + maven-compiler-plugin + + ${compiler.version} + ${compiler.version} + ${project.build.sourceEncoding} + + + + + \ No newline at end of file diff --git a/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/CaseServlet.java b/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/CaseServlet.java new file mode 100644 index 000000000..6ad02af9c --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/CaseServlet.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.testcase.netty.socketio; + +import com.corundumstudio.socketio.SocketIOClient; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +public class CaseServlet extends HttpServlet { + + static { + // start socket io server + SocketIOStarter.startServer(); + + // start client + try { + SocketIOStarter.startClientAndWaitConnect(); + } catch (Exception e) { + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // create socket io client and send data + // test send message interceptor + SocketIOClient client = SocketIOStarter.server.getAllClients().iterator().next(); + client.sendEvent(SocketIOStarter.SEND_EVENT_NAME, "data"); + + // client send message to server + // test for get message from client interceptor + SocketIOStarter.client.emit(SocketIOStarter.LISTEN_EVENT_NAME, "hello"); + + PrintWriter printWriter = resp.getWriter(); + printWriter.write("success"); + printWriter.flush(); + printWriter.close(); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } + +} diff --git a/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/HealthCheckServlet.java b/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/HealthCheckServlet.java new file mode 100644 index 000000000..ac81af30d --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/HealthCheckServlet.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.apm.testcase.netty.socketio; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +public class HealthCheckServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + PrintWriter writer = resp.getWriter(); + writer.write("Success"); + writer.flush(); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } +} diff --git a/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/SocketIOStarter.java b/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/SocketIOStarter.java new file mode 100644 index 000000000..1c26fa8d7 --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/src/main/java/org/apache/skywalking/apm/testcase/netty/socketio/SocketIOStarter.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.apm.testcase.netty.socketio; + +import com.corundumstudio.socketio.AckRequest; +import com.corundumstudio.socketio.Configuration; +import com.corundumstudio.socketio.SocketIOClient; +import com.corundumstudio.socketio.SocketIOServer; +import com.corundumstudio.socketio.listener.ConnectListener; +import com.corundumstudio.socketio.listener.DataListener; +import io.socket.client.IO; +import io.socket.client.Socket; +import io.socket.emitter.Emitter; + +import java.net.URISyntaxException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author MrPro + */ +public class SocketIOStarter { + + public static final Integer SERVER_PORT = 9092; + public static final String LISTEN_EVENT_NAME = "send_data"; + public static final String SEND_EVENT_NAME = "get_data"; + + public static SocketIOServer server; + public static Socket client; + + public static void startServer() { + Configuration config = new Configuration(); + config.setHostname("localhost"); + config.setPort(SERVER_PORT); + + server = new SocketIOServer(config); + server.addEventListener(LISTEN_EVENT_NAME, String.class, new DataListener() { + @Override + public void onData(SocketIOClient client, String data, AckRequest ackRequest) { + // get message + } + }); + + server.addConnectListener(new ConnectClientListener()); + + server.start(); + + // close server on kill signal + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + server.stop(); + } + }); + } + + private static class ConnectClientListener implements ConnectListener { + + @Override + public void onConnect(SocketIOClient client) { + // connect client + } + } + + public static void startClientAndWaitConnect() throws URISyntaxException, InterruptedException { + client = IO.socket("http://localhost:" + SERVER_PORT); + LinkedBlockingQueue connected = new LinkedBlockingQueue<>(1); + client.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... objects) { + connected.add(true); + } + }); + client.connect(); + + // wait connect to server + connected.poll(5, TimeUnit.SECONDS); + } + +} diff --git a/test/plugin/scenarios/netty-socketio-scenario/src/main/webapp/WEB-INF/web.xml b/test/plugin/scenarios/netty-socketio-scenario/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..655eb6565 --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,45 @@ + + + skywalking-netty-socketio-scenario + + + caseServlet + org.apache.skywalking.apm.testcase.netty.socketio.CaseServlet + + + caseServlet + /case/netty-socketio + + + + healthCheck + org.apache.skywalking.apm.testcase.netty.socketio.HealthCheckServlet + + + healthCheck + /healthCheck + + + + + diff --git a/test/plugin/scenarios/netty-socketio-scenario/support-version.list b/test/plugin/scenarios/netty-socketio-scenario/support-version.list new file mode 100644 index 000000000..141011deb --- /dev/null +++ b/test/plugin/scenarios/netty-socketio-scenario/support-version.list @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +1.7.12 +1.7.14 +1.7.16 +1.7.17 \ No newline at end of file