From fa90fad37347aeccba34141749e9f99bfcafa2bc Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 19 Dec 2024 17:31:12 +0530 Subject: [PATCH] chore: add pvcs list (#6654) --- pkg/query-service/app/http_handler.go | 9 + pkg/query-service/app/infra.go | 53 +++ pkg/query-service/app/inframetrics/common.go | 4 + pkg/query-service/app/inframetrics/pvcs.go | 378 ++++++++++++++++++ .../app/inframetrics/pvcs_query.go | 204 ++++++++++ pkg/query-service/model/infra.go | 27 ++ 6 files changed, 675 insertions(+) create mode 100644 pkg/query-service/app/inframetrics/pvcs.go create mode 100644 pkg/query-service/app/inframetrics/pvcs_query.go diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index ee88eb7056..a1833c534b 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -125,6 +125,8 @@ type APIHandler struct { daemonsetsRepo *inframetrics.DaemonSetsRepo statefulsetsRepo *inframetrics.StatefulSetsRepo jobsRepo *inframetrics.JobsRepo + + pvcsRepo *inframetrics.PvcsRepo } type APIHandlerOpts struct { @@ -208,6 +210,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { daemonsetsRepo := inframetrics.NewDaemonSetsRepo(opts.Reader, querierv2) statefulsetsRepo := inframetrics.NewStatefulSetsRepo(opts.Reader, querierv2) jobsRepo := inframetrics.NewJobsRepo(opts.Reader, querierv2) + pvcsRepo := inframetrics.NewPvcsRepo(opts.Reader, querierv2) aH := &APIHandler{ reader: opts.Reader, @@ -237,6 +240,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { daemonsetsRepo: daemonsetsRepo, statefulsetsRepo: statefulsetsRepo, jobsRepo: jobsRepo, + pvcsRepo: pvcsRepo, } logsQueryBuilder := logsv3.PrepareLogsQuery @@ -408,6 +412,11 @@ func (aH *APIHandler) RegisterInfraMetricsRoutes(router *mux.Router, am *AuthMid podsSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getPodAttributeValues)).Methods(http.MethodGet) podsSubRouter.HandleFunc("/list", am.ViewAccess(aH.getPodList)).Methods(http.MethodPost) + pvcsSubRouter := router.PathPrefix("/api/v1/pvcs").Subrouter() + pvcsSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getPvcAttributeKeys)).Methods(http.MethodGet) + pvcsSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getPvcAttributeValues)).Methods(http.MethodGet) + pvcsSubRouter.HandleFunc("/list", am.ViewAccess(aH.getPvcList)).Methods(http.MethodPost) + nodesSubRouter := router.PathPrefix("/api/v1/nodes").Subrouter() nodesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getNodeAttributeKeys)).Methods(http.MethodGet) nodesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getNodeAttributeValues)).Methods(http.MethodGet) diff --git a/pkg/query-service/app/infra.go b/pkg/query-service/app/infra.go index b1f741e244..b48fb06c35 100644 --- a/pkg/query-service/app/infra.go +++ b/pkg/query-service/app/infra.go @@ -544,3 +544,56 @@ func (aH *APIHandler) getJobList(w http.ResponseWriter, r *http.Request) { aH.Respond(w, jobList) } + +func (aH *APIHandler) getPvcList(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + req := model.VolumeListRequest{} + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + + pvcList, err := aH.pvcsRepo.GetPvcList(ctx, req) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + + aH.Respond(w, pvcList) +} + +func (aH *APIHandler) getPvcAttributeKeys(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + req, err := parseFilterAttributeKeyRequest(r) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + + keys, err := aH.pvcsRepo.GetPvcAttributeKeys(ctx, *req) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + + aH.Respond(w, keys) +} + +func (aH *APIHandler) getPvcAttributeValues(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + req, err := parseFilterAttributeValueRequest(r) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + + values, err := aH.pvcsRepo.GetPvcAttributeValues(ctx, *req) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + + aH.Respond(w, values) +} diff --git a/pkg/query-service/app/inframetrics/common.go b/pkg/query-service/app/inframetrics/common.go index c4c280cb98..b57cc7ee41 100644 --- a/pkg/query-service/app/inframetrics/common.go +++ b/pkg/query-service/app/inframetrics/common.go @@ -89,6 +89,10 @@ func getParamsForTopJobs(req model.JobListRequest) (int64, string, string) { return getParamsForTopItems(req.Start, req.End) } +func getParamsForTopVolumes(req model.VolumeListRequest) (int64, string, string) { + return getParamsForTopItems(req.Start, req.End) +} + // TODO(srikanthccv): remove this // What is happening here? // The `PrepareTimeseriesFilterQuery` uses the local time series table for sub-query because each fingerprint diff --git a/pkg/query-service/app/inframetrics/pvcs.go b/pkg/query-service/app/inframetrics/pvcs.go new file mode 100644 index 0000000000..9eb41190ed --- /dev/null +++ b/pkg/query-service/app/inframetrics/pvcs.go @@ -0,0 +1,378 @@ +package inframetrics + +import ( + "context" + "math" + "sort" + + "go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers" + "go.signoz.io/signoz/pkg/query-service/common" + "go.signoz.io/signoz/pkg/query-service/interfaces" + "go.signoz.io/signoz/pkg/query-service/model" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" + "go.signoz.io/signoz/pkg/query-service/postprocess" + "golang.org/x/exp/slices" +) + +var ( + metricToUseForVolumes = "k8s_volume_available" + + volumeAttrsToEnrich = []string{ + "k8s_pod_uid", + "k8s_pod_name", + "k8s_namespace_name", + "k8s_node_name", + "k8s_statefulset_name", + "k8s_cluster_name", + "k8s_persistentvolumeclaim_name", + } + + k8sPersistentVolumeClaimNameAttrKey = "k8s_persistentvolumeclaim_name" + + queryNamesForVolumes = map[string][]string{ + "available": {"A"}, + "capacity": {"B", "A"}, + "usage": {"F1", "B", "A"}, + "inodes": {"C", "A"}, + "inodes_free": {"D", "A"}, + "inodes_used": {"E", "A"}, + } + + volumeQueryNames = []string{"A", "B", "C", "D", "E", "F1"} + + metricNamesForVolumes = map[string]string{ + "available": "k8s_volume_available", + "capacity": "k8s_volume_capacity", + "inodes": "k8s_volume_inodes", + "inodes_free": "k8s_volume_inodes_free", + "inodes_used": "k8s_volume_inodes_used", + } +) + +type PvcsRepo struct { + reader interfaces.Reader + querierV2 interfaces.Querier +} + +func NewPvcsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *PvcsRepo { + return &PvcsRepo{reader: reader, querierV2: querierV2} +} + +func (p *PvcsRepo) GetPvcAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) { + req.DataSource = v3.DataSourceMetrics + req.AggregateAttribute = metricToUseForVolumes + if req.Limit == 0 { + req.Limit = 50 + } + + attributeKeysResponse, err := p.reader.GetMetricAttributeKeys(ctx, &req) + if err != nil { + return nil, err + } + + // TODO(srikanthccv): only return resource attributes when we have a way to + // distinguish between resource attributes and other attributes. + filteredKeys := []v3.AttributeKey{} + for _, key := range attributeKeysResponse.AttributeKeys { + if slices.Contains(pointAttrsToIgnore, key.Key) { + continue + } + filteredKeys = append(filteredKeys, key) + } + + return &v3.FilterAttributeKeyResponse{AttributeKeys: filteredKeys}, nil +} + +func (p *PvcsRepo) GetPvcAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) { + req.DataSource = v3.DataSourceMetrics + req.AggregateAttribute = metricToUseForVolumes + if req.Limit == 0 { + req.Limit = 50 + } + + attributeValuesResponse, err := p.reader.GetMetricAttributeValues(ctx, &req) + if err != nil { + return nil, err + } + return attributeValuesResponse, nil +} + +func (p *PvcsRepo) getMetadataAttributes(ctx context.Context, req model.VolumeListRequest) (map[string]map[string]string, error) { + volumeAttrs := map[string]map[string]string{} + + for _, key := range volumeAttrsToEnrich { + hasKey := false + for _, groupByKey := range req.GroupBy { + if groupByKey.Key == key { + hasKey = true + break + } + } + if !hasKey { + req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key}) + } + } + + mq := v3.BuilderQuery{ + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: metricToUseForVolumes, + DataType: v3.AttributeKeyDataTypeFloat64, + }, + Temporality: v3.Unspecified, + GroupBy: req.GroupBy, + } + + query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq) + if err != nil { + return nil, err + } + + query = localQueryToDistributedQuery(query) + + attrsListResponse, err := p.reader.GetListResultV3(ctx, query) + if err != nil { + return nil, err + } + + for _, row := range attrsListResponse { + stringData := map[string]string{} + for key, value := range row.Data { + if str, ok := value.(string); ok { + stringData[key] = str + } else if strPtr, ok := value.(*string); ok { + stringData[key] = *strPtr + } + } + + volumeName := stringData[k8sPersistentVolumeClaimNameAttrKey] + if _, ok := volumeAttrs[volumeName]; !ok { + volumeAttrs[volumeName] = map[string]string{} + } + + for _, key := range req.GroupBy { + volumeAttrs[volumeName][key.Key] = stringData[key.Key] + } + } + + return volumeAttrs, nil +} + +func (p *PvcsRepo) getTopVolumeGroups(ctx context.Context, req model.VolumeListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) { + step, timeSeriesTableName, samplesTableName := getParamsForTopVolumes(req) + + queryNames := queryNamesForVolumes[req.OrderBy.ColumnName] + topVolumeGroupsQueryRangeParams := &v3.QueryRangeParamsV3{ + Start: req.Start, + End: req.End, + Step: step, + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{}, + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeTable, + }, + } + + for _, queryName := range queryNames { + query := q.CompositeQuery.BuilderQueries[queryName].Clone() + query.StepInterval = step + query.MetricTableHints = &v3.MetricTableHints{ + TimeSeriesTableName: timeSeriesTableName, + SamplesTableName: samplesTableName, + } + if req.Filters != nil && len(req.Filters.Items) > 0 { + if query.Filters == nil { + query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}} + } + query.Filters.Items = append(query.Filters.Items, req.Filters.Items...) + } + topVolumeGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query + } + + queryResponse, _, err := p.querierV2.QueryRange(ctx, topVolumeGroupsQueryRangeParams) + if err != nil { + return nil, nil, err + } + formattedResponse, err := postprocess.PostProcessResult(queryResponse, topVolumeGroupsQueryRangeParams) + if err != nil { + return nil, nil, err + } + + if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 { + return nil, nil, nil + } + + if req.OrderBy.Order == v3.DirectionDesc { + sort.Slice(formattedResponse[0].Series, func(i, j int) bool { + return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value + }) + } else { + sort.Slice(formattedResponse[0].Series, func(i, j int) bool { + return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value + }) + } + + limit := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series))) + + paginatedTopVolumeGroupsSeries := formattedResponse[0].Series[req.Offset:int(limit)] + + topVolumeGroups := []map[string]string{} + for _, series := range paginatedTopVolumeGroupsSeries { + topVolumeGroups = append(topVolumeGroups, series.Labels) + } + allVolumeGroups := []map[string]string{} + for _, series := range formattedResponse[0].Series { + allVolumeGroups = append(allVolumeGroups, series.Labels) + } + + return topVolumeGroups, allVolumeGroups, nil +} + +func (p *PvcsRepo) GetPvcList(ctx context.Context, req model.VolumeListRequest) (model.VolumeListResponse, error) { + resp := model.VolumeListResponse{} + + if req.Limit == 0 { + req.Limit = 10 + } + + if req.OrderBy == nil { + req.OrderBy = &v3.OrderBy{ColumnName: "usage", Order: v3.DirectionDesc} + } + + if req.GroupBy == nil { + req.GroupBy = []v3.AttributeKey{{Key: k8sPersistentVolumeClaimNameAttrKey}} + resp.Type = model.ResponseTypeList + } else { + resp.Type = model.ResponseTypeGroupedList + } + + step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60)) + + query := PvcsTableListQuery.Clone() + + query.Start = req.Start + query.End = req.End + query.Step = step + + for _, query := range query.CompositeQuery.BuilderQueries { + query.StepInterval = step + if req.Filters != nil && len(req.Filters.Items) > 0 { + if query.Filters == nil { + query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}} + } + query.Filters.Items = append(query.Filters.Items, req.Filters.Items...) + } + query.GroupBy = req.GroupBy + } + + volumeAttrs, err := p.getMetadataAttributes(ctx, req) + if err != nil { + return resp, err + } + + topVolumeGroups, allVolumeGroups, err := p.getTopVolumeGroups(ctx, req, query) + if err != nil { + return resp, err + } + + groupFilters := map[string][]string{} + for _, topVolumeGroup := range topVolumeGroups { + for k, v := range topVolumeGroup { + groupFilters[k] = append(groupFilters[k], v) + } + } + + for groupKey, groupValues := range groupFilters { + hasGroupFilter := false + if req.Filters != nil && len(req.Filters.Items) > 0 { + for _, filter := range req.Filters.Items { + if filter.Key.Key == groupKey { + hasGroupFilter = true + break + } + } + } + + if !hasGroupFilter { + for _, query := range query.CompositeQuery.BuilderQueries { + query.Filters.Items = append(query.Filters.Items, v3.FilterItem{ + Key: v3.AttributeKey{Key: groupKey}, + Value: groupValues, + Operator: v3.FilterOperatorIn, + }) + } + } + } + + queryResponse, _, err := p.querierV2.QueryRange(ctx, query) + if err != nil { + return resp, err + } + + formattedResponse, err := postprocess.PostProcessResult(queryResponse, query) + if err != nil { + return resp, err + } + + records := []model.VolumeListRecord{} + + for _, result := range formattedResponse { + for _, row := range result.Table.Rows { + + record := model.VolumeListRecord{ + VolumeUsage: -1, + VolumeAvailable: -1, + VolumeCapacity: -1, + VolumeInodes: -1, + VolumeInodesFree: -1, + VolumeInodesUsed: -1, + Meta: map[string]string{}, + } + + if volumeName, ok := row.Data[k8sPersistentVolumeClaimNameAttrKey].(string); ok { + record.PersistentVolumeClaimName = volumeName + } + + if volumeAvailable, ok := row.Data["A"].(float64); ok { + record.VolumeAvailable = volumeAvailable + } + if volumeCapacity, ok := row.Data["B"].(float64); ok { + record.VolumeCapacity = volumeCapacity + } + + if volumeInodes, ok := row.Data["C"].(float64); ok { + record.VolumeInodes = volumeInodes + } + + if volumeInodesFree, ok := row.Data["D"].(float64); ok { + record.VolumeInodesFree = volumeInodesFree + } + + if volumeInodesUsed, ok := row.Data["E"].(float64); ok { + record.VolumeInodesUsed = volumeInodesUsed + } + + record.VolumeUsage = record.VolumeCapacity - record.VolumeAvailable + + record.Meta = map[string]string{} + if _, ok := volumeAttrs[record.PersistentVolumeClaimName]; ok { + record.Meta = volumeAttrs[record.PersistentVolumeClaimName] + } + + for k, v := range row.Data { + if slices.Contains(volumeQueryNames, k) { + continue + } + if labelValue, ok := v.(string); ok { + record.Meta[k] = labelValue + } + } + + records = append(records, record) + } + } + resp.Total = len(allVolumeGroups) + resp.Records = records + + return resp, nil +} diff --git a/pkg/query-service/app/inframetrics/pvcs_query.go b/pkg/query-service/app/inframetrics/pvcs_query.go new file mode 100644 index 0000000000..1e4ddd4510 --- /dev/null +++ b/pkg/query-service/app/inframetrics/pvcs_query.go @@ -0,0 +1,204 @@ +package inframetrics + +import v3 "go.signoz.io/signoz/pkg/query-service/model/v3" + +var PvcsTableListQuery = v3.QueryRangeParamsV3{ + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + // k8s.volume.available + "A": { + QueryName: "A", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: metricNamesForVolumes["available"], + DataType: v3.AttributeKeyDataTypeFloat64, + }, + Temporality: v3.Unspecified, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + Operator: v3.FilterOperatorNotEqual, + Value: "", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + { + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + }, + Expression: "A", + ReduceTo: v3.ReduceToOperatorLast, + TimeAggregation: v3.TimeAggregationAvg, + SpaceAggregation: v3.SpaceAggregationSum, + Disabled: false, + }, + // k8s.volume.capacity + "B": { + QueryName: "B", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: metricNamesForVolumes["capacity"], + DataType: v3.AttributeKeyDataTypeFloat64, + }, + Temporality: v3.Unspecified, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + Operator: v3.FilterOperatorNotEqual, + Value: "", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + { + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + }, + Expression: "B", + ReduceTo: v3.ReduceToOperatorLast, + TimeAggregation: v3.TimeAggregationAvg, + SpaceAggregation: v3.SpaceAggregationSum, + Disabled: false, + }, + "F1": { + QueryName: "F1", + DataSource: v3.DataSourceMetrics, + Expression: "B - A", + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{}, + }, + ReduceTo: v3.ReduceToOperatorLast, + }, + // k8s.volume.inodes + "C": { + QueryName: "C", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: metricNamesForVolumes["inodes"], + DataType: v3.AttributeKeyDataTypeFloat64, + }, + Temporality: v3.Unspecified, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + Operator: v3.FilterOperatorNotEqual, + Value: "", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + { + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + }, + Expression: "C", + ReduceTo: v3.ReduceToOperatorLast, + TimeAggregation: v3.TimeAggregationAvg, + SpaceAggregation: v3.SpaceAggregationSum, + Disabled: false, + }, + // k8s.volume.inodes_free + "D": { + QueryName: "D", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: metricNamesForVolumes["inodes_free"], + DataType: v3.AttributeKeyDataTypeFloat64, + }, + Temporality: v3.Unspecified, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + Operator: v3.FilterOperatorNotEqual, + Value: "", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + { + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + }, + Expression: "D", + ReduceTo: v3.ReduceToOperatorLast, + TimeAggregation: v3.TimeAggregationAvg, + SpaceAggregation: v3.SpaceAggregationSum, + Disabled: false, + }, + // k8s.volume.inodes_used + "E": { + QueryName: "E", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: metricNamesForVolumes["inodes_used"], + DataType: v3.AttributeKeyDataTypeFloat64, + }, + Temporality: v3.Unspecified, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + Operator: v3.FilterOperatorNotEqual, + Value: "", + }, + }, + }, + GroupBy: []v3.AttributeKey{ + { + Key: k8sPersistentVolumeClaimNameAttrKey, + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + }, + Expression: "E", + ReduceTo: v3.ReduceToOperatorLast, + TimeAggregation: v3.TimeAggregationAvg, + SpaceAggregation: v3.SpaceAggregationSum, + Disabled: false, + }, + }, + PanelType: v3.PanelTypeTable, + QueryType: v3.QueryTypeBuilder, + }, + Version: "v4", + FormatForWeb: true, +} diff --git a/pkg/query-service/model/infra.go b/pkg/query-service/model/infra.go index 6a83dbc0c2..24728d349c 100644 --- a/pkg/query-service/model/infra.go +++ b/pkg/query-service/model/infra.go @@ -337,3 +337,30 @@ type JobListRecord struct { SuccessfulPods int `json:"successfulPods"` Meta map[string]string `json:"meta"` } + +type VolumeListRequest struct { + Start int64 `json:"start"` // epoch time in ms + End int64 `json:"end"` // epoch time in ms + Filters *v3.FilterSet `json:"filters"` + GroupBy []v3.AttributeKey `json:"groupBy"` + OrderBy *v3.OrderBy `json:"orderBy"` + Offset int `json:"offset"` + Limit int `json:"limit"` +} + +type VolumeListResponse struct { + Type ResponseType `json:"type"` + Records []VolumeListRecord `json:"records"` + Total int `json:"total"` +} + +type VolumeListRecord struct { + PersistentVolumeClaimName string `json:"persistentVolumeClaimName"` + VolumeAvailable float64 `json:"volumeAvailable"` + VolumeCapacity float64 `json:"volumeCapacity"` + VolumeInodes float64 `json:"volumeInodes"` + VolumeInodesFree float64 `json:"volumeInodesFree"` + VolumeInodesUsed float64 `json:"volumeInodesUsed"` + VolumeUsage float64 `json:"volumeUsage"` + Meta map[string]string `json:"meta"` +}