internal/hostmetrics/cloudmetricreader/cloudmetricreader.go (470 lines of code) (raw):
/*
Copyright 2022 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package cloudmetricreader provides functionality for interfacing with the cloud monitoring API.
package cloudmetricreader
import (
"context"
"fmt"
"math"
"strconv"
"strings"
"time"
"cloud.google.com/go/monitoring/apiv3/v2"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log"
"github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/agenttime"
"github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/metricsformatter"
"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/cloudmonitoring"
cpb "google.golang.org/genproto/googleapis/monitoring/v3"
mpb "google.golang.org/genproto/googleapis/monitoring/v3"
mrpb "google.golang.org/genproto/googleapis/monitoring/v3"
configpb "github.com/GoogleCloudPlatform/sapagent/protos/configuration"
instancepb "github.com/GoogleCloudPlatform/sapagent/protos/instanceinfo"
metricspb "github.com/GoogleCloudPlatform/sapagent/protos/metrics"
)
// QueryClient is a mockable wrapper around the cloud monitoring API Go client.
//
// See [QueryClient] for the real client.
//
// [QueryClient]: https://pkg.go.dev/cloud.google.com/go/monitoring/apiv3/v2
type QueryClient struct {
Client *monitoring.QueryClient
}
// QueryTimeSeries fetches time series data that matches a query.
func (c *QueryClient) QueryTimeSeries(ctx context.Context, req *mpb.QueryTimeSeriesRequest, opts ...gax.CallOption) ([]*mrpb.TimeSeriesData, error) {
it := c.Client.QueryTimeSeries(ctx, req, opts...)
data := []*mrpb.TimeSeriesData{}
for timeseries, err := it.Next(); err != iterator.Done; timeseries, err = it.Next() {
if err != nil {
return nil, err
}
data = append(data, timeseries)
}
return data, nil
}
type queryCallOptions struct {
filter string
start time.Time
end time.Time
alignmentPeriod string
alignment string
aggregation string
}
// CloudMetricReader handles how metrics will be read and returned.
type CloudMetricReader struct {
QueryClient cloudmonitoring.TimeSeriesQuerier
BackOffs *cloudmonitoring.BackOffIntervals
}
// Read reads metrics from the cloud monitoring API and returns a MetricsCollection.
func (r *CloudMetricReader) Read(ctx context.Context, config *configpb.Configuration, ip *instancepb.InstanceProperties, at agenttime.AgentTime) *metricspb.MetricsCollection {
log.CtxLogger(ctx).Debug("Getting metrics from cloud monitoring...")
return r.readQueryTimeSeries(ctx, config, ip, at)
}
// readQueryTimeSeries reads metrics from the cloud monitoring API and returns a MetricsCollection.
func (r *CloudMetricReader) readQueryTimeSeries(ctx context.Context, config *configpb.Configuration, ip *instancepb.InstanceProperties, at agenttime.AgentTime) *metricspb.MetricsCollection {
// We do not collect any metrics from cloud monitoring on Bare Metal.
if config.GetBareMetal() == true {
return &metricspb.MetricsCollection{}
}
var (
metrics []*metricspb.Metric
refresh = at.CloudMetricRefresh()
)
log.CtxLogger(ctx).Info("Collecting metrics via QueryTimeSeries.")
metrics = append(metrics, r.createPassthroughMetrics(ctx, config, refresh)...)
metrics = append(metrics, r.createNetworkMetrics(ctx, ip.GetNetworkAdapters(), config, refresh)...)
metrics = append(metrics, r.createDiskMetrics(ctx, ip.GetDisks(), config, refresh)...)
return &metricspb.MetricsCollection{Metrics: metrics}
}
// createPassthroughMetrics generates metrics associated with the overall state of the system.
func (r *CloudMetricReader) createPassthroughMetrics(ctx context.Context, config *configpb.Configuration, refresh time.Time) []*metricspb.Metric {
passthroughMetrics := []sapMetricKey{metricCPUUtilization}
var data []*mrpb.TimeSeriesData
cp := config.GetCloudProperties()
if cp != nil {
data = r.queryTimeSeriesData(ctx, passthroughMetrics, cp.GetProjectId(), queryCallOptions{
filter: fmt.Sprintf("resource.instance_id='%s'", cp.GetInstanceId()),
start: refresh.Add(-3 * time.Minute),
end: refresh,
alignmentPeriod: "60s",
alignment: "mean",
})
}
metricValues := parseTimeSeriesData(ctx, passthroughMetrics, data)
log.CtxLogger(ctx).Debugw("Metric values", "values", metricValues)
var metrics []*metricspb.Metric
for k, v := range metricValues {
metrics = append(metrics, buildMetric(ctx, k, v, refresh, ""))
}
return metrics
}
// createNetworkMetrics generates metrics associated with each of the network adapters in use in the system.
func (r *CloudMetricReader) createNetworkMetrics(ctx context.Context, adapters []*instancepb.NetworkAdapter, config *configpb.Configuration, refresh time.Time) []*metricspb.Metric {
var metrics []*metricspb.Metric
if len(adapters) == 0 {
return metrics
}
networkMetrics := []sapMetricKey{metricRXBytesCount, metricTXBytesCount}
var data []*mrpb.TimeSeriesData
cp := config.GetCloudProperties()
if cp != nil {
data = r.queryTimeSeriesData(ctx, networkMetrics, cp.GetProjectId(), queryCallOptions{
filter: fmt.Sprintf("resource.instance_id='%s'", cp.GetInstanceId()),
start: refresh.Add(-3 * time.Minute),
end: refresh,
alignmentPeriod: "60s",
alignment: "delta",
})
}
metricValues := parseTimeSeriesData(ctx, networkMetrics, data)
log.CtxLogger(ctx).Debugw("Network metric values", "values", metricValues)
// Since device is shared across adapters, it is expected that the metrics returned will be the same for each.
for _, adapter := range adapters {
for _, k := range networkMetrics {
if v, ok := metricValues[k]; ok {
metrics = append(metrics, buildMetric(ctx, k, v, refresh, adapter.GetName()))
}
}
}
return metrics
}
// createDiskMetrics generates metrics associated with each of the disks in use in the system.
func (r *CloudMetricReader) createDiskMetrics(ctx context.Context, disks []*instancepb.Disk, config *configpb.Configuration, refresh time.Time) []*metricspb.Metric {
var metrics []*metricspb.Metric
if len(disks) == 0 {
return metrics
}
var deviceNames, deviceNamesFilter []string
for _, disk := range disks {
name := disk.GetDiskName()
if name == "" {
name = disk.GetDeviceName()
}
deviceNames = append(deviceNames, name)
deviceNamesFilter = append(deviceNamesFilter, fmt.Sprintf("metric.device_name='%s'", name))
if disk.GetProvisionedIops() > 0 {
log.CtxLogger(ctx).Infow("Adding Guaranteed IOps for disk", "disk", name,
"iops", disk.GetProvisionedIops())
metrics = append(metrics, buildMetric(ctx, metricDiskGuaranteedIops, float64(disk.GetProvisionedIops()), refresh, name))
}
if disk.GetProvisionedThroughput() > 0 {
log.CtxLogger(ctx).Infow("Adding Guaranteed Throughput for disk", "disk", name,
"throughput", disk.GetProvisionedThroughput())
metrics = append(metrics, buildMetric(ctx, metricDiskGuaranteedThroughput, float64(disk.GetProvisionedThroughput()), refresh, name))
}
}
diskDeltaMetrics := []sapMetricKey{metricDiskReadBytesCount, metricDiskWriteBytesCount, metricDiskReadOpsCount, metricDiskWriteOpsCount}
diskRateMetrics := []sapMetricKey{metricDiskReadOpsCountRate, metricDiskWriteOpsCountRate}
diskMaxMetrics := []sapMetricKey{metricDiskMaxReadOpsCount, metricDiskMaxWriteOpsCount}
var diskDeltaData []*mrpb.TimeSeriesData
var diskRateData []*mrpb.TimeSeriesData
var diskMaxData []*mrpb.TimeSeriesData
cp := config.GetCloudProperties()
if cp != nil {
filter := fmt.Sprintf("resource.instance_id='%s' && (%s)", cp.GetInstanceId(), strings.Join(deviceNamesFilter, " || "))
diskDeltaData = r.queryTimeSeriesData(ctx, diskDeltaMetrics, cp.GetProjectId(), queryCallOptions{
filter: filter,
start: refresh.Add(-3 * time.Minute),
end: refresh,
alignmentPeriod: "60s",
alignment: "delta",
})
diskRateData = r.queryTimeSeriesData(ctx, diskRateMetrics, cp.GetProjectId(), queryCallOptions{
filter: filter,
start: refresh.Add(-3 * time.Minute),
end: refresh,
alignmentPeriod: "60s",
alignment: "rate",
})
diskMaxData = r.queryTimeSeriesData(ctx, diskMaxMetrics, cp.GetProjectId(), queryCallOptions{
filter: filter,
start: refresh.Add(-3 * time.Minute),
end: refresh,
alignmentPeriod: "60s",
aggregation: "max",
})
}
diskDeltaMetricValues := parseTimeSeriesDataByDisk(ctx, deviceNames, diskDeltaMetrics, diskDeltaData)
log.CtxLogger(ctx).Debugw("Disk delta metric values", "values", diskDeltaMetricValues)
diskRateMetricValues := parseTimeSeriesDataByDisk(ctx, deviceNames, diskRateMetrics, diskRateData)
log.CtxLogger(ctx).Debugw("Disk rate metric values", "values", diskRateMetricValues)
diskMaxMetricValues := parseTimeSeriesDataByDisk(ctx, deviceNames, diskMaxMetrics, diskMaxData)
log.CtxLogger(ctx).Debugw("Disk max metric values", "values", diskMaxMetricValues)
for _, deviceID := range deviceNames {
for _, k := range diskDeltaMetrics {
if v, ok := diskDeltaMetricValues[deviceID][k]; ok {
metrics = append(metrics, buildMetric(ctx, k, v, refresh, deviceID))
}
}
readOpsRate := diskRateMetricValues[deviceID][metricDiskReadOpsCountRate]
writeOpsRate := diskRateMetricValues[deviceID][metricDiskWriteOpsCountRate]
maxReadOps := diskMaxMetricValues[deviceID][metricDiskMaxReadOpsCount]
maxWriteOps := diskMaxMetricValues[deviceID][metricDiskMaxWriteOpsCount]
metrics = append(metrics, buildVolumeUtilization(readOpsRate, writeOpsRate, maxReadOps, maxWriteOps, refresh, deviceID))
}
return metrics
}
// queryTimeSeriesData builds and executes a query to fetch time series data for a series of metrics.
//
// The query is constructed using [Monitoring Query Language] syntax reference documentation.
//
// [Monitoring Query Language]: https://cloud.google.com/monitoring/mql/reference
func (r *CloudMetricReader) queryTimeSeriesData(ctx context.Context, metrics []sapMetricKey, projectID string, opts queryCallOptions) []*mrpb.TimeSeriesData {
var fetch []string
for _, m := range metrics {
fetch = append(fetch, fmt.Sprintf("fetch gce_instance :: %s | value cast_double(val())", sapMetrics[m].name))
}
b := new(strings.Builder)
fmt.Fprintf(b, "{%s}", strings.Join(fetch, "; "))
if opts.filter != "" {
fmt.Fprintf(b, " | filter %s", opts.filter)
}
if !opts.start.IsZero() && !opts.end.IsZero() {
fmt.Fprintf(b, " | within d'%s', d'%s'", formatTime(opts.start), formatTime(opts.end))
}
if opts.alignmentPeriod != "" {
fmt.Fprintf(b, " | every %s", opts.alignmentPeriod)
}
if opts.alignment != "" {
fmt.Fprintf(b, " | %s", opts.alignment)
}
if opts.aggregation != "" {
fmt.Fprintf(b, " | %s", opts.aggregation)
}
if len(fetch) > 1 {
fmt.Fprintf(b, " | join")
}
req := &mpb.QueryTimeSeriesRequest{
Name: fmt.Sprintf("projects/%s", projectID),
Query: b.String(),
}
log.CtxLogger(ctx).Debugw("QueryTimeSeries request", "request", b.String())
data, err := cloudmonitoring.QueryTimeSeriesWithRetry(ctx, r.QueryClient, req, r.BackOffs)
log.CtxLogger(ctx).Debugw("QueryTimeSeries response", "response", data, "error", err)
// Log any error that is encountered but allow the collection of metrics to proceed with a zero time series.
if err != nil {
log.CtxLogger(ctx).Errorw("The QueryTimeSeries request for metrics has failed", "metrics", metrics, "error", err)
}
log.CtxLogger(ctx).Debugw("Number of time series data entries returned", "count", len(data))
return data
}
// buildMetric returns a formatted Metric proto containing the time series data value found for a given metric key and refresh time.
func buildMetric(ctx context.Context, key sapMetricKey, value float64, refresh time.Time, deviceID string) *metricspb.Metric {
log.CtxLogger(ctx).Debugw("Building metric with value", "metric", key, "value", value)
sap := sapMetrics[key]
metric := &metricspb.Metric{
Context: sap.context,
Category: sap.category,
Type: sap.metricType,
Name: sap.sapName,
LastRefresh: refresh.Unix(),
Unit: sap.unit,
RefreshInterval: metricspb.RefreshInterval_REFRESHINTERVAL_PER_MINUTE,
}
if deviceID != "" {
metric.DeviceId = deviceID
}
switch {
case value == metricsformatter.Unavailable:
metric.Value = strconv.FormatFloat(value, 'f', 1, 64)
case sap == sapMetrics[metricDiskReadBytesCount] || sap == sapMetrics[metricDiskWriteBytesCount]:
metric.Value = strconv.FormatInt(metricsformatter.PerMinuteToPerSec(value), 10)
case sap == sapMetrics[metricDiskReadOpsCount] || sap == sapMetrics[metricDiskWriteOpsCount]:
metric.Value = strconv.FormatInt(metricsformatter.PerMinuteToPerSec(value), 10)
case metric.GetType() == metricspb.Type_TYPE_INT64:
metric.Value = strconv.FormatInt(int64(value), 10)
case metric.GetUnit() == metricspb.Unit_UNIT_PERCENT:
metric.Value = strconv.FormatFloat(metricsformatter.ToPercentage(math.Min(value, 1), 3), 'f', 1, 64)
default:
metric.Value = strconv.FormatFloat(value, 'f', -1, 64)
}
return metric
}
// buildVolumeUtilization returns a formatted Metric proto containing the calculated volume utilization.
func buildVolumeUtilization(readOps, writeOps, maxReadOps, maxWriteOps float64, refresh time.Time, deviceID string) *metricspb.Metric {
var (
readUtilization = float64(0)
writeUtilization = float64(0)
)
if readOps > 0 && maxReadOps > 0 {
readUtilization = math.Min(readOps/maxReadOps, 1)
}
if writeOps > 0 && maxWriteOps > 0 {
writeUtilization = math.Min(writeOps/maxWriteOps, 1)
}
volumeUtilization := metricsformatter.ToPercentage((readUtilization+writeUtilization)/2, 3)
return &metricspb.Metric{
Context: metricspb.Context_CONTEXT_VM,
Category: metricspb.Category_CATEGORY_DISK,
Type: metricspb.Type_TYPE_DOUBLE,
Name: "Volume Utilization",
LastRefresh: refresh.Unix(),
Unit: metricspb.Unit_UNIT_PERCENT,
RefreshInterval: metricspb.RefreshInterval_REFRESHINTERVAL_PER_MINUTE,
DeviceId: deviceID,
Value: strconv.FormatFloat(volumeUtilization, 'f', 1, 64),
}
}
// formatTime specifies a YYYY/MM/DD-HH:mm timestamp for use in a time series query.
func formatTime(t time.Time) string {
return fmt.Sprintf("%d/%02d/%02d-%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute())
}
// parseTimeSeriesData maps a series of metric keys to the value returned in a time series response.
//
// The structure of the map is as follows:
//
// metricValues = {
// "metricKey1": float64(metricValue1),
// "metricKey2": float64(metricValue2),
// ...
// }
func parseTimeSeriesData(ctx context.Context, metrics []sapMetricKey, data []*mrpb.TimeSeriesData) map[sapMetricKey]float64 {
metricValues := make(map[sapMetricKey]float64)
// Assign default metric values of unavailable.
for _, m := range metrics {
metricValues[m] = metricsformatter.Unavailable
}
if len(data) == 0 {
log.CtxLogger(ctx).Debugw("There is no time series data for metrics", "metrics", metrics)
return metricValues
}
points := data[0].GetPointData()
if len(points) == 0 {
log.CtxLogger(ctx).Debugw("There is no point data in the time series for metrics", "metrics", metrics)
return metricValues
}
// In the event of multiple points in the time series, the first entry is the most recent.
values := points[0].GetValues()
// MQL reference documentation specifies that values returned can be referenced by index.
// The exact index position corresponds to the entries in the point descriptors field.
// The point descriptors field appears to return data in the same order that it was fetched.
// Therefore, constructing the map based on the metric keys that were fetched should be sufficient.
// If there are any issues with the ordering of metric data from the time series, this is the likely source of contention.
for i, v := range values {
if i < len(metrics) {
metricValues[metrics[i]] = v.GetDoubleValue()
}
}
return metricValues
}
// parseTimeSeriesDataByDisk maps a series of device names to a nested map of metric keys/values for that device.
//
// The structure of the map is as follows:
//
// metricValues = {
// "deviceName1": {
// "metricKey1": float64(metricValue1),
// "metricKey2": float64(metricValue2),
// ...
// },
// "deviceName2": {
// "metricKey1": float64(metricValue1),
// "metricKey2": float64(metricValue2),
// ...
// },
// ...
// }
func parseTimeSeriesDataByDisk(ctx context.Context, deviceNames []string, metrics []sapMetricKey, data []*mrpb.TimeSeriesData) map[string]map[sapMetricKey]float64 {
metricValues := make(map[string]map[sapMetricKey]float64)
// Assign default metric values of unavailable.
for _, d := range deviceNames {
metricValues[d] = make(map[sapMetricKey]float64)
for _, m := range metrics {
metricValues[d][m] = metricsformatter.Unavailable
}
}
if len(data) == 0 {
log.CtxLogger(ctx).Debugw("There is no time series data for metrics", "metrics", metrics)
return metricValues
}
for i, d := range data {
// Each entry for time series data should correspond to a device name supplied in the query filter.
if i < len(deviceNames) {
metricValues[deviceNames[i]] = parseTimeSeriesData(ctx, metrics, []*mrpb.TimeSeriesData{d})
}
}
return metricValues
}
// sapMetric defines the format of the metrics that are returned.
type sapMetric struct {
sapName, name string
seriesAligner cpb.Aggregation_Aligner
category metricspb.Category
metricType metricspb.Type
unit metricspb.Unit
context metricspb.Context
}
type sapMetricKey string
const (
metricCPUUtilization sapMetricKey = "CPU_UTILIZATION"
metricRXBytesCount sapMetricKey = "RX_BYTES_COUNT"
metricTXBytesCount sapMetricKey = "TX_BYTES_COUNT"
metricDiskReadBytesCount sapMetricKey = "DISK_READ_BYTES_COUNT"
metricDiskWriteBytesCount sapMetricKey = "DISK_WRITE_BYTES_COUNT"
metricDiskReadOpsCount sapMetricKey = "DISK_READ_OPS_COUNT"
metricDiskWriteOpsCount sapMetricKey = "DISK_WRITE_OPS_COUNT"
metricDiskWriteOpsCountRate sapMetricKey = "DISK_WRITE_OPS_COUNT_RATE"
metricDiskMaxWriteOpsCount sapMetricKey = "DISK_MAX_WRITE_OPS_COUNT"
metricDiskReadOpsCountRate sapMetricKey = "DISK_READ_OPS_COUNT_RATE"
metricDiskMaxReadOpsCount sapMetricKey = "DISK_MAX_READ_OPS_COUNT"
metricDiskGuaranteedIops sapMetricKey = "DISK_GUARANTEED_IOPS"
metricDiskGuaranteedThroughput sapMetricKey = "DISK_GUARANTEED_THROUGHPUT"
)
var sapMetrics = map[sapMetricKey]sapMetric{
metricCPUUtilization: {
"VM Processing Power Consumption",
"compute.googleapis.com/instance/cpu/utilization",
cpb.Aggregation_ALIGN_MEAN,
metricspb.Category_CATEGORY_CPU,
metricspb.Type_TYPE_DOUBLE,
metricspb.Unit_UNIT_PERCENT,
metricspb.Context_CONTEXT_VM,
},
metricRXBytesCount: {
"Network Read Throughput",
"compute.googleapis.com/instance/network/received_bytes_count",
cpb.Aggregation_ALIGN_DELTA,
metricspb.Category_CATEGORY_NETWORK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_BPS,
metricspb.Context_CONTEXT_VM,
},
metricTXBytesCount: {
"Network Write Throughput",
"compute.googleapis.com/instance/network/sent_bytes_count",
cpb.Aggregation_ALIGN_DELTA,
metricspb.Category_CATEGORY_NETWORK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_BPS,
metricspb.Context_CONTEXT_VM,
},
metricDiskReadBytesCount: {
"Volume Read Throughput",
"compute.googleapis.com/instance/disk/read_bytes_count",
cpb.Aggregation_ALIGN_DELTA,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_BPS,
metricspb.Context_CONTEXT_VM,
},
metricDiskWriteBytesCount: {
"Volume Write Throughput",
"compute.googleapis.com/instance/disk/write_bytes_count",
cpb.Aggregation_ALIGN_DELTA,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_BPS,
metricspb.Context_CONTEXT_VM,
},
metricDiskReadOpsCount: {
"Volume Read Ops",
"compute.googleapis.com/instance/disk/read_ops_count",
cpb.Aggregation_ALIGN_DELTA,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_OPS_PER_SEC,
metricspb.Context_CONTEXT_VM,
},
metricDiskWriteOpsCount: {
"Volume Write Ops",
"compute.googleapis.com/instance/disk/write_ops_count",
cpb.Aggregation_ALIGN_DELTA,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_OPS_PER_SEC,
metricspb.Context_CONTEXT_VM,
},
metricDiskWriteOpsCountRate: {
"Write Ops Count Rate",
"compute.googleapis.com/instance/disk/write_ops_count",
cpb.Aggregation_ALIGN_RATE,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_DOUBLE,
metricspb.Unit_UNIT_OPS_PER_SEC,
metricspb.Context_CONTEXT_VM,
},
metricDiskMaxWriteOpsCount: {
"Max Write Ops Count",
"compute.googleapis.com/instance/disk/max_write_ops_count",
cpb.Aggregation_ALIGN_MAX,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_DOUBLE,
metricspb.Unit_UNIT_OPS_PER_SEC,
metricspb.Context_CONTEXT_VM,
},
metricDiskReadOpsCountRate: {
"Read Ops Count Rate",
"compute.googleapis.com/instance/disk/read_ops_count",
cpb.Aggregation_ALIGN_RATE,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_DOUBLE,
metricspb.Unit_UNIT_OPS_PER_SEC,
metricspb.Context_CONTEXT_VM,
},
metricDiskMaxReadOpsCount: {
"Max Read Ops Count",
"compute.googleapis.com/instance/disk/max_read_ops_count",
cpb.Aggregation_ALIGN_MAX,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_DOUBLE,
metricspb.Unit_UNIT_OPS_PER_SEC,
metricspb.Context_CONTEXT_VM,
},
metricDiskGuaranteedIops: {
"Guaranteed IOps",
"",
cpb.Aggregation_ALIGN_NONE,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_NONE,
metricspb.Context_CONTEXT_VM,
},
metricDiskGuaranteedThroughput: {
"Guaranteed Throughput",
"",
cpb.Aggregation_ALIGN_NONE,
metricspb.Category_CATEGORY_DISK,
metricspb.Type_TYPE_INT64,
metricspb.Unit_UNIT_NONE,
metricspb.Context_CONTEXT_VM,
},
}