diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java index bdab2c6d4..7d7222e25 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java @@ -1,10 +1,12 @@ package io.arex.agent.bootstrap.constants; public class ConfigConstants { - + private ConfigConstants() { + } public static final String ENABLE_DEBUG = "arex.enable.debug"; public static final String SERVICE_NAME = "arex.service.name"; public static final String STORAGE_SERVICE_HOST = "arex.storage.service.host"; + public static final String CONFIG_SERVICE_HOST = "arex.config.service.host"; public static final String CONFIG_PATH = "arex.config.path"; public static final String STORAGE_MODE = "local"; public static final String RECORD_RATE = "arex.rate.limit"; @@ -25,8 +27,7 @@ public class ConfigConstants { public static final String DURING_WORK = "arex.during.work"; public static final String AGENT_VERSION = "arex.agent.version"; public static final String IP_VALIDATE = "arex.ip.validate"; - - public static final String ENABLE_REPORT_STATUS = "arex.enable.report.status"; public static final String CURRENT_RATE = "arex.current.rate"; public static final String DECELERATE_CODE = "arex.decelerate.code"; + public static final String SERIALIZER_CONFIG = "arex.serializer.config"; } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java new file mode 100644 index 000000000..657e16675 --- /dev/null +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java @@ -0,0 +1,30 @@ +package io.arex.agent.bootstrap.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class IOUtils { + private IOUtils() {} + public static final int EOF = -1; + private static final int DEFAULT_BUFFER_SIZE = 4096; + + public static long copy(InputStream inputStream, OutputStream outputStream) throws IOException { + final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long total = 0; + int read; + while (EOF != (read = inputStream.read(buffer))) { + outputStream.write(buffer, 0, read); + total += read; + } + outputStream.flush(); + return total; + } + + public static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(DEFAULT_BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } +} diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java index d69630f90..5d7d60f74 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java @@ -351,6 +351,10 @@ public static boolean containsIgnoreCase(final CharSequence str, final CharSeque return false; } + public static boolean startWith(String source, String prefix) { + return startWithFrom(source, prefix, 0); + } + public static boolean startWithFrom(String source, String prefix, int checkStartIndex) { int length = prefix.length(); if (checkStartIndex < 0 || checkStartIndex + length > source.length()) { diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java new file mode 100644 index 000000000..06310a10f --- /dev/null +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java @@ -0,0 +1,21 @@ +package io.arex.agent.bootstrap.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class IOUtilsTest { + @Test + void copyToByteArray() throws IOException { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 2000; i++) { + builder.append(i); + } + InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes()); + byte[] bytes = IOUtils.copyToByteArray(inputStream); + assertEquals(builder.toString(), new String(bytes)); + } +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java index a5745c98c..7e9bfc071 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java @@ -309,4 +309,10 @@ void isNullWord() { assertTrue(StringUtil.isNullWord("NULL")); assertFalse(StringUtil.isNullWord("mock")); } + + @Test + void startWith() { + assertTrue(StringUtil.startWith("mock", "m")); + assertFalse(StringUtil.startWith("mock", "o")); + } } \ No newline at end of file diff --git a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java index 53ccddc3b..4a5b63251 100644 --- a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java +++ b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java @@ -8,11 +8,14 @@ import io.arex.foundation.healthy.HealthManager; import io.arex.foundation.serializer.GsonSerializer; import io.arex.foundation.serializer.JacksonSerializer; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactory; +import io.arex.foundation.serializer.custom.GuavaRangeSerializer; import io.arex.foundation.services.ConfigService; import io.arex.foundation.services.DataCollectorService; import io.arex.foundation.services.TimerService; import io.arex.foundation.util.NetUtils; import io.arex.foundation.util.NumberTypeAdaptor; +import io.arex.inst.extension.ExtensionTransformer; import io.arex.inst.runtime.context.RecordLimiter; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.service.DataCollector; @@ -49,34 +52,58 @@ public void install() { ClassLoader savedContextClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(getClassLoader()); - // Timed load config for agent delay start and dynamic retransform + Runtime.getRuntime().addShutdownHook(new Thread(ConfigService.INSTANCE::shutdown, "arex-agent-shutdown-hook")); + // Timed load config for dynamic retransform long delayMinutes = ConfigService.INSTANCE.loadAgentConfig(agentArgs); - if (delayMinutes > 0) { - TimerService.schedule(this::install, delayMinutes, TimeUnit.MINUTES); - timedReportStatus(); - } - if (!ConfigManager.INSTANCE.valid()) { + if (!allowStartAgent()) { ConfigService.INSTANCE.reportStatus(); if (!ConfigManager.FIRST_TRANSFORM.get()) { - LOGGER.warn("[AREX] Agent would not install due to {}.", ConfigManager.INSTANCE.getInvalidReason()); + LOGGER.warn("[AREX] Agent would not install due to {}.", getInvalidReason()); } return; } + if (delayMinutes > 0) { + TimerService.schedule(this::install, delayMinutes, TimeUnit.MINUTES); + timedReportStatus(); + } initDependentComponents(); transform(); + + for (ExtensionTransformer transformer : loadTransformers()) { + if (transformer.validate()) { + instrumentation.addTransformer(transformer, true); + } + } + ConfigService.INSTANCE.reportStatus(); } finally { Thread.currentThread().setContextClassLoader(savedContextClassLoader); } } + private List loadTransformers() { + return ServiceLoader.load(ExtensionTransformer.class, getClassLoader()); + } + + boolean allowStartAgent() { + if (ConfigManager.INSTANCE.isLocalStorage()) { + return true; + } + return ConfigManager.INSTANCE.checkTargetAddress(); + } + + String getInvalidReason() { + if (!ConfigManager.INSTANCE.checkTargetAddress()) { + return "response [targetAddress] is not match"; + } + + return "invalid config"; + } + private void timedReportStatus() { if (reportStatusTask != null) { return; } - if (!ConfigManager.INSTANCE.isEnableReportStatus()) { - return; - } reportStatusTask = TimerService.scheduleAtFixedRate(() -> { try { ConfigService.INSTANCE.reportStatus(); @@ -106,6 +133,8 @@ private void initSerializer() { AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(JacksonSerializer.class); AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(GsonSerializer.class); AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(NumberTypeAdaptor.class); + AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(GuavaRangeSerializer.class); + AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(FastUtilAdapterFactory.class); Serializer.builder(JacksonSerializer.INSTANCE).build(); } private void initDataCollector() { diff --git a/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java b/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java index 8d21f2519..c3d79bfc2 100644 --- a/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java +++ b/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java @@ -1,17 +1,26 @@ package io.arex.agent.instrumentation; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; import io.arex.agent.bootstrap.cache.AdviceInjectorCache; +import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.agent.bootstrap.util.AdviceClassesCollector; +import io.arex.foundation.config.ConfigManager; +import io.arex.foundation.model.ConfigQueryResponse; +import io.arex.foundation.model.ConfigQueryResponse.ResponseBody; +import io.arex.foundation.model.ConfigQueryResponse.ServiceCollectConfig; import io.arex.foundation.model.HttpClientResponse; import io.arex.foundation.serializer.JacksonSerializer; import io.arex.foundation.util.NetUtils; import io.arex.foundation.util.httpclient.AsyncHttpClientUtil; +import java.lang.instrument.Instrumentation; import java.util.concurrent.CompletableFuture; -import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.ResettableClassFileTransformer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -21,19 +30,28 @@ import org.mockito.Mockito; class BaseAgentInstallerTest { - + static BaseAgentInstaller installer = null; @BeforeAll static void beforeAll() { mockStatic(AdviceInjectorCache.class); + + Instrumentation inst = Mockito.mock(Instrumentation.class); + installer = new BaseAgentInstaller(inst, null, null) { + @Override + protected ResettableClassFileTransformer transform() { + return null; + } + }; } @AfterAll static void afterAll() { + installer = null; Mockito.clearAllCaches(); } @Test - void install() { + void install() throws Throwable { Mockito.when(AdviceInjectorCache.contains(any())).thenReturn(true); try (MockedStatic ahc = mockStatic(AsyncHttpClientUtil.class); MockedStatic netUtils = mockStatic(NetUtils.class); @@ -42,18 +60,60 @@ void install() { Mockito.verify(mock, Mockito.times(1)).addClassToLoaderSearch(JacksonSerializer.class); })) { + // allow start agent = false netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), any())).thenReturn( CompletableFuture.completedFuture(HttpClientResponse.emptyResponse())); - BaseAgentInstaller installer = new BaseAgentInstaller(ByteBuddyAgent.install(), null, null) { - @Override - protected ResettableClassFileTransformer transform() { - return null; - } - }; installer.install(); + + // allow start agent = true + netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); + ConfigQueryResponse configQueryResponse = new ConfigQueryResponse(); + ServiceCollectConfig serviceCollectConfig = new ServiceCollectConfig(); + serviceCollectConfig.setAllowDayOfWeeks(127); + serviceCollectConfig.setAllowTimeOfDayFrom("00:00"); + serviceCollectConfig.setAllowTimeOfDayTo("23:59"); + serviceCollectConfig.setSampleRate(1); + + ResponseBody responseBody = new ResponseBody(); + responseBody.setTargetAddress("127.0.0.1"); + responseBody.setServiceCollectConfiguration(serviceCollectConfig); + configQueryResponse.setBody(responseBody); + CompletableFuture response = CompletableFuture.completedFuture(new HttpClientResponse(200, null, JacksonSerializer.INSTANCE.serialize(configQueryResponse))); + ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), eq(null))).thenReturn(response); + installer.install(); + } + } + + @Test + void allowStartAgent() { + ConfigManager.INSTANCE.setStorageServiceMode(ConfigConstants.STORAGE_MODE); + assertTrue(installer.allowStartAgent()); + + try (MockedStatic netUtils = mockStatic(NetUtils.class)) { + netUtils.when(NetUtils::getIpAddress).thenReturn("172.0.0.3"); + + ConfigManager.INSTANCE.setStorageServiceMode("not " + ConfigConstants.STORAGE_MODE); + ConfigManager.INSTANCE.setTargetAddress("172.0.0.1"); + + assertFalse(installer.allowStartAgent()); + } + } + + @Test + void getInvalidReason() { + try (MockedStatic netUtils = mockStatic(NetUtils.class)) { + netUtils.when(NetUtils::getIpAddress).thenReturn("172.0.0.3"); + + ConfigManager.INSTANCE.setTargetAddress("172.0.0.1"); + + assertEquals("response [targetAddress] is not match", installer.getInvalidReason()); + + // checkTargetAddress = true + ConfigManager.INSTANCE.setTargetAddress("172.0.0.3"); + assertEquals("invalid config", installer.getInvalidReason()); } } } diff --git a/arex-agent/pom.xml b/arex-agent/pom.xml index f18676058..f3b2e8163 100644 --- a/arex-agent/pom.xml +++ b/arex-agent/pom.xml @@ -81,6 +81,11 @@ arex-redis-common ${project.version} + + ${project.groupId} + arex-jedis-v2 + ${project.version} + ${project.groupId} arex-jedis-v4 diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java b/arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java new file mode 100644 index 000000000..bdb0d5152 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java @@ -0,0 +1,17 @@ +package io.arex.inst.extension; + +import java.lang.instrument.ClassFileTransformer; + +public abstract class ExtensionTransformer implements ClassFileTransformer { + private String name; + + public ExtensionTransformer(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public abstract boolean validate(); +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java index 6903184f8..e1f5293de 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java @@ -24,6 +24,7 @@ public class ArexContext { private Map attachments = null; private boolean isRedirectRequest; + private boolean isInvalidCase; public static ArexContext of(String caseId) { return of(caseId, null); @@ -61,7 +62,7 @@ public boolean isReplay() { } public int calculateSequence() { - return sequence.get(); + return sequence.getAndIncrement(); } public Set getMethodSignatureHashList() { @@ -109,6 +110,14 @@ public void setRedirectRequest(boolean redirectRequest) { isRedirectRequest = redirectRequest; } + public boolean isInvalidCase() { + return isInvalidCase; + } + + public void setInvalidCase(boolean invalidCase) { + isInvalidCase = invalidCase; + } + public boolean isRedirectRequest(String referer) { if (attachments == null) { isRedirectRequest = false; diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java index c1c62fe6c..8b8616caa 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java @@ -2,11 +2,15 @@ import io.arex.agent.bootstrap.TraceContextManager; import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.listener.ContextListener; +import java.util.ArrayList; +import java.util.List; import java.util.Map; public class ContextManager { private static final Map RECORD_MAP = new LatencyContextHashMap(); + private static final List LISTENERS = new ArrayList<>(); /** * agent call this method @@ -23,6 +27,7 @@ public static ArexContext currentContext(boolean createIfAbsent, String caseId) if (StringUtil.isNotEmpty(caseId)) { TraceContextManager.set(caseId); ArexContext context = ArexContext.of(caseId, TraceContextManager.generateId()); + publish(context, true); // Each replay init generates the latest context(maybe exist previous recorded context) RECORD_MAP.put(caseId, context); return context; @@ -35,7 +40,9 @@ public static ArexContext currentContext(boolean createIfAbsent, String caseId) } // first init execute if (createIfAbsent) { - return RECORD_MAP.computeIfAbsent(caseId, ArexContext::of); + ArexContext context = ArexContext.of(caseId); + publish(context, true); + return RECORD_MAP.put(caseId, context); } return RECORD_MAP.get(caseId); } @@ -63,6 +70,23 @@ public static void remove() { if (StringUtil.isEmpty(caseId)) { return; } - RECORD_MAP.remove(caseId); + ArexContext context = RECORD_MAP.remove(caseId); + publish(context, false); + } + + public static void registerListener(ContextListener listener) { + LISTENERS.add(listener); + } + + private static void publish(ArexContext context, boolean isCreate) { + if (LISTENERS.size() > 0) { + LISTENERS.stream().forEach(listener -> { + if (isCreate) { + listener.onCreate(context); + } else { + listener.onComplete(context); + } + }); + } } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java index 6bd685c87..f392cb790 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java @@ -40,7 +40,7 @@ private ArexContext initOrGet(Object key) { } private void overdueCleanUp() { - if (latencyMap != null && CLEANUP_LOCK.tryLock() && latencyMap.mappingCount() > CLEANUP_THRESHOLD) { + if (latencyMap != null && CLEANUP_LOCK.tryLock()) { try { long now = System.currentTimeMillis(); for (Map.Entry entry: latencyMap.entrySet()) { @@ -56,7 +56,7 @@ private void overdueCleanUp() { } // Compatible where map.remove() not called - if (CLEANUP_LOCK.tryLock() && this.mappingCount() > CLEANUP_THRESHOLD) { + if (this.mappingCount() > CLEANUP_THRESHOLD && CLEANUP_LOCK.tryLock()) { try { long now = System.currentTimeMillis(); for (Map.Entry entry: super.entrySet()) { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java new file mode 100644 index 000000000..d0ada3639 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java @@ -0,0 +1,8 @@ +package io.arex.inst.runtime.listener; + +import io.arex.inst.runtime.context.ArexContext; + +public interface ContextListener { + void onCreate(ArexContext arexContext); + void onComplete(ArexContext arexContext); +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java index 12504f3a1..9e442128f 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java @@ -57,6 +57,7 @@ private static void addEnterLog() { * user loader to load serializer, ex: ParallelWebappClassLoader */ private static void initSerializer() { + Serializer.initSerializerConfigMap(); final List serializableList = ServiceLoader.load(StringSerializable.class, Thread.currentThread().getContextClassLoader()); Serializer.builder(serializableList).build(); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java index 831651e2a..a6aafde15 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java @@ -19,16 +19,17 @@ public static void init() { } public static void preHandle(Object request, String name) { - try { - final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); - if (CollectionUtil.isEmpty(requestHandlers)) { - return; - } - for (RequestHandler requestHandler : requestHandlers) { + final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); + if (CollectionUtil.isEmpty(requestHandlers)) { + return; + } + for (RequestHandler requestHandler : requestHandlers) { + try { requestHandler.preHandle(request); + } catch (Throwable ex) { + // avoid affecting the remaining handlers when one handler fails + LogManager.warn("preHandler", ex.getMessage()); } - } catch (Throwable ex) { - LogManager.warn("preHandler", ex.getMessage()); } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java index 9101d4f59..778716a23 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java @@ -1,19 +1,24 @@ package io.arex.inst.runtime.serializer; +import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.agent.bootstrap.util.ArrayUtils; import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.agent.bootstrap.util.ReflectUtil; import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.util.TypeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Type; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class Serializer { private static final Logger LOGGER = LoggerFactory.getLogger(Serializer.class); + private static final Map SERIALIZER_CONFIG_MAP = new ConcurrentHashMap<>(); private static Serializer INSTANCE; @@ -33,6 +38,28 @@ public static Builder builder(List serializableList) { private final StringSerializable defaultSerializer; private final Map serializers; + /** + * ex: DubboProvider:jackson,DubboConsumer:gson + */ + public static void initSerializerConfigMap() { + try { + String serializerConfig = Config.get().getString(ConfigConstants.SERIALIZER_CONFIG); + if (StringUtil.isEmpty(serializerConfig)) { + return; + } + final String[] configArray = StringUtil.split(serializerConfig, ','); + for (String config : configArray) { + final String[] configElement = StringUtil.split(config, ':'); + if (configElement.length != 2) { + continue; + } + SERIALIZER_CONFIG_MAP.put(configElement[0], configElement[1]); + } + } catch (Exception ex) { + LogManager.warn("serializer.config", StringUtil.format("can not init serializer config, cause: %s", ex.toString())); + } + } + /** * serialize throw throwable */ @@ -41,6 +68,10 @@ public static String serializeWithException(Object object, String serializer) th return null; } + if (object instanceof Throwable) { + return INSTANCE.getSerializer(ArexConstants.GSON_SERIALIZER).serialize(object); + } + Collection> nestedCollection = TypeUtil.toNestedCollection(object); if (nestedCollection != null) { return serializeNestedCollection(serializer, nestedCollection); @@ -68,6 +99,10 @@ private static String serializeNestedCollection(String serializer, Collection T deserialize(String value, Type type, String serializer) { } try { + if (Throwable.class.isAssignableFrom(TypeUtil.getRawClass(type))) { + serializer = ArexConstants.GSON_SERIALIZER; + } return INSTANCE.getSerializer(serializer).deserialize(value, type); } catch (Throwable ex) { LogManager.warn("serializer-deserialize-type", StringUtil.format("can not deserialize value %s to type %s, cause: %s", value, type.getTypeName(), ex.toString())); @@ -146,10 +181,6 @@ public static T deserialize(String value, String typeName, String serializer return null; } - if (typeName.endsWith("Exception")) { - serializer = "gson"; - } - if (typeName.startsWith(HASH_MAP_VALUES_CLASS)) { return (T) restoreHashMapValues(value, typeName, serializer); } @@ -265,7 +296,6 @@ public Builder(List serializableList) { for (StringSerializable serializable : serializableList) { if (serializable.isDefault()) { this.defaultSerializer = serializable; - continue; } this.serializers.put(serializable.name(), serializable); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java index 344b52522..3926ea4f6 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java @@ -1,11 +1,12 @@ package io.arex.inst.runtime.service; import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; public interface DataCollector { void start(); - void save(String mockData); + void save(Mocker requestMocker); String query(String postData, MockStrategyEnum mockStrategy); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java index fc5303680..99d6e3031 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java @@ -1,6 +1,7 @@ package io.arex.inst.runtime.service; import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; public class DataService { @@ -16,8 +17,8 @@ public static Builder builder() { this.saver = dataSaver; } - public void save(String data) { - saver.save(data); + public void save(Mocker requestMocker) { + saver.save(requestMocker); } public String query(String data, MockStrategyEnum mockStrategy) { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java new file mode 100644 index 000000000..7ab772ff5 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java @@ -0,0 +1,33 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.log.LogManager; + +public class CaseManager { + private CaseManager() { + } + + public static void invalid(String recordId, String operationName) { + try { + final ArexContext context = ContextManager.getRecordContext(recordId); + if (context == null || context.isInvalidCase()) { + return; + } + LogManager.warn("invalidCase", + StringUtil.format("invalid case: recordId: %s operation: %s", recordId, operationName)); + context.setInvalidCase(true); + } catch (Exception ex) { + LogManager.warn("invalidCase.remove", ex); + } + } + + public static boolean isInvalidCase(String recordId) { + final ArexContext context = ContextManager.getRecordContext(recordId); + if (context == null) { + return false; + } + return context.isInvalidCase(); + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java index e1e4d41dc..11187623b 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java @@ -85,13 +85,15 @@ public static ArexMocker create(MockCategoryType categoryType, String operationN } public static void recordMocker(Mocker requestMocker) { - String postJson = Serializer.serialize(requestMocker); + if (CaseManager.isInvalidCase(requestMocker.getRecordId())) { + return; + } if (Config.get().isEnableDebug()) { - LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), postJson)); + LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); } - DataService.INSTANCE.save(postJson); + DataService.INSTANCE.save(requestMocker); } public static Mocker replayMocker(Mocker requestMocker) { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java index b918e4f5e..f8e1b2318 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java @@ -128,7 +128,16 @@ private static String genericTypeToString(Object result) { field.setAccessible(true); GENERIC_FIELD_CACHE.put(cacheKey, field); } - builder.append(filterRawGenericType(invokeGetFieldType(field, result))); + + String genericType = invokeGetFieldType(field, result); + // only collection field need to filter raw generic type + if (isCollection(field.getType().getName())) { + genericType = filterRawGenericType(genericType); + } + + if (StringUtil.isNotEmpty(genericType)) { + builder.append(genericType); + } if (i == typeParameters.length - 1) { return builder.toString(); } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java index 11282a441..46de4e0df 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java @@ -88,7 +88,7 @@ void testInit() { // serializer Assertions.assertNotNull(Serializer.getINSTANCE()); Assertions.assertEquals("gson", Serializer.getINSTANCE().getSerializer().name()); - Assertions.assertEquals(1, Serializer.getINSTANCE().getSerializers().size()); + Assertions.assertEquals(2, Serializer.getINSTANCE().getSerializers().size()); // atomic load, only load once Mockito.when(ServiceLoader.load(StringSerializable.class, Thread.currentThread() diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java index db2a4d91f..a1453668d 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java @@ -2,9 +2,13 @@ import static org.junit.jupiter.api.Assertions.*; +import io.arex.agent.bootstrap.constants.ConfigConstants; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.config.ConfigBuilder; import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; import io.arex.inst.runtime.listener.EventProcessorTest.TestGsonSerializer; import io.arex.inst.runtime.util.TypeUtil; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; @@ -33,7 +37,7 @@ static void tearDown() { @Test void builder() { assertNotNull(Serializer.getINSTANCE()); - assertEquals(1, Serializer.getINSTANCE().getSerializers().size()); + assertEquals(2, Serializer.getINSTANCE().getSerializers().size()); } @Test @@ -121,4 +125,38 @@ void nullObjectOrType() { // serialize Throwable Assertions.assertDoesNotThrow(() -> Serializer.serialize(new Throwable())); } + + @Test + void testInitSerializerConfigMap() throws Exception { + // null config + final Field instance = Config.class.getDeclaredField("INSTANCE"); + instance.setAccessible(true); + instance.set(null, null); + Assertions.assertDoesNotThrow(Serializer::initSerializerConfigMap); + + // empty serializer config + ConfigBuilder builder = new ConfigBuilder("testSerializer"); + builder.build(); + Serializer.initSerializerConfigMap(); + assertNull(Serializer.getSerializerFromType("dubboRequest")); + + // serializer config + builder = new ConfigBuilder("testSerializer"); + builder.addProperty(ConfigConstants.SERIALIZER_CONFIG, "soa:gson,dubboRequest:jackson,httpRequest"); + builder.build(); + Serializer.initSerializerConfigMap(); + assertEquals("jackson", Serializer.getSerializerFromType("dubboRequest")); + assertEquals("gson", Serializer.getSerializerFromType("soa")); + assertNull(Serializer.getSerializerFromType("httpRequest")); + } + + @Test + void testTypeIsException() { + final RuntimeException runtimeException = new RuntimeException(); + final String json = Serializer.serialize(runtimeException); + String typeName = TypeUtil.getName(runtimeException); + assertNotNull(json); + final RuntimeException actualResult = Serializer.deserialize(json, TypeUtil.forName(typeName)); + assertEquals(runtimeException.getClass(), actualResult.getClass()); + } } \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java new file mode 100644 index 000000000..ff689e786 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java @@ -0,0 +1,51 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; +import io.arex.inst.runtime.serializer.Serializer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class CaseManagerTest { + static MockedStatic contextManagerMocked; + + @BeforeAll + static void setUp() { + Serializer.builder(new TestJacksonSerializable()).build(); + contextManagerMocked = Mockito.mockStatic(ContextManager.class); + } + + @AfterAll + static void tearDown() { + contextManagerMocked = null; + Mockito.clearAllCaches(); + } + + @Test + void invalid() { + final ArexContext context = ArexContext.of("testRecordId"); + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(context); + Assertions.assertFalse(context.isInvalidCase()); + Assertions.assertFalse(CaseManager.isInvalidCase("testRecordId")); + + CaseManager.invalid("testRecordId", "testOperationName"); + Assertions.assertTrue(context.isInvalidCase()); + Assertions.assertTrue(CaseManager.isInvalidCase("testRecordId")); + + // test invalid case with null context + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(null); + Assertions.assertFalse(CaseManager.isInvalidCase("testRecordId")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", "testOperationName")); + + // test invalid case with exception + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenThrow(new RuntimeException("test exception")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", "testOperationName")); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java index f664e6a31..8bc650f11 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java @@ -29,6 +29,7 @@ class MockUtilsTest { @BeforeAll static void setUp() { Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(CaseManager.class); configBuilder = ConfigBuilder.create("test"); dataCollector = Mockito.mock(DataCollector.class); @@ -52,6 +53,10 @@ void recordMocker() { configBuilder.build(); ArexMocker dynamicClass = MockUtils.createDynamicClass("test", "test"); Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(dynamicClass)); + + // invalid case + Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(true); + Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(dynamicClass)); } @Test diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java index d94f46199..127bd6e27 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java @@ -170,7 +170,7 @@ void testDoubleGenericType() { final Pair pairList = Pair.of(System.currentTimeMillis(), Arrays.asList("mock")); final String genericList = TypeUtil.getName(pairList); - assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,java.lang.String", genericList); + assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,java.util.Arrays$ArrayList-java.lang.String", genericList); } @Test diff --git a/arex-instrumentation-foundation/pom.xml b/arex-instrumentation-foundation/pom.xml index 5ec3895d9..188d49abb 100644 --- a/arex-instrumentation-foundation/pom.xml +++ b/arex-instrumentation-foundation/pom.xml @@ -54,7 +54,7 @@ org.apache.httpcomponents httpasyncclient - 4.1.4 + 4.1.5 com.github.luben @@ -66,6 +66,12 @@ joda-time 2.9 + + it.unimi.dsi + fastutil + 8.2.2 + test + @@ -89,6 +95,11 @@ com.google shaded.com.google + + com.google.common.collect.Range + com.google.common.collect.RangeGwtSerializationDependencies + com.google.common.collect.BoundType + diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java index 885142ff0..ed980d9d7 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java @@ -40,10 +40,10 @@ public class ConfigManager { public static final AtomicBoolean FIRST_TRANSFORM = new AtomicBoolean(false); private static final int DEFAULT_RECORDING_RATE = 1; private boolean enableDebug; - private boolean enableReportStatus; private String agentVersion; private String serviceName; private String storageServiceHost; + private String configServiceHost; private String configPath; private String storageServiceMode; @@ -89,19 +89,6 @@ public void setEnableDebug(String enableDebug) { System.setProperty(ENABLE_DEBUG, enableDebug); } - public boolean isEnableReportStatus() { - return enableReportStatus; - } - - public void setEnableReportStatus(String enableReportStatus) { - if (StringUtil.isEmpty(enableReportStatus)) { - return; - } - - this.enableReportStatus = Boolean.parseBoolean(enableReportStatus); - System.setProperty(ENABLE_REPORT_STATUS, enableReportStatus); - } - public String getServiceName() { return serviceName; } @@ -131,6 +118,21 @@ public void setStorageServiceHost(String storageServiceHost) { System.setProperty(STORAGE_SERVICE_HOST, storageServiceHost); } + public String getConfigServiceHost() { + if (StringUtil.isNotEmpty(configServiceHost)) { + return configServiceHost; + } + return storageServiceHost; + } + + public void setConfigServiceHost(String configServiceHost) { + if (StringUtil.isEmpty(configServiceHost)) { + return; + } + this.configServiceHost = configServiceHost; + System.setProperty(CONFIG_SERVICE_HOST, configServiceHost); + } + public void setRecordRate(int recordRate) { if (recordRate < 0) { return; @@ -230,9 +232,9 @@ private DynamicClassEntity createDynamicClass(DynamicClassConfiguration config, void init() { agentVersion = System.getProperty(AGENT_VERSION); setEnableDebug(System.getProperty(ENABLE_DEBUG)); - setEnableReportStatus(System.getProperty(ENABLE_REPORT_STATUS, Boolean.TRUE.toString())); setServiceName(StringUtil.strip(System.getProperty(SERVICE_NAME))); setStorageServiceHost(StringUtil.strip(System.getProperty(STORAGE_SERVICE_HOST))); + setConfigServiceHost(StringUtil.strip(System.getProperty(CONFIG_SERVICE_HOST))); configPath = StringUtil.strip(System.getProperty(CONFIG_PATH)); setRecordRate(DEFAULT_RECORDING_RATE); @@ -255,14 +257,14 @@ void readConfigFromFile(String configPath) { } Map configMap = parseConfigFile(configPath); - if (configMap.size() == 0) { + if (configMap.isEmpty()) { return; } setEnableDebug(configMap.get(ENABLE_DEBUG)); - setEnableReportStatus(System.getProperty(ENABLE_REPORT_STATUS)); setServiceName(configMap.get(SERVICE_NAME)); setStorageServiceHost(configMap.get(STORAGE_SERVICE_HOST)); + setConfigServiceHost(configMap.get(CONFIG_SERVICE_HOST)); setDynamicResultSizeLimit(configMap.get(DYNAMIC_RESULT_SIZE_LIMIT)); setTimeMachine(configMap.get(TIME_MACHINE)); setStorageServiceMode(configMap.get(STORAGE_SERVICE_MODE)); @@ -295,8 +297,28 @@ private static Map parseConfigFile(String configPath) { public void parseAgentConfig(String args) { Map agentMap = StringUtil.asMap(args); if (!agentMap.isEmpty()) { - setStorageServiceMode(agentMap.get(STORAGE_SERVICE_MODE)); - setEnableDebug(agentMap.get(ENABLE_DEBUG)); + for (Map.Entry entry : agentMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (StringUtil.isEmpty(key) || StringUtil.isEmpty(value)) { + continue; + } + + switch (key) { + case ENABLE_DEBUG: + setEnableDebug(value); + break; + case STORAGE_SERVICE_MODE: + setStorageServiceMode(value); + break; + case STORAGE_SERVICE_HOST: + case DISABLE_MODULE: + continue; + default: + System.setProperty(key, value); + break; + } + } updateRuntimeConfig(); } } @@ -322,7 +344,7 @@ private void updateRuntimeConfig() { configMap.put(TIME_MACHINE, String.valueOf(startTimeMachine())); configMap.put(DISABLE_REPLAY, System.getProperty(DISABLE_REPLAY)); configMap.put(DISABLE_RECORD, System.getProperty(DISABLE_RECORD)); - configMap.put(DURING_WORK, Boolean.toString(nextWorkTime() <= 0)); + configMap.put(DURING_WORK, Boolean.toString(inWorkingTime())); configMap.put(AGENT_VERSION, agentVersion); configMap.put(IP_VALIDATE, Boolean.toString(checkTargetAddress())); configMap.put(STORAGE_SERVICE_MODE, storageServiceMode); @@ -351,13 +373,6 @@ private void publish(Config config) { } } - public boolean valid() { - if (isLocalStorage()) { - return true; - } - return checkTargetAddress() && inWorkingTime(); - } - public void setConfigInvalid() { setRecordRate(0); setAllowDayOfWeeks(0); @@ -522,7 +537,7 @@ public void setTargetAddress(String targetAddress) { this.targetAddress = targetAddress; } - private boolean checkTargetAddress() { + public boolean checkTargetAddress() { String localHost = NetUtils.getIpAddress(); // Compatible containers can't get IPAddress if (StringUtil.isEmpty(localHost)) { @@ -565,16 +580,4 @@ public String toString() { ", dynamicClassList='" + dynamicClassList + '\'' + '}'; } - - public String getInvalidReason() { - if (!checkTargetAddress()) { - return "response [targetAddress] is not match"; - } - - if (!inWorkingTime()) { - return "not in working time, allow day of weeks is " + allowDayOfWeeks + ", time is " + allowTimeOfDayFrom + "-" + allowTimeOfDayTo; - } - - return "invalid config"; - } } diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java index 8a8b0ec63..5938169e2 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java @@ -1,12 +1,19 @@ package io.arex.foundation.internal; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.serializer.Serializer; + public class DataEntity { private final long queueTime; private final String postData; + private final String recordId; + private final String operationName; - public DataEntity(String postData) { - this.postData = postData; + public DataEntity(Mocker requestMocker) { + this.postData = Serializer.serialize(requestMocker); this.queueTime = System.nanoTime(); + this.recordId = requestMocker.getRecordId(); + this.operationName = requestMocker.getOperationName(); } public long getQueueTime() { @@ -16,4 +23,13 @@ public long getQueueTime() { public String getPostData() { return postData; } + + public String getRecordId() { + return recordId; + } + + public String getOperationName() { + return operationName; + } + } diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java index de1c1b4e0..0e15484a9 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java @@ -20,7 +20,11 @@ public enum AgentStatusEnum { /** * AREX is up, but not recording maybe rate=0 or allowDayOfWeeks is not match */ - SLEEPING(4, "sleeping"); + SLEEPING(4, "sleeping"), + /** + * AREX is shutdown, need to restart + */ + SHUTDOWN(5, "shutdown"); private final int code; private final String value; diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java index 483bacfff..cd74ba622 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java @@ -2,8 +2,11 @@ import com.google.auto.service.AutoService; +import com.google.common.collect.Range; import io.arex.agent.thirdparty.util.time.DateFormatUtils; import io.arex.foundation.serializer.JacksonSerializer.DateFormatParser; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactory; +import io.arex.foundation.serializer.custom.GuavaRangeSerializer; import io.arex.foundation.util.NumberTypeAdaptor; import io.arex.agent.bootstrap.util.StringUtil; import com.google.gson.Gson; @@ -201,6 +204,8 @@ public GsonSerializer() { .registerTypeAdapter(Instant.class, INSTANT_JSON_DESERIALIZER) .registerTypeAdapter(Class.class, CLASS_JSON_SERIALIZER) .registerTypeAdapter(Class.class, CLASS_JSON_DESERIALIZER) + .registerTypeAdapter(Range.class, new GuavaRangeSerializer.GsonRangeSerializer()) + .registerTypeAdapterFactory(new FastUtilAdapterFactory()) .enableComplexMapKeySerialization() .setExclusionStrategies(new ExcludeField()) .disableHtmlEscaping(); diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java index f06d077a5..280f6ebc0 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java @@ -1,6 +1,12 @@ package io.arex.foundation.serializer; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTypeResolverBuilder; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.google.auto.service.AutoService; +import com.google.common.collect.Range; import io.arex.agent.bootstrap.util.StringUtil; import com.fasterxml.jackson.annotation.JsonIgnoreType; @@ -13,6 +19,8 @@ import io.arex.agent.thirdparty.util.time.DateFormatUtils; import io.arex.agent.thirdparty.util.time.FastDateFormat; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactory; +import io.arex.foundation.serializer.custom.GuavaRangeSerializer; import io.arex.foundation.util.JdkUtils; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.config.Config; @@ -20,7 +28,6 @@ import io.arex.inst.runtime.model.SerializeSkipInfo; import io.arex.inst.runtime.serializer.StringSerializable; import io.arex.inst.runtime.util.TypeUtil; - import java.sql.Time; import java.time.Instant; import org.joda.time.DateTime; @@ -68,10 +75,18 @@ public JacksonSerializer() { configMapper(); customTimeFormatSerializer(MODULE); customTimeFormatDeserializer(MODULE); - + customTypeResolver(); MAPPER.registerModule(MODULE); } + private void customTypeResolver() { + TypeResolverBuilder typeResolver = new CustomTypeResolverBuilder(); + typeResolver.init(JsonTypeInfo.Id.CLASS, null); + typeResolver.inclusion(JsonTypeInfo.As.PROPERTY); + typeResolver.typeProperty("@CLASS"); + MAPPER.setDefaultTyping(typeResolver); + } + private void buildSkipInfoMap() { try { Config config = Config.get(); @@ -162,7 +177,6 @@ private void configMapper() { MAPPER.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false); MAPPER.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); MAPPER.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); - MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); } private void customTimeFormatSerializer(SimpleModule module) { @@ -180,6 +194,7 @@ private void customTimeFormatSerializer(SimpleModule module) { // java.sql.Date/Time serialize same as java.util.Date module.addSerializer(Date.class, new DateSerialize()); module.addSerializer(Instant.class, new InstantSerialize()); + module.addSerializer(Range.class, new GuavaRangeSerializer.JacksonRangeSerializer()); } private void customTimeFormatDeserializer(SimpleModule module) { @@ -198,6 +213,7 @@ private void customTimeFormatDeserializer(SimpleModule module) { module.addDeserializer(java.sql.Date.class, new SqlDateDeserialize()); module.addDeserializer(Time.class, new SqlTimeDeserialize()); module.addDeserializer(Instant.class, new InstantDeserialize()); + module.addDeserializer(Range.class, new GuavaRangeSerializer.JacksonRangeDeserializer()); } private static class JacksonSimpleModule extends SimpleModule { @@ -730,4 +746,19 @@ public DateTimeFormatter getFormatter(final String pattern) { } } + private static class CustomTypeResolverBuilder extends DefaultTypeResolverBuilder { + + public CustomTypeResolverBuilder() { + super(DefaultTyping.NON_FINAL, LaissezFaireSubTypeValidator.instance); + } + + /** + * @return true will serialize with runtime type info + */ + @Override + public boolean useForType(JavaType type) { + return type.getRawClass().isInterface() && + StringUtil.startWith(type.getRawClass().getName(), FastUtilAdapterFactory.FASTUTIL_PACKAGE); + } + } } diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java new file mode 100644 index 000000000..142fb0914 --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java @@ -0,0 +1,57 @@ +package io.arex.foundation.serializer.custom; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.log.LogManager; + +public class FastUtilAdapterFactory implements TypeAdapterFactory { + public static final String FASTUTIL_PACKAGE = "it.unimi.dsi.fastutil"; + private static final String SET_NAME = "Set"; + private static final String LIST_NAME = "List"; + private static final String MAP_NAME = "Map"; + private static final Gson GSON = new Gson(); + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + final Class rawClass = type.getRawType(); + if (rawClass == null) { + return null; + } + String rawClassName = rawClass.getName(); + if (rawClass.isInterface() && StringUtil.startWith(rawClassName, FASTUTIL_PACKAGE)) { + // example: it.unimi.dsi.fastutil.ints.IntSet -> IntOpenHashSet + if (rawClassName.endsWith(SET_NAME)) { + return getImplType(getImplClassPrefix(rawClassName, 3), "OpenHashSet"); + } + if (rawClassName.endsWith(LIST_NAME)) { + return getImplType(getImplClassPrefix(rawClassName, 4), "ArrayList"); + } + if (rawClassName.endsWith(MAP_NAME)) { + return getImplType(getImplClassPrefix(rawClassName, 3), "OpenHashMap"); + } + } + + return null; + } + + private String getImplClassPrefix(String rawClassName, int endIndex) { + return rawClassName.substring(0, rawClassName.length() - endIndex); + } + + private TypeAdapter getImplType(String implClassPrefix, String implClassSuffix) { + String implName = implClassPrefix + implClassSuffix; + try { + return (TypeAdapter) GSON.getAdapter(Class.forName(implName)); + } catch (Exception ex) { + LogManager.warn("getImplClass",StringUtil.format("Failed to load class: %s", implName), ex); + return null; + } + } +} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java new file mode 100644 index 000000000..80670e142 --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java @@ -0,0 +1,160 @@ +package io.arex.foundation.serializer.custom; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.arex.inst.runtime.util.TypeUtil; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; + +public class GuavaRangeSerializer { + + private static final String LOWER_BOUND_TYPE = "lowerBoundType"; + private static final String UPPER_BOUND_TYPE = "upperBoundType"; + + private static final String LOWER_BOUND = "lowerBound"; + private static final String UPPER_BOUND = "upperBound"; + + private static final String LOWER_BOUND_VALUE_TYPE = "lowerBoundValueType"; + private static final String UPPER_BOUND_VALUE_TYPE = "upperBoundValueType"; + + private static Range restoreRange(Comparable lowerBound, BoundType lowerBoundType, Comparable upperBound, + BoundType upperBoundType) { + if (lowerBound == null && upperBound != null) { + return Range.upTo(upperBound, upperBoundType); + } + + if (lowerBound != null && upperBound == null) { + return Range.downTo(lowerBound, lowerBoundType); + } + + if (lowerBound == null) { + return Range.all(); + } + + return Range.range(lowerBound, lowerBoundType, upperBound, upperBoundType); + } + + public static class GsonRangeSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Range range, Type type, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + + if (range.hasLowerBound()) { + jsonObject.add(LOWER_BOUND_TYPE, context.serialize(range.lowerBoundType())); + jsonObject.add(LOWER_BOUND, context.serialize(range.lowerEndpoint())); + jsonObject.addProperty(LOWER_BOUND_VALUE_TYPE, TypeUtil.getName(range.lowerEndpoint())); + } else { + jsonObject.add(LOWER_BOUND_TYPE, context.serialize(BoundType.OPEN)); + } + + if (range.hasUpperBound()) { + jsonObject.add(UPPER_BOUND_TYPE, context.serialize(range.upperBoundType())); + jsonObject.add(UPPER_BOUND, context.serialize(range.upperEndpoint())); + jsonObject.addProperty(UPPER_BOUND_VALUE_TYPE, TypeUtil.getName(range.upperEndpoint())); + } else { + jsonObject.add(UPPER_BOUND_TYPE, context.serialize(BoundType.OPEN)); + } + return jsonObject; + } + + @Override + public Range deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + + final JsonObject jsonObject = json.getAsJsonObject(); + final JsonElement lowerBoundTypeJsonElement = jsonObject.get(LOWER_BOUND_TYPE); + final JsonElement upperBoundTypeJsonElement = jsonObject.get(UPPER_BOUND_TYPE); + + final BoundType lowerBoundType = context.deserialize(lowerBoundTypeJsonElement, BoundType.class); + final JsonElement lowerBoundJsonElement = jsonObject.get(LOWER_BOUND); + final Comparable lowerBound = + lowerBoundJsonElement == null ? null : context.deserialize(lowerBoundJsonElement, + TypeUtil.forName(jsonObject.get(LOWER_BOUND_VALUE_TYPE).getAsString())); + + final BoundType upperBoundType = context.deserialize(upperBoundTypeJsonElement, BoundType.class); + final JsonElement upperBoundJsonElement = jsonObject.get(UPPER_BOUND); + final Comparable upperBound = + upperBoundJsonElement == null ? null : context.deserialize(upperBoundJsonElement, + TypeUtil.forName(jsonObject.get(UPPER_BOUND_VALUE_TYPE).getAsString())); + + return restoreRange(lowerBound, lowerBoundType, upperBound, upperBoundType); + } + } + + public static class JacksonRangeSerializer extends com.fasterxml.jackson.databind.JsonSerializer { + + @Override + public void serialize(Range range, JsonGenerator gen, SerializerProvider serializers) throws IOException { + final HashMap map = new HashMap<>(3); + if (range.hasLowerBound()) { + map.put(LOWER_BOUND_TYPE, range.lowerBoundType()); + map.put(LOWER_BOUND, range.lowerEndpoint()); + map.put(LOWER_BOUND_VALUE_TYPE, TypeUtil.getName(range.lowerEndpoint())); + } else { + map.put(LOWER_BOUND_TYPE, BoundType.OPEN); + } + + if (range.hasUpperBound()) { + map.put(UPPER_BOUND_TYPE, range.upperBoundType()); + map.put(UPPER_BOUND, range.upperEndpoint()); + map.put(UPPER_BOUND_VALUE_TYPE, TypeUtil.getName(range.upperEndpoint())); + } else { + map.put(UPPER_BOUND_TYPE, BoundType.OPEN); + } + gen.writeObject(map); + } + } + + public static class JacksonRangeDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer> { + + @Override + public Range deserialize(com.fasterxml.jackson.core.JsonParser p, + com.fasterxml.jackson.databind.DeserializationContext ctxt) throws IOException { + final JsonNode treeNode = p.getCodec().readTree(p); + final JsonNode lowBoundTypeNode = treeNode.get(LOWER_BOUND_TYPE); + final JsonNode upBoundTypeNode = treeNode.get(UPPER_BOUND_TYPE); + final JsonNode lowBoundNode = treeNode.get(LOWER_BOUND); + final JsonNode upBoundNode = treeNode.get(UPPER_BOUND); + final JsonNode lowBoundValueTypeNode = treeNode.get(LOWER_BOUND_VALUE_TYPE); + final JsonNode upBoundValueTypeNode = treeNode.get(UPPER_BOUND_VALUE_TYPE); + + final BoundType lowerBoundType = lowBoundTypeNode == null ? null : ctxt.readTreeAsValue(lowBoundTypeNode, BoundType.class); + final BoundType upperBoundType = upBoundTypeNode == null ? null : ctxt.readTreeAsValue(upBoundTypeNode, BoundType.class); + final JavaType lowerBoundJavaType = lowBoundValueTypeNode == null ? null : ctxt.constructType( + TypeUtil.forName(lowBoundValueTypeNode.asText())); + final JavaType upperBoundJavaType = upBoundValueTypeNode == null ? null : ctxt.constructType( + TypeUtil.forName(upBoundValueTypeNode.asText())); + + Comparable lowerBound; + Comparable upperBound ; + + if (lowerBoundJavaType == null || lowBoundNode == null) { + lowerBound = null; + } else { + lowerBound = ctxt.readTreeAsValue(lowBoundNode, lowerBoundJavaType); + } + + if (upperBoundJavaType == null || upBoundNode == null) { + upperBound = null; + } else { + upperBound = ctxt.readTreeAsValue(upBoundNode, upperBoundJavaType); + } + + return restoreRange(lowerBound, lowerBoundType, upperBound, upperBoundType); + } + } + +} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java index b810b1e0f..68d90f3b0 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java @@ -34,7 +34,7 @@ public class ConfigService { public static final ConfigService INSTANCE = new ConfigService(); private static final String CONFIG_LOAD_URI = - String.format("http://%s/api/config/agent/load", ConfigManager.INSTANCE.getStorageServiceHost()); + String.format("http://%s/api/config/agent/load", ConfigManager.INSTANCE.getConfigServiceHost()); private final AtomicBoolean firstLoad = new AtomicBoolean(false); private final AtomicBoolean reloadConfig = new AtomicBoolean(false); @@ -113,16 +113,20 @@ public ConfigQueryRequest buildConfigQueryRequest() { } AgentStatusEnum getAgentStatus() { - if (firstLoad.compareAndSet(false, true)) { - return AgentStatusEnum.START; + if (AgentStatusService.INSTANCE.isShutdown()) { + return AgentStatusEnum.SHUTDOWN; } - if (ConfigManager.INSTANCE.valid() && ConfigManager.INSTANCE.getRecordRate() > 0) { - return AgentStatusEnum.WORKING; + if (firstLoad.compareAndSet(false, true)) { + return AgentStatusEnum.START; } if (ConfigManager.FIRST_TRANSFORM.get()) { - return AgentStatusEnum.SLEEPING; + if (ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0) { + return AgentStatusEnum.WORKING; + } else { + return AgentStatusEnum.SLEEPING; + } } return AgentStatusEnum.UN_START; @@ -165,6 +169,18 @@ public void reportStatus() { AgentStatusService.INSTANCE.report(); } + public void shutdown() { + try { + if (AgentStatusService.INSTANCE.shutdown()) { + LOGGER.info("[AREX] Agent shutdown, stop working now."); + ConfigManager.INSTANCE.setConfigInvalid(); + reportStatus(); + } + } catch (Exception e) { + LOGGER.error("[AREX] Agent shutdown error, {}", e.getMessage()); + } + } + public boolean reloadConfig() { return reloadConfig.get(); } @@ -175,7 +191,17 @@ private static class AgentStatusService { private String prevLastModified; private static final String AGENT_STATUS_URI = - String.format("http://%s/api/config/agent/agentStatus", ConfigManager.INSTANCE.getStorageServiceHost()); + String.format("http://%s/api/config/agent/agentStatus", ConfigManager.INSTANCE.getConfigServiceHost()); + + private final AtomicBoolean shutdown = new AtomicBoolean(false); + + private boolean shutdown() { + return shutdown.compareAndSet(false, true); + } + + public boolean isShutdown() { + return shutdown.get(); + } public void report() { AgentStatusEnum agentStatus = ConfigService.INSTANCE.getAgentStatus(); diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java index 7a26cf8e7..da778908c 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java @@ -1,6 +1,7 @@ package io.arex.foundation.services; import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.util.MapUtils; import io.arex.foundation.config.ConfigManager; import io.arex.foundation.healthy.HealthManager; @@ -10,6 +11,7 @@ import io.arex.foundation.model.HttpClientResponse; import io.arex.foundation.util.httpclient.async.ThreadFactoryImpl; import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.util.CaseManager; import io.arex.inst.runtime.service.DataCollector; import java.util.Map; @@ -39,13 +41,14 @@ public class DataCollectorService implements DataCollector { } @Override - public void save(String mockData) { + public void save(Mocker requestMocker) { if (HealthManager.isFastRejection()) { return; } - if (!buffer.put(new DataEntity(mockData))) { + if (!buffer.put(new DataEntity(requestMocker))) { HealthManager.onEnqueueRejection(); + CaseManager.invalid(requestMocker.getRecordId(), requestMocker.getOperationName()); } } @@ -109,6 +112,9 @@ static void doSleep(long millis) { private static final String MOCK_STRATEGY = "X-AREX-Mock-Strategy-Code"; void saveData(DataEntity entity) { + if (entity == null || CaseManager.isInvalidCase(entity.getRecordId())) { + return; + } AsyncHttpClientUtil.postAsyncWithZstdJson(saveApiUrl, entity.getPostData(), null) .whenComplete(saveMockDataConsumer(entity)); } @@ -131,6 +137,7 @@ private BiConsumer saveMockDataConsumer(DataEntity entity) { return (response, throwable) -> { long usedTime = System.nanoTime() - entity.getQueueTime(); if (Objects.nonNull(throwable)) { + CaseManager.invalid(entity.getRecordId(), entity.getOperationName()); LogManager.warn("saveMockDataConsumer", "save mock data error"); usedTime = -1; // -1:reject HealthManager.onDataServiceRejection(); diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java deleted file mode 100644 index b1c0d3e52..000000000 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.arex.foundation.util; - -import java.io.*; - -public class IOUtils { - - public static String toString(InputStream input) throws IOException { - BufferedReader br = null; - try { - StringBuilder sb = new StringBuilder(); - br = new BufferedReader(new InputStreamReader(input)); - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append("\n"); - } - return sb.toString(); - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException e) { - // ignore - } - } - } - } - - public static void copy(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - } -} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java index 22e22eaa9..88fb4f73c 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java @@ -3,7 +3,6 @@ import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.foundation.model.ConfigQueryResponse; import io.arex.foundation.model.ConfigQueryResponse.DynamicClassConfiguration; -import io.arex.foundation.util.NetUtils; import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.model.DynamicClassEntity; import io.arex.inst.runtime.model.DynamicClassStatusEnum; @@ -17,9 +16,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import java.io.File; import java.net.URISyntaxException; @@ -28,20 +24,14 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Stream; -import org.mockito.MockedStatic; -import static io.arex.agent.bootstrap.constants.ConfigConstants.ENABLE_REPORT_STATUS; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.mockStatic; class ConfigManagerTest { static ConfigManager configManager = null; @BeforeAll - static void setUp() throws URISyntaxException { + static void setUp() { configManager = ConfigManager.INSTANCE; } @@ -59,7 +49,11 @@ void initFromSystemPropertyTest() { assertEquals("test-your-service", configManager.getServiceName()); assertEquals("test-storage-service.host", configManager.getStorageServiceHost()); - assertTrue(configManager.isEnableReportStatus()); + assertEquals("test-storage-service.host", configManager.getConfigServiceHost()); + + System.setProperty("arex.config.service.host", "test-config-service.host"); + configManager.init(); + assertEquals("test-config-service.host", configManager.getConfigServiceHost()); } @Test @@ -94,33 +88,6 @@ void setExcludeServiceOperations() { assertFalse(configManager.getExcludeServiceOperations().isEmpty()); } - @ParameterizedTest - @MethodSource("validCase") - void valid(Runnable mocker, Predicate predicate) { - mocker.run(); - assertTrue(predicate.test(configManager.valid())); - } - - static Stream validCase() { - Runnable mocker1 = () -> { - configManager.setStorageServiceMode(ConfigConstants.STORAGE_MODE); - }; - Runnable mocker2 = () -> { - configManager.setStorageServiceMode("xxx"); - }; - Runnable mocker3 = () -> { - configManager.setTargetAddress("mock"); - }; - - Predicate predicate1 = result -> result; - Predicate predicate2 = result -> !result; - return Stream.of( - arguments(mocker1, predicate1), - arguments(mocker2, predicate2), - arguments(mocker3, predicate2) - ); - } - @Test void replaceConfigFromService() { ConfigQueryResponse.ResponseBody serviceConfig = new ConfigQueryResponse.ResponseBody(); @@ -264,24 +231,6 @@ void setDynamicClassList() { assertNotNull(ConfigManager.INSTANCE.toString()); } - @Test - void getInvalidReason() { - try (MockedStatic netUtils = mockStatic(NetUtils.class)) { - netUtils.when(NetUtils::getIpAddress).thenReturn("172.0.0.3"); - - // check target address is not match - ConfigManager.INSTANCE.setTargetAddress("172.0.0.1"); - String reason = ConfigManager.INSTANCE.getInvalidReason(); - assertEquals("response [targetAddress] is not match", reason); - - // check inWorkingTime is false - ConfigManager.INSTANCE.setTargetAddress("172.0.0.3"); - reason = ConfigManager.INSTANCE.getInvalidReason(); - assertTrue(reason.startsWith("not in working time")); - assertFalse(reason.contains(LocalDate.now().getDayOfWeek().name())); - } - } - @Test void setDynamicClassListWithKeyFormula() throws Exception { DynamicClassConfiguration dynamicClassConfiguration1 = new DynamicClassConfiguration(); diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java index c989a646e..19c664272 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java @@ -3,9 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import com.google.gson.internal.LinkedTreeMap; import io.arex.inst.runtime.util.TypeUtil; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; import java.sql.Time; import java.time.LocalDateTime; import java.util.Map; @@ -134,4 +137,13 @@ void testAddCustomSerializer() { json = GsonSerializer.INSTANCE.serialize(map); assertEquals("{\"key\":\"value\",\"long-java.lang.Long\":2}", json); } + + @Test + void testFastUtil() throws Throwable { + final IntOpenHashSet hashSet = new IntOpenHashSet(); + final String json = GsonSerializer.INSTANCE.serialize(hashSet); + final IntSet deserialize = GsonSerializer.INSTANCE.deserialize(json, IntSet.class); + assert deserialize != null; + assertEquals(hashSet, deserialize); + } } \ No newline at end of file diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java index 7f8b043fa..011623a7f 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java @@ -1,11 +1,13 @@ package io.arex.foundation.serializer; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactoryTest; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactoryTest.TestType; + import static org.junit.jupiter.api.Assertions.*; import io.arex.inst.runtime.util.TypeUtil; import java.sql.Time; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -139,4 +141,45 @@ void deserializeType() throws Throwable { assertNotNull(JacksonSerializer.INSTANCE.deserialize(json, TypeUtil.forName(TypeUtil.getName(LocalDateTime.now())))); } + @Test + void testFastUtil() throws Throwable { + final TestType testType = FastUtilAdapterFactoryTest.getTestType(); + final String jackJson = JacksonSerializer.INSTANCE.serialize(testType); + final TestType deserializeJackTestType = JacksonSerializer.INSTANCE.deserialize(jackJson, TestType.class); + assertNotNull(deserializeJackTestType); + } + + @Test + void testCaseSensitiveProperties() throws Throwable { + final CaseSensitive caseSensitive = new CaseSensitive(); + caseSensitive.setAmountPaid("100"); + caseSensitive.setAmountpaid("200"); + final String jackJson = JacksonSerializer.INSTANCE.serialize(caseSensitive); + final CaseSensitive deserializeJackTestType = JacksonSerializer.INSTANCE.deserialize(jackJson, CaseSensitive.class); + assertNotNull(deserializeJackTestType); + assertEquals("100", deserializeJackTestType.getAmountPaid()); + assertEquals("200", deserializeJackTestType.getAmountpaid()); + } + + static class CaseSensitive { + private String amountPaid; + private String amountpaid; + + public String getAmountPaid() { + return amountPaid; + } + + public void setAmountPaid(String amountPaid) { + this.amountPaid = amountPaid; + } + + public String getAmountpaid() { + return amountpaid; + } + + public void setAmountpaid(String amountpaid) { + this.amountpaid = amountpaid; + } + } + } \ No newline at end of file diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java index 102640dfd..feaf178cb 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java @@ -1,5 +1,6 @@ package io.arex.foundation.serializer; +import com.google.common.collect.Range; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -43,6 +44,16 @@ public class TimeTestInfo { private Instant instant = Instant.now(); + private Range range; + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + public Instant getInstant() { return instant; } diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java new file mode 100644 index 000000000..21d318f3d --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java @@ -0,0 +1,106 @@ +package io.arex.foundation.serializer.custom; + +import static org.junit.jupiter.api.Assertions.*; + +import io.arex.foundation.serializer.GsonSerializer; +import it.unimi.dsi.fastutil.floats.FloatArrayList; +import it.unimi.dsi.fastutil.floats.FloatList; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +public class FastUtilAdapterFactoryTest { + public static TestType getTestType() { + final TestType testType = new TestType(1, "test"); + final IntOpenHashSet intOpenHashSet = new IntOpenHashSet(); + intOpenHashSet.add(2); + testType.setIntSet(intOpenHashSet); + final FloatArrayList floats = new FloatArrayList(); + floats.add(1.0f); + testType.setFloats(floats); + final LongLinkedOpenHashSet linkedOpenHashSet = new LongLinkedOpenHashSet(); + linkedOpenHashSet.add(3L); + testType.setLongSet(linkedOpenHashSet); + final HashSet hashSet = new HashSet<>(); + hashSet.add(1); + testType.setSet(new Set[]{hashSet}); + return testType; + } + + @Test + void testFastUtilAdapter() throws Throwable { + final TestType testType = getTestType(); + final String json = GsonSerializer.INSTANCE.serialize(testType); + final TestType deserializeTestType = GsonSerializer.INSTANCE.deserialize(json, TestType.class); + assertNotNull(deserializeTestType); + } + + public static class TestType { + private int id; + private String name; + private IntSet intSet; + private FloatList floats; + private LongSet longSet; + private Set[] set; + + public TestType() { + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TestType(int id, String name) { + this.id = id; + this.name = name; + } + + public IntSet getIntSet() { + return intSet; + } + + public void setIntSet(IntSet intSet) { + this.intSet = intSet; + } + + public FloatList getFloats() { + return floats; + } + + public void setFloats(FloatList floats) { + this.floats = floats; + } + + public LongSet getLongSet() { + return longSet; + } + + public void setLongSet(LongSet longSet) { + this.longSet = longSet; + } + + public Set[] getSet() { + return set; + } + + public void setSet(Set[] set) { + this.set = set; + } + } +} \ No newline at end of file diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java new file mode 100644 index 000000000..15654297d --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java @@ -0,0 +1,85 @@ +package io.arex.foundation.serializer.custom; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.common.collect.Range; +import io.arex.foundation.serializer.GsonSerializer; +import io.arex.foundation.serializer.JacksonSerializer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GuavaRangeSerializerTest { + static Range range1, range2, range3, range4; + @BeforeAll + static void setUp() { + range1 = Range.closed(1, 10); + range2 = Range.lessThan(2); + range3 = Range.greaterThan(3); + range4 = Range.openClosed(4, 40); + } + + @AfterAll + static void tearDown() { + range1 = null; + range2 = null; + range3 = null; + range4 = null; + } + + @Test + void testRangeSerializeWithGson() { + final String range1Json = GsonSerializer.INSTANCE.serialize(range1); + final Range deserializeRange1 = GsonSerializer.INSTANCE.deserialize(range1Json, Range.class); + assertEquals(range1.lowerEndpoint(), deserializeRange1.lowerEndpoint()); + assertEquals(range1.upperEndpoint(), deserializeRange1.upperEndpoint()); + + final String range2Json = GsonSerializer.INSTANCE.serialize(range2); + final Range deserializeRange2 = GsonSerializer.INSTANCE.deserialize(range2Json, Range.class); + assertFalse(range2.hasLowerBound()); + assertFalse(deserializeRange2.hasLowerBound()); + assertEquals(range2.upperEndpoint(), deserializeRange2.upperEndpoint()); + + final String range3Json = GsonSerializer.INSTANCE.serialize(range3); + final Range deserializeRange3 = GsonSerializer.INSTANCE.deserialize(range3Json, Range.class); + assertEquals(range3.lowerEndpoint(), deserializeRange3.lowerEndpoint()); + assertFalse(range3.hasUpperBound()); + assertFalse(deserializeRange3.hasUpperBound()); + + final String range4Json = GsonSerializer.INSTANCE.serialize(range4); + final Range deserializeRange4 = GsonSerializer.INSTANCE.deserialize(range4Json, Range.class); + assertEquals(range4.lowerEndpoint(), deserializeRange4.lowerEndpoint()); + assertEquals(range4.upperEndpoint(), deserializeRange4.upperEndpoint()); + assertEquals(range4.lowerBoundType(), deserializeRange4.lowerBoundType()); + assertEquals(range4.upperBoundType(), deserializeRange4.upperBoundType()); + } + + @Test + void testRangeSerializeWithJackson() throws Throwable { + final String range1Json = JacksonSerializer.INSTANCE.serialize(range1); + final Range deserializeRange1 = JacksonSerializer.INSTANCE.deserialize(range1Json, Range.class); + assertEquals(range1.lowerEndpoint(), deserializeRange1.lowerEndpoint()); + assertEquals(range1.upperEndpoint(), deserializeRange1.upperEndpoint()); + + final String range2Json = JacksonSerializer.INSTANCE.serialize(range2); + final Range deserializeRange2 = JacksonSerializer.INSTANCE.deserialize(range2Json, Range.class); + assertFalse(range2.hasLowerBound()); + assertFalse(deserializeRange2.hasLowerBound()); + assertEquals(range2.upperEndpoint(), deserializeRange2.upperEndpoint()); + + final String range3Json = JacksonSerializer.INSTANCE.serialize(range3); + final Range deserializeRange3 = JacksonSerializer.INSTANCE.deserialize(range3Json, Range.class); + assertEquals(range3.lowerEndpoint(), deserializeRange3.lowerEndpoint()); + assertFalse(range3.hasUpperBound()); + assertFalse(deserializeRange3.hasUpperBound()); + + final String range4Json = JacksonSerializer.INSTANCE.serialize(range4); + final Range deserializeRange4 = JacksonSerializer.INSTANCE.deserialize(range4Json, Range.class); + assertEquals(range4.lowerEndpoint(), deserializeRange4.lowerEndpoint()); + assertEquals(range4.upperEndpoint(), deserializeRange4.upperEndpoint()); + assertEquals(range4.lowerBoundType(), deserializeRange4.lowerBoundType()); + assertEquals(range4.upperBoundType(), deserializeRange4.upperBoundType()); + } +} \ No newline at end of file diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java index e360ab8d2..3a20c3722 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java @@ -40,7 +40,7 @@ void tearDown() { } @Test - void loadAgentConfig() { + void loadAgentConfig() throws Throwable { long DELAY_MINUTES = 15L; // local long actualResult = ConfigService.INSTANCE.loadAgentConfig("arex.service.name=unit-test-service;arex.enable.debug=true;arex.storage.mode=local"); @@ -104,10 +104,11 @@ void loadAgentConfig() { CompletableFuture response = CompletableFuture.completedFuture(new HttpClientResponse(200, null, JacksonSerializer.INSTANCE.serialize(configQueryResponse))); ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), eq(null))).thenReturn(response); assertEquals(DELAY_MINUTES, ConfigService.INSTANCE.loadAgentConfig(null)); - assertTrue(ConfigManager.INSTANCE.valid() && ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); + assertTrue(ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); + ConfigManager.FIRST_TRANSFORM.compareAndSet(false, true); assertEquals(AgentStatusEnum.WORKING, ConfigService.INSTANCE.getAgentStatus()); - ConfigManager.FIRST_TRANSFORM.compareAndSet(false, true); + // valid response, agentStatus=SLEEPING serviceCollectConfig.setAllowDayOfWeeks(0); response = CompletableFuture.completedFuture( @@ -123,10 +124,8 @@ void loadAgentConfig() { new HttpClientResponse(200, null, JacksonSerializer.INSTANCE.serialize(configQueryResponse))); ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), eq(null))).thenReturn(response); assertEquals(DELAY_MINUTES, ConfigService.INSTANCE.loadAgentConfig(null)); - assertTrue(ConfigManager.INSTANCE.valid() && ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); + assertTrue(ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); assertEquals(AgentStatusEnum.WORKING, ConfigService.INSTANCE.getAgentStatus()); - } catch (Throwable e) { - throw new RuntimeException(e); } } @@ -189,4 +188,23 @@ void reportStatus() { assertFalse(ConfigService.INSTANCE.reloadConfig()); } } + + @Test + void shutdown() { + try (MockedStatic ahc = mockStatic(AsyncHttpClientUtil.class); + MockedStatic netUtils = mockStatic(NetUtils.class)){ + netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); + Map responseHeaders = new HashMap<>(); + responseHeaders.put("Last-Modified2", "Thu, 01 Jan 1970 00:00:00 GMT"); + ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), anyMap())).thenReturn( + CompletableFuture.completedFuture(new HttpClientResponse(200, responseHeaders, null))); + + ConfigService.INSTANCE.shutdown(); + + AgentStatusEnum actualResult = ConfigService.INSTANCE.getAgentStatus(); + assertEquals(AgentStatusEnum.SHUTDOWN, actualResult); + + ConfigService.INSTANCE.shutdown(); + } + } } diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java index fa6eb7a66..04f66ec5e 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java @@ -4,11 +4,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.foundation.healthy.HealthManager; import io.arex.foundation.internal.DataEntity; import io.arex.foundation.model.HttpClientResponse; import io.arex.foundation.util.httpclient.AsyncHttpClientUtil; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -20,6 +23,7 @@ class DataCollectorServiceTest { static void setUp() { Mockito.mockStatic(AsyncHttpClientUtil.class); Mockito.mockStatic(HealthManager.class); + Mockito.mockStatic(ContextManager.class); } @AfterAll @@ -29,14 +33,25 @@ static void tearDown() { @Test void saveData() { + final ArexMocker mocker = new ArexMocker(); CompletableFuture mockResponse = CompletableFuture.completedFuture(HttpClientResponse.emptyResponse()); - Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), anyString(), any())).thenReturn(mockResponse); - assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity("test"))); + Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockResponse); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); CompletableFuture mockException = new CompletableFuture<>(); mockException.completeExceptionally(new RuntimeException("mock exception")); - Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), anyString(), any())).thenReturn(mockException); - assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity("test"))); + Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockException); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); + + // null entity + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(null)); + + // invalid case + final ArexContext context = ArexContext.of("testRecordId"); + context.setInvalidCase(true); + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(context); + mocker.setRecordId("testRecordId"); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); } @Test diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java new file mode 100644 index 000000000..53176c7cd --- /dev/null +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java @@ -0,0 +1,19 @@ +package io.arex.inst.config.apollo; + +public class ApolloConfigChecker { + + private static boolean isLoadedApollo = false; + + static { + try { + Class.forName("com.ctrip.framework.apollo.ConfigService"); + isLoadedApollo = true; + } catch (ClassNotFoundException e) { + // ignore, means business application unLoad apollo-client + } + } + + public static boolean unloadApollo() { + return !isLoadedApollo; + } +} diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java index 2ee6cf16c..515a52310 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java @@ -50,16 +50,6 @@ */ public class ApolloConfigHelper { private static Field configInstancesField; - private static boolean isLoadedApollo = false; - - static { - try { - Class.forName("com.ctrip.framework.apollo.ConfigService"); - isLoadedApollo = true; - } catch (ClassNotFoundException e) { - // ignore, means business application unLoad apollo-client - } - } public static void initAndRecord(Supplier recordIdSpl, Supplier versionSpl) { String recordId = recordIdSpl.get(); @@ -223,8 +213,4 @@ because this configuration has already been replayed, during the first full repl private static String getReleaseKey() { return ArexConstants.PREFIX + ApolloConfigExtractor.currentReplayConfigBatchNo(); } - - public static boolean unloadApollo() { - return !isLoadedApollo; - } } diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java index b3083a894..be6666369 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java @@ -18,7 +18,7 @@ public String name() { @Override public void preHandle(Map request) { // check business application if loaded apollo-client - if (ApolloConfigHelper.unloadApollo()) { + if (ApolloConfigChecker.unloadApollo()) { return; } ApolloConfigHelper.initAndRecord( @@ -44,6 +44,6 @@ private boolean postInvalid(Map request, Map res if (request == null) { return true; } - return !ContextManager.needRecord() || ApolloConfigHelper.unloadApollo(); + return !ContextManager.needRecord() || ApolloConfigChecker.unloadApollo(); } } diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java index 90ed744ea..1c2c3767d 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java @@ -18,7 +18,7 @@ public String name() { @Override public void preHandle(HttpServletRequest request) { // check business application if loaded apollo-client - if (ApolloConfigHelper.unloadApollo()) { + if (ApolloConfigChecker.unloadApollo()) { return; } ApolloConfigHelper.initAndRecord( @@ -47,6 +47,6 @@ private boolean postInvalid(HttpServletRequest request, HttpServletResponse resp if (response.getHeader(ArexConstants.RECORD_ID) != null) { return true; } - return !ContextManager.needRecord() || ApolloConfigHelper.unloadApollo(); + return !ContextManager.needRecord() || ApolloConfigChecker.unloadApollo(); } } diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java new file mode 100644 index 000000000..6ddee0279 --- /dev/null +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java @@ -0,0 +1,13 @@ +package io.arex.inst.config.apollo; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ApolloConfigCheckerTest { + + @Test + void unloadApollo() { + assertFalse(ApolloConfigChecker.unloadApollo()); + } +} \ No newline at end of file diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java index 384d9b613..42a8d09cc 100644 --- a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java @@ -176,9 +176,4 @@ static Stream getReplayConfigCase() { arguments(mocker2, previous3, predicate_nonNull) ); } - - @Test - void unloadApollo() { - assertFalse(ApolloConfigHelper.unloadApollo()); - } } \ No newline at end of file diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java index 76d92be97..5bae2fa40 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java @@ -3,6 +3,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import io.arex.agent.bootstrap.model.MockResult; +import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.util.ArrayUtils; import io.arex.agent.bootstrap.util.StringUtil; @@ -128,7 +129,7 @@ public MockResult replay() { // If not in cache, get replay result from mock server if (replayResult == null) { - Mocker replayMocker = MockUtils.replayMocker(makeMocker()); + Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); if (MockUtils.checkResponseMocker(replayMocker)) { String typeName = replayMocker.getTargetResponse().getType(); replayResult = deserializeResult(replayMocker, typeName); @@ -288,9 +289,10 @@ private boolean needRecord() { size = Array.getLength(result); } if (size > RESULT_SIZE_MAX) { + String methodInfo = methodSignatureKey == null ? buildDuplicateMethodKey() : methodSignatureKey; LogManager.warn(NEED_RECORD_TITLE, StringUtil.format("do not record method, cuz result size:%s > max limit: %s, method info: %s", - String.valueOf(size), String.valueOf(RESULT_SIZE_MAX), methodSignatureKey)); + String.valueOf(size), String.valueOf(RESULT_SIZE_MAX), methodInfo)); return false; } } catch (Throwable e) { diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java index 8dc828507..b2b516dde 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java @@ -42,6 +42,7 @@ import java.util.stream.Stream; import org.mockito.stubbing.Answer; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -152,7 +153,7 @@ void replay(Runnable mocker, Object[] args, Predicate predicate) thr arexMocker2.setTargetResponse(new Target()); arexMocker2.getTargetResponse().setBody("mock Body"); arexMocker2.getTargetResponse().setType("mock Type"); - mockService.when(() -> MockUtils.replayMocker(any())).thenReturn(arexMocker2); + mockService.when(() -> MockUtils.replayMocker(any(), any())).thenReturn(arexMocker2); Mockito.when(Serializer.serializeWithException(any(), anyString())).thenReturn("mock Serializer.serialize"); Mockito.when(Serializer.serializeWithException(anyString(), anyString())).thenReturn(""); @@ -384,7 +385,7 @@ public void testProtoBufResultReplay() { mockService.when(() -> MockUtils.createDynamicClass(any(), any())).thenReturn(arexMocker); mockService.when(() -> MockUtils.checkResponseMocker(any())).thenReturn(true); Mockito.when(ContextManager.currentContext()).thenReturn(ArexContext.of("")); - Mockito.when(MockUtils.replayMocker(any())).thenReturn(arexMocker2); + Mockito.when(MockUtils.replayMocker(any(), any())).thenReturn(arexMocker2); Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod( "testWithArexMock", String.class); @@ -438,4 +439,11 @@ void invalidOperation() throws Throwable { final MockResult replay = extractor.replay(); assertEquals(MockResult.IGNORE_MOCK_RESULT, replay); } + + @Test + void emptyMethodKeyAndExceedSize() throws NoSuchMethodException { + Method testEmptyArgs = DynamicClassExtractorTest.class.getDeclaredMethod("invalidOperation"); + DynamicClassExtractor extractor = new DynamicClassExtractor(testEmptyArgs, new Object[0]); + assertDoesNotThrow(() -> extractor.recordResponse(new int[1001])); + } } diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml index 188f5e6bf..a325a8ef4 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml @@ -20,7 +20,7 @@ org.apache.httpcomponents httpasyncclient - 4.1.4 + 4.1.5 provided diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java index 9d0fefc5f..ca6231d4b 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java @@ -9,12 +9,8 @@ import org.apache.http.HttpResponse; import org.apache.http.concurrent.BasicFuture; import org.apache.http.concurrent.FutureCallback; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class FutureCallbackWrapper implements FutureCallback { - private static final Logger LOGGER = LoggerFactory.getLogger(FutureCallbackWrapper.class); private final FutureCallback delegate; private final TraceTransmitter traceTransmitter; @@ -79,27 +75,24 @@ public Future replay(MockResult mockResult) { return basicFuture; } - public static FutureCallbackWrapper get(HttpAsyncRequestProducer requestProducer, FutureCallback delegate) { + public static FutureCallback wrap(HttpRequest httpRequest, FutureCallback delegate) { if (delegate instanceof FutureCallbackWrapper) { - return ((FutureCallbackWrapper) delegate); + return delegate; } - ApacheHttpClientAdapter adapter; - HttpClientExtractor extractor; - - try { - adapter = new ApacheHttpClientAdapter(requestProducer.generateRequest()); - if (adapter.skipRemoteStorageRequest()) { - return null; - } - extractor = new HttpClientExtractor<>(adapter); - } catch (Throwable ex) { - LOGGER.warn("create async wrapper error:{}, record or replay was skipped", ex.getMessage(), ex); + ApacheHttpClientAdapter adapter = new ApacheHttpClientAdapter(httpRequest); + if (adapter.skipRemoteStorageRequest()) { return null; } - return new FutureCallbackWrapper<>(extractor, delegate); + return new FutureCallbackWrapper<>(new HttpClientExtractor<>(adapter), delegate); } + /** + * Wrap the delegate with FutureCallbackWrapper for arex trace propagation + */ public static FutureCallback wrap(FutureCallback delegate) { + if (delegate instanceof FutureCallbackWrapper) { + return delegate; + } return new FutureCallbackWrapper<>(delegate); } -} \ No newline at end of file +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java index f58b8c143..aef6ea403 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java @@ -9,8 +9,8 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import org.apache.http.HttpRequest; import org.apache.http.concurrent.FutureCallback; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; import java.util.List; import java.util.concurrent.Future; @@ -25,7 +25,7 @@ public class InternalHttpAsyncClientInstrumentation extends TypeInstrumentation @Override public ElementMatcher typeMatcher() { - return named("org.apache.http.impl.nio.client.InternalHttpAsyncClient"); + return named("org.apache.http.impl.nio.client.CloseableHttpAsyncClient"); } @Override @@ -33,8 +33,8 @@ public List methodAdvices() { return singletonList(new MethodInstrumentation( isMethod().and(named("execute")) .and(takesArguments(4)) - .and(takesArgument(0, named("org.apache.http.nio.protocol.HttpAsyncRequestProducer"))) - .and(takesArgument(1, named("org.apache.http.nio.protocol.HttpAsyncResponseConsumer"))) + .and(takesArgument(0, named("org.apache.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.http.HttpRequest"))) .and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))) .and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback"))), this.getClass().getName() + "$ExecuteAdvice")); @@ -43,22 +43,22 @@ public List methodAdvices() { @SuppressWarnings("unused") public static class ExecuteAdvice { @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) - public static boolean onEnter(@Advice.Argument(0) HttpAsyncRequestProducer producer, + public static boolean onEnter(@Advice.Argument(1) HttpRequest httpRequest, @Advice.Argument(value = 3, readOnly = false) FutureCallback callback, @Advice.Local("mockResult") MockResult mockResult) { try { - if (ApacheHttpClientHelper.ignoreRequest(producer.generateRequest())) { + if (ApacheHttpClientHelper.ignoreRequest(httpRequest)) { callback = FutureCallbackWrapper.wrap(callback); return false; } - } catch (Throwable ignored) { + } catch (Exception ignored) { callback = FutureCallbackWrapper.wrap(callback); return false; } if (ContextManager.needRecordOrReplay() && RepeatedCollectManager.validate()) { // recording works in callback wrapper - FutureCallbackWrapper callbackWrapper = FutureCallbackWrapper.get(producer, callback); + FutureCallback callbackWrapper = FutureCallbackWrapper.wrap(httpRequest, callback); if (callbackWrapper != null) { callback = callbackWrapper; if (ContextManager.needReplay()) { diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java index c87897e0c..b7a28bb1e 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java @@ -1,11 +1,11 @@ package io.arex.inst.httpclient.apache.common; +import io.arex.agent.bootstrap.util.IOUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.httpclient.common.HttpClientAdapter; import io.arex.inst.httpclient.common.HttpResponseWrapper; import io.arex.inst.httpclient.common.HttpResponseWrapper.StringTuple; import io.arex.inst.runtime.log.LogManager; -import java.io.ByteArrayOutputStream; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -15,8 +15,6 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.HttpEntityWrapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.net.URI; @@ -25,11 +23,11 @@ import java.util.Locale; public class ApacheHttpClientAdapter implements HttpClientAdapter { - private static final Logger LOGGER = LoggerFactory.getLogger(ApacheHttpClientAdapter.class); private final HttpUriRequest httpRequest; public ApacheHttpClientAdapter(HttpRequest httpRequest) { this.httpRequest = (HttpUriRequest) httpRequest; + wrapHttpEntity(httpRequest); } @Override @@ -39,23 +37,23 @@ public String getMethod() { @Override public byte[] getRequestBytes() { - if (!(this.httpRequest instanceof HttpEntityEnclosingRequest)) { + HttpEntityEnclosingRequest enclosingRequest = enclosingRequest(httpRequest); + if (enclosingRequest == null) { return ZERO_BYTE; } - HttpEntityEnclosingRequest enclosingRequestBase = (HttpEntityEnclosingRequest) this.httpRequest; - HttpEntity entity = enclosingRequestBase.getEntity(); + HttpEntity entity = enclosingRequest.getEntity(); if (entity == null) { return ZERO_BYTE; } - byte[] content; - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){ - entity.writeTo(byteArrayOutputStream); - content = byteArrayOutputStream.toByteArray(); - } catch (Throwable e) { - LogManager.warn("getRequestBytes", e); - content = ZERO_BYTE; + if (entity instanceof CachedHttpEntityWrapper) { + return ((CachedHttpEntityWrapper) entity).getCachedBody(); + } + try { + return IOUtils.copyToByteArray(entity.getContent()); + } catch (Exception e) { + LogManager.warn("copyToByteArray", "getRequestBytes error, uri: " + getUri(), e); + return ZERO_BYTE; } - return content; } @Override @@ -82,22 +80,21 @@ public HttpResponseWrapper wrap(HttpResponse response) { return null; } - byte[] content; - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){ - httpEntity.writeTo(byteArrayOutputStream); - content = byteArrayOutputStream.toByteArray(); - } catch (Throwable e) { - LogManager.warn("readContent", e); + byte[] responseBody; + try { + responseBody = IOUtils.copyToByteArray(httpEntity.getContent()); + } catch (Exception e) { + LogManager.warn("copyToByteArray", "getResponseBody error, uri: " + getUri(), e); return null; } if (httpEntity instanceof BasicHttpEntity) { - ((BasicHttpEntity) httpEntity).setContent(new ByteArrayInputStream(content)); + ((BasicHttpEntity) httpEntity).setContent(new ByteArrayInputStream(responseBody)); response.setEntity(httpEntity); } else if (httpEntity instanceof HttpEntityWrapper) { // Output response normally now, later need to check revert DecompressingEntity BasicHttpEntity entity = ApacheHttpClientHelper.createHttpEntity(response); - entity.setContent(new ByteArrayInputStream(content)); + entity.setContent(new ByteArrayInputStream(responseBody)); response.setEntity(entity); } @@ -110,9 +107,9 @@ public HttpResponseWrapper wrap(HttpResponse response) { headers.add(new HttpResponseWrapper.StringTuple(header.getName(), header.getValue())); } - return new HttpResponseWrapper(response.getStatusLine().toString(), content, - new HttpResponseWrapper.StringTuple(locale.getLanguage(), locale.getCountry()), - headers); + return new HttpResponseWrapper(response.getStatusLine().toString(), responseBody, + new HttpResponseWrapper.StringTuple(locale.getLanguage(), locale.getCountry()), + headers); } @Override @@ -149,4 +146,26 @@ private static boolean ignoreUserAgent(String userAgent) { return userAgent != null && userAgent.contains("arex"); } -} \ No newline at end of file + private void wrapHttpEntity(HttpRequest httpRequest) { + HttpEntityEnclosingRequest enclosingRequest = enclosingRequest(httpRequest); + if (enclosingRequest == null) { + return; + } + HttpEntity entity = enclosingRequest.getEntity(); + if (entity == null || entity.isRepeatable()) { + return; + } + try { + enclosingRequest.setEntity(new CachedHttpEntityWrapper(entity)); + } catch (Exception ignore) { + // ignore exception + } + } + + private HttpEntityEnclosingRequest enclosingRequest(HttpRequest httpRequest) { + if (httpRequest instanceof HttpEntityEnclosingRequest) { + return (HttpEntityEnclosingRequest) httpRequest; + } + return null; + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java new file mode 100644 index 000000000..00be09451 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java @@ -0,0 +1,54 @@ +package io.arex.inst.httpclient.apache.common; + +import io.arex.agent.bootstrap.util.IOUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.http.HttpEntity; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.util.Args; + +public class CachedHttpEntityWrapper extends AbstractHttpEntity { + + private final byte[] cachedBody; + private final InputStream content; + private final HttpEntity entity; + + public CachedHttpEntityWrapper(HttpEntity entity) throws IOException { + this.entity = entity; + this.cachedBody = IOUtils.copyToByteArray(entity.getContent()); + this.content = new ByteArrayInputStream(cachedBody); + + } + + @Override + public boolean isRepeatable() { + return this.entity.isRepeatable(); + } + + @Override + public long getContentLength() { + return this.entity.getContentLength(); + } + + @Override + public InputStream getContent() throws UnsupportedOperationException { + return new ByteArrayInputStream(this.cachedBody); + } + + @Override + public void writeTo(OutputStream outStream) throws IOException { + Args.notNull(outStream, "Output stream"); + IOUtils.copy(this.content, outStream); + } + + @Override + public boolean isStreaming() { + return this.entity.isStreaming(); + } + + public byte[] getCachedBody() { + return this.cachedBody; + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java index b40f10ea0..e3af9de8c 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java @@ -14,18 +14,14 @@ import io.arex.inst.httpclient.common.HttpClientExtractor; import io.arex.inst.runtime.context.ContextManager; -import java.io.IOException; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Stream; -import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.concurrent.FutureCallback; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -95,32 +91,25 @@ void testReplayWithMockResult() { } @ParameterizedTest - @MethodSource("getCase") - void get(FutureCallback delegate, boolean skip, Predicate predicate, HttpAsyncRequestProducer requestProducer) { + @MethodSource("wrapTestCase") + void wrap(FutureCallback delegate, boolean skip, Predicate> predicate, HttpRequest httpRequest) { try (MockedConstruction mocked = Mockito.mockConstruction(ApacheHttpClientAdapter.class, (mock, context) -> { Mockito.when(mock.skipRemoteStorageRequest()).thenReturn(skip); })) { - FutureCallbackWrapper result = target.get(requestProducer, delegate); + FutureCallback result = target.wrap(httpRequest, delegate); assertTrue(predicate.test(result)); - } catch (Exception e) { - e.printStackTrace(); + } catch (Exception ignore) { + // ignore exception } } - static Stream getCase() throws Exception { - FutureCallback delegate1 = Mockito.mock(FutureCallbackWrapper.class); - HttpAsyncRequestProducer requestProducer1 = Mockito.mock(HttpAsyncRequestProducer.class); - Mockito.when(requestProducer1.generateRequest()).thenThrow(new IOException()); - - HttpAsyncRequestProducer requestProducer2 = Mockito.mock(HttpAsyncRequestProducer.class); - - Predicate predicate1 = Objects::nonNull; - Predicate predicate2 = Objects::isNull; + static Stream wrapTestCase() throws Exception { + Predicate> nonNull = Objects::nonNull; + Predicate> isNull = Objects::isNull; return Stream.of( - arguments(delegate1, true, predicate1, requestProducer2), - arguments(delegate, true, predicate2, requestProducer2), - arguments(delegate, false, predicate1, requestProducer2), - arguments(delegate, false, predicate2, requestProducer1) + arguments(new FutureCallbackWrapper<>(null), true, nonNull, null), + arguments(delegate, true, isNull, null), + arguments(delegate, false, nonNull, null) ); } @@ -129,4 +118,4 @@ void wrap() { FutureCallback delegateCallback = Mockito.mock(FutureCallback.class); assertInstanceOf(FutureCallbackWrapper.class, FutureCallbackWrapper.wrap(delegateCallback)); } -} \ No newline at end of file +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java index 43683c5f8..a1ccef0c7 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java @@ -1,7 +1,6 @@ package io.arex.inst.httpclient.apache.async; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,11 +11,9 @@ import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.context.RepeatedCollectManager; import io.arex.inst.runtime.util.IgnoreUtils; -import java.io.IOException; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.http.client.methods.HttpUriRequest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -51,25 +48,25 @@ void methodAdvices() { } @Test - void onEnter() throws HttpException, IOException { - HttpAsyncRequestProducer producer1 = Mockito.mock(HttpAsyncRequestProducer.class); - Mockito.when(producer1.generateRequest()).thenThrow(new RuntimeException("mock exception")); - boolean actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer1, null, null); - assertFalse(actualResult); - - HttpAsyncRequestProducer producer2 = Mockito.mock(HttpAsyncRequestProducer.class); - Mockito.when(producer2.generateRequest()).thenReturn(Mockito.mock(HttpRequest.class)); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer2, null, null); + void onEnter() throws URISyntaxException { + HttpUriRequest request1 = Mockito.mock(HttpUriRequest.class); + Mockito.when(request1.getURI()).thenThrow(new RuntimeException("mock exception")); + boolean actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request1, null, null); assertFalse(actualResult); try (MockedStatic contextManager = mockStatic(ContextManager.class); MockedStatic repeatedCollectManager = mockStatic(RepeatedCollectManager.class); MockedStatic futureCallbackWrapper = mockStatic(FutureCallbackWrapper.class); MockedStatic ignoreUtils = mockStatic(IgnoreUtils.class)) { - Mockito.when(producer2.generateRequest()).thenReturn(new HttpPost("localhost")); + ignoreUtils.when(() -> IgnoreUtils.excludeOperation(any())).thenReturn(true); + HttpUriRequest request2 = Mockito.mock(HttpUriRequest.class); + Mockito.when(request2.getURI()).thenReturn(new URI("http://localhost")); + actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); + assertFalse(actualResult); + ignoreUtils.when(() -> IgnoreUtils.excludeOperation(any())).thenReturn(false); contextManager.when(ContextManager::needRecordOrReplay).thenReturn(false); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer2, null, null); + actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); assertFalse(actualResult); repeatedCollectManager.when(RepeatedCollectManager::validate).thenReturn(true); @@ -77,10 +74,10 @@ void onEnter() throws HttpException, IOException { contextManager.when(ContextManager::needReplay).thenReturn(true); FutureCallbackWrapper wrapper = Mockito.mock(FutureCallbackWrapper.class); - Mockito.when(FutureCallbackWrapper.get(any(), any())).thenReturn(wrapper); + Mockito.when(FutureCallbackWrapper.wrap(any(), any())).thenReturn(wrapper); Mockito.when(wrapper.replay()).thenReturn(MockResult.success("mock")); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer2, null, null); + actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); assertTrue(actualResult); } } diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java new file mode 100644 index 000000000..755776676 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java @@ -0,0 +1,64 @@ +package io.arex.inst.httpclient.apache.common; + +import static org.junit.jupiter.api.Assertions.*; + +import io.arex.agent.bootstrap.util.IOUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.http.HttpEntity; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.InputStreamEntity; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CachedHttpEntityWrapperTest { + static CachedHttpEntityWrapper wrapper; + + @BeforeAll + static void setUp() throws IOException { + HttpEntity httpEntity = new InputStreamEntity(new ByteArrayInputStream("mock".getBytes())); + wrapper = new CachedHttpEntityWrapper(httpEntity); + } + + @AfterAll + static void tearDown() { + wrapper = null; + } + + @Test + void isRepeatable() { + assertFalse(wrapper.isRepeatable()); + } + + @Test + void getContentLength() { + assertEquals(-1, wrapper.getContentLength()); + } + + @Test + void getContent() throws IOException { + byte[] content = IOUtils.copyToByteArray(wrapper.getContent()); + assertEquals("mock", new String(content)); + } + + @Test + void writeTo() { + ByteArrayOutputStream baous = new ByteArrayOutputStream(); + assertDoesNotThrow(() -> wrapper.writeTo(baous)); + assertEquals("mock", baous.toString()); + } + + @Test + void isStreaming() { + assertTrue(wrapper.isStreaming()); + } + + @Test + void getCachedBody() { + assertEquals("mock", new String(wrapper.getCachedBody())); + } +} diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java index bdcc58070..e288db42d 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java @@ -108,8 +108,8 @@ private void doExecute() { requestAttributes.put("RequestPath", requestPath); Map requestHeaders = getRequestHeaders(); requestAttributes.put("Headers", requestHeaders); - requestAttributes.put(ArexConstants.CONFIG_VERSION, - adapter.getAttribute(httpServletRequest, ArexConstants.CONFIG_VERSION)); + requestAttributes.computeIfAbsent(ArexConstants.CONFIG_VERSION, + key -> adapter.getAttribute(httpServletRequest, ArexConstants.CONFIG_VERSION)); String originalMocker = requestHeaders.get(ArexConstants.REPLAY_ORIGINAL_MOCKER); MockCategoryType mockCategoryType = diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java index 6ee07ffbf..fe7f40d64 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java @@ -1,5 +1,6 @@ package io.arex.inst.httpservlet.adapter.impl; +import io.arex.agent.bootstrap.util.IOUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.httpservlet.adapter.ServletAdapter; import io.arex.inst.httpservlet.wrapper.CachedBodyRequestWrapperV3; @@ -165,7 +166,20 @@ public Collection getResponseHeaderNames(HttpServletResponse httpServlet @Override public byte[] getRequestBytes(HttpServletRequest httpServletRequest) { - return ((CachedBodyRequestWrapperV3) httpServletRequest).getContentAsByteArray(); + CachedBodyRequestWrapperV3 requestWrapper = (CachedBodyRequestWrapperV3) httpServletRequest; + byte[] content = requestWrapper.getContentAsByteArray(); + if (content.length > 0) { + return content; + } + // read request body to cache + if (httpServletRequest.getContentLength() > 0) { + try { + return IOUtils.copyToByteArray(requestWrapper.getInputStream()); + } catch (Exception ignore) { + // ignore exception + } + } + return content; } @Override diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java index 64147b8ba..cac4f4ee4 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java @@ -1,5 +1,6 @@ package io.arex.inst.httpservlet.adapter.impl; +import io.arex.agent.bootstrap.util.IOUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.httpservlet.adapter.ServletAdapter; import io.arex.inst.httpservlet.listener.ServletAsyncListenerV5; @@ -165,7 +166,20 @@ public Collection getResponseHeaderNames(HttpServletResponse httpServlet @Override public byte[] getRequestBytes(HttpServletRequest httpServletRequest) { - return ((CachedBodyRequestWrapperV5) httpServletRequest).getContentAsByteArray(); + CachedBodyRequestWrapperV5 requestWrapper = (CachedBodyRequestWrapperV5) httpServletRequest; + byte[] content = requestWrapper.getContentAsByteArray(); + if (content.length > 0) { + return content; + } + // read request body to cache + if (httpServletRequest.getContentLength() > 0) { + try { + return IOUtils.copyToByteArray(requestWrapper.getInputStream()); + } catch (Exception ignore) { + // ignore exception + } + } + return content; } @Override diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java index 237cfa9ed..280fd35ac 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java @@ -7,10 +7,14 @@ import io.arex.inst.httpservlet.wrapper.CachedBodyRequestWrapperV3; import io.arex.inst.httpservlet.wrapper.CachedBodyResponseWrapperV3; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.Enumeration; import javax.servlet.AsyncContext; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; @@ -189,9 +193,48 @@ void getResponseHeaderNames() { assertEquals("mock-header-name", instance.getResponseHeaderNames(mockResponse).stream().findFirst().get()); } + static class MockServletInputStream extends ServletInputStream { + private final InputStream delegate; + public MockServletInputStream(byte[] body) { + this.delegate = new ByteArrayInputStream(body); + } + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return this.delegate.read(); + } + } + @Test - void getRequestBytes() { - assertEquals(0, instance.getRequestBytes(instance.wrapRequest(mockRequest)).length); + void getRequestBytes() throws IOException { + HttpServletRequest requestWrapper = instance.wrapRequest(mockRequest); + // content empty + assertArrayEquals(new byte[0], instance.getRequestBytes(requestWrapper)); + + + byte[] body = "mock".getBytes(); + when(mockRequest.getInputStream()).thenReturn(new MockServletInputStream(body)); + when(mockRequest.getContentLength()).thenReturn(body.length); + + // read request body to cache + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); + + // getContentAsByteArray.length > 0 + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); } @Test diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java index 874c42e1e..7ff256027 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java @@ -7,6 +7,11 @@ import io.arex.inst.httpservlet.wrapper.CachedBodyRequestWrapperV5; import io.arex.inst.httpservlet.wrapper.CachedBodyResponseWrapperV5; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.Enumeration; import jakarta.servlet.AsyncContext; @@ -190,9 +195,48 @@ void getResponseHeaderNames() { assertEquals("mock-header-name", instance.getResponseHeaderNames(mockResponse).stream().findFirst().get()); } + static class MockServletInputStream extends ServletInputStream { + private final InputStream delegate; + public MockServletInputStream(byte[] body) { + this.delegate = new ByteArrayInputStream(body); + } + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return this.delegate.read(); + } + } + @Test - void getRequestBytes() { - assertEquals(0, instance.getRequestBytes(instance.wrapRequest(mockRequest)).length); + void getRequestBytes() throws IOException { + HttpServletRequest requestWrapper = instance.wrapRequest(mockRequest); + // content empty + assertArrayEquals(new byte[0], instance.getRequestBytes(requestWrapper)); + + + byte[] body = "mock".getBytes(); + when(mockRequest.getInputStream()).thenReturn(new MockServletInputStream(body)); + when(mockRequest.getContentLength()).thenReturn(body.length); + + // read request body to cache + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); + + // getContentAsByteArray.length > 0 + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); } @Test diff --git a/docs/design/AREX-Agent-Stratup-Flowchart.png b/docs/design/AREX-Agent-Stratup-Flowchart.png new file mode 100644 index 000000000..19ef0099f Binary files /dev/null and b/docs/design/AREX-Agent-Stratup-Flowchart.png differ