From 389a9efc545e8393cd3e7e1a90dfbe61fa285448 Mon Sep 17 00:00:00 2001 From: Ramgopal Nagaboina Date: Fri, 11 Sep 2026 17:02:58 -0400 Subject: [PATCH] metrics: add host usage history Store host stats samples in a new host_stats table and expose them through a new listHostsUsageHistory API, the host counterpart of listVirtualMachinesUsageHistory, listSystemVmsUsageHistory and listVolumesUsageHistory. Samples are written on every host stats collection and pruned by the new host.stats.max.retention.time setting (minutes, default 720). Setting it to 0 or less disables storing and pruning. --- .../cloud/agent/api/HostStatsEntryBase.java | 134 ++++++++++++++++ .../main/java/com/cloud/host/HostStatsVO.java | 88 +++++++++++ .../java/com/cloud/host/dao/HostStatsDao.java | 42 +++++ .../com/cloud/host/dao/HostStatsDaoImpl.java | 115 ++++++++++++++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-42300to2400.sql | 12 ++ .../api/ListHostsUsageHistoryCmd.java | 71 +++++++++ .../cloudstack/metrics/MetricsService.java | 3 + .../metrics/MetricsServiceImpl.java | 147 ++++++++++++++++++ .../response/HostMetricsStatsResponse.java | 53 +++++++ .../metrics/MetricsServiceImplTest.java | 110 +++++++++++++ .../java/com/cloud/server/StatsCollector.java | 61 ++++++++ .../com/cloud/server/StatsCollectorTest.java | 64 ++++++++ test/integration/smoke/test_metrics_api.py | 23 +++ 14 files changed, 924 insertions(+) create mode 100644 core/src/main/java/com/cloud/agent/api/HostStatsEntryBase.java create mode 100644 engine/schema/src/main/java/com/cloud/host/HostStatsVO.java create mode 100644 engine/schema/src/main/java/com/cloud/host/dao/HostStatsDao.java create mode 100644 engine/schema/src/main/java/com/cloud/host/dao/HostStatsDaoImpl.java create mode 100644 plugins/metrics/src/main/java/org/apache/cloudstack/api/ListHostsUsageHistoryCmd.java create mode 100644 plugins/metrics/src/main/java/org/apache/cloudstack/response/HostMetricsStatsResponse.java diff --git a/core/src/main/java/com/cloud/agent/api/HostStatsEntryBase.java b/core/src/main/java/com/cloud/agent/api/HostStatsEntryBase.java new file mode 100644 index 000000000000..2ce3f326401b --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/HostStatsEntryBase.java @@ -0,0 +1,134 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api; + +import com.cloud.host.HostStats; + +/** + * Serializable host-stats payload persisted as JSON in {@code host_stats.host_stats_data}. Unlike + * {@link HostStatsEntry} it carries no {@code HostVO}, so serialization does not pull in a host entity. + */ +public class HostStatsEntryBase implements HostStats { + + private long hostId; + private String entityType; + private double cpuUtilization; + private double averageLoad; + private double networkReadKBs; + private double networkWriteKBs; + private double totalMemoryKBs; + private double freeMemoryKBs; + + public HostStatsEntryBase() { + } + + public HostStatsEntryBase(long hostId, String entityType, double cpuUtilization, double averageLoad, + double networkReadKBs, double networkWriteKBs, double totalMemoryKBs, double freeMemoryKBs) { + this.hostId = hostId; + this.entityType = entityType; + this.cpuUtilization = cpuUtilization; + this.averageLoad = averageLoad; + this.networkReadKBs = networkReadKBs; + this.networkWriteKBs = networkWriteKBs; + this.totalMemoryKBs = totalMemoryKBs; + this.freeMemoryKBs = freeMemoryKBs; + } + + public long getHostId() { + return hostId; + } + + public void setHostId(long hostId) { + this.hostId = hostId; + } + + @Override + public String getEntityType() { + return entityType; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + @Override + public double getCpuUtilization() { + return cpuUtilization; + } + + public void setCpuUtilization(double cpuUtilization) { + this.cpuUtilization = cpuUtilization; + } + + @Override + public double getLoadAverage() { + return averageLoad; + } + + public void setAverageLoad(double averageLoad) { + this.averageLoad = averageLoad; + } + + @Override + public double getNetworkReadKBs() { + return networkReadKBs; + } + + public void setNetworkReadKBs(double networkReadKBs) { + this.networkReadKBs = networkReadKBs; + } + + @Override + public double getNetworkWriteKBs() { + return networkWriteKBs; + } + + public void setNetworkWriteKBs(double networkWriteKBs) { + this.networkWriteKBs = networkWriteKBs; + } + + @Override + public double getTotalMemoryKBs() { + return totalMemoryKBs; + } + + public void setTotalMemoryKBs(double totalMemoryKBs) { + this.totalMemoryKBs = totalMemoryKBs; + } + + @Override + public double getFreeMemoryKBs() { + return freeMemoryKBs; + } + + public void setFreeMemoryKBs(double freeMemoryKBs) { + this.freeMemoryKBs = freeMemoryKBs; + } + + @Override + public double getUsedMemory() { + return (totalMemoryKBs - freeMemoryKBs) * 1024; + } + + @Override + public HostStats getHostStats() { + return this; + } +} diff --git a/engine/schema/src/main/java/com/cloud/host/HostStatsVO.java b/engine/schema/src/main/java/com/cloud/host/HostStatsVO.java new file mode 100644 index 000000000000..30869fa5cbef --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/host/HostStatsVO.java @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.host; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +/** One persisted historical host-stats sample. */ +@Entity +@Table(name = "host_stats") +public class HostStatsVO { + + @Id + @Column(name = "id", updatable = false, nullable = false) + protected long id; + + @Column(name = "host_id", updatable = false, nullable = false) + protected Long hostId; + + @Column(name = "mgmt_server_id", updatable = false, nullable = false) + protected Long mgmtServerId; + + @Column(name = "timestamp", updatable = false) + @Temporal(value = TemporalType.TIMESTAMP) + protected Date timestamp; + + @Column(name = "host_stats_data", updatable = false, nullable = false, length = 65535) + protected String hostStatsData; + + public HostStatsVO(Long hostId, Long mgmtServerId, Date timestamp, String hostStatsData) { + this.hostId = hostId; + this.mgmtServerId = mgmtServerId; + this.timestamp = timestamp; + this.hostStatsData = hostStatsData; + } + + public HostStatsVO() { + + } + + public long getId() { + return id; + } + + public Long getHostId() { + return hostId; + } + + public Long getMgmtServerId() { + return mgmtServerId; + } + + public Date getTimestamp() { + return timestamp; + } + + public String getHostStatsData() { + return hostStatsData; + } + + @Override + public String toString() { + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "hostId", "mgmtServerId", "timestamp", "hostStatsData"); + } + +} diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostStatsDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostStatsDao.java new file mode 100644 index 000000000000..2a4bdd982f60 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostStatsDao.java @@ -0,0 +1,42 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.host.dao; + +import java.util.Date; +import java.util.List; + +import com.cloud.host.HostStatsVO; +import com.cloud.utils.db.GenericDao; + +/** DAO for the host_stats table. */ +public interface HostStatsDao extends GenericDao { + + List findByHostId(long hostId); + + List findByHostIdAndTimestampGreaterThanEqual(long hostId, Date time); + + List findByHostIdAndTimestampLessThanEqual(long hostId, Date time); + + List findByHostIdAndTimestampBetween(long hostId, Date startTime, Date endTime); + + /** + * Expunges all host stats older than {@code limitDate}. + * @param limitPerQuery max rows removed per query; 0 or negative means no limit. + */ + void removeAllByTimestampLessThan(Date limitDate, long limitPerQuery); + +} diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostStatsDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostStatsDaoImpl.java new file mode 100644 index 000000000000..ac2211e24771 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostStatsDaoImpl.java @@ -0,0 +1,115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.host.dao; + +import java.util.Date; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.host.HostStatsVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SearchCriteria.Op; + +/** DAO for the host_stats table. */ +@Component +public class HostStatsDaoImpl extends GenericDaoBase implements HostStatsDao { + + protected Logger logger = LogManager.getLogger(getClass()); + + protected SearchBuilder hostIdSearch; + protected SearchBuilder hostIdTimestampGreaterThanEqualSearch; + protected SearchBuilder hostIdTimestampLessThanEqualSearch; + protected SearchBuilder hostIdTimestampBetweenSearch; + protected SearchBuilder timestampSearch; + + @PostConstruct + protected void init() { + hostIdSearch = createSearchBuilder(); + hostIdSearch.and("hostId", hostIdSearch.entity().getHostId(), Op.EQ); + hostIdSearch.done(); + + hostIdTimestampGreaterThanEqualSearch = createSearchBuilder(); + hostIdTimestampGreaterThanEqualSearch.and("hostId", hostIdTimestampGreaterThanEqualSearch.entity().getHostId(), Op.EQ); + hostIdTimestampGreaterThanEqualSearch.and("timestamp", hostIdTimestampGreaterThanEqualSearch.entity().getTimestamp(), Op.GTEQ); + hostIdTimestampGreaterThanEqualSearch.done(); + + hostIdTimestampLessThanEqualSearch = createSearchBuilder(); + hostIdTimestampLessThanEqualSearch.and("hostId", hostIdTimestampLessThanEqualSearch.entity().getHostId(), Op.EQ); + hostIdTimestampLessThanEqualSearch.and("timestamp", hostIdTimestampLessThanEqualSearch.entity().getTimestamp(), Op.LTEQ); + hostIdTimestampLessThanEqualSearch.done(); + + hostIdTimestampBetweenSearch = createSearchBuilder(); + hostIdTimestampBetweenSearch.and("hostId", hostIdTimestampBetweenSearch.entity().getHostId(), Op.EQ); + hostIdTimestampBetweenSearch.and("timestamp", hostIdTimestampBetweenSearch.entity().getTimestamp(), Op.BETWEEN); + hostIdTimestampBetweenSearch.done(); + + timestampSearch = createSearchBuilder(); + timestampSearch.and("timestamp", timestampSearch.entity().getTimestamp(), Op.LT); + timestampSearch.done(); + } + + @Override + public List findByHostId(long hostId) { + SearchCriteria sc = hostIdSearch.create(); + sc.setParameters("hostId", hostId); + return listBy(sc); + } + + @Override + public List findByHostIdAndTimestampGreaterThanEqual(long hostId, Date time) { + SearchCriteria sc = hostIdTimestampGreaterThanEqualSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("timestamp", time); + return listBy(sc); + } + + @Override + public List findByHostIdAndTimestampLessThanEqual(long hostId, Date time) { + SearchCriteria sc = hostIdTimestampLessThanEqualSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("timestamp", time); + return listBy(sc); + } + + @Override + public List findByHostIdAndTimestampBetween(long hostId, Date startTime, Date endTime) { + SearchCriteria sc = hostIdTimestampBetweenSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("timestamp", startTime, endTime); + return listBy(sc); + } + + @Override + public void removeAllByTimestampLessThan(Date limitDate, long limitPerQuery) { + SearchCriteria sc = timestampSearch.create(); + sc.setParameters("timestamp", limitDate); + + logger.debug(String.format("Starting to remove all host_stats rows older than [%s].", limitDate)); + + long totalRemoved = batchExpunge(sc, limitPerQuery); + + logger.info(String.format("Removed a total of [%s] host_stats rows older than [%s].", totalRemoved, limitDate)); + } + +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 932db538f30b..adc7b4b49b9d 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -227,6 +227,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 7c11013a17d2..773257f1e8af 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -18,3 +18,15 @@ --; -- Schema upgrade from 4.23.0.0 to 24.0.0 --; + +-- Add host_stats table for the host usage history +CREATE TABLE IF NOT EXISTS `cloud`.`host_stats` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `host_id` bigint unsigned NOT NULL, + `mgmt_server_id` bigint unsigned NOT NULL, + `timestamp` datetime NOT NULL, + `host_stats_data` text NOT NULL, + PRIMARY KEY (`id`), + KEY `i_host_stats__host_id` (`host_id`), + KEY `i_host_stats__timestamp` (`timestamp`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='historical per-host stats samples'; diff --git a/plugins/metrics/src/main/java/org/apache/cloudstack/api/ListHostsUsageHistoryCmd.java b/plugins/metrics/src/main/java/org/apache/cloudstack/api/ListHostsUsageHistoryCmd.java new file mode 100644 index 000000000000..b565ff04146a --- /dev/null +++ b/plugins/metrics/src/main/java/org/apache/cloudstack/api/ListHostsUsageHistoryCmd.java @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api; + +import java.util.List; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.response.HostMetricsStatsResponse; + +@APICommand(name = "listHostsUsageHistory", description = "Lists host stats", responseObject = HostMetricsStatsResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "24.0.0", + authorized = {RoleType.Admin}) +public class ListHostsUsageHistoryCmd extends BaseResourceUsageHistoryCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HostResponse.class, description = "The ID of the host.") + private Long id; + + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = HostResponse.class, description = "The IDs of the hosts, mutually exclusive with id.") + private List ids; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the host (a substring match is made against the parameter value returning the data for all matching hosts).") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public List getIds() { + return ids; + } + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + ListResponse response = metricsService.searchForHostMetricsStats(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsService.java b/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsService.java index bb7763688385..d81ab2db37f1 100644 --- a/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsService.java +++ b/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsService.java @@ -19,6 +19,7 @@ import java.util.List; +import org.apache.cloudstack.api.ListHostsUsageHistoryCmd; import org.apache.cloudstack.api.ListSystemVMsUsageHistoryCmd; import org.apache.cloudstack.api.ListVMsUsageHistoryCmd; import org.apache.cloudstack.api.ListVolumesUsageHistoryCmd; @@ -34,6 +35,7 @@ import org.apache.cloudstack.response.ClusterMetricsResponse; import org.apache.cloudstack.response.DbMetricsResponse; import org.apache.cloudstack.response.HostMetricsResponse; +import org.apache.cloudstack.response.HostMetricsStatsResponse; import org.apache.cloudstack.response.InfrastructureResponse; import org.apache.cloudstack.response.ManagementServerMetricsResponse; import org.apache.cloudstack.response.StoragePoolMetricsResponse; @@ -58,6 +60,7 @@ public interface MetricsService extends PluggableService { ListResponse searchForVmMetricsStats(ListVMsUsageHistoryCmd cmd); ListResponse searchForSystemVmMetricsStats(ListSystemVMsUsageHistoryCmd cmd); ListResponse searchForVolumeMetricsStats(ListVolumesUsageHistoryCmd cmd); + ListResponse searchForHostMetricsStats(ListHostsUsageHistoryCmd cmd); List listVolumeMetrics(List volumeResponses); List listVmMetrics(List vmResponses); List listStoragePoolMetrics(List poolResponses); diff --git a/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java b/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java index 0321d7c08d9e..194d6851b768 100644 --- a/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java +++ b/plugins/metrics/src/main/java/org/apache/cloudstack/metrics/MetricsServiceImpl.java @@ -37,6 +37,7 @@ import org.apache.cloudstack.api.ListClustersMetricsCmd; import org.apache.cloudstack.api.ListDbMetricsCmd; import org.apache.cloudstack.api.ListHostsMetricsCmd; +import org.apache.cloudstack.api.ListHostsUsageHistoryCmd; import org.apache.cloudstack.api.ListInfrastructureCmd; import org.apache.cloudstack.api.ListMgmtsMetricsCmd; import org.apache.cloudstack.api.ListStoragePoolsMetricsCmd; @@ -67,6 +68,7 @@ import org.apache.cloudstack.response.ClusterMetricsResponse; import org.apache.cloudstack.response.DbMetricsResponse; import org.apache.cloudstack.response.HostMetricsResponse; +import org.apache.cloudstack.response.HostMetricsStatsResponse; import org.apache.cloudstack.response.HostMetricsSummary; import org.apache.cloudstack.response.InfrastructureResponse; import org.apache.cloudstack.response.ManagementServerMetricsResponse; @@ -88,6 +90,7 @@ import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import com.cloud.agent.api.HostStatsEntryBase; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmStatsEntryBase; import com.cloud.alert.AlertManager; @@ -110,8 +113,11 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.host.Host; import com.cloud.host.HostStats; +import com.cloud.host.HostStatsVO; +import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostStatsDao; import com.cloud.network.router.VirtualRouter; import com.cloud.org.Cluster; import com.cloud.projects.Project; @@ -184,6 +190,8 @@ public class MetricsServiceImpl extends MutualExclusiveIdsManagerBase implements private VolumeDao volumeDao; @Inject private VolumeStatsDao volumeStatsDao; + @Inject + protected HostStatsDao hostStatsDao; @Inject private ObjectStoreDao objectStoreDao; @@ -248,6 +256,144 @@ public ListResponse searchForVolumeMetricsStats(List return createVolumeMetricsStatsResponse(volumeList, volumeStatsList); } + /** + * Searches for host stats based on the {@code ListHostsUsageHistoryCmd} parameters. + * + * @param cmd the {@link ListHostsUsageHistoryCmd} specifying what should be searched. + * @return the list of host metrics stats found. + */ + @Override + public ListResponse searchForHostMetricsStats(ListHostsUsageHistoryCmd cmd) { + Pair, Integer> hostList = searchForHostsInternal(cmd); + Map> hostStatsList = searchForHostMetricsStatsInternal(cmd.getStartDate(), cmd.getEndDate(), hostList.first()); + return createHostMetricsStatsResponse(hostList, hostStatsList); + } + + /** + * Searches routing hosts based on {@code ListHostsUsageHistoryCmd} parameters. + * + * @param cmd the {@link ListHostsUsageHistoryCmd} specifying the parameters. + * @return the list of hosts and the total count. + */ + protected Pair, Integer> searchForHostsInternal(ListHostsUsageHistoryCmd cmd) { + Filter searchFilter = new Filter(HostVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + List ids = getIdsListFromCmd(cmd.getId(), cmd.getIds()); + String name = cmd.getName(); + String keyword = cmd.getKeyword(); + + SearchBuilder sb = hostDao.createSearchBuilder(); + sb.and("idIN", sb.entity().getId(), SearchCriteria.Op.IN); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and("type", sb.entity().getType(), SearchCriteria.Op.EQ); + + SearchCriteria sc = sb.create(); + sc.setParameters("type", Host.Type.Routing); + if (CollectionUtils.isNotEmpty(ids)) { + sc.setParameters("idIN", ids.toArray()); + } + if (StringUtils.isNotBlank(name)) { + sc.setParameters("name", "%" + name + "%"); + } + if (StringUtils.isNotBlank(keyword)) { + SearchCriteria ssc = hostDao.createSearchCriteria(); + ssc.addOr("name", SearchCriteria.Op.LIKE, "%" + keyword + "%"); + sc.addAnd("name", SearchCriteria.Op.SC, ssc); + } + + return hostDao.searchAndCount(sc, searchFilter); + } + + /** + * Searches stats for a list of hosts, based on date filtering parameters. + * + * @param startDate the start date for which stats should be searched. + * @param endDate the end date for which stats should be searched. + * @param hostList the list of hosts for which stats should be searched. + * @return the key-value map in which keys are host IDs and values are lists of host stats. + */ + protected Map> searchForHostMetricsStatsInternal(Date startDate, Date endDate, List hostList) { + Map> hostStatsVOList = new HashMap<>(); + validateDateParams(startDate, endDate); + + for (HostVO hostVO : hostList) { + Long hostId = hostVO.getId(); + hostStatsVOList.put(hostId, findHostStatsAccordingToDateParams(hostId, startDate, endDate)); + } + + return hostStatsVOList; + } + + /** + * Finds stats for a specific host based on date parameters. + * + * @param hostId the specific host. + * @param startDate the start date to filtering. + * @param endDate the end date to filtering. + * @return the list of stats for the specified host. + */ + protected List findHostStatsAccordingToDateParams(Long hostId, Date startDate, Date endDate) { + if (startDate != null && endDate != null) { + return hostStatsDao.findByHostIdAndTimestampBetween(hostId, startDate, endDate); + } + if (startDate != null) { + return hostStatsDao.findByHostIdAndTimestampGreaterThanEqual(hostId, startDate); + } + if (endDate != null) { + return hostStatsDao.findByHostIdAndTimestampLessThanEqual(hostId, endDate); + } + return hostStatsDao.findByHostId(hostId); + } + + /** + * Creates a {@code ListResponse}. For each host, this joins essential host info + * with its respective list of stats. + * + * @param hostList the list of hosts and the total count. + * @param hostStatsList the respective list of stats. + * @return the list of responses that was created. + */ + protected ListResponse createHostMetricsStatsResponse(Pair, Integer> hostList, + Map> hostStatsList) { + List responses = new ArrayList<>(); + for (HostVO hostVO : hostList.first()) { + HostMetricsStatsResponse hostMetricsStatsResponse = new HostMetricsStatsResponse(); + hostMetricsStatsResponse.setObjectName("host"); + hostMetricsStatsResponse.setId(hostVO.getUuid()); + hostMetricsStatsResponse.setName(hostVO.getName()); + hostMetricsStatsResponse.setStats(createHostStatsResponse(hostStatsList.get(hostVO.getId()))); + responses.add(hostMetricsStatsResponse); + } + + ListResponse response = new ListResponse<>(); + response.setResponses(responses, hostList.second()); + return response; + } + + /** + * Creates a {@code List} from a given {@code List}. + * + * @param hostStatsList the list of host stats. + * @return the list of responses that was created. + */ + protected List createHostStatsResponse(List hostStatsList) { + List statsResponseList = new ArrayList<>(); + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + for (HostStatsVO hostStats : hostStatsList) { + StatsResponse response = new StatsResponse(); + response.setTimestamp(hostStats.getTimestamp()); + + HostStatsEntryBase statsEntry = gson.fromJson(hostStats.getHostStatsData(), HostStatsEntryBase.class); + response.setCpuUsed(decimalFormat.format(statsEntry.getCpuUtilization()) + "%"); + response.setNetworkKbsRead((long) statsEntry.getNetworkReadKBs()); + response.setNetworkKbsWrite((long) statsEntry.getNetworkWriteKBs()); + response.setMemoryKBs((long) statsEntry.getTotalMemoryKBs()); + response.setMemoryIntFreeKBs((long) statsEntry.getFreeMemoryKBs()); + + statsResponseList.add(response); + } + return statsResponseList; + } + /** * Outputs the parameters that should be used for access control in the query of a resource to * {@code permittedAccounts} and {@code domainIdRecursiveListProject}. @@ -1194,6 +1340,7 @@ public List> getCommands() { cmdList.add(ListVMsUsageHistoryCmd.class); cmdList.add(ListSystemVMsUsageHistoryCmd.class); cmdList.add(ListVolumesUsageHistoryCmd.class); + cmdList.add(ListHostsUsageHistoryCmd.class); // separate Admin commands cmdList.add(ListVMsMetricsCmdByAdmin.class); return cmdList; diff --git a/plugins/metrics/src/main/java/org/apache/cloudstack/response/HostMetricsStatsResponse.java b/plugins/metrics/src/main/java/org/apache/cloudstack/response/HostMetricsStatsResponse.java new file mode 100644 index 000000000000..2b3ba89929f5 --- /dev/null +++ b/plugins/metrics/src/main/java/org/apache/cloudstack/response/HostMetricsStatsResponse.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.response; + +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.response.StatsResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class HostMetricsStatsResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the host") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the host") + private String name; + + @SerializedName("stats") + @Param(description = "The list of host stats", responseObject = StatsResponse.class) + private List stats; + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setStats(List stats) { + this.stats = stats; + } +} diff --git a/plugins/metrics/src/test/java/org/apache/cloudstack/metrics/MetricsServiceImplTest.java b/plugins/metrics/src/test/java/org/apache/cloudstack/metrics/MetricsServiceImplTest.java index 9184d7444105..f3d888814047 100644 --- a/plugins/metrics/src/test/java/org/apache/cloudstack/metrics/MetricsServiceImplTest.java +++ b/plugins/metrics/src/test/java/org/apache/cloudstack/metrics/MetricsServiceImplTest.java @@ -28,7 +28,9 @@ import org.apache.cloudstack.api.ListVMsUsageHistoryCmd; import org.apache.cloudstack.api.ListVolumesUsageHistoryCmd; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.StatsResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.response.HostMetricsStatsResponse; import org.apache.cloudstack.response.VmMetricsStatsResponse; import org.apache.commons.lang3.time.DateUtils; import org.junit.Assert; @@ -43,7 +45,11 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.agent.api.HostStatsEntryBase; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.host.HostStatsVO; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostStatsDao; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.Account; @@ -57,6 +63,8 @@ import com.cloud.vm.VmStatsVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VmStatsDao; +import com.google.gson.Gson; +import com.google.gson.JsonObject; @RunWith(MockitoJUnitRunner.class) @@ -122,6 +130,10 @@ public class MetricsServiceImplTest { SearchCriteria volumeSearchCriteriaMock; @Mock Filter filterMock; + @Mock + HostStatsDao hostStatsDaoMock; + @Mock + HostVO hostVOMock; private void prepareSearchCriteriaWhenUseSetParameters() { @@ -330,6 +342,104 @@ public void findVmStatsAccordingToDateParamsTestWithNoDate() { Mockito.verify(vmStatsDaoMock).findByVmId(Mockito.anyLong()); } + @Test + public void searchForHostMetricsStatsInternalTestWithAPopulatedListOfHosts() { + Mockito.doReturn(new ArrayList()).when(spy).findHostStatsAccordingToDateParams( + Mockito.anyLong(), Mockito.any(), Mockito.any()); + Mockito.doReturn(1L).when(hostVOMock).getId(); + Map> expected = new HashMap<>(); + expected.put(1L, new ArrayList<>()); + + Map> result = spy.searchForHostMetricsStatsInternal(null, null, Arrays.asList(hostVOMock)); + + Mockito.verify(spy).findHostStatsAccordingToDateParams(1L, null, null); + Assert.assertEquals(expected, result); + } + + @Test + public void searchForHostMetricsStatsInternalTestWithAnEmptyListOfHosts() { + Map> result = spy.searchForHostMetricsStatsInternal(null, null, new ArrayList<>()); + + Mockito.verify(spy, Mockito.never()).findHostStatsAccordingToDateParams( + Mockito.anyLong(), Mockito.any(), Mockito.any()); + Assert.assertTrue(result.isEmpty()); + } + + @Test(expected = InvalidParameterValueException.class) + public void searchForHostMetricsStatsInternalTestWithEndDateBeforeStartDate() { + Date startDate = new Date(); + + spy.searchForHostMetricsStatsInternal(startDate, DateUtils.addSeconds(startDate, -1), Arrays.asList(hostVOMock)); + } + + @Test + public void findHostStatsAccordingToDateParamsTestWithStartDateAndEndDate() { + Date startDate = new Date(); + Date endDate = DateUtils.addSeconds(startDate, 1); + + spy.findHostStatsAccordingToDateParams(1L, startDate, endDate); + + Mockito.verify(hostStatsDaoMock).findByHostIdAndTimestampBetween(1L, startDate, endDate); + } + + @Test + public void findHostStatsAccordingToDateParamsTestWithOnlyStartDate() { + Date startDate = new Date(); + + spy.findHostStatsAccordingToDateParams(1L, startDate, null); + + Mockito.verify(hostStatsDaoMock).findByHostIdAndTimestampGreaterThanEqual(1L, startDate); + } + + @Test + public void findHostStatsAccordingToDateParamsTestWithOnlyEndDate() { + Date endDate = new Date(); + + spy.findHostStatsAccordingToDateParams(1L, null, endDate); + + Mockito.verify(hostStatsDaoMock).findByHostIdAndTimestampLessThanEqual(1L, endDate); + } + + @Test + public void findHostStatsAccordingToDateParamsTestWithNoDate() { + spy.findHostStatsAccordingToDateParams(1L, null, null); + + Mockito.verify(hostStatsDaoMock).findByHostId(1L); + } + + @Test + public void createHostMetricsStatsResponseTestWithValidInput() { + Mockito.doReturn(1L).when(hostVOMock).getId(); + Mockito.doReturn("host-uuid").when(hostVOMock).getUuid(); + Mockito.doReturn("host-name").when(hostVOMock).getName(); + Map> statsMap = new HashMap<>(); + statsMap.put(1L, new ArrayList<>()); + + ListResponse result = spy.createHostMetricsStatsResponse( + new Pair<>(Arrays.asList(hostVOMock), 5), statsMap); + + Assert.assertEquals(Integer.valueOf(5), result.getCount()); + Assert.assertEquals(1, result.getResponses().size()); + } + + @Test + public void createHostStatsResponseTestMapsTheStoredValues() { + Date timestamp = new Date(); + HostStatsEntryBase entry = new HostStatsEntryBase(1L, "host", 12.345, 0.5, 100.0, 200.0, 4096.0, 1024.0); + HostStatsVO hostStatsVO = new HostStatsVO(1L, 2L, timestamp, new Gson().toJson(entry)); + + List result = spy.createHostStatsResponse(Arrays.asList(hostStatsVO)); + + Assert.assertEquals(1, result.size()); + JsonObject response = new Gson().toJsonTree(result.get(0)).getAsJsonObject(); + Assert.assertTrue(response.has("timestamp")); + Assert.assertEquals("12.35%", response.get("cpuused").getAsString()); + Assert.assertEquals(100L, response.get("networkkbsread").getAsLong()); + Assert.assertEquals(200L, response.get("networkkbswrite").getAsLong()); + Assert.assertEquals(4096L, response.get("memorykbs").getAsLong()); + Assert.assertEquals(1024L, response.get("memoryintfreekbs").getAsLong()); + } + @Test public void createVmMetricsStatsResponseTestWithValidInput() { Mockito.doReturn("").when(userVmVOMock).getUuid(); diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index 456792d14b75..787205ed502e 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -84,6 +84,7 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.GetStorageStatsCommand; import com.cloud.agent.api.HostStatsEntry; +import com.cloud.agent.api.HostStatsEntryBase; import com.cloud.agent.api.VgpuTypesInfo; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmNetworkStatsEntry; @@ -110,7 +111,9 @@ import com.cloud.host.HostStats; import com.cloud.host.HostVO; import com.cloud.host.Status; +import com.cloud.host.HostStatsVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostStatsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.as.AutoScaleManager; @@ -301,6 +304,9 @@ public String toString() { protected static ConfigKey vmDiskStatsMaxRetentionTime = new ConfigKey<>("Advanced", Integer.class, "vm.disk.stats.max.retention.time", "720", "The maximum time (in minutes) for keeping VM disks stats records in the database. The VM disks stats cleanup process will be disabled if this is set to 0 or less than 0.", true); + protected static ConfigKey hostStatsMaxRetentionTime = new ConfigKey<>("Advanced", Integer.class, "host.stats.max.retention.time", "720", + "The maximum time (in minutes) for keeping host stats records in the database. Host stats are not stored and the cleanup process is disabled if this is set to 0 or less than 0.", true); + private static StatsCollector s_instance = null; private static Gson gson = new Gson(); @@ -322,6 +328,8 @@ public String toString() { @Inject protected VmStatsDao vmStatsDao; @Inject + protected HostStatsDao hostStatsDao; + @Inject private VolumeDao _volsDao; @Inject protected VolumeStatsDao volumeStatsDao; @@ -510,6 +518,8 @@ protected void init(Map configs) { _executor.scheduleWithFixedDelay(new VmStatsCleaner(), DEFAULT_INITIAL_DELAY, 60000L, TimeUnit.MILLISECONDS); + _executor.scheduleWithFixedDelay(new HostStatsCleaner(), DEFAULT_INITIAL_DELAY, 60000L, TimeUnit.MILLISECONDS); + _executor.scheduleWithFixedDelay(new VolumeStatsCleaner(), DEFAULT_INITIAL_DELAY, 60000L, TimeUnit.MILLISECONDS); scheduleCollection(MANAGEMENT_SERVER_STATUS_COLLECTION_INTERVAL, new ManagementServerCollector(), 1L); @@ -676,12 +686,17 @@ protected void runInContext() { logger.debug(String.format("HostStatsCollector is running to process %d UP hosts", hosts.size())); Map metrics = new HashMap<>(); + boolean persistHostStats = hostStatsMaxRetentionTime.value() > 0; + Date timestamp = new Date(); for (HostVO host : hosts) { HostStatsEntry hostStatsEntry = (HostStatsEntry) _resourceMgr.getHostStatistics(host); if (hostStatsEntry != null) { hostStatsEntry.setHostVo(host); metrics.put(hostStatsEntry.getHostId(), hostStatsEntry); _hostStats.put(host.getId(), hostStatsEntry); + if (persistHostStats) { + persistHostStats(hostStatsEntry, timestamp); + } } else { logger.warn("The Host stats is null for host: {}", host); } @@ -1316,6 +1331,17 @@ protected void runInContext() { } } + class HostStatsCleaner extends ManagedContextRunnable{ + @Override + protected void runInContext() { + try { + cleanUpHostStats(); + } catch (RuntimeException e) { + logger.error("Error trying to clean up host stats", e); + } + } + } + class VolumeStatsCleaner extends ManagedContextRunnable{ @Override protected void runInContext() { @@ -2001,6 +2027,23 @@ protected void persistVirtualMachineStats(VmStatsEntry statsForCurrentIteration, vmStatsDao.persist(vmStatsVO); } + /** + * Persists the host stats of the current collection in the host_stats table. + * + * @param statsForCurrentIteration the host metrics to persist. + * @param timestamp the time that will be stamped. + */ + protected void persistHostStats(HostStatsEntry statsForCurrentIteration, Date timestamp) { + HostStatsEntryBase hostStats = new HostStatsEntryBase(statsForCurrentIteration.getHostId(), + statsForCurrentIteration.getEntityType(), statsForCurrentIteration.getCpuUtilization(), + statsForCurrentIteration.getLoadAverage(), statsForCurrentIteration.getNetworkReadKBs(), + statsForCurrentIteration.getNetworkWriteKBs(), statsForCurrentIteration.getTotalMemoryKBs(), + statsForCurrentIteration.getFreeMemoryKBs()); + HostStatsVO hostStatsVO = new HostStatsVO(statsForCurrentIteration.getHostId(), msId, timestamp, gson.toJson(hostStats)); + logger.trace(String.format("Recording host stats: [%s].", hostStatsVO.toString())); + hostStatsDao.persist(hostStatsVO); + } + private String getVmDiskStatsEntryAsString(VmDiskStatsEntry statsForCurrentIteration, Hypervisor.HypervisorType hypervisorType) { VmDiskStatsEntry entry; if (Hypervisor.HypervisorType.KVM.equals(hypervisorType)) { @@ -2051,6 +2094,23 @@ protected void cleanUpVirtualMachineStats() { vmStatsDao.removeAllByTimestampLessThan(limit, DELETE_QUERY_BATCH_SIZE.value()); } + /** + * Removes the oldest host stats records according to the global + * parameter {@code host.stats.max.retention.time}. + */ + protected void cleanUpHostStats() { + Integer maxRetentionTime = hostStatsMaxRetentionTime.value(); + if (maxRetentionTime <= 0) { + logger.debug(String.format("Skipping host stats cleanup. The [%s] parameter [%s] is set to 0 or less than 0.", + ConfigKey.Scope.decodeAsCsv(hostStatsMaxRetentionTime.getScopeBitmask()), hostStatsMaxRetentionTime.toString())); + return; + } + logger.trace("Removing older host stats records."); + Date now = new Date(); + Date limit = DateUtils.addMinutes(now, -maxRetentionTime); + hostStatsDao.removeAllByTimestampLessThan(limit, DELETE_QUERY_BATCH_SIZE.value()); + } + /** * Removes the oldest Volume stats records according to the global * parameter {@code vm.disk.stats.max.retention.time}. @@ -2245,6 +2305,7 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[] {vmDiskStatsInterval, vmDiskStatsIntervalMin, vmNetworkStatsInterval, vmNetworkStatsIntervalMin, StatsTimeout, statsOutputUri, vmStatsIncrementMetrics, vmStatsMaxRetentionTime, vmStatsCollectUserVMOnly, vmDiskStatsRetentionEnabled, vmDiskStatsMaxRetentionTime, + hostStatsMaxRetentionTime, MANAGEMENT_SERVER_STATUS_COLLECTION_INTERVAL, DATABASE_SERVER_STATUS_COLLECTION_INTERVAL, DATABASE_SERVER_LOAD_HISTORY_RETENTION_NUMBER}; diff --git a/server/src/test/java/com/cloud/server/StatsCollectorTest.java b/server/src/test/java/com/cloud/server/StatsCollectorTest.java index cb00d1652c9a..e10552e1208e 100644 --- a/server/src/test/java/com/cloud/server/StatsCollectorTest.java +++ b/server/src/test/java/com/cloud/server/StatsCollectorTest.java @@ -64,11 +64,15 @@ import com.cloud.agent.api.GetStorageStatsAnswer; import com.cloud.agent.api.GetStorageStatsCommand; +import com.cloud.agent.api.HostStatsEntry; +import com.cloud.agent.api.HostStatsEntryBase; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmStatsEntry; import com.cloud.dc.Vlan.VlanType; import com.cloud.dc.VlanVO; import com.cloud.dc.dao.VlanDao; +import com.cloud.host.HostStatsVO; +import com.cloud.host.dao.HostStatsDaoImpl; import com.cloud.hypervisor.Hypervisor; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; @@ -101,6 +105,12 @@ public class StatsCollectorTest { @Mock VmStatsDaoImpl vmStatsDaoMock; + @Mock + HostStatsDaoImpl hostStatsDaoMock; + + @Captor + ArgumentCaptor hostStatsVOCaptor = ArgumentCaptor.forClass(HostStatsVO.class); + @Mock VmStatsEntry statsForCurrentIterationMock; @@ -146,6 +156,7 @@ public class StatsCollectorTest { public void setUp() throws Exception { closeable = MockitoAnnotations.openMocks(this); statsCollector.vmStatsDao = vmStatsDaoMock; + statsCollector.hostStatsDao = hostStatsDaoMock; statsCollector.volumeStatsDao = volumeStatsDao; Field msStatsGsonField = StatsCollector.class.getDeclaredField("msStatsGson"); msStatsGsonField.setAccessible(true); @@ -394,6 +405,59 @@ public void volumeStatsCleanerTestCatchesCloudRuntimeExceptionAndKeepsRunning() Mockito.verify(statsCollector).cleanUpVolumeStats(); } + // host stats persistence + retention + + private void setHostStatsMaxRetentionTimeValue(String value) { + StatsCollector.hostStatsMaxRetentionTime = new ConfigKey("Advanced", Integer.class, "host.stats.max.retention.time", value, + "The maximum time (in minutes) for keeping host stats records in the database. The host stats cleanup process will be disabled if this is set to 0 or less than 0.", true); + } + + @Test + public void cleanUpHostStatsTestIsDisabled() { + setHostStatsMaxRetentionTimeValue("0"); + + statsCollector.cleanUpHostStats(); + + Mockito.verify(hostStatsDaoMock, Mockito.never()).removeAllByTimestampLessThan(Mockito.any(), Mockito.anyLong()); + } + + @Test + public void cleanUpHostStatsTestIsEnabled() { + setHostStatsMaxRetentionTimeValue("1"); + + statsCollector.cleanUpHostStats(); + + Mockito.verify(hostStatsDaoMock).removeAllByTimestampLessThan(Mockito.any(), Mockito.anyLong()); + } + + @Test + public void persistHostStatsTestPersistsSuccessfully() { + statsCollector.msId = 7L; + Date timestamp = new Date(); + // hostId, cpuUtilization, networkReadKBs, networkWriteKBs, entityType, totalMemoryKBs, freeMemoryKBs, xapiMemoryUsageKBs, averageLoad + HostStatsEntry statsForCurrentIteration = new HostStatsEntry(5L, 10.0, 20.0, 30.0, "host", 1000.0, 400.0, 0.0, 2.0); + Mockito.doReturn(new HostStatsVO()).when(hostStatsDaoMock).persist(Mockito.any()); + + statsCollector.persistHostStats(statsForCurrentIteration, timestamp); + + Mockito.verify(hostStatsDaoMock).persist(hostStatsVOCaptor.capture()); + HostStatsVO actual = hostStatsVOCaptor.getValue(); + Assert.assertEquals(Long.valueOf(5L), actual.getHostId()); + Assert.assertEquals(Long.valueOf(7L), actual.getMgmtServerId()); + Assert.assertEquals(timestamp, actual.getTimestamp()); + HostStatsEntryBase persisted = gson.fromJson(actual.getHostStatsData(), HostStatsEntryBase.class); + Assert.assertEquals(5L, persisted.getHostId()); + Assert.assertEquals("host", persisted.getEntityType()); + Assert.assertEquals(10.0, persisted.getCpuUtilization(), 0); + Assert.assertEquals(2.0, persisted.getLoadAverage(), 0); + Assert.assertEquals(20.0, persisted.getNetworkReadKBs(), 0); + Assert.assertEquals(30.0, persisted.getNetworkWriteKBs(), 0); + Assert.assertEquals(1000.0, persisted.getTotalMemoryKBs(), 0); + Assert.assertEquals(400.0, persisted.getFreeMemoryKBs(), 0); + // Lean payload must NOT carry a HostVO blob. + Assert.assertFalse(actual.getHostStatsData().contains("hostVo")); + } + @Test public void persistVirtualMachineStatsTestPersistsSuccessfully() { statsCollector.msId = 1L; diff --git a/test/integration/smoke/test_metrics_api.py b/test/integration/smoke/test_metrics_api.py index ab2644fc1aad..85f8e122bc2f 100644 --- a/test/integration/smoke/test_metrics_api.py +++ b/test/integration/smoke/test_metrics_api.py @@ -547,6 +547,29 @@ def test_list_volumes_metrics_history(self): return + @attr(tags = ["advanced", "advancedns", "smoke", "basic"], required_hardware="true") + @skipTestIf("hypervisorNotSupported") + def test_list_hosts_metrics_history(self): + cmd = listHostsUsageHistory.listHostsUsageHistoryCmd() + now = datetime.datetime.now() - datetime.timedelta(minutes=15) + start_time = now.strftime("%Y-%m-%d %H:%M:%S") + cmd.startdate = start_time + + result = self.apiclient.listHostsUsageHistory(cmd)[0] + + self.assertTrue(hasattr(result, 'stats')) + self.assertTrue(type(result.stats) == list and len(result.stats) > 0) + stats = result.stats[0] + self.assertTrue(hasattr(stats, 'cpuused')) + self.assertTrue(hasattr(stats, 'memorykbs')) + self.assertTrue(hasattr(stats, 'memoryintfreekbs')) + self.assertTrue(hasattr(stats, 'networkkbsread')) + self.assertTrue(hasattr(stats, 'networkkbswrite')) + self.assertTrue(hasattr(stats, 'timestamp')) + self.assertTrue(self.valid_date(stats.timestamp)) + + return + def validate_vm_stats(self, stats): self.assertTrue(hasattr(stats, 'cpuused')) self.assertTrue(hasattr(stats, 'diskiopstotal'))