diff --git a/.chloggen/vcenterreceiver-vm-vsan.yaml b/.chloggen/vcenterreceiver-vm-vsan.yaml new file mode 100644 index 000000000000..cf27a787d552 --- /dev/null +++ b/.chloggen/vcenterreceiver-vm-vsan.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'enhancement' + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: 'vcenterreceiver' + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Adds a number of default disabled vSAN metrics for Virtual Machines. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [33556] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/.gitignore b/.gitignore index 52616038f614..8e73986606cd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ dist/ # Miscellaneous files *.sw[op] *.DS_Store +__debug_bin* # Coverage coverage/* diff --git a/cmd/otelcontribcol/go.sum b/cmd/otelcontribcol/go.sum index 10b46bf08406..81a7bf63c487 100644 --- a/cmd/otelcontribcol/go.sum +++ b/cmd/otelcontribcol/go.sum @@ -1217,6 +1217,8 @@ github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02 h1:tR3jsKPiO/mb6ntzk/dJlHZtm37CPfVp1C9KIo534+4= +github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02/go.mod h1:7NQ3kWOx2cZOSjtcveTa5nqupVr2s6/83sG+rTlI7uA= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= diff --git a/receiver/vcenterreceiver/client.go b/receiver/vcenterreceiver/client.go index ea4b873aab10..3c584e16d87d 100644 --- a/receiver/vcenterreceiver/client.go +++ b/receiver/vcenterreceiver/client.go @@ -7,35 +7,45 @@ import ( "context" "errors" "fmt" + "maps" "net/url" + "reflect" + "strconv" + "strings" + "time" "github.com/vmware/govmomi" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/performance" - "github.com/vmware/govmomi/property" "github.com/vmware/govmomi/view" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" vt "github.com/vmware/govmomi/vim25/types" + "github.com/vmware/govmomi/vsan" + "github.com/vmware/govmomi/vsan/types" + "go.uber.org/zap" ) // vcenterClient is a client that collects data from a vCenter endpoint. type vcenterClient struct { - moClient *govmomi.Client - vimDriver *vim25.Client - finder *find.Finder - pc *property.Collector - pm *performance.Manager - vm *view.Manager - cfg *Config + logger *zap.Logger + moClient *govmomi.Client + vimDriver *vim25.Client + vsanDriver *vsan.Client + finder *find.Finder + pm *performance.Manager + vm *view.Manager + cfg *Config } var newVcenterClient = defaultNewVcenterClient -func defaultNewVcenterClient(c *Config) *vcenterClient { +func defaultNewVcenterClient(l *zap.Logger, c *Config) *vcenterClient { return &vcenterClient{ - cfg: c, + logger: l, + cfg: c, } } @@ -70,10 +80,15 @@ func (vc *vcenterClient) EnsureConnection(ctx context.Context) error { } vc.moClient = client vc.vimDriver = client.Client - vc.pc = property.DefaultCollector(vc.vimDriver) vc.finder = find.NewFinder(vc.vimDriver) vc.pm = performance.NewManager(vc.vimDriver) vc.vm = view.NewManager(vc.vimDriver) + vsanDriver, err := vsan.NewClient(ctx, vc.vimDriver) + if err != nil { + vc.logger.Info(fmt.Errorf("could not create VSAN client: %w", err).Error()) + } else { + vc.vsanDriver = vsanDriver + } return nil } @@ -320,3 +335,240 @@ func (vc *vcenterClient) PerfMetricsQuery( resultsByRef: resultsByRef, }, nil } + +// VSANQueryResults contains all returned vSAN metric related data +type VSANQueryResults struct { + // Contains vSAN metric data keyed by UUID string + MetricResultsByUUID map[string]*VSANMetricResults +} + +// VSANMetricResults contains vSAN metric related data for a single resource +type VSANMetricResults struct { + // Contains UUID info for related resource + UUID string + // Contains returned metric value info for all metrics + MetricDetails []*VSANMetricDetails +} + +// VSANMetricDetails contains vSAN metric data for a single metric +type VSANMetricDetails struct { + // Contains the metric label + MetricLabel string + // Contains the metric interval in seconds + Interval int32 + // Contains timestamps for all metric values + Timestamps []*time.Time + // Contains all values for vSAN metric label + Values []int64 +} + +// vSANQueryType represents the type of VSAN query +type vSANQueryType string + +const ( + VSANQueryTypeVirtualMachines vSANQueryType = "virtual-machine:*" +) + +// getLabelsForQueryType returns the appropriate labels for each query type +func (vc *vcenterClient) getLabelsForQueryType(queryType vSANQueryType) []string { + switch queryType { + case VSANQueryTypeVirtualMachines: + return []string{ + "iopsRead", "iopsWrite", "throughputRead", "throughputWrite", + "latencyRead", "latencyWrite", + } + default: + return []string{} + } +} + +// VSANVirtualMachines returns back virtual machine vSAN performance metrics +func (vc *vcenterClient) VSANVirtualMachines( + ctx context.Context, + clusterRefs []*vt.ManagedObjectReference, +) (*VSANQueryResults, error) { + results, err := vc.vSANQuery(ctx, VSANQueryTypeVirtualMachines, clusterRefs) + err = vc.handleVSANError(err, VSANQueryTypeVirtualMachines) + return results, err +} + +// vSANQuery performs a vSAN query for the specified type across all clusters +func (vc *vcenterClient) vSANQuery( + ctx context.Context, + queryType vSANQueryType, + clusterRefs []*vt.ManagedObjectReference, +) (*VSANQueryResults, error) { + allResults := VSANQueryResults{ + MetricResultsByUUID: map[string]*VSANMetricResults{}, + } + + for _, clusterRef := range clusterRefs { + results, err := vc.vSANQueryByCluster(ctx, queryType, clusterRef) + if err != nil { + return &allResults, err + } + + maps.Copy(allResults.MetricResultsByUUID, results.MetricResultsByUUID) + } + + return &allResults, nil +} + +// vSANQueryByCluster performs a vSAN query for the specified type for one cluster +func (vc *vcenterClient) vSANQueryByCluster( + ctx context.Context, + queryType vSANQueryType, + clusterRef *vt.ManagedObjectReference, +) (*VSANQueryResults, error) { + queryResults := VSANQueryResults{ + MetricResultsByUUID: map[string]*VSANMetricResults{}, + } + // Not all vCenters support vSAN so just return an empty result + if vc.vsanDriver == nil { + return &queryResults, nil + } + + now := time.Now() + querySpec := []types.VsanPerfQuerySpec{ + { + EntityRefId: string(queryType), + StartTime: &now, + EndTime: &now, + Labels: vc.getLabelsForQueryType(queryType), + }, + } + rawResults, err := vc.vsanDriver.VsanPerfQueryPerf(ctx, clusterRef, querySpec) + if err != nil { + return nil, fmt.Errorf("problem retrieving %s vSAN metrics for cluster %s: %w", queryType, clusterRef.Value, err) + } + + queryResults.MetricResultsByUUID = map[string]*VSANMetricResults{} + for _, rawResult := range rawResults { + metricResults, err := vc.convertVSANResultToMetricResults(rawResult) + if err != nil && metricResults != nil { + return &queryResults, fmt.Errorf("problem processing %s [%s] vSAN metrics for cluster %s: %w", queryType, metricResults.UUID, clusterRef.Value, err) + } + if err != nil { + return &queryResults, fmt.Errorf("problem processing %s vSAN metrics for cluster %s: %w", queryType, clusterRef.Value, err) + } + + queryResults.MetricResultsByUUID[metricResults.UUID] = metricResults + } + return &queryResults, nil +} + +func (vc *vcenterClient) handleVSANError( + err error, + queryType vSANQueryType, +) error { + faultErr := errors.Unwrap(err) + if faultErr == nil { + return err + } + if !soap.IsSoapFault(faultErr) { + return err + } + + fault := soap.ToSoapFault(faultErr) + msg := fault.String + + if fault.Detail.Fault != nil { + msg = reflect.TypeOf(fault.Detail.Fault).Name() + } + switch msg { + case "NotSupported": + vc.logger.Debug(fmt.Sprintf("%s vSAN metrics not supported: %s", queryType, err.Error())) + return nil + case "NotFound": + vc.logger.Debug(fmt.Sprintf("no %s vSAN metrics found: %s", queryType, err.Error())) + return nil + default: + return err + } +} + +func (vc *vcenterClient) convertVSANResultToMetricResults(vSANResult types.VsanPerfEntityMetricCSV) (*VSANMetricResults, error) { + uuid, err := vc.uuidFromEntityRefID(vSANResult.EntityRefId) + if err != nil { + return nil, err + } + + metricResults := VSANMetricResults{ + UUID: uuid, + MetricDetails: []*VSANMetricDetails{}, + } + + // Parse all timestamps + localZone, _ := time.Now().Local().Zone() + timeStrings := strings.Split(vSANResult.SampleInfo, ",") + timestamps := []time.Time{} + for _, timeString := range timeStrings { + // Assuming the collector is making the request in the same time zone as the localized response + // from the vSAN API. Not a great assumption, but otherwise it will almost definitely be wrong + // if we assume that it is UTC. There is precedent for this method at least. + timestamp, err := time.Parse("2006-01-02 15:04:05 MST", fmt.Sprintf("%s %s", timeString, localZone)) + if err != nil { + return &metricResults, fmt.Errorf("problem parsing timestamp from %s: %w", timeString, err) + } + + timestamps = append(timestamps, timestamp) + } + + // Parse all metrics + for _, vSANValue := range vSANResult.Value { + metricDetails, err := vc.convertVSANValueToMetricDetails(vSANValue, timestamps) + if err != nil { + return &metricResults, err + } + + metricResults.MetricDetails = append(metricResults.MetricDetails, metricDetails) + } + return &metricResults, nil +} + +func (vc *vcenterClient) convertVSANValueToMetricDetails( + vSANValue types.VsanPerfMetricSeriesCSV, + timestamps []time.Time, +) (*VSANMetricDetails, error) { + metricLabel := vSANValue.MetricId.Label + metricInterval := vSANValue.MetricId.MetricsCollectInterval + // If not found assume the interval is 5m + if metricInterval == 0 { + vc.logger.Warn(fmt.Sprintf("no interval found for vSAN metric [%s] so assuming 5m", metricLabel)) + metricInterval = 300 + } + metricDetails := VSANMetricDetails{ + MetricLabel: metricLabel, + Interval: metricInterval, + Timestamps: []*time.Time{}, + Values: []int64{}, + } + valueStrings := strings.Split(vSANValue.Values, ",") + if len(valueStrings) != len(timestamps) { + return nil, fmt.Errorf("number of timestamps [%d] doesn't match number of values [%d] for metric %s", len(timestamps), len(valueStrings), metricLabel) + } + + // Match up timestamps with metric values + for i, valueString := range valueStrings { + value, err := strconv.ParseInt(valueString, 10, 64) + if err != nil { + return nil, fmt.Errorf("problem converting value [%s] for metric %s", valueString, metricLabel) + } + + metricDetails.Timestamps = append(metricDetails.Timestamps, ×tamps[i]) + metricDetails.Values = append(metricDetails.Values, value) + } + + return &metricDetails, nil +} + +// uuidFromEntityRefID returns the UUID portion of the EntityRefId +func (vc *vcenterClient) uuidFromEntityRefID(id string) (string, error) { + colonIndex := strings.Index(id, ":") + if colonIndex != -1 { + uuid := id[colonIndex+1:] + return uuid, nil + } + + return "", fmt.Errorf("no ':' found in EntityRefId [%s] to parse UUID", id) +} diff --git a/receiver/vcenterreceiver/documentation.md b/receiver/vcenterreceiver/documentation.md index 762417ef7069..55dd1bc8a23c 100644 --- a/receiver/vcenterreceiver/documentation.md +++ b/receiver/vcenterreceiver/documentation.md @@ -642,6 +642,58 @@ As measured over the most recent 20s interval. | ---- | ----------- | ------ | | object | The object on the virtual machine or host that is being reported on. | Any Str | +## Optional Metrics + +The following metrics are not emitted by default. Each of them can be enabled by applying the following configuration: + +```yaml +metrics: + : + enabled: true +``` + +### vcenter.vm.vsan.latency.avg + +The virtual machine latency while accessing vSAN storage. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| us | Gauge | Int | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| type | The type of vSAN latency. | Str: ``read``, ``write`` | + +### vcenter.vm.vsan.operations + +The vSAN IOPs of a virtual machine. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| {operations/sec} | Gauge | Int | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| type | The type of vSAN operation. | Str: ``read``, ``write``, ``unmap`` | + +### vcenter.vm.vsan.throughput + +The vSAN throughput of a virtual machine. + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| By/s | Gauge | Double | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| direction | The type of vSAN throughput. | Str: ``read``, ``write`` | + ## Resource Attributes | Name | Description | Values | Enabled | diff --git a/receiver/vcenterreceiver/go.sum b/receiver/vcenterreceiver/go.sum index 15b52ba1e098..4d1976876cac 100644 --- a/receiver/vcenterreceiver/go.sum +++ b/receiver/vcenterreceiver/go.sum @@ -37,6 +37,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02 h1:tR3jsKPiO/mb6ntzk/dJlHZtm37CPfVp1C9KIo534+4= +github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02/go.mod h1:7NQ3kWOx2cZOSjtcveTa5nqupVr2s6/83sG+rTlI7uA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= diff --git a/receiver/vcenterreceiver/integration_test.go b/receiver/vcenterreceiver/integration_test.go index 7ab2fa40c958..13d2e9f3c74c 100644 --- a/receiver/vcenterreceiver/integration_test.go +++ b/receiver/vcenterreceiver/integration_test.go @@ -21,6 +21,7 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/config/configtls" + "go.uber.org/zap" "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/scraperinttest" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest" @@ -77,9 +78,10 @@ func TestIntegration(t *testing.T) { require.True(t, set) s := session.NewManager(c) - newVcenterClient = func(cfg *Config) *vcenterClient { + newVcenterClient = func(l *zap.Logger, cfg *Config) *vcenterClient { client := &vcenterClient{ - cfg: cfg, + logger: l, + cfg: cfg, moClient: &govmomi.Client{ Client: c, SessionManager: s, @@ -88,10 +90,11 @@ func TestIntegration(t *testing.T) { require.NoError(t, client.EnsureConnection(context.Background())) client.vimDriver = c client.finder = find.NewFinder(c) - // Performance metrics rely on time based publishing so this is inherently flaky for an + // Performance/vSAN metrics rely on time based publishing so this is inherently flaky for an // integration test, so setting the performance manager to nil to not attempt to compare // performance metrics. Coverage for this is encompassed in ./scraper_test.go client.pm = nil + client.vsanDriver = nil return client } defer func() { diff --git a/receiver/vcenterreceiver/internal/metadata/generated_config.go b/receiver/vcenterreceiver/internal/metadata/generated_config.go index 309a7c372ce6..2c2951990416 100644 --- a/receiver/vcenterreceiver/internal/metadata/generated_config.go +++ b/receiver/vcenterreceiver/internal/metadata/generated_config.go @@ -82,6 +82,9 @@ type MetricsConfig struct { VcenterVMNetworkPacketRate MetricConfig `mapstructure:"vcenter.vm.network.packet.rate"` VcenterVMNetworkThroughput MetricConfig `mapstructure:"vcenter.vm.network.throughput"` VcenterVMNetworkUsage MetricConfig `mapstructure:"vcenter.vm.network.usage"` + VcenterVMVsanLatencyAvg MetricConfig `mapstructure:"vcenter.vm.vsan.latency.avg"` + VcenterVMVsanOperations MetricConfig `mapstructure:"vcenter.vm.vsan.operations"` + VcenterVMVsanThroughput MetricConfig `mapstructure:"vcenter.vm.vsan.throughput"` } func DefaultMetricsConfig() MetricsConfig { @@ -248,6 +251,15 @@ func DefaultMetricsConfig() MetricsConfig { VcenterVMNetworkUsage: MetricConfig{ Enabled: true, }, + VcenterVMVsanLatencyAvg: MetricConfig{ + Enabled: false, + }, + VcenterVMVsanOperations: MetricConfig{ + Enabled: false, + }, + VcenterVMVsanThroughput: MetricConfig{ + Enabled: false, + }, } } diff --git a/receiver/vcenterreceiver/internal/metadata/generated_config_test.go b/receiver/vcenterreceiver/internal/metadata/generated_config_test.go index 33b8803cca26..62a041715759 100644 --- a/receiver/vcenterreceiver/internal/metadata/generated_config_test.go +++ b/receiver/vcenterreceiver/internal/metadata/generated_config_test.go @@ -79,6 +79,9 @@ func TestMetricsBuilderConfig(t *testing.T) { VcenterVMNetworkPacketRate: MetricConfig{Enabled: true}, VcenterVMNetworkThroughput: MetricConfig{Enabled: true}, VcenterVMNetworkUsage: MetricConfig{Enabled: true}, + VcenterVMVsanLatencyAvg: MetricConfig{Enabled: true}, + VcenterVMVsanOperations: MetricConfig{Enabled: true}, + VcenterVMVsanThroughput: MetricConfig{Enabled: true}, }, ResourceAttributes: ResourceAttributesConfig{ VcenterClusterName: ResourceAttributeConfig{Enabled: true}, @@ -154,6 +157,9 @@ func TestMetricsBuilderConfig(t *testing.T) { VcenterVMNetworkPacketRate: MetricConfig{Enabled: false}, VcenterVMNetworkThroughput: MetricConfig{Enabled: false}, VcenterVMNetworkUsage: MetricConfig{Enabled: false}, + VcenterVMVsanLatencyAvg: MetricConfig{Enabled: false}, + VcenterVMVsanOperations: MetricConfig{Enabled: false}, + VcenterVMVsanThroughput: MetricConfig{Enabled: false}, }, ResourceAttributes: ResourceAttributesConfig{ VcenterClusterName: ResourceAttributeConfig{Enabled: false}, diff --git a/receiver/vcenterreceiver/internal/metadata/generated_metrics.go b/receiver/vcenterreceiver/internal/metadata/generated_metrics.go index 9f58299f2867..aa60a223dd69 100644 --- a/receiver/vcenterreceiver/internal/metadata/generated_metrics.go +++ b/receiver/vcenterreceiver/internal/metadata/generated_metrics.go @@ -300,6 +300,88 @@ var MapAttributeVMCountPowerState = map[string]AttributeVMCountPowerState{ "unknown": AttributeVMCountPowerStateUnknown, } +// AttributeVsanLatencyType specifies the a value vsan_latency_type attribute. +type AttributeVsanLatencyType int + +const ( + _ AttributeVsanLatencyType = iota + AttributeVsanLatencyTypeRead + AttributeVsanLatencyTypeWrite +) + +// String returns the string representation of the AttributeVsanLatencyType. +func (av AttributeVsanLatencyType) String() string { + switch av { + case AttributeVsanLatencyTypeRead: + return "read" + case AttributeVsanLatencyTypeWrite: + return "write" + } + return "" +} + +// MapAttributeVsanLatencyType is a helper map of string to AttributeVsanLatencyType attribute value. +var MapAttributeVsanLatencyType = map[string]AttributeVsanLatencyType{ + "read": AttributeVsanLatencyTypeRead, + "write": AttributeVsanLatencyTypeWrite, +} + +// AttributeVsanOperationType specifies the a value vsan_operation_type attribute. +type AttributeVsanOperationType int + +const ( + _ AttributeVsanOperationType = iota + AttributeVsanOperationTypeRead + AttributeVsanOperationTypeWrite + AttributeVsanOperationTypeUnmap +) + +// String returns the string representation of the AttributeVsanOperationType. +func (av AttributeVsanOperationType) String() string { + switch av { + case AttributeVsanOperationTypeRead: + return "read" + case AttributeVsanOperationTypeWrite: + return "write" + case AttributeVsanOperationTypeUnmap: + return "unmap" + } + return "" +} + +// MapAttributeVsanOperationType is a helper map of string to AttributeVsanOperationType attribute value. +var MapAttributeVsanOperationType = map[string]AttributeVsanOperationType{ + "read": AttributeVsanOperationTypeRead, + "write": AttributeVsanOperationTypeWrite, + "unmap": AttributeVsanOperationTypeUnmap, +} + +// AttributeVsanThroughputDirection specifies the a value vsan_throughput_direction attribute. +type AttributeVsanThroughputDirection int + +const ( + _ AttributeVsanThroughputDirection = iota + AttributeVsanThroughputDirectionRead + AttributeVsanThroughputDirectionWrite +) + +// String returns the string representation of the AttributeVsanThroughputDirection. +func (av AttributeVsanThroughputDirection) String() string { + switch av { + case AttributeVsanThroughputDirectionRead: + return "read" + case AttributeVsanThroughputDirectionWrite: + return "write" + } + return "" +} + +// MapAttributeVsanThroughputDirection is a helper map of string to AttributeVsanThroughputDirection attribute value. +var MapAttributeVsanThroughputDirection = map[string]AttributeVsanThroughputDirection{ + "read": AttributeVsanThroughputDirectionRead, + "write": AttributeVsanThroughputDirectionWrite, +} + type metricVcenterClusterCPUEffective struct { data pmetric.Metric // data buffer for generated metric. config MetricConfig // metric config provided by user. @@ -3086,6 +3168,159 @@ func newMetricVcenterVMNetworkUsage(cfg MetricConfig) metricVcenterVMNetworkUsag return m } +type metricVcenterVMVsanLatencyAvg struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills vcenter.vm.vsan.latency.avg metric with initial data. +func (m *metricVcenterVMVsanLatencyAvg) init() { + m.data.SetName("vcenter.vm.vsan.latency.avg") + m.data.SetDescription("The virtual machine latency while accessing vSAN storage.") + m.data.SetUnit("us") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricVcenterVMVsanLatencyAvg) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, vsanLatencyTypeAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) + dp.Attributes().PutStr("type", vsanLatencyTypeAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricVcenterVMVsanLatencyAvg) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricVcenterVMVsanLatencyAvg) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricVcenterVMVsanLatencyAvg(cfg MetricConfig) metricVcenterVMVsanLatencyAvg { + m := metricVcenterVMVsanLatencyAvg{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricVcenterVMVsanOperations struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills vcenter.vm.vsan.operations metric with initial data. +func (m *metricVcenterVMVsanOperations) init() { + m.data.SetName("vcenter.vm.vsan.operations") + m.data.SetDescription("The vSAN IOPs of a virtual machine.") + m.data.SetUnit("{operations/sec}") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricVcenterVMVsanOperations) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, vsanOperationTypeAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) + dp.Attributes().PutStr("type", vsanOperationTypeAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricVcenterVMVsanOperations) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricVcenterVMVsanOperations) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricVcenterVMVsanOperations(cfg MetricConfig) metricVcenterVMVsanOperations { + m := metricVcenterVMVsanOperations{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricVcenterVMVsanThroughput struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills vcenter.vm.vsan.throughput metric with initial data. +func (m *metricVcenterVMVsanThroughput) init() { + m.data.SetName("vcenter.vm.vsan.throughput") + m.data.SetDescription("The vSAN throughput of a virtual machine.") + m.data.SetUnit("By/s") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricVcenterVMVsanThroughput) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64, vsanThroughputDirectionAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) + dp.Attributes().PutStr("direction", vsanThroughputDirectionAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricVcenterVMVsanThroughput) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricVcenterVMVsanThroughput) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricVcenterVMVsanThroughput(cfg MetricConfig) metricVcenterVMVsanThroughput { + m := metricVcenterVMVsanThroughput{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + // MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations // required to produce metric representation defined in metadata and user config. type MetricsBuilder struct { @@ -3150,6 +3385,9 @@ type MetricsBuilder struct { metricVcenterVMNetworkPacketRate metricVcenterVMNetworkPacketRate metricVcenterVMNetworkThroughput metricVcenterVMNetworkThroughput metricVcenterVMNetworkUsage metricVcenterVMNetworkUsage + metricVcenterVMVsanLatencyAvg metricVcenterVMVsanLatencyAvg + metricVcenterVMVsanOperations metricVcenterVMVsanOperations + metricVcenterVMVsanThroughput metricVcenterVMVsanThroughput } // metricBuilderOption applies changes to default metrics builder. @@ -3163,6 +3401,15 @@ func WithStartTime(startTime pcommon.Timestamp) metricBuilderOption { } func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, options ...metricBuilderOption) *MetricsBuilder { + if !mbc.Metrics.VcenterVMVsanLatencyAvg.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `vcenter.vm.vsan.latency.avg`: this metric will be enabled by default starting in release v0.107.0") + } + if !mbc.Metrics.VcenterVMVsanOperations.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `vcenter.vm.vsan.operations`: this metric will be enabled by default starting in release v0.107.0") + } + if !mbc.Metrics.VcenterVMVsanThroughput.enabledSetByUser { + settings.Logger.Warn("[WARNING] Please set `enabled` field explicitly for `vcenter.vm.vsan.throughput`: this metric will be enabled by default starting in release v0.107.0") + } mb := &MetricsBuilder{ config: mbc, startTime: pcommon.NewTimestampFromTime(time.Now()), @@ -3222,6 +3469,9 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, opt metricVcenterVMNetworkPacketRate: newMetricVcenterVMNetworkPacketRate(mbc.Metrics.VcenterVMNetworkPacketRate), metricVcenterVMNetworkThroughput: newMetricVcenterVMNetworkThroughput(mbc.Metrics.VcenterVMNetworkThroughput), metricVcenterVMNetworkUsage: newMetricVcenterVMNetworkUsage(mbc.Metrics.VcenterVMNetworkUsage), + metricVcenterVMVsanLatencyAvg: newMetricVcenterVMVsanLatencyAvg(mbc.Metrics.VcenterVMVsanLatencyAvg), + metricVcenterVMVsanOperations: newMetricVcenterVMVsanOperations(mbc.Metrics.VcenterVMVsanOperations), + metricVcenterVMVsanThroughput: newMetricVcenterVMVsanThroughput(mbc.Metrics.VcenterVMVsanThroughput), resourceAttributeIncludeFilter: make(map[string]filter.Filter), resourceAttributeExcludeFilter: make(map[string]filter.Filter), } @@ -3412,6 +3662,9 @@ func (mb *MetricsBuilder) EmitForResource(rmo ...ResourceMetricsOption) { mb.metricVcenterVMNetworkPacketRate.emit(ils.Metrics()) mb.metricVcenterVMNetworkThroughput.emit(ils.Metrics()) mb.metricVcenterVMNetworkUsage.emit(ils.Metrics()) + mb.metricVcenterVMVsanLatencyAvg.emit(ils.Metrics()) + mb.metricVcenterVMVsanOperations.emit(ils.Metrics()) + mb.metricVcenterVMVsanThroughput.emit(ils.Metrics()) for _, op := range rmo { op(rm) @@ -3713,6 +3966,21 @@ func (mb *MetricsBuilder) RecordVcenterVMNetworkUsageDataPoint(ts pcommon.Timest mb.metricVcenterVMNetworkUsage.recordDataPoint(mb.startTime, ts, val, objectNameAttributeValue) } +// RecordVcenterVMVsanLatencyAvgDataPoint adds a data point to vcenter.vm.vsan.latency.avg metric. +func (mb *MetricsBuilder) RecordVcenterVMVsanLatencyAvgDataPoint(ts pcommon.Timestamp, val int64, vsanLatencyTypeAttributeValue AttributeVsanLatencyType) { + mb.metricVcenterVMVsanLatencyAvg.recordDataPoint(mb.startTime, ts, val, vsanLatencyTypeAttributeValue.String()) +} + +// RecordVcenterVMVsanOperationsDataPoint adds a data point to vcenter.vm.vsan.operations metric. +func (mb *MetricsBuilder) RecordVcenterVMVsanOperationsDataPoint(ts pcommon.Timestamp, val int64, vsanOperationTypeAttributeValue AttributeVsanOperationType) { + mb.metricVcenterVMVsanOperations.recordDataPoint(mb.startTime, ts, val, vsanOperationTypeAttributeValue.String()) +} + +// RecordVcenterVMVsanThroughputDataPoint adds a data point to vcenter.vm.vsan.throughput metric. +func (mb *MetricsBuilder) RecordVcenterVMVsanThroughputDataPoint(ts pcommon.Timestamp, val float64, vsanThroughputDirectionAttributeValue AttributeVsanThroughputDirection) { + mb.metricVcenterVMVsanThroughput.recordDataPoint(mb.startTime, ts, val, vsanThroughputDirectionAttributeValue.String()) +} + // Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, // and metrics builder should update its startTime and reset it's internal state accordingly. func (mb *MetricsBuilder) Reset(options ...metricBuilderOption) { diff --git a/receiver/vcenterreceiver/internal/metadata/generated_metrics_test.go b/receiver/vcenterreceiver/internal/metadata/generated_metrics_test.go index b2a973287351..c6c659c3af8b 100644 --- a/receiver/vcenterreceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/vcenterreceiver/internal/metadata/generated_metrics_test.go @@ -62,6 +62,18 @@ func TestMetricsBuilder(t *testing.T) { mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, test.name), settings, WithStartTime(start)) expectedWarnings := 0 + if test.metricsSet == testDataSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `vcenter.vm.vsan.latency.avg`: this metric will be enabled by default starting in release v0.107.0", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.metricsSet == testDataSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `vcenter.vm.vsan.operations`: this metric will be enabled by default starting in release v0.107.0", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } + if test.metricsSet == testDataSetDefault { + assert.Equal(t, "[WARNING] Please set `enabled` field explicitly for `vcenter.vm.vsan.throughput`: this metric will be enabled by default starting in release v0.107.0", observedLogs.All()[expectedWarnings].Message) + expectedWarnings++ + } assert.Equal(t, expectedWarnings, observedLogs.Len()) @@ -284,6 +296,15 @@ func TestMetricsBuilder(t *testing.T) { allMetricsCount++ mb.RecordVcenterVMNetworkUsageDataPoint(ts, 1, "object_name-val") + allMetricsCount++ + mb.RecordVcenterVMVsanLatencyAvgDataPoint(ts, 1, AttributeVsanLatencyTypeRead) + + allMetricsCount++ + mb.RecordVcenterVMVsanOperationsDataPoint(ts, 1, AttributeVsanOperationTypeRead) + + allMetricsCount++ + mb.RecordVcenterVMVsanThroughputDataPoint(ts, 1, AttributeVsanThroughputDirectionRead) + rb := mb.NewResourceBuilder() rb.SetVcenterClusterName("vcenter.cluster.name-val") rb.SetVcenterDatacenterName("vcenter.datacenter.name-val") @@ -1161,6 +1182,51 @@ func TestMetricsBuilder(t *testing.T) { attrVal, ok := dp.Attributes().Get("object") assert.True(t, ok) assert.EqualValues(t, "object_name-val", attrVal.Str()) + case "vcenter.vm.vsan.latency.avg": + assert.False(t, validatedMetrics["vcenter.vm.vsan.latency.avg"], "Found a duplicate in the metrics slice: vcenter.vm.vsan.latency.avg") + validatedMetrics["vcenter.vm.vsan.latency.avg"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "The virtual machine latency while accessing vSAN storage.", ms.At(i).Description()) + assert.Equal(t, "us", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + attrVal, ok := dp.Attributes().Get("type") + assert.True(t, ok) + assert.EqualValues(t, "read", attrVal.Str()) + case "vcenter.vm.vsan.operations": + assert.False(t, validatedMetrics["vcenter.vm.vsan.operations"], "Found a duplicate in the metrics slice: vcenter.vm.vsan.operations") + validatedMetrics["vcenter.vm.vsan.operations"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "The vSAN IOPs of a virtual machine.", ms.At(i).Description()) + assert.Equal(t, "{operations/sec}", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + attrVal, ok := dp.Attributes().Get("type") + assert.True(t, ok) + assert.EqualValues(t, "read", attrVal.Str()) + case "vcenter.vm.vsan.throughput": + assert.False(t, validatedMetrics["vcenter.vm.vsan.throughput"], "Found a duplicate in the metrics slice: vcenter.vm.vsan.throughput") + validatedMetrics["vcenter.vm.vsan.throughput"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "The vSAN throughput of a virtual machine.", ms.At(i).Description()) + assert.Equal(t, "By/s", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.Equal(t, float64(1), dp.DoubleValue()) + attrVal, ok := dp.Attributes().Get("direction") + assert.True(t, ok) + assert.EqualValues(t, "read", attrVal.Str()) } } }) diff --git a/receiver/vcenterreceiver/internal/metadata/testdata/config.yaml b/receiver/vcenterreceiver/internal/metadata/testdata/config.yaml index 521098442bbc..02a574a95b1b 100644 --- a/receiver/vcenterreceiver/internal/metadata/testdata/config.yaml +++ b/receiver/vcenterreceiver/internal/metadata/testdata/config.yaml @@ -109,6 +109,12 @@ all_set: enabled: true vcenter.vm.network.usage: enabled: true + vcenter.vm.vsan.latency.avg: + enabled: true + vcenter.vm.vsan.operations: + enabled: true + vcenter.vm.vsan.throughput: + enabled: true resource_attributes: vcenter.cluster.name: enabled: true @@ -244,6 +250,12 @@ none_set: enabled: false vcenter.vm.network.usage: enabled: false + vcenter.vm.vsan.latency.avg: + enabled: false + vcenter.vm.vsan.operations: + enabled: false + vcenter.vm.vsan.throughput: + enabled: false resource_attributes: vcenter.cluster.name: enabled: false diff --git a/receiver/vcenterreceiver/internal/mockserver/client_mock.go b/receiver/vcenterreceiver/internal/mockserver/client_mock.go index 467dfc21a78d..2992e786d34c 100644 --- a/receiver/vcenterreceiver/internal/mockserver/client_mock.go +++ b/receiver/vcenterreceiver/internal/mockserver/client_mock.go @@ -81,6 +81,10 @@ func routeBody(t *testing.T, requestType string, body map[string]any) ([]byte, e return routePerformanceQuery(t, body) case "CreateContainerView": return loadResponse("create-container-view.xml") + case "DestroyView": + return loadResponse("destroy-view.xml") + case "VsanPerfQueryPerf": + return routeVsanPerfQueryPerf(t, body) } return []byte{}, errNotFound @@ -207,6 +211,20 @@ func routePerformanceQuery(t *testing.T, body map[string]any) ([]byte, error) { return []byte{}, errNotFound } +func routeVsanPerfQueryPerf(t *testing.T, body map[string]any) ([]byte, error) { + queryPerf := body["VsanPerfQueryPerf"].(map[string]any) + require.NotNil(t, queryPerf) + querySpecs, ok := queryPerf["querySpecs"].(map[string]any) + if !ok { + return []byte{}, errNotFound + } + entityRefID := querySpecs["entityRefId"].(string) + if entityRefID == "virtual-machine:*" { + return loadResponse("vm-vsan.xml") + } + return []byte{}, errNotFound +} + func loadResponse(filename string) ([]byte, error) { return os.ReadFile(filepath.Join("internal", "mockserver", "responses", filename)) } diff --git a/receiver/vcenterreceiver/internal/mockserver/responses/destroy-view.xml b/receiver/vcenterreceiver/internal/mockserver/responses/destroy-view.xml new file mode 100644 index 000000000000..2f71aba2cdd6 --- /dev/null +++ b/receiver/vcenterreceiver/internal/mockserver/responses/destroy-view.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/receiver/vcenterreceiver/internal/mockserver/responses/vm-vsan.xml b/receiver/vcenterreceiver/internal/mockserver/responses/vm-vsan.xml new file mode 100644 index 000000000000..cf7dfaa98857 --- /dev/null +++ b/receiver/vcenterreceiver/internal/mockserver/responses/vm-vsan.xml @@ -0,0 +1,99 @@ + + + + + + virtual-machine:5000bbe0-993e-5813-c56a-198eaa62fb61 + 2022-05-24 15:45:00,2022-05-24 15:46:00 + + + + 300 + + 10,11 + + + + + 300 + + 12,13 + + + + + 300 + + 300,600 + + + + + 300 + + 900,1200 + + + + + 300 + + 14,15 + + + + + 300 + + 16,17 + + + + virtual-machine:5000bbe0-993e-5813-c56a-198eaa62fb62 + 2022-05-24 15:45:00,2022-05-24 15:46:00 + + + + 300 + + 20,21 + + + + + 300 + + 22,23 + + + + + 300 + + 1800,2100 + + + + + 300 + + 2400,2700 + + + + + 300 + + 24,25 + + + + + 300 + + 26,27 + + + + + diff --git a/receiver/vcenterreceiver/metadata.yaml b/receiver/vcenterreceiver/metadata.yaml index 1ada781aeafe..d47272cf6f2f 100644 --- a/receiver/vcenterreceiver/metadata.yaml +++ b/receiver/vcenterreceiver/metadata.yaml @@ -143,6 +143,28 @@ attributes: name_override: object description: The object on the virtual machine or host that is being reported on. type: string + vsan_throughput_direction: + name_override: direction + description: The type of vSAN throughput. + type: string + enum: + - read + - write + vsan_latency_type: + name_override: type + description: The type of vSAN latency. + type: string + enum: + - read + - write + vsan_operation_type: + name_override: type + description: The type of vSAN operation. + type: string + enum: + - read + - write + - unmap metrics: vcenter.datacenter.cluster.count: @@ -611,3 +633,30 @@ metrics: gauge: value_type: double attributes: [] + vcenter.vm.vsan.throughput: + enabled: false + description: The vSAN throughput of a virtual machine. + unit: "By/s" + gauge: + value_type: double + attributes: [vsan_throughput_direction] + warnings: + if_enabled_not_set: "this metric will be enabled by default starting in release v0.107.0" + vcenter.vm.vsan.operations: + enabled: false + description: The vSAN IOPs of a virtual machine. + unit: "{operations/sec}" + gauge: + value_type: int + attributes: [vsan_operation_type] + warnings: + if_enabled_not_set: "this metric will be enabled by default starting in release v0.107.0" + vcenter.vm.vsan.latency.avg: + enabled: false + description: The virtual machine latency while accessing vSAN storage. + unit: "us" + gauge: + value_type: int + attributes: [vsan_latency_type] + warnings: + if_enabled_not_set: "this metric will be enabled by default starting in release v0.107.0" diff --git a/receiver/vcenterreceiver/metrics.go b/receiver/vcenterreceiver/metrics.go index c9efdb0aa74b..f66ac50109ed 100644 --- a/receiver/vcenterreceiver/metrics.go +++ b/receiver/vcenterreceiver/metrics.go @@ -406,3 +406,28 @@ func (v *vcenterMetricScraper) recordVMPerformanceMetrics(entityMetric *performa } } } + +// recordVMVSANMetrics records vSAN metrics for a vSphere Virtual Machine +func (v *vcenterMetricScraper) recordVMVSANMetrics(vSANMetrics *VSANMetricResults) { + for _, metric := range vSANMetrics.MetricDetails { + for i, value := range metric.Values { + timestamp := metric.Timestamps[i] + switch metric.MetricLabel { + case "iopsRead": + v.mb.RecordVcenterVMVsanOperationsDataPoint(pcommon.NewTimestampFromTime(*timestamp), value, metadata.AttributeVsanOperationTypeRead) + case "iopsWrite": + v.mb.RecordVcenterVMVsanOperationsDataPoint(pcommon.NewTimestampFromTime(*timestamp), value, metadata.AttributeVsanOperationTypeWrite) + case "throughputRead": + readRate := float64(value) / float64(metric.Interval) + v.mb.RecordVcenterVMVsanThroughputDataPoint(pcommon.NewTimestampFromTime(*timestamp), readRate, metadata.AttributeVsanThroughputDirectionRead) + case "throughputWrite": + writeRate := float64(value) / float64(metric.Interval) + v.mb.RecordVcenterVMVsanThroughputDataPoint(pcommon.NewTimestampFromTime(*timestamp), writeRate, metadata.AttributeVsanThroughputDirectionWrite) + case "latencyRead": + v.mb.RecordVcenterVMVsanLatencyAvgDataPoint(pcommon.NewTimestampFromTime(*timestamp), value, metadata.AttributeVsanLatencyTypeRead) + case "latencyWrite": + v.mb.RecordVcenterVMVsanLatencyAvgDataPoint(pcommon.NewTimestampFromTime(*timestamp), value, metadata.AttributeVsanLatencyTypeWrite) + } + } + } +} diff --git a/receiver/vcenterreceiver/processors.go b/receiver/vcenterreceiver/processors.go index 4ee2f520cf18..b79da04f9271 100644 --- a/receiver/vcenterreceiver/processors.go +++ b/receiver/vcenterreceiver/processors.go @@ -317,11 +317,15 @@ func (v *vcenterMetricScraper) buildVMMetrics( } // Record VM metric data points - perfMetrics := v.scrapeData.vmPerfMetricsByRef[vm.Reference().Value] v.recordVMStats(ts, vm, hs) + perfMetrics := v.scrapeData.vmPerfMetricsByRef[vm.Reference().Value] if perfMetrics != nil { v.recordVMPerformanceMetrics(perfMetrics) } + vSANMetrics := v.scrapeData.vmVSANMetricsByUUID[vm.Config.InstanceUuid] + if vSANMetrics != nil { + v.recordVMVSANMetrics(vSANMetrics) + } v.mb.EmitForResource(metadata.WithResource(rb.Emit())) return crRef, groupInfo, err diff --git a/receiver/vcenterreceiver/scraper.go b/receiver/vcenterreceiver/scraper.go index 42963268840b..4b4cab8fe312 100644 --- a/receiver/vcenterreceiver/scraper.go +++ b/receiver/vcenterreceiver/scraper.go @@ -30,6 +30,7 @@ type vmGroupInfo struct { type vcenterScrapeData struct { datacenters []*mo.Datacenter datastores []*mo.Datastore + clusterRefs []*types.ManagedObjectReference rPoolIPathsByRef map[string]*string vAppIPathsByRef map[string]*string rPoolsByRef map[string]*mo.ResourcePool @@ -38,6 +39,7 @@ type vcenterScrapeData struct { hostPerfMetricsByRef map[string]*performance.EntityMetric vmsByRef map[string]*mo.VirtualMachine vmPerfMetricsByRef map[string]*performance.EntityMetric + vmVSANMetricsByUUID map[string]*VSANMetricResults } type vcenterMetricScraper struct { @@ -53,7 +55,7 @@ func newVmwareVcenterScraper( config *Config, settings receiver.Settings, ) *vcenterMetricScraper { - client := newVcenterClient(config) + client := newVcenterClient(logger, config) scrapeData := newVcenterScrapeData() return &vcenterMetricScraper{ @@ -69,6 +71,7 @@ func newVcenterScrapeData() *vcenterScrapeData { return &vcenterScrapeData{ datacenters: make([]*mo.Datacenter, 0), datastores: make([]*mo.Datastore, 0), + clusterRefs: make([]*types.ManagedObjectReference, 0), rPoolIPathsByRef: make(map[string]*string), vAppIPathsByRef: make(map[string]*string), computesByRef: make(map[string]*mo.ComputeResource), @@ -77,6 +80,7 @@ func newVcenterScrapeData() *vcenterScrapeData { rPoolsByRef: make(map[string]*mo.ResourcePool), vmsByRef: make(map[string]*mo.VirtualMachine), vmPerfMetricsByRef: make(map[string]*performance.EntityMetric), + vmVSANMetricsByUUID: make(map[string]*VSANMetricResults), } } @@ -93,7 +97,7 @@ func (v *vcenterMetricScraper) Shutdown(ctx context.Context) error { } func (v *vcenterMetricScraper) scrape(ctx context.Context) (pmetric.Metrics, error) { if v.client == nil { - v.client = newVcenterClient(v.config) + v.client = newVcenterClient(v.logger, v.config) } // ensure connection before scraping if err := v.client.EnsureConnection(ctx); err != nil { @@ -219,6 +223,7 @@ func (v *vcenterMetricScraper) scrapeDatastores(ctx context.Context, dc *mo.Data func (v *vcenterMetricScraper) scrapeComputes(ctx context.Context, dc *mo.Datacenter, errs *scrapererror.ScrapeErrors) { // Init for current collection v.scrapeData.computesByRef = make(map[string]*mo.ComputeResource) + v.scrapeData.clusterRefs = []*types.ManagedObjectReference{} // Get ComputeResources/ClusterComputeResources w/properties and store for later retrieval computes, err := v.client.ComputeResources(ctx, dc.Reference()) @@ -228,7 +233,11 @@ func (v *vcenterMetricScraper) scrapeComputes(ctx context.Context, dc *mo.Datace } for i := range computes { - v.scrapeData.computesByRef[computes[i].Reference().Value] = &computes[i] + computeRef := computes[i].Reference() + v.scrapeData.computesByRef[computeRef.Value] = &computes[i] + if computeRef.Type == "ClusterComputeResource" { + v.scrapeData.clusterRefs = append(v.scrapeData.clusterRefs, &computeRef) + } } } @@ -309,7 +318,16 @@ func (v *vcenterMetricScraper) scrapeVirtualMachines(ctx context.Context, dc *mo results, err := v.client.PerfMetricsQuery(ctx, spec, vmPerfMetricList, vmRefs) if err != nil { errs.AddPartial(1, fmt.Errorf("failed to retrieve perf metrics for VirtualMachines: %w", err)) + } else { + v.scrapeData.vmPerfMetricsByRef = results.resultsByRef + } + + // Get all VirtualMachine vSAN metrics and store for later retrieval + vSANMetrics, err := v.client.VSANVirtualMachines(ctx, v.scrapeData.clusterRefs) + if err != nil { + errs.AddPartial(1, fmt.Errorf("failed to retrieve vSAN metrics for VirtualMachines: %w", err)) return } - v.scrapeData.vmPerfMetricsByRef = results.resultsByRef + + v.scrapeData.vmVSANMetricsByUUID = vSANMetrics.MetricResultsByUUID } diff --git a/receiver/vcenterreceiver/scraper_test.go b/receiver/vcenterreceiver/scraper_test.go index c550f4d441f4..a45f9165f123 100644 --- a/receiver/vcenterreceiver/scraper_test.go +++ b/receiver/vcenterreceiver/scraper_test.go @@ -41,6 +41,9 @@ func TestScrapeConfigsEnabled(t *testing.T) { optConfigs := metadata.DefaultMetricsBuilderConfig() setResourcePoolMemoryUsageAttrFeatureGate(t, true) + optConfigs.Metrics.VcenterVMVsanLatencyAvg.Enabled = true + optConfigs.Metrics.VcenterVMVsanOperations.Enabled = true + optConfigs.Metrics.VcenterVMVsanThroughput.Enabled = true cfg := &Config{ MetricsBuilderConfig: optConfigs, diff --git a/receiver/vcenterreceiver/testdata/metrics/expected-all-enabled.yaml b/receiver/vcenterreceiver/testdata/metrics/expected-all-enabled.yaml index 049f2670486c..d97fcb627842 100644 --- a/receiver/vcenterreceiver/testdata/metrics/expected-all-enabled.yaml +++ b/receiver/vcenterreceiver/testdata/metrics/expected-all-enabled.yaml @@ -7083,6 +7083,105 @@ resourceMetrics: startTimeUnixNano: "2000000" timeUnixNano: "1000000" unit: '{KiBy/s}' + - description: The virtual machine latency while accessing vSAN storage. + gauge: + dataPoints: + - asInt: "14" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "15" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + - asInt: "16" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "17" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + name: vcenter.vm.vsan.latency.avg + unit: us + - description: The vSAN IOPs of a virtual machine. + gauge: + dataPoints: + - asInt: "10" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "11" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + - asInt: "12" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "13" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + name: vcenter.vm.vsan.operations + unit: '{operations/sec}' + - description: The vSAN throughput of a virtual machine. + gauge: + dataPoints: + - asDouble: 1 + attributes: + - key: direction + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asDouble: 2 + attributes: + - key: direction + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + - asDouble: 3 + attributes: + - key: direction + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asDouble: 4 + attributes: + - key: direction + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + name: vcenter.vm.vsan.throughput + unit: By/s scope: name: otelcol/vcenterreceiver version: latest @@ -7720,6 +7819,105 @@ resourceMetrics: startTimeUnixNano: "2000000" timeUnixNano: "1000000" unit: '{KiBy/s}' + - description: The virtual machine latency while accessing vSAN storage. + gauge: + dataPoints: + - asInt: "24" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "25" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + - asInt: "26" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "27" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + name: vcenter.vm.vsan.latency.avg + unit: us + - description: The vSAN IOPs of a virtual machine. + gauge: + dataPoints: + - asInt: "20" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "21" + attributes: + - key: type + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + - asInt: "22" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asInt: "23" + attributes: + - key: type + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + name: vcenter.vm.vsan.operations + unit: '{operations/sec}' + - description: The vSAN throughput of a virtual machine. + gauge: + dataPoints: + - asDouble: 6 + attributes: + - key: direction + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asDouble: 7 + attributes: + - key: direction + value: + stringValue: read + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + - asDouble: 8 + attributes: + - key: direction + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "1000000" + - asDouble: 9 + attributes: + - key: direction + value: + stringValue: write + startTimeUnixNano: "3000000" + timeUnixNano: "2000000" + name: vcenter.vm.vsan.throughput + unit: By/s scope: name: otelcol/vcenterreceiver version: latest