diff --git a/server/src/main/java/com/cloud/server/ManagementServerHostStatsEntry.java b/server/src/main/java/com/cloud/server/ManagementServerHostStatsEntry.java index 172ab1e83eb..08cc54f9799 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerHostStatsEntry.java +++ b/server/src/main/java/com/cloud/server/ManagementServerHostStatsEntry.java @@ -105,7 +105,7 @@ public class ManagementServerHostStatsEntry implements ManagementServerHostStats } @Override - public Date getCollectionTime(){ + public Date getCollectionTime() { return collectionTime; } diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index 4e1e78f1610..7e83d452bb9 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -46,6 +46,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.utils.DateUtil; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; @@ -170,10 +171,10 @@ import com.codahale.metrics.jvm.GarbageCollectorMetricSet; import com.codahale.metrics.jvm.MemoryUsageGaugeSet; import com.codahale.metrics.jvm.ThreadStatesGaugeSet; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; import com.sun.management.OperatingSystemMXBean; /** @@ -294,6 +295,9 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc private static StatsCollector s_instance = null; private static Gson gson = new Gson(); + private static Gson msStatsGson = new GsonBuilder() + .setDateFormat(DateUtil.ZONED_DATETIME_FORMAT) + .create(); private ScheduledExecutorService _executor = null; @Inject @@ -739,7 +743,6 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc dbStats.put(uptime, (Long.valueOf(stats.get(uptime)))); } - @Override protected Point createInfluxDbPoint(Object metricsObject) { return null; @@ -759,7 +762,7 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc hostStatsEntry = getDataFrom(mshost); managementServerHostStats.put(mshost.getUuid(), hostStatsEntry); // send to other hosts - clusterManager.publishStatus(gson.toJson(hostStatsEntry)); + clusterManager.publishStatus(msStatsGson.toJson(hostStatsEntry)); } catch (Throwable t) { // pokemon catch to make sure the thread stays running logger.error("Error trying to retrieve management server host statistics", t); @@ -1158,9 +1161,9 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc logger.debug(String.format("StatusUpdate from %s, json: %s", pdu.getSourcePeer(), pdu.getJsonPackage())); } - ManagementServerHostStatsEntry hostStatsEntry = null; + ManagementServerHostStatsEntry hostStatsEntry; try { - hostStatsEntry = gson.fromJson(pdu.getJsonPackage(),new TypeToken(){}.getType()); + hostStatsEntry = msStatsGson.fromJson(pdu.getJsonPackage(), ManagementServerHostStatsEntry.class); managementServerHostStats.put(hostStatsEntry.getManagementServerHostUuid(), hostStatsEntry); // Update peer state to Up in mshost_peer diff --git a/server/src/test/java/com/cloud/server/StatsCollectorTest.java b/server/src/test/java/com/cloud/server/StatsCollectorTest.java index 6a979259cd9..3578e6948a4 100644 --- a/server/src/test/java/com/cloud/server/StatsCollectorTest.java +++ b/server/src/test/java/com/cloud/server/StatsCollectorTest.java @@ -20,8 +20,10 @@ package com.cloud.server; import static org.mockito.Mockito.when; +import java.lang.reflect.Field; import java.net.URI; import java.net.URISyntaxException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -33,6 +35,8 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import com.cloud.utils.DateUtil; +import com.google.gson.JsonSyntaxException; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.commons.collections.CollectionUtils; @@ -116,6 +120,8 @@ public class StatsCollectorTest { private static Gson gson = new Gson(); + private Gson msStatsGson; + private MockedStatic influxDBFactoryMocked; private AutoCloseable closeable; @@ -125,6 +131,9 @@ public class StatsCollectorTest { closeable = MockitoAnnotations.openMocks(this); statsCollector.vmStatsDao = vmStatsDaoMock; statsCollector.volumeStatsDao = volumeStatsDao; + Field msStatsGsonField = StatsCollector.class.getDeclaredField("msStatsGson"); + msStatsGsonField.setAccessible(true); + msStatsGson = (Gson) msStatsGsonField.get(null); } @After @@ -612,4 +621,107 @@ public class StatsCollectorTest { Mockito.verify(mockPool, Mockito.never()).setCapacityIops(Mockito.anyLong()); Mockito.verify(mockPool, Mockito.never()).setUsedIops(Mockito.anyLong()); } + + @Test + public void testGsonDateFormatSerialization() { + Date now = new Date(); + TestClass testObj = new TestClass("TestString", 999, now); + String json = msStatsGson.toJson(testObj); + + Assert.assertTrue(json.contains("TestString")); + Assert.assertTrue(json.contains("999")); + String expectedDate = new SimpleDateFormat(DateUtil.ZONED_DATETIME_FORMAT).format(now); + Assert.assertTrue(json.contains(expectedDate)); + } + + @Test + public void testGsonDateFormatDeserializationWithSameDateFormat() throws Exception { + String json = "{\"str\":\"TestString\",\"num\":999,\"date\":\"2025-08-22T15:39:43+0000\"}"; + TestClass testObj = msStatsGson.fromJson(json, TestClass.class); + + Assert.assertEquals("TestString", testObj.getStr()); + Assert.assertEquals(999, testObj.getNum()); + Date expectedDate = new SimpleDateFormat(DateUtil.ZONED_DATETIME_FORMAT).parse("2025-08-22T15:39:43+0000"); + Assert.assertEquals(expectedDate, testObj.getDate()); + } + + @Test (expected = JsonSyntaxException.class) + public void testGsonDateFormatDeserializationWithDifferentDateFormat() throws Exception { + String json = "{\"str\":\"TestString\",\"num\":999,\"date\":\"22/08/2025T15:39:43+0000\"}"; + msStatsGson.fromJson(json, TestClass.class); + /* Deserialization throws the below exception: + com.google.gson.JsonSyntaxException: 22/08/2025T15:39:43+0000 + at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserializeToDate(DefaultTypeAdapters.java:376) + at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserialize(DefaultTypeAdapters.java:351) + at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserialize(DefaultTypeAdapters.java:307) + at com.google.gson.JsonDeserializationVisitor.invokeCustomDeserializer(JsonDeserializationVisitor.java:92) + at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler(JsonObjectDeserializationVisitor.java:117) + at com.google.gson.ReflectingFieldNavigator.visitFieldsReflectively(ReflectingFieldNavigator.java:63) + at com.google.gson.ObjectNavigator.accept(ObjectNavigator.java:120) + at com.google.gson.JsonDeserializationContextDefault.fromJsonObject(JsonDeserializationContextDefault.java:76) + at com.google.gson.JsonDeserializationContextDefault.deserialize(JsonDeserializationContextDefault.java:54) + at com.google.gson.Gson.fromJson(Gson.java:551) + at com.google.gson.Gson.fromJson(Gson.java:498) + at com.google.gson.Gson.fromJson(Gson.java:467) + at com.google.gson.Gson.fromJson(Gson.java:417) + at com.google.gson.Gson.fromJson(Gson.java:389) + at com.cloud.serializer.GsonHelperTest.testGsonDateFormatDeserializationWithDifferentDateFormat(GsonHelperTest.java:113) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:566) + at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) + at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) + at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) + at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) + at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) + at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) + at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) + at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) + at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) + at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) + at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) + at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) + at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) + at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) + at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) + at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) + at org.junit.runners.ParentRunner.run(ParentRunner.java:413) + at org.junit.runner.JUnitCore.run(JUnitCore.java:137) + at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69) + at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) + at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) + at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) + at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:231) + at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55) + Caused by: java.text.ParseException: Unparseable date: "22/08/2025T15:39:43+0000" + at java.base/java.text.DateFormat.parse(DateFormat.java:395) + at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserializeToDate(DefaultTypeAdapters.java:374) + ... 42 more + */ + } + + private static class TestClass { + private String str; + private int num; + private Date date; + + public TestClass(String str, int num, Date date) { + this.str = str; + this.num = num; + this.date = date; + } + + public String getStr() { + return str; + } + + public int getNum() { + return num; + } + + public Date getDate() { + return date; + } + } } diff --git a/utils/src/main/java/com/cloud/utils/DateUtil.java b/utils/src/main/java/com/cloud/utils/DateUtil.java index fdf2ba8fe01..00ae5565dad 100644 --- a/utils/src/main/java/com/cloud/utils/DateUtil.java +++ b/utils/src/main/java/com/cloud/utils/DateUtil.java @@ -48,7 +48,7 @@ public class DateUtil { public static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT"); public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss"; - private static final String ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + public static final String ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; private static final DateFormat ZONED_DATETIME_SIMPLE_FORMATTER = new SimpleDateFormat(ZONED_DATETIME_FORMAT); private static final DateTimeFormatter[] parseFormats = new DateTimeFormatter[]{