Repository: NetApp/harvest
Pull request #811 (2022-01-31 11:49:20)

feat: snapmirror, volume plugin
![image](https://user-images.githubusercontent.com/83282894/151787909-1d93ea09-9c97-41a0-9c8a-c16f0363a897.png)

![image](https://user-images.githubusercontent.com/83282894/151787955-5e9c0b25-407d-40a2-816a-484f78528519.png)

![image](https://user-images.githubusercontent.com/83282894/151788016-01540627-41bf-4f0e-8ba0-fe2f93dfdb93.png)

![image](https://user-images.githubusercontent.com/83282894/151788157-ea498d0b-cccd-44e9-b995-2af46a0be93b.png)



Hardikl: ```
hardikl@hardikl-mac-0 harvest % bin/harvest doctor diffzapirest --zapidatacenter zapi --restdatacenter rest
################## Label value Diffs in prometheus ##############
******* Mismatch in label length. Check data volume_labels aggr
******* Mismatch in label length. Check data volume_labels node
******* Mismatch in label length. Check data volume_labels snapshot_policy
******* Mismatch in label length. Check data volume_labels all_sm_healthy
******* Mismatch in label length. Check data node_labels location
******* Mismatch in label length. Check data qtree_labels qtree
******* Mismatch in label length. Check data snapmirror_labels source_node
******* Mismatch in label length. Check data snapmirror_labels last_transfer_type
******* Mismatch in label length. Check data snapmirror_labels schedule
******* Mismatch in label length. Check data snapmirror_labels unhealthy_reason
################## Missing Labels ##############
shelf_fan_labels []
shelf_voltage_labels []
shelf_labels []
disk_labels []
lun_labels []
qtree_labels []
shelf_sensor_labels []
snapmirror_labels []
volume_labels []
aggr_labels []
node_labels []
shelf_psu_labels [location]
shelf_temperature_labels []
################## Metrics Diffs in prometheus ##############
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
strconv.ParseFloat: parsing "": invalid syntax
################## Missing Metrics by Object in prometheus ##############
qtree [qtree_threshold]
cluster [cluster_outstanding_alerts cluster_suppressed_alerts]
aggr [aggr_space_performance_tier_inactive_user_data aggr_space_reserved aggr_inode_maxfiles_custom_possible aggr_space_performance_tier_inactive_user_data_percent aggr_aggr_inode_maxfiles_custom_possible aggr_space_physical_used_percent aggr_snapshot_inode_used_percent aggr_hybrid_cache_size_total aggr_maxfiles_custom_possible]
################## Missing Metrics from dashboard ##############
aggr [aggr_snapshot_inode_used_percent]
##################
hardikl@hardikl-mac-0 harvest % 
```
cgrinds: we also need to update the template DSL documentation with the new metrics type notation

Patch:
diff --git a/cmd/collectors/rest/plugins/qtree/qtree.go b/cmd/collectors/rest/plugins/qtree/qtree.go
index bf2a9acb..57394185 100644
--- a/cmd/collectors/rest/plugins/qtree/qtree.go
+++ b/cmd/collectors/rest/plugins/qtree/qtree.go
@@ -84,7 +84,7 @@ func (my *Qtree) Init() error {
 	}
 
 	for _, obj := range quotaMetric {
-		metricName, display, _ := util.ParseMetric(obj)
+		metricName, display, _, _ := util.ParseMetric(obj)
 
 		metric, err := my.data.NewMetricFloat64(metricName)
 		if err != nil {
diff --git a/cmd/collectors/rest/plugins/shelf/shelf.go b/cmd/collectors/rest/plugins/shelf/shelf.go
index 0c051e5d..d54fff03 100644
--- a/cmd/collectors/rest/plugins/shelf/shelf.go
+++ b/cmd/collectors/rest/plugins/shelf/shelf.go
@@ -119,7 +119,7 @@ func (my *Shelf) Init() error {
 
 		for _, c := range childObj {
 
-			metricName, display, kind := util.ParseMetric(c)
+			metricName, display, kind, _ := util.ParseMetric(c)
 
 			switch kind {
 			case "key":
diff --git a/cmd/collectors/rest/plugins/snapmirror/snapmirror.go b/cmd/collectors/rest/plugins/snapmirror/snapmirror.go
new file mode 100644
index 00000000..035adc6b
--- /dev/null
+++ b/cmd/collectors/rest/plugins/snapmirror/snapmirror.go
@@ -0,0 +1,142 @@
+/*
+ * Copyright NetApp Inc, 2022 All rights reserved
+ */
+package snapmirror
+
+import (
+	"encoding/json"
+	"github.com/tidwall/gjson"
+	"goharvest2/cmd/collectors/rest/plugins"
+	"goharvest2/cmd/poller/plugin"
+	"goharvest2/cmd/tools/rest"
+	"goharvest2/pkg/conf"
+	"goharvest2/pkg/errors"
+	"goharvest2/pkg/matrix"
+	"time"
+)
+
+const PluginInvocationRate = 10
+
+type SnapMirror struct {
+	*plugin.AbstractPlugin
+	client         *rest.Client
+	query          string
+	nodeUpdCounter int
+	svmVolToNode   map[string]string
+}
+
+func New(p *plugin.AbstractPlugin) plugin.Plugin {
+	return &SnapMirror{AbstractPlugin: p}
+}
+
+func (my *SnapMirror) Init() error {
+
+	var err error
+
+	if err = my.InitAbc(); err != nil {
+		return err
+	}
+
+	timeout := rest.DefaultTimeout * time.Second
+	if my.client, err = rest.New(conf.ZapiPoller(my.ParentParams), timeout); err != nil {
+		my.Logger.Error().Stack().Err(err).Msg("connecting")
+		return err
+	}
+
+	if err = my.client.Init(5); err != nil {
+		return err
+	}
+
+	my.query = "api/private/cli/volume"
+	my.svmVolToNode = make(map[string]string)
+
+	// Assigned the value to nodeUpdCounter so that plugin would be invoked first time to populate cache.
+	my.nodeUpdCounter = PluginInvocationRate
+
+	return nil
+}
+
+func (my *SnapMirror) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
+
+	if my.nodeUpdCounter >= PluginInvocationRate {
+		my.nodeUpdCounter = 0
+		if err := my.updateNodeCache(); err != nil {
+			return nil, err
+		}
+		my.Logger.Debug().Msg("updated node cache")
+	}
+
+	// update volume instance labels
+	my.updateSMLabels(data)
+	my.nodeUpdCounter++
+
+	return nil, nil
+}
+
+func (my *SnapMirror) updateNodeCache() error {
+	var (
+		records []interface{}
+		content []byte
+		err     error
+	)
+
+	// Clean svmVolToNode map
+	my.svmVolToNode = make(map[string]string)
+
+	href := rest.BuildHref("", "node", nil, "", "", "", "", my.query)
+
+	err = rest.FetchData(my.client, href, &records)
+	if err != nil {
+		my.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		return err
+	}
+
+	all := rest.Pagination{
+		Records:    records,
+		NumRecords: len(records),
+	}
+
+	content, err = json.Marshal(all)
+	if err != nil {
+		my.Logger.Error().Err(err).Str("ApiPath", my.query).Msg("Unable to marshal rest pagination")
+	}
+
+	if !gjson.ValidBytes(content) {
+		my.Logger.Error().Err(err).Str("Api", my.query).Msg("Invalid json")
+		return errors.New(errors.API_RESPONSE, "Invalid json")
+	}
+
+	results := gjson.GetManyBytes(content, "num_records", "records")
+	numRecords := results[0]
+	if numRecords.Int() == 0 {
+		return errors.New(errors.ERR_NO_INSTANCE, "no "+my.query+" instances on cluster")
+	}
+
+	for _, volume := range results[1].Array() {
+		volumeName := volume.Get("volume").String()
+		vserverName := volume.Get("vserver").String()
+		nodeName := volume.Get("node").String()
+		key := vserverName + volumeName
+
+		if _, ok := my.svmVolToNode[key]; ok {
+			my.Logger.Warn().Str("key", key).Msg("Duplicate key found")
+		}
+		my.svmVolToNode[key] = nodeName
+	}
+	return nil
+}
+
+func (my *SnapMirror) updateSMLabels(data *matrix.Matrix) {
+	for _, instance := range data.GetInstances() {
+		volumeName := instance.GetLabel("source_volume")
+		vserverName := instance.GetLabel("source_vserver")
+
+		// Update source_node label in snapmirror
+		if node, ok := my.svmVolToNode[vserverName+volumeName]; ok {
+			instance.SetLabel("source_node", node)
+		}
+
+		// update the protectedBy and protectionSourceType fields and derivedRelationshipType in snapmirror_labels
+		plugins.UpdateProtectedFields(instance)
+	}
+}
diff --git a/cmd/collectors/rest/plugins/util.go b/cmd/collectors/rest/plugins/util.go
new file mode 100644
index 00000000..72a1e458
--- /dev/null
+++ b/cmd/collectors/rest/plugins/util.go
@@ -0,0 +1,62 @@
+package plugins
+
+import (
+	"goharvest2/pkg/matrix"
+	"strings"
+)
+
+func UpdateProtectedFields(instance *matrix.Instance) {
+
+	// check for group_type
+	// Supported group_type are: "none", "vserver", "infinitevol", "consistencygroup", "flexgroup"
+	if instance.GetLabel("group_type") != "" {
+
+		groupType := instance.GetLabel("group_type")
+		destinationVolume := instance.GetLabel("destination_volume")
+		sourceVolume := instance.GetLabel("source_volume")
+		destinationLocation := instance.GetLabel("destination_location")
+
+		isSvmDr := groupType == "vserver" && destinationVolume == "" && sourceVolume == ""
+		isCg := groupType == "CONSISTENCYGROUP" && strings.Contains(destinationLocation, ":/cg/")
+		isConstituentVolumeRelationshipWithinSvmDr := groupType == "vserver" && !strings.HasSuffix(destinationLocation, ":")
+		isConstituentVolumeRelationshipWithinCG := groupType == "CONSISTENCYGROUP" && !strings.Contains(destinationLocation, ":/cg/")
+
+		// Update protectedBy label
+		if isSvmDr || isConstituentVolumeRelationshipWithinSvmDr {
+			instance.SetLabel("protectedBy", "storage_vm")
+		} else if isCg || isConstituentVolumeRelationshipWithinCG {
+			instance.SetLabel("protectedBy", "cg")
+		} else {
+			instance.SetLabel("protectedBy", "volume")
+		}
+
+		// SVM-DR related information is populated, Update protectionSourceType label
+		if isSvmDr {
+			instance.SetLabel("protectionSourceType", "storage_vm")
+		} else if isCg {
+			instance.SetLabel("protectionSourceType", "cg")
+		} else if isConstituentVolumeRelationshipWithinSvmDr || isConstituentVolumeRelationshipWithinCG || groupType == "none" || groupType == "flexgroup" {
+			instance.SetLabel("protectionSourceType", "volume")
+		} else {
+			instance.SetLabel("protectionSourceType", "not_mapped")
+		}
+	}
+
+	// Update derived_relationship_type field based on the policyType
+	relationshipType := instance.GetLabel("relationship_type")
+	if policyType := instance.GetLabel("policy_type"); policyType != "" {
+		if policyType == "strict_sync_mirror" {
+			instance.SetLabel("derived_relationship_type", "sync_mirror_strict")
+		} else if policyType == "sync_mirror" {
+			instance.SetLabel("derived_relationship_type", "sync_mirror")
+		} else if policyType == "mirror_vault" {
+			instance.SetLabel("derived_relationship_type", "mirror_vault")
+		} else if policyType == "automated_failover" {
+			instance.SetLabel("derived_relationship_type", "sync_mirror")
+		} else {
+			instance.SetLabel("derived_relationship_type", relationshipType)
+		}
+	} else {
+		instance.SetLabel("derived_relationship_type", relationshipType)
+	}
+}
diff --git a/cmd/collectors/rest/plugins/volume/volume.go b/cmd/collectors/rest/plugins/volume/volume.go
new file mode 100644
index 00000000..3b314bb0
--- /dev/null
+++ b/cmd/collectors/rest/plugins/volume/volume.go
@@ -0,0 +1,312 @@
+/*
+ * Copyright NetApp Inc, 2022 All rights reserved
+ */
+
+package volume
+
+import (
+	"encoding/json"
+	"github.com/tidwall/gjson"
+	"goharvest2/cmd/collectors/rest/plugins"
+	"goharvest2/cmd/poller/plugin"
+	"goharvest2/cmd/tools/rest"
+	"goharvest2/pkg/conf"
+	"goharvest2/pkg/dict"
+	"goharvest2/pkg/errors"
+	"goharvest2/pkg/matrix"
+	"goharvest2/pkg/tree/node"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const DefaultPluginDuration = 1800 * time.Second
+const DefaultDataPollDuration = 180 * time.Second
+
+type Volume struct {
+	*plugin.AbstractPlugin
+	data                 *matrix.Matrix
+	instanceKeys         map[string]string
+	instanceLabels       map[string]*dict.Dict
+	pluginInvocationRate int
+	currentVal           int
+	client               *rest.Client
+	query                string
+	snapmirrorFields     []string
+	outgoingSM           map[string][]string
+	incomingSM           map[string]string
+	isHealthySM          map[string]bool
+}
+
+func New(p *plugin.AbstractPlugin) plugin.Plugin {
+	return &Volume{AbstractPlugin: p}
+}
+
+func (my *Volume) Init() error {
+
+	var err error
+	my.snapmirrorFields = []string{"relationship_id", "relationship_group_type", "destination_volume",
+		"source_volume", "destination_path", "type", "healthy", "source_vserver", "destination_vserver"}
+
+	if err = my.InitAbc(); err != nil {
+		return err
+	}
+
+	timeout := rest.DefaultTimeout * time.Second
+	if my.client, err = rest.New(conf.ZapiPoller(my.ParentParams), timeout); err != nil {
+		my.Logger.Error().Stack().Err(err).Msg("connecting")
+		return err
+	}
+
+	if err = my.client.Init(5); err != nil {
+		return err
+	}
+
+	my.query = "api/private/cli/snapmirror"
+	my.data = matrix.New(my.Parent+".Volume", "volume", "volume")
+
+	my.outgoingSM = make(map[string][]string)
+	my.incomingSM = make(map[string]string)
+	my.isHealthySM = make(map[string]bool)
+
+	// Assigned the value to currentVal so that plugin would be invoked first time to populate cache.
+	if my.currentVal, err = my.setPluginInterval(); err != nil {
+		my.Logger.Error().Err(err).Stack().Msg("Failed while setting the plugin interval")
+		return err
+	}
+
+	return nil
+}
+
+func (my *Volume) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
+
+	// Purge and reset data
+	my.data.PurgeInstances()
+	my.data.Reset()
+
+	// Set all global labels from zapi.go if already not exist
+	my.data.SetGlobalLabels(data.GetGlobalLabels())
+
+	if my.currentVal >= my.pluginInvocationRate {
+		my.currentVal = 0
+
+		// invoke snapmirror zapi and populate info in source and destination snapmirror maps
+		if smSourceMap, smDestinationMap, err := my.GetSnapMirrors(); err != nil {
+			my.Logger.Error().Stack().Err(err).Msg("Failed to collect snapmirror data")
+		} else {
+			// update internal cache based on volume and SM maps
+			my.updateMaps(data, smSourceMap, smDestinationMap)
+		}
+	}
+
+	// update volume instance labels
+	my.updateVolumeLabels(data)
+
+	my.currentVal++
+	return nil, nil
+}
+
+func (my *Volume) setPluginInterval() (int, error) {
+
+	volumeDataInterval := my.getDataInterval(my.ParentParams, DefaultDataPollDuration)
+	pluginDataInterval := my.getDataInterval(my.Params, DefaultPluginDuration)
+	my.Logger.Debug().Float64("VolumeDataInterval", volumeDataInterval).Float64("PluginDataInterval", pluginDataInterval).Msg("Poll interval duration")
+	my.pluginInvocationRate = int(pluginDataInterval / volumeDataInterval)
+
+	return my.pluginInvocationRate, nil
+}
+
+func (my *Volume) getDataInterval(param *node.Node, defaultInterval time.Duration) float64 {
+	var dataIntervalStr = ""
+	schedule := param.GetChildS("schedule")
+	if schedule != nil {
+		dataInterval := schedule.GetChildS("data")
+		if dataInterval != nil {
+			dataIntervalStr = dataInterval.GetContentS()
+			if durationVal, err := time.ParseDuration(dataIntervalStr); err == nil {
+				return durationVal.Seconds()
+			}
+		}
+	}
+	return defaultInterval.Seconds()
+}
+
+func (my *Volume) GetSnapMirrors() (map[string][]*matrix.Instance, map[string]*matrix.Instance, error) {
+	var (
+		records []interface{}
+		content []byte
+		err     error
+	)
+
+	smSourceMap := make(map[string][]*matrix.Instance)
+	smDestinationMap := make(map[string]*matrix.Instance)
+
+	snapmirrorData := matrix.New(my.Parent+".SnapMirror", "sm", "sm")
+
+	href := rest.BuildHref("", strings.Join(my.snapmirrorFields, ","), nil, "", "", "", "", my.query)
+
+	err = rest.FetchData(my.client, href, &records)
+	if err != nil {
+		my.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		return nil, nil, err
+	}
+
+	all := rest.Pagination{
+		Records:    records,
+		NumRecords: len(records),
+	}
+
+	content, err = json.Marshal(all)
+	if err != nil {
+		my.Logger.Error().Err(err).Str("ApiPath", my.query).Msg("Unable to marshal rest pagination")
+	}
+
+	if !gjson.ValidBytes(content) {
+		my.Logger.Error().Err(err).Str("Api", my.query).Msg("Invalid json")
+		return nil, nil, errors.New(errors.API_RESPONSE, "Invalid json")
+	}
+
+	results := gjson.GetManyBytes(content, "num_records", "records")
+	numRecords := results[0]
+	if numRecords.Int() == 0 {
+		return nil, nil, errors.New(errors.ERR_NO_INSTANCE, "no "+my.query+" instances on cluster")
+	}
+
+	for _, snapMirror := range results[1].Array() {
+
+		relationshipId := snapMirror.Get("relationship_id").String()
+		groupType := snapMirror.Get("relationship_group_type").String()
+		destinationVolume := snapMirror.Get("destination_volume").String()
+		sourceVolume := snapMirror.Get("source_volume").String()
+		destinationLocation := snapMirror.Get("destination_path").String()
+		relationshipType := snapMirror.Get("type").String()
+		isHealthy := snapMirror.Get("healthy").String()
+		sourceSvm := snapMirror.Get("source_vserver").String()
+		destinationSvm := snapMirror.Get("destination_vserver").String()
+
+		instanceKey := relationshipId
+		instance, err := snapmirrorData.NewInstance(instanceKey)
+
+		if err != nil {
+			my.Logger.Error().Err(err).Stack().Str("relationshipId", relationshipId).Msg("Failed to create snapmirror cache instance")
+			return nil, nil, err
+		}
+
+		instance.SetLabel("relationship_id", relationshipId)
+		instance.SetLabel("group_type", groupType)
+		instance.SetLabel("destination_volume", destinationVolume)
+		instance.SetLabel("source_volume", sourceVolume)
+		instance.SetLabel("destination_location", destinationLocation)
+		instance.SetLabel("relationship_type", relationshipType)
+		instance.SetLabel("is_healthy", isHealthy)
+		instance.SetLabel("source_svm", sourceSvm)
+		instance.SetLabel("destination_svm", destinationSvm)
+
+		// Update the protectedBy and protectionSourceType fields in snapmirror
+		plugins.UpdateProtectedFields(instance)
+
+		// Update source snapmirror and destination snapmirror info in maps
+		if relationshipType == "data_protection" || relationshipType == "extended_data_protection" || relationshipType == "vault" || relationshipType == "xdp" {
+			sourceKey := sourceVolume + "-" + sourceSvm
+			destinationKey := destinationVolume + "-" + destinationSvm
+			if instance.GetLabel("protectionSourceType") == "volume" {
+				smSourceMap[sourceKey] = append(smSourceMap[sourceKey], instance)
+			}
+			smDestinationMap[destinationKey] = instance
+		}
+	}
+
+	return smSourceMap, smDestinationMap, nil
+}
+
+func (my *Volume) updateMaps(data *matrix.Matrix, smSourceMap map[string][]*matrix.Instance, smDestinationMap map[string]*matrix.Instance) {
+	// Clean all the snapmirror maps
+	my.outgoingSM = make(map[string][]string)
+	my.incomingSM = make(map[string]string)
+	my.isHealthySM = make(map[string]bool)
+
+	for _, volume := range data.GetInstances() {
+		volumeName := volume.GetLabel("volume")
+		svmName := volume.GetLabel("svm")
+		volumeType := volume.GetLabel("type")
+		key := volumeName + "-" + svmName
+
+		protectedByMap := make(map[string]string)
+		var protectedByValue []string
+		healthStatus := true
+		for _, smRelationship := range smSourceMap[key] {
+			/* Example: If 3 relationships belongs to a volume, and out of 3, 2 are at snapmirror and one is svmdr,
+			   So, protectedByMap map has 2 records, and outgoingSM map value would be snapmirror, svmdr
+			*/
+			protectedByMap[smRelationship.GetLabel("protectedBy")] = ""
+
+			// Update isHealthySM map based on the source snapmirror info
+			if volumeType == "rw" {
+				/* Example: If 3 relationships belongs to a volume, and out of 3, 2 are healthy and one is not,
+				   So, isHealthySM map value would be unhealthy - false
+				*/
+				currentVal, _ := strconv.ParseBool(smRelationship.GetLabel("is_healthy"))
+				healthStatus = healthStatus && currentVal
+				my.isHealthySM[key] = healthStatus
+			}
+		}
+
+		// Update outgoingSM map based on the protectedByMap
+		protectedByValue = nil
+		for protectedByKey, _ := range protectedByMap {
+			protectedByValue = append(protectedByValue, protectedByKey)
+		}
+		if protectedByValue != nil {
+			my.outgoingSM[key] = protectedByValue
+		}
+
+		// Update incomingSM map based on the destination snapmirror info
+		if smDestinationMap[key] != nil {
+			my.incomingSM[key] = "destination"
+		}
+	}
+}
+
+func (my *Volume) updateVolumeLabels(data *matrix.Matrix) {
+	for _, volume := range data.GetInstances() {
+		volumeName := volume.GetLabel("volume")
+		svmName := volume.GetLabel("svm")
+		volumeType := volume.GetLabel("type")
+		key := volumeName + "-" + svmName
+
+		// Update protectionRole label in volume
+		if volumeType == "rw" && my.incomingSM[key] == "" && my.outgoingSM[key] == nil {
+			volume.SetLabel("protectionRole", "unprotected")
+		} else if volumeType == "rw" && my.outgoingSM[key] != nil {
+			volume.SetLabel("protectionRole", "protected")
+		} else if volumeType == "dp" || (volumeType == "rw" && my.incomingSM[key] != "") {
+			volume.SetLabel("protectionRole", "destination")
+		} else {
+			volume.SetLabel("protectionRole", "not_applicable")
+		}
+
+		// Update protectedBy label in volume
+		if outgoing, ok := my.outgoingSM[key]; ok {
+			outgoingJoinStr := strings.Join(outgoing, ",")
+			if outgoingJoinStr == "volume,storage_vm" || outgoingJoinStr == "storage_vm,volume" {
+				volume.SetLabel("protectedBy", "svmdr_and_snapmirror")
+			} else if outgoingJoinStr == "cg,volume" || outgoingJoinStr == "volume,cg" {
+				volume.SetLabel("protectedBy", "cg_and_snapmirror")
+			} else if outgoingJoinStr == "cg" {
+				volume.SetLabel("protectedBy", "consistency_group")
+			} else if outgoingJoinStr == "storage_vm" {
+				volume.SetLabel("protectedBy", "storage_vm_dr")
+			} else if outgoingJoinStr == "volume" {
+				volume.SetLabel("protectedBy", "snapmirror")
+			}
+		} else {
+			volume.SetLabel("protectedBy", "not_applicable")
+		}
+
+		// Update all_sm_healthy label in volume, when all relationships belongs to this volume are healthy then true, otherwise false
+		if healthy, ok := my.isHealthySM[key]; ok {
+			volume.SetLabel("all_sm_healthy", strconv.FormatBool(healthy))
+		}
+
+	}
+}
diff --git a/cmd/collectors/rest/rest.go b/cmd/collectors/rest/rest.go
index b15b5a95..cfe4bdf6 100644
--- a/cmd/collectors/rest/rest.go
+++ b/cmd/collectors/rest/rest.go
@@ -7,6 +7,8 @@ import (
 	"goharvest2/cmd/collectors/rest/plugins/disk"
 	"goharvest2/cmd/collectors/rest/plugins/qtree"
 	"goharvest2/cmd/collectors/rest/plugins/shelf"
+	"goharvest2/cmd/collectors/rest/plugins/snapmirror"
+	"goharvest2/cmd/collectors/rest/plugins/volume"
 	"goharvest2/cmd/poller/collector"
 	"goharvest2/cmd/poller/plugin"
 	"goharvest2/cmd/tools/rest"
@@ -14,7 +16,6 @@ import (
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/matrix"
 	"goharvest2/pkg/tree/node"
-	"goharvest2/pkg/util"
 	"os"
 	"strings"
 	"time"
@@ -45,8 +46,9 @@ type prop struct {
 }
 
 type metric struct {
-	label string
-	name  string
+	label      string
+	name       string
+	metricType string
 }
 
 func init() {
@@ -153,9 +155,6 @@ func (r *Rest) initEndPoints() error {
 	endpoints := r.Params.GetChildS("endpoints")
 	if endpoints != nil {
 		for _, line := range endpoints.GetChildren() {
-			var (
-				display, name, kind string
-			)
 
 			n := line.GetNameS()
 			e := endPoint{name: n}
@@ -167,6 +166,7 @@ func (r *Rest) initEndPoints() error {
 			prop.counters = make(map[string]string)
 			prop.metrics = make([]metric, 0)
 			prop.apiType = "public"
+			prop.returnTimeOut = r.prop.returnTimeOut
 
 			for _, line1 := range line.GetChildren() {
 				if line1.GetNameS() == "query" {
@@ -174,37 +174,7 @@ func (r *Rest) initEndPoints() error {
 					prop.apiType = checkQueryType(prop.query)
 				}
 				if line1.GetNameS() == "counters" {
-					for _, c := range line1.GetAllChildContentS() {
-						name, display, kind = util.ParseMetric(c)
-
-						prop.counters[name] = display
-						switch kind {
-						case "key":
-							prop.instanceLabels[name] = display
-							prop.instanceKeys = append(prop.instanceKeys, name)
-						case "label":
-							prop.instanceLabels[name] = display
-						case "float":
-							m := metric{label: display, name: name}
-							prop.metrics = append(prop.metrics, m)
-						}
-					}
-					if prop.apiType == "private" {
-						counterKey := make([]string, len(prop.counters))
-						i := 0
-						for k := range prop.counters {
-							counterKey[i] = k
-							i++
-						}
-						prop.fields = counterKey
-					}
-
-					if prop.apiType == "public" {
-						prop.fields = []string{"*"}
-						if x := line1.GetChildS("hidden_fields"); x != nil {
-							prop.fields = append(prop.fields, x.GetAllChildContentS()...)
-						}
-					}
+					r.ParseRestCounters(line1, &prop)
 				}
 			}
 			e.prop = &prop
@@ -267,16 +237,8 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 
 	startTime = time.Now()
 
-	href := rest.BuildHref(r.prop.query, strings.Join(r.prop.fields, ","), nil, "", "", "", r.prop.returnTimeOut, r.prop.query)
-
-	r.Logger.Debug().Str("href", href).Msg("")
-	if href == "" {
-		return nil, errors.New(errors.ERR_CONFIG, "empty url")
-	}
-
-	err = rest.FetchData(r.client, href, &records)
-	if err != nil {
-		r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+	if records, err = r.GetRestData(r.prop, r.client); err != nil {
+		r.Logger.Error().Stack().Err(err).Msg("Failed to fetch data")
 		return nil, err
 	}
 
@@ -291,11 +253,9 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 		r.Logger.Error().Err(err).Str("ApiPath", r.prop.query).Msg("Unable to marshal rest pagination")
 	}
 
-	startTime = time.Now()
 	if !gjson.ValidBytes(content) {
 		return nil, fmt.Errorf("json is not valid for: %s", r.prop.query)
 	}
-	parseD = time.Since(startTime)
 
 	results := gjson.GetManyBytes(content, "num_records", "records")
 	numRecords := results[0]
@@ -305,88 +265,15 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 
 	r.Logger.Debug().Str("object", r.Object).Str("number of records extracted", numRecords.String()).Msg("")
 
-	results[1].ForEach(func(key, instanceData gjson.Result) bool {
-		var (
-			instanceKey string
-			instance    *matrix.Instance
-		)
-
-		if !instanceData.IsObject() {
-			r.Logger.Warn().Str("type", instanceData.Type.String()).Msg("Instance data is not object, skipping")
-			return true
-		}
-
-		// extract instance key(s)
-		for _, k := range r.prop.instanceKeys {
-			value := instanceData.Get(k)
-			if value.Exists() {
-				instanceKey += value.String()
-			} else {
-				r.Logger.Warn().Str("key", k).Msg("skip instance, missing key")
-				break
-			}
-		}
-
-		if r.Params.GetChildContentS("only_cluster_instance") != "true" {
-			if instanceKey == "" {
-				return true
-			}
-		}
-
-		if instance = r.Matrix.GetInstance(instanceKey); instance == nil {
-			if instance, err = r.Matrix.NewInstance(instanceKey); err != nil {
-				r.Logger.Error().Err(err).Str("Instance key", instanceKey).Msg("")
-				return true
-			}
-		}
-
-		for label, display := range r.prop.instanceLabels {
-			value := instanceData.Get(label)
-			if value.Exists() {
-				if value.IsArray() {
-					var labelArray []string
-					for _, r := range value.Array() {
-						labelString := r.String()
-						labelArray = append(labelArray, labelString)
-					}
-					instance.SetLabel(display, strings.Join(labelArray, ","))
-				} else {
-					instance.SetLabel(display, value.String())
-				}
-				count++
-			} else {
-				// spams a lot currently due to missing label mappings. Moved to debug for now till rest gaps are filled
-				r.Logger.Debug().Str("Instance key", instanceKey).Str("label", label).Msg("Missing label value")
-			}
-		}
-
-		for _, metric := range r.prop.metrics {
-			metr, ok := r.Matrix.GetMetrics()[metric.name]
-			if !ok {
-				if metr, err = r.Matrix.NewMetricFloat64(metric.name); err != nil {
-					r.Logger.Error().Err(err).
-						Str("name", metric.name).
-						Msg("NewMetricFloat64")
-				}
-			}
-			f := instanceData.Get(metric.name)
-			if f.Exists() {
-				metr.SetName(metric.label)
-				if err = metr.SetValueFloat64(instance, f.Float()); err != nil {
-					r.Logger.Error().Err(err).Str("key", metric.name).Str("metric", metric.label).
-						Msg("Unable to set float key on metric")
-				}
-				count++
-			}
-		}
-		return true
-	})
+	count = r.HandleResults(results[1], r.prop)
 
 	// process endpoints
-	err = r.processEndPoints(r.Matrix)
+	startTime = time.Now()
+	err = r.processEndPoints()
 	if err != nil {
 		r.Logger.Error().Err(err).Msg("Error while processing end points")
 	}
+	parseD = time.Since(startTime)
 
 	r.Logger.Info().
 		Uint64("dataPoints", count).
@@ -402,7 +289,7 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 	return r.Matrix, nil
 }
 
-func (r *Rest) processEndPoints(data *matrix.Matrix) error {
+func (r *Rest) processEndPoints() error {
 	var (
 		err error
 	)
@@ -419,16 +306,8 @@ func (r *Rest) processEndPoints(data *matrix.Matrix) error {
 			i++
 		}
 
-		href := rest.BuildHref(endpoint.prop.query, strings.Join(endpoint.prop.fields, ","), nil, "", "", "", r.prop.returnTimeOut, endpoint.prop.query)
-
-		r.Logger.Debug().Str("href", href).Msg("")
-
-		if href == "" {
-			return errors.New(errors.ERR_CONFIG, "empty url")
-		}
-		err = rest.FetchData(r.client, href, &records)
-		if err != nil {
-			r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		if records, err = r.GetRestData(endpoint.prop, r.client); err != nil {
+			r.Logger.Error().Stack().Err(err).Msg("Failed to fetch data")
 			return err
 		}
 
@@ -452,71 +331,7 @@ func (r *Rest) processEndPoints(data *matrix.Matrix) error {
 			return errors.New(errors.ERR_NO_INSTANCE, "no "+endpoint.prop.query+" instances on cluster")
 		}
 
-		results[1].ForEach(func(key, instanceData gjson.Result) bool {
-			var (
-				instanceKey string
-				instance    *matrix.Instance
-			)
-
-			if !instanceData.IsObject() {
-				r.Logger.Warn().Str("type", instanceData.Type.String()).Msg("Instance data is not object, skipping")
-				return true
-			}
-
-			// extract instance key(s)
-			for _, k := range endpoint.prop.instanceKeys {
-				value := instanceData.Get(k)
-				if value.Exists() {
-					instanceKey += value.String()
-				} else {
-					r.Logger.Warn().Str("key", k).Msg("skip instance, missing key")
-					break
-				}
-			}
-
-			if instance = data.GetInstance(instanceKey); instance != nil {
-
-				for label, display := range endpoint.prop.instanceLabels {
-					value := instanceData.Get(label)
-					if value.Exists() {
-						if value.IsArray() {
-							var labelArray []string
-							for _, r := range value.Array() {
-								labelString := r.String()
-								labelArray = append(labelArray, labelString)
-							}
-							instance.SetLabel(display, strings.Join(labelArray, ","))
-						} else {
-							instance.SetLabel(display, value.String())
-						}
-					} else {
-						// spams a lot currently due to missing label mappings. Moved to debug for now till rest gaps are filled
-						r.Logger.Debug().Str("Instance key", instanceKey).Str("label", label).Msg("Missing label value")
-					}
-				}
-
-				for _, metric := range endpoint.prop.metrics {
-					metr, ok := data.GetMetrics()[metric.name]
-					if !ok {
-						if metr, err = data.NewMetricFloat64(metric.name); err != nil {
-							r.Logger.Error().Err(err).
-								Str("name", metric.name).
-								Msg("NewMetricFloat64")
-						}
-					}
-					f := instanceData.Get(metric.name)
-					if f.Exists() {
-						metr.SetName(metric.label)
-						if err = metr.SetValueFloat64(instance, f.Float()); err != nil {
-							r.Logger.Error().Err(err).Str("key", metric.name).Str("metric", metric.label).
-								Msg("Unable to set float key on metric")
-						}
-					}
-				}
-			}
-			return true
-		})
-
+		r.HandleResults(results[1], endpoint.prop)
 	}
 
 	return nil
@@ -539,12 +354,137 @@ func (r *Rest) LoadPlugin(kind string, abc *plugin.AbstractPlugin) plugin.Plugin
 		return qtree.New(abc)
 	case "Shelf":
 		return shelf.New(abc)
+	case "Snapmirror":
+		return snapmirror.New(abc)
+	case "Volume":
+		return volume.New(abc)
 	default:
 		r.Logger.Warn().Str("kind", kind).Msg("no rest plugin found ")
 	}
 	return nil
 }
 
+func (r *Rest) HandleResults(result gjson.Result, prop *prop) uint64 {
+	var (
+		err   error
+		count uint64
+	)
+
+	result.ForEach(func(key, instanceData gjson.Result) bool {
+		var (
+			instanceKey string
+			instance    *matrix.Instance
+		)
+
+		if !instanceData.IsObject() {
+			r.Logger.Warn().Str("type", instanceData.Type.String()).Msg("Instance data is not object, skipping")
+			return true
+		}
+
+		// extract instance key(s)
+		for _, k := range prop.instanceKeys {
+			value := instanceData.Get(k)
+			if value.Exists() {
+				instanceKey += value.String()
+			} else {
+				r.Logger.Warn().Str("key", k).Msg("skip instance, missing key")
+				break
+			}
+		}
+
+		if r.Params.GetChildContentS("only_cluster_instance") != "true" {
+			if instanceKey == "" {
+				return true
+			}
+		}
+
+		if instance = r.Matrix.GetInstance(instanceKey); instance == nil {
+			if instance, err = r.Matrix.NewInstance(instanceKey); err != nil {
+				r.Logger.Error().Err(err).Str("Instance key", instanceKey).Msg("")
+				return true
+			}
+		}
+
+		for label, display := range prop.instanceLabels {
+			value := instanceData.Get(label)
+			if value.Exists() {
+				if value.IsArray() {
+					var labelArray []string
+					for _, r := range value.Array() {
+						labelString := r.String()
+						labelArray = append(labelArray, labelString)
+					}
+					instance.SetLabel(display, strings.Join(labelArray, ","))
+				} else {
+					instance.SetLabel(display, value.String())
+				}
+				count++
+			} else {
+				// spams a lot currently due to missing label mappings. Moved to debug for now till rest gaps are filled
+				r.Logger.Debug().Str("Instance key", instanceKey).Str("label", label).Msg("Missing label value")
+			}
+		}
+
+		for _, metric := range prop.metrics {
+			metr, ok := r.Matrix.GetMetrics()[metric.name]
+			if !ok {
+				if metr, err = r.Matrix.NewMetricFloat64(metric.name); err != nil {
+					r.Logger.Error().Err(err).
+						Str("name", metric.name).
+						Msg("NewMetricFloat64")
+				}
+			}
+			f := instanceData.Get(metric.name)
+			if f.Exists() {
+				metr.SetName(metric.label)
+
+				var floatValue float64
+				switch metric.metricType {
+				case "duration":
+					floatValue = HandleDuration(f.String())
+				case "timestamp":
+					floatValue = HandleTimestamp(f.String())
+				case "":
+					floatValue = f.Float()
+				default:
+					r.Logger.Warn().Str("type", metric.metricType).Str("metric", metric.name).Msg("unknown metric type")
+				}
+
+				if err = metr.SetValueFloat64(instance, floatValue); err != nil {
+					r.Logger.Error().Err(err).Str("key", metric.name).Str("metric", metric.label).
+						Msg("Unable to set float key on metric")
+				}
+				count++
+			}
+		}
+
+		return true
+	})
+	return count
+}
+
+func (r *Rest) GetRestData(prop *prop, client *rest.Client) ([]interface{}, error) {
+	var (
+		err     error
+		records []interface{}
+	)
+
+	href := rest.BuildHref(prop.query, strings.Join(prop.fields, ","), nil, "", "", "", prop.returnTimeOut, prop.query)
+
+	r.Logger.Debug().Str("href", href).Msg("")
+	if href == "" {
+		return nil, errors.New(errors.ERR_CONFIG, "empty url")
+	}
+
+	err = rest.FetchData(client, href, &records)
+	if err != nil {
+		r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		return nil, err
+	}
+
+	return records, nil
+}
+
 // Interface guards
 var (
 	_ collector.Collector = (*Rest)(nil)
diff --git a/cmd/collectors/rest/templating.go b/cmd/collectors/rest/templating.go
index 28fbf26a..7462f518 100644
--- a/cmd/collectors/rest/templating.go
+++ b/cmd/collectors/rest/templating.go
@@ -1,10 +1,14 @@
 package rest
 
 import (
+	"fmt"
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/tree/node"
 	"goharvest2/pkg/util"
+	"regexp"
+	"strconv"
 	"strings"
+	"time"
 )
 
 func (r *Rest) LoadTemplate() error {
@@ -26,8 +30,7 @@ func (r *Rest) LoadTemplate() error {
 func (r *Rest) initCache() error {
 
 	var (
-		counters            *node.Node
-		display, name, kind string
+		counters *node.Node
 	)
 
 	if x := r.Params.GetChildContentS("object"); x != "" {
@@ -58,57 +61,147 @@ func (r *Rest) initCache() error {
 	r.prop.instanceLabels = make(map[string]string)
 	r.prop.counters = make(map[string]string)
 
-	for _, c := range counters.GetAllChildContentS() {
+	// private end point do not support * as fields. We need to pass fields in endpoint
+	query := r.Params.GetChildS("query")
+	r.prop.apiType = "public"
+	if query != nil {
+		r.prop.apiType = checkQueryType(query.GetContentS())
+	}
+
+	r.ParseRestCounters(counters, r.prop)
+
+	r.Logger.Info().Strs("extracted Instance Keys", r.prop.instanceKeys).Msg("")
+	r.Logger.Info().Int("count metrics", len(r.prop.metrics)).Int("count labels", len(r.prop.instanceLabels)).Msg("initialized metric cache")
+
+	return nil
+}
+
+func HandleDuration(value string) float64 {
+	// Example: duration: PT8H35M42S
+	timeDurationRegex := `^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:.\d+)?)S)?$`
+
+	regexTimeDuration := regexp.MustCompile(timeDurationRegex)
+	if match := regexTimeDuration.MatchString(value); match {
+		// example: PT8H35M42S   ==>  30942
+		matches := regexTimeDuration.FindStringSubmatch(value)
+		if matches == nil {
+			return 0
+		}
+
+		seconds := 0.0
+
+		//years
+		//months
+
+		//days
+		if matches[3] != "" {
+			f, err := strconv.ParseFloat(matches[3], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f * 24 * 60 * 60
+		}
+
+		//hours
+		if matches[4] != "" {
+			f, err := strconv.ParseFloat(matches[4], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f * 60 * 60
+		}
+
+		//minutes
+		if matches[5] != "" {
+			f, err := strconv.ParseFloat(matches[5], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f * 60
+		}
+
+		//seconds & milliseconds
+		if matches[6] != "" {
+			f, err := strconv.ParseFloat(matches[6], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f
+		}
+		return seconds
+	}
+
+	return 0
+}
+
+func HandleTimestamp(value string) float64 {
+	var timestamp time.Time
+	var err error
+
+	// Example: timestamp: 2020-12-02T18:36:19-08:00
+	timestampRegex := `[+-]?\d{4}(-[01]\d(-[0-3]\d(T[0-2]\d:[0-5]\d:?([0-5]\d(\.\d+)?)?[+-][0-2]\d:[0-5]\d?)?)?)?`
+
+	regexTimeStamp := regexp.MustCompile(timestampRegex)
+	if match := regexTimeStamp.MatchString(value); match {
+		// example: 2020-12-02T18:36:19-08:00   ==>  1606962979
+		if timestamp, err = time.Parse(time.RFC3339, value); err != nil {
+			fmt.Printf("%v", err)
+			return 0
+		}
+		return float64(timestamp.Unix())
+	}
+	return 0
+}
+
+func (r *Rest) ParseRestCounters(counter *node.Node, prop *prop) {
+	var (
+		display, name, kind, metricType string
+	)
+
+	for _, c := range counter.GetAllChildContentS() {
 		if c != "" {
-			name, display, kind = util.ParseMetric(c)
+			name, display, kind, metricType = util.ParseMetric(c)
 			r.Logger.Debug().
 				Str("kind", kind).
 				Str("name", name).
 				Str("display", display).
 				Msg("Collected")
 
-			r.prop.counters[name] = display
+			prop.counters[name] = display
 			switch kind {
 			case "key":
-				r.prop.instanceLabels[name] = display
-				r.prop.instanceKeys = append(r.prop.instanceKeys, name)
+				prop.instanceLabels[name] = display
+				prop.instanceKeys = append(prop.instanceKeys, name)
 			case "label":
-				r.prop.instanceLabels[name] = display
+				prop.instanceLabels[name] = display
 			case "float":
-				m := metric{label: display, name: name}
-				r.prop.metrics = append(r.prop.metrics, m)
+				m := metric{label: display, name: name, metricType: metricType}
+				prop.metrics = append(prop.metrics, m)
 			}
 		}
 	}
 
-	// private end point do not support * as fields. We need to pass fields in endpoint
-	query := r.Params.GetChildS("query")
-	r.prop.apiType = "public"
-	if query != nil {
-		r.prop.apiType = checkQueryType(query.GetContentS())
-	}
-
-	if r.prop.apiType == "private" {
-		counterKey := make([]string, len(r.prop.counters))
+	if prop.apiType == "private" {
+		counterKey := make([]string, len(prop.counters))
 		i := 0
-		for k := range r.prop.counters {
+		for k := range prop.counters {
 			counterKey[i] = k
 			i++
 		}
-		r.prop.fields = counterKey
+		prop.fields = counterKey
 	}
 
-	if r.prop.apiType == "public" {
-		r.prop.fields = []string{"*"}
-		if c := r.Params.GetChildS("counters"); c != nil {
-			if x := c.GetChildS("hidden_fields"); x != nil {
-				r.prop.fields = append(r.prop.fields, x.GetAllChildContentS()...)
+	if prop.apiType == "public" {
+		prop.fields = []string{"*"}
+		if counter != nil {
+			if x := counter.GetChildS("hidden_fields"); x != nil {
+				prop.fields = append(prop.fields, x.GetAllChildContentS()...)
 			}
 		}
 	}
 
-	r.Logger.Info().Strs("extracted Instance Keys", r.prop.instanceKeys).Msg("")
-	r.Logger.Info().Int("count metrics", len(r.prop.metrics)).Int("count labels", len(r.prop.instanceLabels)).Msg("initialized metric cache")
-
-	return nil
 }
diff --git a/cmd/collectors/zapi/plugins/quota/quota.go b/cmd/collectors/zapi/plugins/quota/quota.go
index bb97aca8..8967ea37 100644
--- a/cmd/collectors/zapi/plugins/quota/quota.go
+++ b/cmd/collectors/zapi/plugins/quota/quota.go
@@ -79,7 +79,7 @@ func (my *Quota) Init() error {
 	}
 
 	for _, obj := range objects.GetAllChildContentS() {
-		metricName, display, _ := util.ParseMetric(obj)
+		metricName, display, _, _ := util.ParseMetric(obj)
 
 		metric, err := my.data.NewMetricFloat64(metricName)
 		if err != nil {
diff --git a/cmd/collectors/zapi/plugins/shelf/shelf.go b/cmd/collectors/zapi/plugins/shelf/shelf.go
index 9776bcc6..21290838 100644
--- a/cmd/collectors/zapi/plugins/shelf/shelf.go
+++ b/cmd/collectors/zapi/plugins/shelf/shelf.go
@@ -90,7 +90,7 @@ func (my *Shelf) Init() error {
 
 			for _, c := range x.GetAllChildContentS() {
 
-				metricName, display, kind := util.ParseMetric(c)
+				metricName, display, kind, _ := util.ParseMetric(c)
 
 				switch kind {
 				case "key":
diff --git a/cmd/tools/doctor/restZapiDiff.go b/cmd/tools/doctor/restZapiDiff.go
index be7e78d6..ce7e7569 100644
--- a/cmd/tools/doctor/restZapiDiff.go
+++ b/cmd/tools/doctor/restZapiDiff.go
@@ -128,12 +128,10 @@ func metricValueDiff(metricName string) {
 		results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter", "data.result.#.metric.node")
 		keyIndexes = []int{2}
 	}
-	// ignore qtree for now as it is under development
-	//if strings.HasPrefix(metricName, "qtree_") && !strings.HasPrefix(metricName, "qtree_id") {
-	//	results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter","data.result.#.metric.qtree")
-	//keyIndexes = []int{2}
-	//}
-
+	if strings.HasPrefix(metricName, "qtree_") && !strings.HasPrefix(metricName, "qtree_id") {
+		results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter", "data.result.#.metric.qtree")
+		keyIndexes = []int{2}
+	}
 	if strings.HasPrefix(metricName, "environment_sensor_") {
 		results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter", "data.result.#.metric.sensor", "data.result.#.metric.node")
 		keyIndexes = []int{2, 3}
@@ -224,7 +222,8 @@ func labelValueDiff(label string, labelNames []string) {
 	skipMatch := make([]string, 0)
 	skipMatch = append(skipMatch, "datacenter")
 
-	if strings.HasPrefix(label, "disk_") || strings.HasPrefix(label, "shelf_") {
+	// To ignore child labels(ex: shelf_psu_labels) from shelf object, following exact comparison
+	if strings.HasPrefix(label, "disk_") || strings.Compare(label, "shelf_labels") == 0 {
 		keyIndexes = append(keyIndexes, IndexOf(finalLabelNames, "serial_number"))
 		dataCenterIndex = IndexOf(finalLabelNames, "datacenter")
 		results = gjson.GetMany(data, prefixLabelsName...)
diff --git a/conf/rest/9.11.0/snapmirror.yaml b/conf/rest/9.11.0/snapmirror.yaml
deleted file mode 100644
index 087f6a2a..00000000
--- a/conf/rest/9.11.0/snapmirror.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-name:                         SnapMirror
-query:                        api/snapmirror/relationships
-object:                       snapmirror
-
-counters:
-  - ^^uuid => relationship_id
-  - ^destination.path  => destination_volume
-  - ^destination.svm.name => destination_vserver
-  - ^healthy
-  - ^transfer.bytes_transferred  => last_transfer_size
-  - ^transfer.state => relationship_status
-  - ^source.path => source_volume
-  - ^source.svm.name => source_vserver
-  #- ^source-node #missing mapping  ==> Needs plugin development
-  - ^unhealthy_reason.#.message => unhealthy_reason
-  - lag_time
-  - transfer.total_duration => last_transfer_duration
-  - transfer.end_time => last_transfer_end_timestamp
-  - transfer.bytes_transferred  => last_transfer_size
-
-
-endpoints:
-  - query: api/private/cli/snapmirror
-    counters:
-      - ^^relationship_id => relationship_id
-      - ^destination_volume_node => destination_node
-      - ^type => relationship_type
-      - ^last_transfer_type => last_transfer_type
-      - ^relationship_group_type  => group_type
-      - ^schedule => schedule
-      - ^policy_type => policy_type
-      - ^destination_path => destination_location
-      - total_transfer_time_secs => total_transfer_time_secs
-      - total_transfer_bytes => total_transfer_bytes
-      - newest_snapshot_timestamp => newest_snapshot_timestamp
-      - resync_successful_count => resync_successful_count
-      - update_successful_count => update_successful_count
-      - resync_failed_count => resync_failed_count
-      - update_failed_count => update_failed_count
-      - break_successful_count => break_successful_count
-      - break_failed_count => break_failed_count
-      - total_transfer_time_secs => total_transfer_time_secs
-      - newest_snapshot_timestamp => newest_snapshot_timestamp
-
-export_options:
-  instance_keys:
-    - relationship_id
-    - destination_volume
-    - destination_node
-    - destination_vserver
-    - source_volume
-    - source_vserver
-    - source_node
-  instance_labels:
-    - healthy
-    - unhealthy_reason
-    - last_transfer_type
-    - relationship_status
-    - relationship_type
-    - schedule
-    - destination_node_limit
-    - source_node_limit
-    - group_type
-    - policy_type
-    - destination_location
diff --git a/conf/rest/9.11.0/aggr.yaml b/conf/rest/9.12.0/aggr.yaml
similarity index 100%
rename from conf/rest/9.11.0/aggr.yaml
rename to conf/rest/9.12.0/aggr.yaml
diff --git a/conf/rest/9.11.0/disk.yaml b/conf/rest/9.12.0/disk.yaml
similarity index 100%
rename from conf/rest/9.11.0/disk.yaml
rename to conf/rest/9.12.0/disk.yaml
diff --git a/conf/rest/9.11.0/lun.yaml b/conf/rest/9.12.0/lun.yaml
similarity index 100%
rename from conf/rest/9.11.0/lun.yaml
rename to conf/rest/9.12.0/lun.yaml
diff --git a/conf/rest/9.11.0/node.yaml b/conf/rest/9.12.0/node.yaml
similarity index 82%
rename from conf/rest/9.11.0/node.yaml
rename to conf/rest/9.12.0/node.yaml
index 99c07753..4fcd4f9b 100644
--- a/conf/rest/9.11.0/node.yaml
+++ b/conf/rest/9.12.0/node.yaml
@@ -9,9 +9,6 @@ counters:
   - ^controller.failed_power_supply.message.message => failed_power_message
   - ^controller.over_temperature => over_temperature
   - ^state => healthy
-  #- ^system-node-get-iter.maximum-aggregate-size => max_aggr_size  #missing mapping
-  #- ^system-node-get-iter.maximum-number-of-volumes => max_vol_num #missing mapping
-  #- ^system-node-get-iter.maximum-volume-size => max_vol_size #missing mapping
   - ^location
   - ^model
   - ^serial_number => serial
@@ -28,6 +25,9 @@ endpoints:
     counters:
       - ^^node => node
       - ^vendor  => vendor
+      - ^max_aggr_size => max_aggr_size
+      - ^max_node_vvols => max_vol_num
+      - ^max_vol_size => max_vol_size
 
 plugins:
   - LabelAgent:
diff --git a/conf/rest/9.11.0/port.yaml b/conf/rest/9.12.0/port.yaml
similarity index 100%
rename from conf/rest/9.11.0/port.yaml
rename to conf/rest/9.12.0/port.yaml
diff --git a/conf/rest/9.11.0/qtree.yaml b/conf/rest/9.12.0/qtree.yaml
similarity index 100%
rename from conf/rest/9.11.0/qtree.yaml
rename to conf/rest/9.12.0/qtree.yaml
diff --git a/conf/rest/9.11.0/sensor.yaml b/conf/rest/9.12.0/sensor.yaml
similarity index 90%
rename from conf/rest/9.11.0/sensor.yaml
rename to conf/rest/9.12.0/sensor.yaml
index 9fc2a131..8bb03d85 100644
--- a/conf/rest/9.11.0/sensor.yaml
+++ b/conf/rest/9.12.0/sensor.yaml
@@ -12,7 +12,7 @@ counters:
   - ^discrete_value  => discrete_value
   - ^threshold_state  => threshold_state
   - ^value_units => unit
-  - value  => threshold_value
+  - threshold_value  => threshold_value
   - critical_high_threshold => critical_high
   - critical_low_threshold  => critical_low
 
diff --git a/conf/rest/9.11.0/shelf.yaml b/conf/rest/9.12.0/shelf.yaml
similarity index 100%
rename from conf/rest/9.11.0/shelf.yaml
rename to conf/rest/9.12.0/shelf.yaml
diff --git a/conf/rest/9.12.0/snapmirror.yaml b/conf/rest/9.12.0/snapmirror.yaml
new file mode 100644
index 00000000..ca3b2d73
--- /dev/null
+++ b/conf/rest/9.12.0/snapmirror.yaml
@@ -0,0 +1,64 @@
+# As private cli api has all the fields/metrics which we would need for our use-case, So using private api instead of public.
+
+name:                         SnapMirror
+query:                        api/private/cli/snapmirror
+object:                       snapmirror
+
+counters:
+  - ^^relationship_id => relationship_id
+  - ^destination_path => destination_location
+  - ^destination_volume_node => destination_node
+  - ^destination_vserver => destination_vserver
+  - ^destination_volume => destination_volume
+  - ^source_vserver => source_vserver
+  - ^source_volume => source_volume
+  - ^type => relationship_type
+  - ^state => relationship_status
+  - ^policy_type => policy_type
+  - ^relationship_group_type  => group_type
+  - ^schedule => schedule
+  - ^healthy => healthy
+  - ^last_transfer_type => last_transfer_type
+  - ^unhealthy_reason => unhealthy_reason
+  - lag_time(duration) => lag_time
+  - last_transfer_duration(duration) => last_transfer_duration
+  - last_transfer_end_timestamp(timestamp) => last_transfer_end_timestamp
+  - last_transfer_size => last_transfer_size
+  - total_transfer_time_secs => total_transfer_time_secs
+  - total_transfer_bytes => total_transfer_bytes
+  - newest_snapshot_timestamp(timestamp) => newest_snapshot_timestamp
+  - resync_successful_count => resync_successful_count
+  - update_successful_count => update_successful_count
+  - resync_failed_count => resync_failed_count
+  - update_failed_count => update_failed_count
+  - break_successful_count => break_successful_count
+  - break_failed_count => break_failed_count
+
+
+plugins:
+  - Snapmirror
+
+export_options:
+  instance_keys:
+    - relationship_id
+    - destination_volume
+    - destination_node
+    - destination_vserver
+    - source_volume
+    - source_vserver
+    - source_node
+  instance_labels:
+    - healthy
+    - unhealthy_reason
+    - last_transfer_type
+    - relationship_status
+    - relationship_type
+    - schedule
+#    - destination_node_limit
+#    - source_node_limit
+    - group_type
+    - destination_location
+    - protectedBy
+    - protectionSourceType
+    - policy_type
+    - derived_relationship_type
diff --git a/conf/rest/9.11.0/snapshot.yaml b/conf/rest/9.12.0/snapshot.yaml
similarity index 100%
rename from conf/rest/9.11.0/snapshot.yaml
rename to conf/rest/9.12.0/snapshot.yaml
diff --git a/conf/rest/9.11.0/status.yaml b/conf/rest/9.12.0/status.yaml
similarity index 89%
rename from conf/rest/9.11.0/status.yaml
rename to conf/rest/9.12.0/status.yaml
index ce669ae5..dfb2f860 100644
--- a/conf/rest/9.11.0/status.yaml
+++ b/conf/rest/9.12.0/status.yaml
@@ -2,11 +2,11 @@
 name:       Status
 query:      api/cluster
 object:     cluster
-fields:
-  - health
 
 counters:
   - ^health => status
+  - hidden_fields:
+      - health
 
 collect_only_labels: true
 only_cluster_instance: true
diff --git a/conf/rest/9.11.0/subsystem.yaml b/conf/rest/9.12.0/subsystem.yaml
similarity index 100%
rename from conf/rest/9.11.0/subsystem.yaml
rename to conf/rest/9.12.0/subsystem.yaml
diff --git a/conf/rest/9.11.0/volume.yaml b/conf/rest/9.12.0/volume.yaml
similarity index 94%
rename from conf/rest/9.11.0/volume.yaml
rename to conf/rest/9.12.0/volume.yaml
index a6674f1a..318c6069 100644
--- a/conf/rest/9.11.0/volume.yaml
+++ b/conf/rest/9.12.0/volume.yaml
@@ -54,6 +54,9 @@ endpoints:
       - files_used => inode_files_used
 
 plugins:
+  - Volume:
+      schedule:
+        - data: 900s  # should be multiple of data poll duration
   - LabelAgent:
       value_to_num:
         - new_status state online online `0`
@@ -76,5 +79,9 @@ export_options:
     - is_sis_volume
     - snapshot_policy
     - type
+    - protectedByStatus
+    - protectedBy
+    - protectionRole
+    - all_sm_healthy
 
 
diff --git a/pkg/util/util.go b/pkg/util/util.go
index e3e016dc..0e2e4093 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -328,17 +328,20 @@ func Intersection(a []string, b []string) ([]string, []string) {
 	return matches, misses
 }
 
-// ParseMetric parses display name and type of field from the raw name of the metric as defined in (sub)template.
+// ParseMetric parses display name and type of field and metric type from the raw name of the metric as defined in (sub)template.
 // Users can rename a metric with "=>" (e.g. some_long_metric_name => short).
 // Trailing "^" characters are ignored/cleaned as they have special meaning in some collectors.
-func ParseMetric(rawName string) (string, string, string) {
+func ParseMetric(rawName string) (string, string, string, string) {
 	var (
 		name, display string
 		values        []string
 	)
+	metricType := ""
+	// Ex: last_transfer_duration(duration) => last_transfer_duration
 	if values = strings.SplitN(rawName, "=>", 2); len(values) == 2 {
 		name = strings.TrimSpace(values[0])
 		display = strings.TrimSpace(values[1])
+		name, metricType = ParseMetricType(name)
 	} else {
 		name = rawName
 		display = strings.ReplaceAll(rawName, ".", "_")
@@ -346,16 +349,24 @@ func ParseMetric(rawName string) (string, string, string) {
 	}
 
 	if strings.HasPrefix(name, "^^") {
-		return strings.TrimPrefix(name, "^^"), strings.TrimPrefix(display, "^^"), "key"
+		return strings.TrimPrefix(name, "^^"), strings.TrimPrefix(display, "^^"), "key", ""
 	}
 
 	if strings.HasPrefix(name, "^") {
-		return strings.TrimPrefix(name, "^"), strings.TrimPrefix(display, "^"), "label"
+		return strings.TrimPrefix(name, "^"), strings.TrimPrefix(display, "^"), "label", ""
 	}
 
-	if strings.HasPrefix(name, "?") {
-		return strings.TrimPrefix(name, "?"), strings.TrimPrefix(display, "?"), "bool"
-	}
+	return name, display, "float", metricType
+}
 
-	return name, display, "float"
+func ParseMetricType(metricName string) (string, string) {
+	metricTypeRegex := regexp.MustCompile(`(.*)\((.*?)\)`)
+	match := metricTypeRegex.FindAllStringSubmatch(metricName, -1)
+	if match != nil {
+		// For last_transfer_duration(duration), name would have 'last_transfer_duration' and metricType would have 'duration'.
+		name := match[0][1]
+		metricType := match[0][2]
+		return name, metricType
+	}
+	return metricName, ""
 }


Test patch:
diff --git a/cmd/collectors/rest/plugins/util_test.go b/cmd/collectors/rest/plugins/util_test.go
new file mode 100644
index 00000000..d5f17f3e
--- /dev/null
+++ b/cmd/collectors/rest/plugins/util_test.go
@@ -0,0 +1,225 @@
+package plugins
+
+import (
+	"goharvest2/pkg/matrix"
+	"testing"
+)
+
+func TestUpdateProtectedFields(t *testing.T) {
+	instance := matrix.NewInstance(0)
+
+	// Test cases for protectedBy and protectionSourceType
+	testWithoutGroupType(instance, t)
+	testSvmdr(instance, t)
+	testConstituentVolumeWithinSvmdr(instance, t)
+	testCg(instance, t)
+	testConstituentVolumeWithinCg(instance, t)
+	testNegativeCase1(instance, t)
+	testNegativeCase2(instance, t)
+	testGroupTypeNone(instance, t)
+	testGroupTypeFlexgroup(instance, t)
+
+	// Test cases for derived_relationship_type
+	testStrictSyncMirror(instance, t)
+	testSyncMirror(instance, t)
+	testMirrorVault(instance, t)
+	testAutomatedFailover(instance, t)
+	testOtherPolicyType(instance, t)
+	testWithNoPolicyType(instance, t)
+	testWithNoPolicyTypeNoRelationshipType(instance, t)
+}
+
+// Test cases for protectedBy and protectionSourceType
+func testWithoutGroupType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "" && instance.GetLabel("protectionSourceType") == "" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected empty  and protectionSourceType= %s, expected empty", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testSvmdr(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "vserver")
+	instance.SetLabel("destination_volume", "")
+	instance.SetLabel("source_volume", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "storage_vm" && instance.GetLabel("protectionSourceType") == "storage_vm" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: storage_vm and protectionSourceType= %s, expected: storage_vm", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testConstituentVolumeWithinSvmdr(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "vserver")
+	instance.SetLabel("destination_volume", "destvol")
+	instance.SetLabel("source_volume", "sourcevol")
+	instance.SetLabel("destination_location", "test1")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "storage_vm" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: storage_vm and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testCg(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "CONSISTENCYGROUP")
+	instance.SetLabel("destination_location", "test123:/cg/")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "cg" && instance.GetLabel("protectionSourceType") == "cg" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: cg and protectionSourceType= %s, expected: cg", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testConstituentVolumeWithinCg(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "CONSISTENCYGROUP")
+	instance.SetLabel("destination_location", "test123")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "cg" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: cg and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testNegativeCase1(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "infinitevol")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "not_mapped" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: not_mapped", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testNegativeCase2(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "vserver")
+	instance.SetLabel("destination_volume", "destvol")
+	instance.SetLabel("source_volume", "sourcevol")
+	instance.SetLabel("destination_location", "test123:")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "not_mapped" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: not_mapped", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testGroupTypeNone(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "none")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testGroupTypeFlexgroup(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "flexgroup")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+// Test cases for derived_relationship_type
+func testStrictSyncMirror(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "strict_sync_mirror")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "sync_mirror_strict" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: sync_mirror_strict", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testSyncMirror(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "sync_mirror")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "sync_mirror" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: sync_mirror", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testMirrorVault(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "mirror_vault")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "mirror_vault" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: mirror_vault", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testAutomatedFailover(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "automated_failover")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "sync_mirror" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: sync_mirror", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testOtherPolicyType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "vault")
+	instance.SetLabel("policy_type", "vault")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "vault" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: vault", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testWithNoPolicyType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "extended_data_protection")
+	instance.SetLabel("policy_type", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "extended_data_protection" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: extended_data_protection", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testWithNoPolicyTypeNoRelationshipType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: \"\"(empty)", instance.GetLabel("derived_relationship_type"))
+	}
+}
diff --git a/cmd/collectors/rest/templating_test.go b/cmd/collectors/rest/templating_test.go
new file mode 100644
index 00000000..7fd78df4
--- /dev/null
+++ b/cmd/collectors/rest/templating_test.go
@@ -0,0 +1,67 @@
+package rest
+
+import (
+	"testing"
+)
+
+func Test_HandleDuration(t *testing.T) {
+
+	type test struct {
+		timeFieldValue string
+		want           float64
+	}
+
+	var tests = []test{
+		{
+			timeFieldValue: "PT54S",
+			want:           54,
+		},
+		{
+			timeFieldValue: "PT48M",
+			want:           2880,
+		},
+		{
+			timeFieldValue: "P428DT22H45M19S",
+			want:           37061119,
+		},
+		{
+			timeFieldValue: "PT8H35M42S",
+			want:           30942,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.timeFieldValue, func(t *testing.T) {
+			if got := HandleDuration(tt.timeFieldValue); got != tt.want {
+				t.Errorf("actual value = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_HandleTimestamp(t *testing.T) {
+
+	type test struct {
+		timeFieldValue string
+		want           float64
+	}
+
+	var tests = []test{
+		{
+			timeFieldValue: "2020-12-02T18:36:19-08:00",
+			want:           1606962979,
+		},
+		{
+			timeFieldValue: "2022-01-31T04:05:02-05:00",
+			want:           1643619902,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.timeFieldValue, func(t *testing.T) {
+			if got := HandleTimestamp(tt.timeFieldValue); got != tt.want {
+				t.Errorf("actual value = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go
index ba91914d..05ff60be 100644
--- a/pkg/util/util_test.go
+++ b/pkg/util/util_test.go
@@ -42,3 +42,46 @@ func TestIntersection(t *testing.T) {
 		})
 	}
 }
+
+func TestParseMetricType(t *testing.T) {
+
+	type test struct {
+		metricName         string
+		expectedName       string
+		expectedMetricType string
+	}
+
+	tests := []test{
+		{
+			metricName:         "last_transfer_duration(duration)",
+			expectedName:       "last_transfer_duration",
+			expectedMetricType: "duration",
+		},
+		{
+			metricName:         "newest_snapshot_timestamp(timestamp)",
+			expectedName:       "newest_snapshot_timestamp",
+			expectedMetricType: "timestamp",
+		},
+		{
+			metricName:         "resync_successful_count",
+			expectedName:       "resync_successful_count",
+			expectedMetricType: "",
+		},
+		{
+			metricName:         "total_transfer_bytes()",
+			expectedName:       "total_transfer_bytes",
+			expectedMetricType: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.metricName, func(t *testing.T) {
+			name, metricType := ParseMetricType(tt.metricName)
+			if name != tt.expectedName {
+				t.Errorf("metricname not matching, actual name = %v, want %v", name, tt.expectedName)
+			}
+			if metricType != tt.expectedMetricType {
+				t.Errorf("metrictype not matching, actual metric type = %v, want %v", metricType, tt.expectedMetricType)
+			}
+		})
+	}
+}