diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ced0e184e..4dc2ce07dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## master / unreleased +* [FEATURE] Querier: Add experimental per-tenant cardinality API (`GET /api/v1/cardinality`) that exposes top-N metrics by series count, label names by distinct value count, and label-value pairs by series count from ingester TSDB heads (`source=head`) and compacted blocks (`source=blocks`). Gated behind `-querier.cardinality-api-enabled` (default `false`). #7384 * [ENHANCEMENT] Metrics Helper: Add native histogram support for aggregating and merging, including dual-format histogram handling that exposes both native and classic bucket formats. #7359 * [ENHANCEMENT] Cache: Add per-tenant TTL configuration for query results cache to control cache expiration on a per-tenant basis with separate TTLs for regular and out-of-order data. #7357 * [ENHANCEMENT] Tenant Federation: Add a local cache to regex resolver. #7363 diff --git a/docs/configuration/config-file-reference.md b/docs/configuration/config-file-reference.md index 7c1cd7265df..d99086a59e6 100644 --- a/docs/configuration/config-file-reference.md +++ b/docs/configuration/config-file-reference.md @@ -4286,6 +4286,26 @@ The `limits_config` configures default and per-tenant limits imposed by Cortex s # zones are not available. [query_partial_data: | default = false] +# [Experimental] Enables the per-tenant cardinality API endpoint. When disabled, +# the endpoint returns HTTP 403. +# CLI flag: -querier.cardinality-api-enabled +[cardinality_api_enabled: | default = false] + +# [Experimental] Maximum allowed time range (end - start) for source=blocks +# cardinality queries. +# CLI flag: -querier.cardinality-max-query-range +[cardinality_max_query_range: | default = 1d] + +# [Experimental] Maximum number of concurrent cardinality requests per tenant. +# Excess requests are rejected with HTTP 429. +# CLI flag: -querier.cardinality-max-concurrent-requests +[cardinality_max_concurrent_requests: | default = 2] + +# [Experimental] Per-request timeout for cardinality computation. On timeout, +# partial results are returned. +# CLI flag: -querier.cardinality-query-timeout +[cardinality_query_timeout: | default = 1m] + # The maximum number of rows that can be fetched when querying parquet storage. # Each row maps to a series in a parquet file. This limit applies before # materializing chunks. 0 to disable. diff --git a/docs/getting-started/cortex-config.yaml b/docs/getting-started/cortex-config.yaml index 1b24084ad3f..d604a933de3 100644 --- a/docs/getting-started/cortex-config.yaml +++ b/docs/getting-started/cortex-config.yaml @@ -79,6 +79,10 @@ compactor: frontend_worker: match_max_concurrent: true +# https://cortexmetrics.io/docs/configuration/configuration-file/#limits_config +limits: + cardinality_api_enabled: true + # https://cortexmetrics.io/docs/configuration/configuration-file/#ruler_config ruler: enable_api: true diff --git a/integration/cardinality_test.go b/integration/cardinality_test.go new file mode 100644 index 00000000000..ef4069ad65d --- /dev/null +++ b/integration/cardinality_test.go @@ -0,0 +1,175 @@ +//go:build requires_docker + +package integration + +import ( + "encoding/json" + "testing" + "time" + + "github.com/prometheus/prometheus/prompb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cortexproject/cortex/integration/e2e" + e2edb "github.com/cortexproject/cortex/integration/e2e/db" + "github.com/cortexproject/cortex/integration/e2ecortex" +) + +type cardinalityAPIResponse struct { + Status string `json:"status"` + Data struct { + NumSeries uint64 `json:"numSeries"` + Approximated bool `json:"approximated"` + SeriesCountByMetricName []struct { + Name string `json:"name"` + Value uint64 `json:"value"` + } `json:"seriesCountByMetricName"` + LabelValueCountByLabelName []struct { + Name string `json:"name"` + Value uint64 `json:"value"` + } `json:"labelValueCountByLabelName"` + SeriesCountByLabelValuePair []struct { + Name string `json:"name"` + Value uint64 `json:"value"` + } `json:"seriesCountByLabelValuePair"` + } `json:"data"` +} + +func TestCardinalityAPI(t *testing.T) { + const blockRangePeriod = 5 * time.Second + + s, err := e2e.NewScenario(networkName) + require.NoError(t, err) + defer s.Close() + + // Start dependencies. + minio := e2edb.NewMinio(9000, bucketName) + require.NoError(t, s.StartAndWaitReady(minio)) + + // Configure the blocks storage to frequently compact TSDB head and ship blocks to storage. + flags := mergeFlags(BlocksStorageFlags(), AlertmanagerLocalFlags(), map[string]string{ + "-blocks-storage.tsdb.block-ranges-period": blockRangePeriod.String(), + "-blocks-storage.tsdb.ship-interval": "1s", + "-blocks-storage.bucket-store.sync-interval": "1s", + "-blocks-storage.tsdb.retention-period": ((blockRangePeriod * 2) - 1).String(), + "-blocks-storage.bucket-store.bucket-index.enabled": "false", + "-querier.cardinality-api-enabled": "true", + "-alertmanager.web.external-url": "http://localhost/alertmanager", + // Use inmemory ring to avoid needing Consul. + "-ring.store": "inmemory", + "-compactor.ring.store": "inmemory", + "-store-gateway.sharding-ring.store": "inmemory", + "-store-gateway.sharding-enabled": "true", + "-store-gateway.sharding-ring.replication-factor": "1", + }) + + require.NoError(t, writeFileToSharedDir(s, "alertmanager_configs", []byte{})) + + cortex := e2ecortex.NewSingleBinary("cortex-1", flags, "") + require.NoError(t, s.StartAndWaitReady(cortex)) + + c, err := e2ecortex.NewClient(cortex.HTTPEndpoint(), cortex.HTTPEndpoint(), "", "", "user-1") + require.NoError(t, err) + + // Push multiple series with different metric names and labels. + now := time.Now() + series1, _ := generateSeries("test_metric_1", now, prompb.Label{Name: "job", Value: "api"}) + series2, _ := generateSeries("test_metric_2", now, prompb.Label{Name: "job", Value: "worker"}) + series3, _ := generateSeries("test_metric_3", now, prompb.Label{Name: "job", Value: "api"}, prompb.Label{Name: "instance", Value: "host1"}) + + for _, s := range [][]prompb.TimeSeries{series1, series2, series3} { + res, err := c.Push(s) + require.NoError(t, err) + require.Equal(t, 200, res.StatusCode) + } + + // --- Test 1: Head path --- + t.Run("head path returns cardinality data", func(t *testing.T) { + resp, body, err := c.CardinalityRaw("head", 10, time.Time{}, time.Time{}) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode, "body: %s", string(body)) + + var result cardinalityAPIResponse + require.NoError(t, json.Unmarshal(body, &result)) + + assert.Equal(t, "success", result.Status) + assert.GreaterOrEqual(t, result.Data.NumSeries, uint64(3)) + assert.NotEmpty(t, result.Data.SeriesCountByMetricName, "seriesCountByMetricName should not be empty") + assert.NotEmpty(t, result.Data.LabelValueCountByLabelName, "labelValueCountByLabelName should not be empty") + assert.NotEmpty(t, result.Data.SeriesCountByLabelValuePair, "seriesCountByLabelValuePair should not be empty") + }) + + // --- Test 2: Default source (should be head) --- + t.Run("default source is head", func(t *testing.T) { + resp, body, err := c.CardinalityRaw("", 10, time.Time{}, time.Time{}) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode, "body: %s", string(body)) + + var result cardinalityAPIResponse + require.NoError(t, json.Unmarshal(body, &result)) + assert.Equal(t, "success", result.Status) + assert.GreaterOrEqual(t, result.Data.NumSeries, uint64(3)) + }) + + // --- Test 3: Parameter validation --- + t.Run("invalid source returns 400", func(t *testing.T) { + resp, _, err := c.CardinalityRaw("invalid", 0, time.Time{}, time.Time{}) + require.NoError(t, err) + assert.Equal(t, 400, resp.StatusCode) + }) + + // --- Test 4: Blocks path --- + // Push series at timestamps spanning two block ranges to trigger head compaction and shipping. + t.Run("blocks path returns cardinality data", func(t *testing.T) { + // Push a series at a timestamp in a different block range to trigger compaction of the first block. + series4, _ := generateSeries("test_metric_4", now.Add(blockRangePeriod*2), + prompb.Label{Name: "job", Value: "scheduler"}) + res, err := c.Push(series4) + require.NoError(t, err) + require.Equal(t, 200, res.StatusCode) + + // Wait until at least one block is shipped from the ingester. + require.NoError(t, cortex.WaitSumMetricsWithOptions( + e2e.Greater(0), + []string{"cortex_ingester_shipper_uploads_total"}, + e2e.WaitMissingMetrics, + )) + + // Wait until the store gateway has loaded the shipped blocks. + require.NoError(t, cortex.WaitSumMetricsWithOptions( + e2e.Greater(0), + []string{"cortex_bucket_store_blocks_loaded"}, + e2e.WaitMissingMetrics, + )) + + // Query the blocks path with retries. The querier's block finder and + // store gateway may need additional sync cycles before returning data. + start := now.Add(-1 * time.Hour) + end := now.Add(1 * time.Hour) + deadline := time.Now().Add(30 * time.Second) + + var result cardinalityAPIResponse + for time.Now().Before(deadline) { + resp, body, err := c.CardinalityRaw("blocks", 10, start, end) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode, "body: %s", string(body)) + require.NoError(t, json.Unmarshal(body, &result)) + + if len(result.Data.LabelValueCountByLabelName) > 0 { + break + } + time.Sleep(1 * time.Second) + } + + assert.Equal(t, "success", result.Status) + assert.NotEmpty(t, result.Data.LabelValueCountByLabelName, "labelValueCountByLabelName should not be empty after retries") + }) + + // --- Test 5: Blocks path requires start/end --- + t.Run("blocks path without start/end returns 400", func(t *testing.T) { + resp, _, err := c.CardinalityRaw("blocks", 0, time.Time{}, time.Time{}) + require.NoError(t, err) + assert.Equal(t, 400, resp.StatusCode) + }) +} diff --git a/integration/e2ecortex/client.go b/integration/e2ecortex/client.go index 73f3c6bbf32..952d647062b 100644 --- a/integration/e2ecortex/client.go +++ b/integration/e2ecortex/client.go @@ -562,6 +562,31 @@ func (c *Client) LabelValuesRaw(label string, matches []string, startTime, endTi return c.query(u.String(), headers) } +// CardinalityRaw runs a cardinality request directly against the querier API. +func (c *Client) CardinalityRaw(source string, limit int, start, end time.Time) (*http.Response, []byte, error) { + u := &url.URL{ + Scheme: "http", + Path: fmt.Sprintf("%s/api/prom/api/v1/cardinality", c.querierAddress), + } + q := u.Query() + + if source != "" { + q.Set("source", source) + } + if limit > 0 { + q.Set("limit", strconv.Itoa(limit)) + } + if !start.IsZero() { + q.Set("start", FormatTime(start)) + } + if !end.IsZero() { + q.Set("end", FormatTime(end)) + } + + u.RawQuery = q.Encode() + return c.query(u.String(), nil) +} + // RemoteRead runs a remote read query. func (c *Client) RemoteRead(matchers []*labels.Matcher, start, end time.Time, step time.Duration) (*prompb.ReadResponse, error) { startMs := start.UnixMilli() diff --git a/pkg/cortex/cortex.go b/pkg/cortex/cortex.go index 6591e056c7f..51fe46fd2bb 100644 --- a/pkg/cortex/cortex.go +++ b/pkg/cortex/cortex.go @@ -351,7 +351,8 @@ type Cortex struct { // Queryables that the querier should use to query the long // term storage. It depends on the storage engine used. - StoreQueryables []querier.QueryableWithFilter + StoreQueryables []querier.QueryableWithFilter + BlocksStoreQueryable *querier.BlocksStoreQueryable } // New makes a new Cortex. diff --git a/pkg/cortex/modules.go b/pkg/cortex/modules.go index 046f3a631f5..0146a7a51c1 100644 --- a/pkg/cortex/modules.go +++ b/pkg/cortex/modules.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/http" + "path" "runtime" "runtime/debug" @@ -293,6 +294,12 @@ func (t *Cortex) initQueryable() (serv services.Service, err error) { // Register the default endpoints that are always enabled for the querier module t.API.RegisterQueryable(t.QuerierQueryable, t.Distributor) + // Register the cardinality endpoint directly on the external API server. + // This endpoint bypasses the query-frontend and is served directly by the querier. + cardinalityHandler := querier.CardinalityHandler(t.Distributor, t.BlocksStoreQueryable, t.OverridesConfig, prometheus.DefaultRegisterer) + t.API.RegisterRoute(path.Join(t.Cfg.API.PrometheusHTTPPrefix, "/api/v1/cardinality"), cardinalityHandler, true, "GET") + t.API.RegisterRoute(path.Join(t.Cfg.API.LegacyHTTPPrefix, "/api/v1/cardinality"), cardinalityHandler, true, "GET") + return nil, nil } @@ -448,6 +455,7 @@ func (t *Cortex) initStoreQueryables() (services.Service, error) { if q, err := initBlockStoreQueryable(t.Cfg, t.OverridesConfig, prometheus.DefaultRegisterer); err != nil { return nil, fmt.Errorf("failed to initialize querier: %v", err) } else { + t.BlocksStoreQueryable = q queriable = q if t.Cfg.Querier.EnableParquetQueryable { pq, err := querier.NewParquetQueryable(t.Cfg.Querier, t.Cfg.BlocksStorage, t.OverridesConfig, q, util_log.Logger, prometheus.DefaultRegisterer) diff --git a/pkg/cortexpb/cardinality.pb.go b/pkg/cortexpb/cardinality.pb.go new file mode 100644 index 00000000000..9d3cfc4d4e8 --- /dev/null +++ b/pkg/cortexpb/cardinality.pb.go @@ -0,0 +1,447 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: cardinality.proto + +package cortexpb + +import ( + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" + reflect "reflect" + strings "strings" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type CardinalityStatItem struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value uint64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (m *CardinalityStatItem) Reset() { *m = CardinalityStatItem{} } +func (*CardinalityStatItem) ProtoMessage() {} +func (*CardinalityStatItem) Descriptor() ([]byte, []int) { + return fileDescriptor_e0dd571f4aa96317, []int{0} +} +func (m *CardinalityStatItem) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CardinalityStatItem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CardinalityStatItem.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CardinalityStatItem) XXX_Merge(src proto.Message) { + xxx_messageInfo_CardinalityStatItem.Merge(m, src) +} +func (m *CardinalityStatItem) XXX_Size() int { + return m.Size() +} +func (m *CardinalityStatItem) XXX_DiscardUnknown() { + xxx_messageInfo_CardinalityStatItem.DiscardUnknown(m) +} + +var xxx_messageInfo_CardinalityStatItem proto.InternalMessageInfo + +func (m *CardinalityStatItem) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *CardinalityStatItem) GetValue() uint64 { + if m != nil { + return m.Value + } + return 0 +} + +func init() { + proto.RegisterType((*CardinalityStatItem)(nil), "cortexpb.CardinalityStatItem") +} + +func init() { proto.RegisterFile("cardinality.proto", fileDescriptor_e0dd571f4aa96317) } + +var fileDescriptor_e0dd571f4aa96317 = []byte{ + // 181 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4c, 0x4e, 0x2c, 0x4a, + 0xc9, 0xcc, 0x4b, 0xcc, 0xc9, 0x2c, 0xa9, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x48, + 0xce, 0x2f, 0x2a, 0x49, 0xad, 0x28, 0x48, 0x92, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0x0b, 0xea, + 0x83, 0x58, 0x10, 0x79, 0x25, 0x7b, 0x2e, 0x61, 0x67, 0x84, 0xa6, 0xe0, 0x92, 0xc4, 0x12, 0xcf, + 0x92, 0xd4, 0x5c, 0x21, 0x21, 0x2e, 0x96, 0xbc, 0xc4, 0xdc, 0x54, 0x09, 0x46, 0x05, 0x46, 0x0d, + 0xce, 0x20, 0x30, 0x5b, 0x48, 0x84, 0x8b, 0xb5, 0x2c, 0x31, 0xa7, 0x34, 0x55, 0x82, 0x49, 0x81, + 0x51, 0x83, 0x25, 0x08, 0xc2, 0x71, 0xb2, 0xbb, 0xf0, 0x50, 0x8e, 0xe1, 0xc6, 0x43, 0x39, 0x86, + 0x0f, 0x0f, 0xe5, 0x18, 0x1b, 0x1e, 0xc9, 0x31, 0xae, 0x78, 0x24, 0xc7, 0x78, 0xe2, 0x91, 0x1c, + 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0xbe, 0x78, 0x24, 0xc7, 0xf0, 0xe1, 0x91, + 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x17, 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x10, 0x05, + 0x77, 0x56, 0x12, 0x1b, 0xd8, 0x1d, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x32, 0x10, 0xdb, + 0x53, 0xbc, 0x00, 0x00, 0x00, +} + +func (this *CardinalityStatItem) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*CardinalityStatItem) + if !ok { + that2, ok := that.(CardinalityStatItem) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Name != that1.Name { + return false + } + if this.Value != that1.Value { + return false + } + return true +} +func (this *CardinalityStatItem) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 6) + s = append(s, "&cortexpb.CardinalityStatItem{") + s = append(s, "Name: "+fmt.Sprintf("%#v", this.Name)+",\n") + s = append(s, "Value: "+fmt.Sprintf("%#v", this.Value)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func valueToGoStringCardinality(v interface{}, typ string) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("func(v %v) *%v { return &v } ( %#v )", typ, typ, pv) +} +func (m *CardinalityStatItem) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CardinalityStatItem) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CardinalityStatItem) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Value != 0 { + i = encodeVarintCardinality(dAtA, i, uint64(m.Value)) + i-- + dAtA[i] = 0x10 + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintCardinality(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintCardinality(dAtA []byte, offset int, v uint64) int { + offset -= sovCardinality(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *CardinalityStatItem) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + sovCardinality(uint64(l)) + } + if m.Value != 0 { + n += 1 + sovCardinality(uint64(m.Value)) + } + return n +} + +func sovCardinality(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozCardinality(x uint64) (n int) { + return sovCardinality(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *CardinalityStatItem) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&CardinalityStatItem{`, + `Name:` + fmt.Sprintf("%v", this.Name) + `,`, + `Value:` + fmt.Sprintf("%v", this.Value) + `,`, + `}`, + }, "") + return s +} +func valueToStringCardinality(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *CardinalityStatItem) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCardinality + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CardinalityStatItem: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CardinalityStatItem: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCardinality + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthCardinality + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthCardinality + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + m.Value = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCardinality + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Value |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipCardinality(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthCardinality + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthCardinality + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipCardinality(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCardinality + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCardinality + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCardinality + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthCardinality + } + iNdEx += length + if iNdEx < 0 { + return 0, ErrInvalidLengthCardinality + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCardinality + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipCardinality(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + if iNdEx < 0 { + return 0, ErrInvalidLengthCardinality + } + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthCardinality = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowCardinality = fmt.Errorf("proto: integer overflow") +) diff --git a/pkg/cortexpb/cardinality.proto b/pkg/cortexpb/cardinality.proto new file mode 100644 index 00000000000..efe876b80c8 --- /dev/null +++ b/pkg/cortexpb/cardinality.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package cortexpb; + +option go_package = "cortexpb"; + +import "gogoproto/gogo.proto"; + +option (gogoproto.marshaler_all) = true; +option (gogoproto.unmarshaler_all) = true; + +message CardinalityStatItem { + string name = 1; + uint64 value = 2; +} diff --git a/pkg/distributor/cardinality_test.go b/pkg/distributor/cardinality_test.go new file mode 100644 index 00000000000..67015b3bd68 --- /dev/null +++ b/pkg/distributor/cardinality_test.go @@ -0,0 +1,90 @@ +package distributor + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cortexproject/cortex/pkg/cortexpb" + ingester_client "github.com/cortexproject/cortex/pkg/ingester/client" +) + +func TestTopNStats(t *testing.T) { + items := map[string]uint64{ + "metric_a": 300, + "metric_b": 600, + "metric_c": 900, + } + + // With RF=3, values should be divided by 3. + result := topNStats(items, 3, 2) + assert.Equal(t, 2, len(result)) + assert.Equal(t, "metric_c", result[0].Name) + assert.Equal(t, uint64(300), result[0].Value) // 900/3 + assert.Equal(t, "metric_b", result[1].Name) + assert.Equal(t, uint64(200), result[1].Value) // 600/3 +} + +func TestTopNStatsByMax(t *testing.T) { + items := map[string]uint64{ + "label_a": 100, + "label_b": 50, + "label_c": 200, + } + + result := topNStatsByMax(items, 2) + assert.Equal(t, 2, len(result)) + assert.Equal(t, "label_c", result[0].Name) + assert.Equal(t, uint64(200), result[0].Value) + assert.Equal(t, "label_a", result[1].Name) + assert.Equal(t, uint64(100), result[1].Value) +} + +func TestAggregateStatItems(t *testing.T) { + resps := []any{ + &ingester_client.CardinalityResponse{ + SeriesCountByMetricName: []*cortexpb.CardinalityStatItem{ + {Name: "metric_a", Value: 100}, + {Name: "metric_b", Value: 200}, + }, + }, + &ingester_client.CardinalityResponse{ + SeriesCountByMetricName: []*cortexpb.CardinalityStatItem{ + {Name: "metric_a", Value: 150}, + {Name: "metric_c", Value: 300}, + }, + }, + } + + result := aggregateStatItems(resps, func(r *ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem { + return r.SeriesCountByMetricName + }) + + assert.Equal(t, uint64(250), result["metric_a"]) // 100+150 + assert.Equal(t, uint64(200), result["metric_b"]) + assert.Equal(t, uint64(300), result["metric_c"]) +} + +func TestMaxStatItems(t *testing.T) { + resps := []any{ + &ingester_client.CardinalityResponse{ + LabelValueCountByLabelName: []*cortexpb.CardinalityStatItem{ + {Name: "instance", Value: 50}, + {Name: "job", Value: 10}, + }, + }, + &ingester_client.CardinalityResponse{ + LabelValueCountByLabelName: []*cortexpb.CardinalityStatItem{ + {Name: "instance", Value: 30}, + {Name: "job", Value: 15}, + }, + }, + } + + result := maxStatItems(resps, func(r *ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem { + return r.LabelValueCountByLabelName + }) + + assert.Equal(t, uint64(50), result["instance"]) // max(50, 30) + assert.Equal(t, uint64(15), result["job"]) // max(10, 15) +} diff --git a/pkg/distributor/distributor.go b/pkg/distributor/distributor.go index 04f62fabbe6..e4727ffc76c 100644 --- a/pkg/distributor/distributor.go +++ b/pkg/distributor/distributor.go @@ -1678,6 +1678,121 @@ func (d *Distributor) UserStats(ctx context.Context) (*ingester.UserStats, error return totalStats, nil } +// Cardinality returns per-tenant cardinality statistics from ingesters. +func (d *Distributor) Cardinality(ctx context.Context, req *ingester_client.CardinalityRequest) (*ingester_client.CardinalityResponse, error) { + replicationSet, err := d.GetIngestersForMetadata(ctx) + if err != nil { + return nil, err + } + + // All ingesters must respond for the RF-based aggregation to be accurate. + replicationSet.MaxErrors = 0 + + resps, err := d.ForReplicationSet(ctx, replicationSet, false, false, func(ctx context.Context, client ingester_client.IngesterClient) (any, error) { + return client.Cardinality(ctx, req) + }) + if err != nil { + return nil, err + } + + factor := d.ingestersRing.ReplicationFactor() + limit := int(req.Limit) + + // Aggregate numSeries across all ingesters. + var totalNumSeries uint64 + for _, resp := range resps { + r := resp.(*ingester_client.CardinalityResponse) + totalNumSeries += r.NumSeries + } + totalNumSeries /= uint64(factor) + + // Aggregate seriesCountByMetricName: sum per metric, divide by RF, top N. + seriesByMetric := aggregateStatItems(resps, func(r *ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem { + return r.SeriesCountByMetricName + }) + seriesCountByMetricName := topNStats(seriesByMetric, factor, limit) + + // Aggregate labelValueCountByLabelName: max per label (not affected by RF). + labelValueCounts := maxStatItems(resps, func(r *ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem { + return r.LabelValueCountByLabelName + }) + labelValueCountByLabelName := topNStatsByMax(labelValueCounts, limit) + + // Aggregate seriesCountByLabelValuePair: sum per pair, divide by RF, top N. + seriesByPair := aggregateStatItems(resps, func(r *ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem { + return r.SeriesCountByLabelValuePair + }) + seriesCountByLabelValuePair := topNStats(seriesByPair, factor, limit) + + return &ingester_client.CardinalityResponse{ + NumSeries: totalNumSeries, + SeriesCountByMetricName: seriesCountByMetricName, + LabelValueCountByLabelName: labelValueCountByLabelName, + SeriesCountByLabelValuePair: seriesCountByLabelValuePair, + }, nil +} + +// aggregateStatItems sums stat item values across all ingester responses using the provided extractor. +func aggregateStatItems(resps []any, extract func(*ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem) map[string]uint64 { + totals := make(map[string]uint64) + for _, resp := range resps { + r := resp.(*ingester_client.CardinalityResponse) + for _, item := range extract(r) { + totals[item.Name] += item.Value + } + } + return totals +} + +// maxStatItems takes the maximum stat item value across all ingester responses using the provided extractor. +func maxStatItems(resps []any, extract func(*ingester_client.CardinalityResponse) []*cortexpb.CardinalityStatItem) map[string]uint64 { + totals := make(map[string]uint64) + for _, resp := range resps { + r := resp.(*ingester_client.CardinalityResponse) + for _, item := range extract(r) { + if item.Value > totals[item.Name] { + totals[item.Name] = item.Value + } + } + } + return totals +} + +// topNStats divides values by the replication factor, sorts descending, and returns the top N items. +func topNStats(items map[string]uint64, replicationFactor, limit int) []*cortexpb.CardinalityStatItem { + return sortAndTruncateCardinalityItems(items, limit, func(v uint64) uint64 { + return v / uint64(replicationFactor) + }) +} + +// topNStatsByMax sorts descending by value and returns the top N items (no RF division). +func topNStatsByMax(items map[string]uint64, limit int) []*cortexpb.CardinalityStatItem { + return sortAndTruncateCardinalityItems(items, limit, nil) +} + +// sortAndTruncateCardinalityItems converts a map to sorted stat items, optionally transforming values. +func sortAndTruncateCardinalityItems(items map[string]uint64, limit int, transform func(uint64) uint64) []*cortexpb.CardinalityStatItem { + result := make([]*cortexpb.CardinalityStatItem, 0, len(items)) + for name, value := range items { + if transform != nil { + value = transform(value) + } + result = append(result, &cortexpb.CardinalityStatItem{ + Name: name, + Value: value, + }) + } + + sort.Slice(result, func(i, j int) bool { + return result[i].Value > result[j].Value + }) + + if limit > 0 && len(result) > limit { + result = result[:limit] + } + return result +} + // AllUserStats returns statistics about all users. // Note it does not divide by the ReplicationFactor like UserStats() func (d *Distributor) AllUserStats(ctx context.Context) ([]ingester.UserIDStats, int, error) { diff --git a/pkg/ingester/client/cortex_mock_test.go b/pkg/ingester/client/cortex_mock_test.go index e9e493a0204..98f829957bd 100644 --- a/pkg/ingester/client/cortex_mock_test.go +++ b/pkg/ingester/client/cortex_mock_test.go @@ -81,3 +81,8 @@ func (m *IngesterServerMock) MetricsMetadata(ctx context.Context, r *MetricsMeta args := m.Called(ctx, r) return args.Get(0).(*MetricsMetadataResponse), args.Error(1) } + +func (m *IngesterServerMock) Cardinality(ctx context.Context, r *CardinalityRequest) (*CardinalityResponse, error) { + args := m.Called(ctx, r) + return args.Get(0).(*CardinalityResponse), args.Error(1) +} diff --git a/pkg/ingester/client/ingester.pb.go b/pkg/ingester/client/ingester.pb.go index 6976ae7ed45..a3ffced9acb 100644 --- a/pkg/ingester/client/ingester.pb.go +++ b/pkg/ingester/client/ingester.pb.go @@ -1475,6 +1475,116 @@ func (m *TimeSeriesFile) GetData() []byte { return nil } +type CardinalityRequest struct { + Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` +} + +func (m *CardinalityRequest) Reset() { *m = CardinalityRequest{} } +func (*CardinalityRequest) ProtoMessage() {} +func (*CardinalityRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_60f6df4f3586b478, []int{27} +} +func (m *CardinalityRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CardinalityRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CardinalityRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CardinalityRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CardinalityRequest.Merge(m, src) +} +func (m *CardinalityRequest) XXX_Size() int { + return m.Size() +} +func (m *CardinalityRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CardinalityRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CardinalityRequest proto.InternalMessageInfo + +func (m *CardinalityRequest) GetLimit() int32 { + if m != nil { + return m.Limit + } + return 0 +} + +type CardinalityResponse struct { + NumSeries uint64 `protobuf:"varint,1,opt,name=num_series,json=numSeries,proto3" json:"num_series,omitempty"` + SeriesCountByMetricName []*cortexpb.CardinalityStatItem `protobuf:"bytes,2,rep,name=series_count_by_metric_name,json=seriesCountByMetricName,proto3" json:"series_count_by_metric_name,omitempty"` + LabelValueCountByLabelName []*cortexpb.CardinalityStatItem `protobuf:"bytes,3,rep,name=label_value_count_by_label_name,json=labelValueCountByLabelName,proto3" json:"label_value_count_by_label_name,omitempty"` + SeriesCountByLabelValuePair []*cortexpb.CardinalityStatItem `protobuf:"bytes,4,rep,name=series_count_by_label_value_pair,json=seriesCountByLabelValuePair,proto3" json:"series_count_by_label_value_pair,omitempty"` +} + +func (m *CardinalityResponse) Reset() { *m = CardinalityResponse{} } +func (*CardinalityResponse) ProtoMessage() {} +func (*CardinalityResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_60f6df4f3586b478, []int{28} +} +func (m *CardinalityResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CardinalityResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CardinalityResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CardinalityResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CardinalityResponse.Merge(m, src) +} +func (m *CardinalityResponse) XXX_Size() int { + return m.Size() +} +func (m *CardinalityResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CardinalityResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CardinalityResponse proto.InternalMessageInfo + +func (m *CardinalityResponse) GetNumSeries() uint64 { + if m != nil { + return m.NumSeries + } + return 0 +} + +func (m *CardinalityResponse) GetSeriesCountByMetricName() []*cortexpb.CardinalityStatItem { + if m != nil { + return m.SeriesCountByMetricName + } + return nil +} + +func (m *CardinalityResponse) GetLabelValueCountByLabelName() []*cortexpb.CardinalityStatItem { + if m != nil { + return m.LabelValueCountByLabelName + } + return nil +} + +func (m *CardinalityResponse) GetSeriesCountByLabelValuePair() []*cortexpb.CardinalityStatItem { + if m != nil { + return m.SeriesCountByLabelValuePair + } + return nil +} + func init() { proto.RegisterEnum("cortex.MatchType", MatchType_name, MatchType_value) proto.RegisterType((*ReadRequest)(nil), "cortex.ReadRequest") @@ -1504,102 +1614,113 @@ func init() { proto.RegisterType((*LabelMatchers)(nil), "cortex.LabelMatchers") proto.RegisterType((*LabelMatcher)(nil), "cortex.LabelMatcher") proto.RegisterType((*TimeSeriesFile)(nil), "cortex.TimeSeriesFile") + proto.RegisterType((*CardinalityRequest)(nil), "cortex.CardinalityRequest") + proto.RegisterType((*CardinalityResponse)(nil), "cortex.CardinalityResponse") } func init() { proto.RegisterFile("ingester.proto", fileDescriptor_60f6df4f3586b478) } var fileDescriptor_60f6df4f3586b478 = []byte{ - // 1439 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x4b, 0x73, 0x13, 0x47, - 0x10, 0xd6, 0xea, 0x65, 0xa9, 0xf5, 0x40, 0x1e, 0x1b, 0x2c, 0x44, 0x58, 0xc1, 0x52, 0x24, 0xaa, - 0x24, 0xc8, 0xe0, 0x24, 0x55, 0x90, 0x07, 0x94, 0x05, 0x06, 0x0c, 0x18, 0xc3, 0xda, 0x40, 0x2a, - 0x95, 0xd4, 0xd6, 0x5a, 0x1a, 0xcb, 0x1b, 0x76, 0xb5, 0xcb, 0xce, 0x88, 0x02, 0x4e, 0x49, 0xe5, - 0x07, 0x24, 0x87, 0xfc, 0x81, 0xdc, 0x72, 0x4d, 0x55, 0x7e, 0x04, 0x47, 0x1f, 0x72, 0xa0, 0x38, - 0xb8, 0x82, 0xb8, 0x24, 0x37, 0xf2, 0x0f, 0x52, 0x3b, 0x33, 0xfb, 0xb4, 0xfc, 0x20, 0x05, 0xb9, - 0x69, 0xbb, 0xbf, 0xe9, 0xe9, 0xfe, 0xe6, 0x9b, 0xe9, 0xb6, 0xa1, 0x6a, 0x0c, 0xfa, 0x98, 0x50, - 0xec, 0xb6, 0x1d, 0xd7, 0xa6, 0x36, 0xca, 0x77, 0x6d, 0x97, 0xe2, 0x47, 0x8d, 0xe9, 0xbe, 0xdd, - 0xb7, 0x99, 0x69, 0xd6, 0xfb, 0xc5, 0xbd, 0x8d, 0x73, 0x7d, 0x83, 0x6e, 0x0c, 0xd7, 0xda, 0x5d, - 0xdb, 0x9a, 0xe5, 0x40, 0xc7, 0xb5, 0xbf, 0xc5, 0x5d, 0x2a, 0xbe, 0x66, 0x9d, 0xfb, 0x7d, 0xdf, - 0xb1, 0x26, 0x7e, 0xf0, 0xa5, 0xca, 0x17, 0x50, 0x52, 0xb1, 0xde, 0x53, 0xf1, 0x83, 0x21, 0x26, - 0x14, 0xb5, 0x61, 0xe2, 0xc1, 0x10, 0xbb, 0x06, 0x26, 0x75, 0xe9, 0x58, 0xa6, 0x55, 0x9a, 0x9b, - 0x6e, 0x0b, 0xf8, 0xed, 0x21, 0x76, 0x1f, 0x0b, 0x98, 0xea, 0x83, 0x94, 0x0b, 0x50, 0xe6, 0xcb, - 0x89, 0x63, 0x0f, 0x08, 0x46, 0xb3, 0x30, 0xe1, 0x62, 0x32, 0x34, 0xa9, 0xbf, 0xfe, 0x60, 0x62, - 0x3d, 0xc7, 0xa9, 0x3e, 0x4a, 0xb9, 0x0e, 0x95, 0x98, 0x07, 0x7d, 0x0a, 0x40, 0x0d, 0x0b, 0x93, - 0x71, 0x49, 0x38, 0x6b, 0xed, 0x55, 0xc3, 0xc2, 0x2b, 0xcc, 0xd7, 0xc9, 0x3e, 0xdd, 0x6a, 0xa6, - 0xd4, 0x08, 0x5a, 0xf9, 0x39, 0x0d, 0xe5, 0x68, 0x9e, 0xe8, 0x43, 0x40, 0x84, 0xea, 0x2e, 0xd5, - 0x18, 0x88, 0xea, 0x96, 0xa3, 0x59, 0x5e, 0x50, 0xa9, 0x95, 0x51, 0x6b, 0xcc, 0xb3, 0xea, 0x3b, - 0x96, 0x08, 0x6a, 0x41, 0x0d, 0x0f, 0x7a, 0x71, 0x6c, 0x9a, 0x61, 0xab, 0x78, 0xd0, 0x8b, 0x22, - 0x4f, 0x43, 0xc1, 0xd2, 0x69, 0x77, 0x03, 0xbb, 0xa4, 0x9e, 0x89, 0xf3, 0x74, 0x43, 0x5f, 0xc3, - 0xe6, 0x12, 0x77, 0xaa, 0x01, 0x0a, 0x3d, 0x81, 0x8c, 0x8a, 0xd7, 0xeb, 0x7f, 0x4f, 0x1c, 0x93, - 0x5a, 0xa5, 0xb9, 0x23, 0x61, 0x41, 0x4b, 0x98, 0x10, 0xbd, 0x8f, 0xef, 0x19, 0x74, 0xa3, 0x33, - 0x5c, 0x57, 0xf1, 0x7a, 0xe7, 0x9a, 0x57, 0xd7, 0xe6, 0x56, 0x53, 0x7a, 0xbe, 0xd5, 0x3c, 0xff, - 0x3a, 0x27, 0xbb, 0x3d, 0x96, 0xea, 0x6d, 0xaa, 0xfc, 0x22, 0xc1, 0xf4, 0xc2, 0x23, 0x6c, 0x39, - 0xa6, 0xee, 0xfe, 0x2f, 0xf4, 0x9c, 0xd9, 0x46, 0xcf, 0xc1, 0x71, 0xf4, 0x90, 0x90, 0x1f, 0xe5, - 0x6b, 0x98, 0x62, 0xa9, 0xad, 0x50, 0x17, 0xeb, 0x56, 0xa0, 0x86, 0x0b, 0x50, 0xea, 0x6e, 0x0c, - 0x07, 0xf7, 0x63, 0x72, 0x98, 0xf1, 0x83, 0x85, 0x62, 0xb8, 0xe8, 0x81, 0x84, 0x22, 0xa2, 0x2b, - 0xae, 0x65, 0x0b, 0xe9, 0x5a, 0x46, 0x59, 0x81, 0x83, 0x09, 0x02, 0xde, 0x80, 0xda, 0xfe, 0x90, - 0x00, 0xb1, 0x72, 0xee, 0xea, 0xe6, 0x10, 0x13, 0x9f, 0xd4, 0xa3, 0x00, 0xa6, 0x67, 0xd5, 0x06, - 0xba, 0x85, 0x19, 0x99, 0x45, 0xb5, 0xc8, 0x2c, 0x37, 0x75, 0x0b, 0xef, 0xc0, 0x79, 0xfa, 0x35, - 0x38, 0xcf, 0xec, 0xc9, 0x79, 0x96, 0x89, 0x6c, 0x2f, 0xce, 0xd1, 0x34, 0xe4, 0x4c, 0xc3, 0x32, - 0x68, 0x3d, 0xc7, 0x22, 0xf2, 0x0f, 0xe5, 0x2c, 0x4c, 0xc5, 0xaa, 0x12, 0x4c, 0x1d, 0x87, 0x32, - 0x2f, 0xeb, 0x21, 0xb3, 0x33, 0xae, 0x8a, 0x6a, 0xc9, 0x0c, 0xa1, 0xca, 0x79, 0x38, 0x1c, 0x59, - 0x99, 0x38, 0xc9, 0x7d, 0xac, 0xff, 0x5d, 0x82, 0xc9, 0x1b, 0x3e, 0x51, 0xe4, 0x6d, 0x8b, 0x34, - 0xa8, 0x3e, 0x13, 0xa9, 0xfe, 0x3f, 0xd0, 0xa8, 0x7c, 0x22, 0x64, 0x20, 0xb2, 0x16, 0xf5, 0x36, - 0xa1, 0x14, 0xca, 0xc0, 0x2f, 0x17, 0x02, 0x1d, 0x10, 0xe5, 0x33, 0xa8, 0x87, 0xcb, 0x12, 0x64, - 0xed, 0xb9, 0x18, 0x41, 0xed, 0x0e, 0xc1, 0xee, 0x0a, 0xd5, 0xa9, 0x4f, 0x94, 0xf2, 0x7d, 0x1a, - 0x26, 0x23, 0x46, 0x11, 0xea, 0xa4, 0xdf, 0x4b, 0x0c, 0x7b, 0xa0, 0xb9, 0x3a, 0xe5, 0x92, 0x94, - 0xd4, 0x4a, 0x60, 0x55, 0x75, 0x8a, 0x3d, 0xd5, 0x0e, 0x86, 0x96, 0x26, 0x2e, 0x82, 0xc7, 0x58, - 0x56, 0x2d, 0x0e, 0x86, 0x16, 0x57, 0xbf, 0x77, 0x08, 0xba, 0x63, 0x68, 0x89, 0x48, 0x19, 0x16, - 0xa9, 0xa6, 0x3b, 0xc6, 0x62, 0x2c, 0x58, 0x1b, 0xa6, 0xdc, 0xa1, 0x89, 0x93, 0xf0, 0x2c, 0x83, - 0x4f, 0x7a, 0xae, 0x38, 0xfe, 0x04, 0x54, 0xf4, 0x2e, 0x35, 0x1e, 0x62, 0x7f, 0xff, 0x1c, 0xdb, - 0xbf, 0xcc, 0x8d, 0x22, 0x85, 0x13, 0x50, 0x31, 0x6d, 0xbd, 0x87, 0x7b, 0xda, 0x9a, 0x69, 0x77, - 0xef, 0x93, 0x7a, 0x9e, 0x83, 0xb8, 0xb1, 0xc3, 0x6c, 0xca, 0x37, 0x30, 0xe5, 0x51, 0xb0, 0x78, - 0x29, 0x4e, 0xc2, 0x0c, 0x4c, 0x0c, 0x09, 0x76, 0x35, 0xa3, 0x27, 0x2e, 0x64, 0xde, 0xfb, 0x5c, - 0xec, 0xa1, 0x53, 0x90, 0xed, 0xe9, 0x54, 0x67, 0x05, 0x97, 0xe6, 0x0e, 0xfb, 0x47, 0xbd, 0x8d, - 0x46, 0x95, 0xc1, 0x94, 0x2b, 0x80, 0x3c, 0x17, 0x89, 0x47, 0x3f, 0x03, 0x39, 0xe2, 0x19, 0xc4, - 0xfb, 0x71, 0x24, 0x1a, 0x25, 0x91, 0x89, 0xca, 0x91, 0xca, 0x53, 0x09, 0xe4, 0x25, 0x4c, 0x5d, - 0xa3, 0x4b, 0x2e, 0xdb, 0x6e, 0x5c, 0x59, 0x6f, 0x59, 0xf7, 0x67, 0xa1, 0xec, 0x4b, 0x57, 0x23, - 0x98, 0xee, 0xfe, 0x40, 0x97, 0x7c, 0xe8, 0x0a, 0xa6, 0xe1, 0x8d, 0xc9, 0x46, 0xdf, 0x8b, 0xeb, - 0xd0, 0xdc, 0xb1, 0x12, 0x41, 0x50, 0x0b, 0xf2, 0x16, 0x83, 0x08, 0x86, 0x6a, 0xd1, 0xf6, 0xe7, - 0xd9, 0x55, 0xe1, 0x57, 0x6e, 0xc3, 0xc9, 0x1d, 0x82, 0x25, 0x6e, 0xc8, 0xfe, 0x43, 0x3a, 0x70, - 0x48, 0x84, 0x5c, 0xc2, 0x54, 0xf7, 0x8e, 0xd1, 0x67, 0x38, 0xa8, 0x47, 0x8a, 0xbe, 0x00, 0x2d, - 0xa8, 0xb1, 0x1f, 0x9a, 0x83, 0x5d, 0x4d, 0xec, 0x21, 0x98, 0x64, 0xf6, 0x5b, 0xd8, 0xe5, 0xf1, - 0xd0, 0xa1, 0x20, 0x87, 0x0c, 0x17, 0x95, 0xd8, 0x71, 0x19, 0x66, 0xb6, 0xed, 0x28, 0xd2, 0xfe, - 0x18, 0x0a, 0x96, 0xb0, 0x89, 0xc4, 0xeb, 0xc9, 0xc4, 0x83, 0x35, 0x01, 0x52, 0xf9, 0x47, 0x82, - 0x03, 0x89, 0x5e, 0xe7, 0xa5, 0xb9, 0xee, 0xda, 0x96, 0xe6, 0x0f, 0x8a, 0xa1, 0xb6, 0xab, 0x9e, - 0x7d, 0x51, 0x98, 0x17, 0x7b, 0x51, 0xf1, 0xa7, 0x63, 0xe2, 0x1f, 0x40, 0x9e, 0x3d, 0x29, 0x7e, - 0x93, 0x9e, 0x0a, 0x53, 0x61, 0xd4, 0xdf, 0xd2, 0x0d, 0xb7, 0x33, 0xef, 0xf5, 0xbd, 0xe7, 0x5b, - 0xcd, 0xd7, 0x9a, 0x31, 0xf9, 0xfa, 0xf9, 0x9e, 0xee, 0x50, 0xec, 0xaa, 0x62, 0x17, 0xf4, 0x01, - 0xe4, 0x79, 0x6b, 0xae, 0x67, 0xd9, 0x7e, 0x15, 0x5f, 0x73, 0xd1, 0xee, 0x2d, 0x20, 0xca, 0x8f, - 0x12, 0xe4, 0x78, 0xa5, 0x6f, 0xeb, 0x22, 0x34, 0xa0, 0x80, 0x07, 0x5d, 0xbb, 0x67, 0x0c, 0xfa, - 0xec, 0x00, 0x73, 0x6a, 0xf0, 0x8d, 0x90, 0x78, 0x17, 0x3c, 0xa5, 0x97, 0xc5, 0xe5, 0x9f, 0x87, - 0x4a, 0x4c, 0x91, 0xb1, 0x29, 0x50, 0xda, 0xcf, 0x14, 0xa8, 0x68, 0x50, 0x8e, 0x7a, 0xd0, 0x49, - 0xc8, 0xd2, 0xc7, 0x0e, 0x7f, 0x92, 0xab, 0x73, 0x93, 0xfe, 0x6a, 0xe6, 0x5e, 0x7d, 0xec, 0x60, - 0x95, 0xb9, 0xbd, 0x6c, 0xd8, 0x30, 0xc1, 0x8f, 0x8f, 0xfd, 0xf6, 0xc4, 0xcb, 0x3a, 0xa9, 0xd0, - 0x1e, 0xff, 0x50, 0x7e, 0x90, 0xa0, 0x1a, 0x2a, 0xe5, 0xb2, 0x61, 0xe2, 0x37, 0x21, 0x94, 0x06, - 0x14, 0xd6, 0x0d, 0x13, 0xb3, 0x1c, 0xf8, 0x76, 0xc1, 0xf7, 0x38, 0xa6, 0xde, 0xbf, 0x06, 0xc5, - 0xa0, 0x04, 0x54, 0x84, 0xdc, 0xc2, 0xed, 0x3b, 0xf3, 0x37, 0x6a, 0x29, 0x54, 0x81, 0xe2, 0xcd, - 0xe5, 0x55, 0x8d, 0x7f, 0x4a, 0xe8, 0x00, 0x94, 0xd4, 0x85, 0x2b, 0x0b, 0x5f, 0x6a, 0x4b, 0xf3, - 0xab, 0x17, 0xaf, 0xd6, 0xd2, 0x08, 0x41, 0x95, 0x1b, 0x6e, 0x2e, 0x0b, 0x5b, 0x66, 0xee, 0xb7, - 0x02, 0x14, 0xfc, 0x1c, 0xd1, 0x39, 0xc8, 0xde, 0x1a, 0x92, 0x0d, 0x74, 0x28, 0x54, 0xea, 0x3d, - 0xd7, 0xa0, 0x58, 0xdc, 0xe8, 0xc6, 0xcc, 0x36, 0x3b, 0xbf, 0x77, 0x4a, 0x0a, 0x2d, 0x02, 0x78, - 0x4b, 0xf9, 0x33, 0x82, 0xde, 0x09, 0x81, 0xdc, 0xb2, 0xcf, 0x30, 0x2d, 0xe9, 0xb4, 0x84, 0x2e, - 0x41, 0x29, 0x32, 0xab, 0xa2, 0xb1, 0x7f, 0x22, 0x35, 0x8e, 0xc4, 0xac, 0xf1, 0xd7, 0x4b, 0x49, - 0x9d, 0x96, 0xd0, 0x32, 0x54, 0x99, 0xcb, 0x1f, 0x4c, 0x49, 0x90, 0x54, 0x7b, 0xdc, 0xb0, 0xde, - 0x38, 0xba, 0x83, 0x37, 0xa8, 0xf0, 0x2a, 0x94, 0x22, 0xe3, 0x17, 0x6a, 0xc4, 0xb4, 0x18, 0x9b, - 0x51, 0xc3, 0xe4, 0xc6, 0x4c, 0x7a, 0x4a, 0x0a, 0xdd, 0x15, 0x73, 0x58, 0x74, 0x90, 0xdb, 0x35, - 0xde, 0xf1, 0x31, 0xbe, 0x31, 0x25, 0x2f, 0x00, 0x84, 0x23, 0x0f, 0x3a, 0x1c, 0x5b, 0x14, 0x9d, - 0xf9, 0x1a, 0x8d, 0x71, 0xae, 0x20, 0xbd, 0x15, 0xa8, 0x25, 0x27, 0xa7, 0xdd, 0x82, 0x1d, 0xdb, - 0xee, 0x1a, 0x93, 0x5b, 0x07, 0x8a, 0x41, 0xd7, 0x47, 0xf5, 0x31, 0x83, 0x00, 0x0f, 0xb6, 0xf3, - 0x88, 0xa0, 0xa4, 0xd0, 0x65, 0x28, 0xcf, 0x9b, 0xe6, 0x7e, 0xc2, 0x34, 0xa2, 0x1e, 0x92, 0x8c, - 0x63, 0x06, 0x0d, 0x24, 0xd9, 0x05, 0xd1, 0xbb, 0xc1, 0x1b, 0xb1, 0xeb, 0xf4, 0xd0, 0x78, 0x6f, - 0x4f, 0x5c, 0xb0, 0xdb, 0x13, 0x38, 0xba, 0x6b, 0xcf, 0xdd, 0xf7, 0x9e, 0xa7, 0xf6, 0xc0, 0x8d, - 0x61, 0x7d, 0x15, 0x0e, 0x24, 0x5a, 0x25, 0x92, 0x13, 0x51, 0x12, 0x5d, 0xbb, 0xd1, 0xdc, 0xd1, - 0xef, 0xc7, 0xed, 0x7c, 0xbe, 0xf9, 0x42, 0x4e, 0x3d, 0x7b, 0x21, 0xa7, 0x5e, 0xbd, 0x90, 0xa5, - 0xef, 0x46, 0xb2, 0xf4, 0xeb, 0x48, 0x96, 0x9e, 0x8e, 0x64, 0x69, 0x73, 0x24, 0x4b, 0x7f, 0x8e, - 0x64, 0xe9, 0xaf, 0x91, 0x9c, 0x7a, 0x35, 0x92, 0xa5, 0x9f, 0x5e, 0xca, 0xa9, 0xcd, 0x97, 0x72, - 0xea, 0xd9, 0x4b, 0x39, 0xf5, 0x55, 0xbe, 0x6b, 0x1a, 0x78, 0x40, 0xd7, 0xf2, 0xec, 0x3f, 0x23, - 0x1f, 0xfd, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x3c, 0x76, 0xeb, 0xb3, 0x84, 0x11, 0x00, 0x00, + // 1581 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xcb, 0x72, 0x13, 0xc7, + 0x1a, 0xd6, 0xe8, 0x66, 0xeb, 0x97, 0x6c, 0xe4, 0xb6, 0xc1, 0x42, 0x3e, 0x1e, 0x99, 0xa1, 0x38, + 0xc7, 0xc5, 0x39, 0xd8, 0xe0, 0x93, 0x54, 0x41, 0x2e, 0x50, 0x96, 0x31, 0x60, 0xc0, 0x18, 0xc6, + 0x06, 0x52, 0xb9, 0xd4, 0xd4, 0x48, 0x6a, 0xdb, 0x13, 0xe6, 0xc6, 0x4c, 0x0f, 0x85, 0x59, 0x25, + 0x95, 0x07, 0x48, 0x16, 0x79, 0x81, 0xec, 0xf2, 0x00, 0x79, 0x08, 0x96, 0x5e, 0x64, 0x41, 0x91, + 0x2a, 0x57, 0x30, 0x9b, 0x64, 0x47, 0xd6, 0xd9, 0xa4, 0xa6, 0xbb, 0xe7, 0xea, 0xb1, 0x2d, 0xa7, + 0x20, 0x3b, 0xf5, 0x7f, 0xff, 0xbf, 0xfe, 0xba, 0xfb, 0x1f, 0xc1, 0xb0, 0x66, 0x6e, 0x60, 0x97, + 0x60, 0x67, 0xc6, 0x76, 0x2c, 0x62, 0xa1, 0x72, 0xd7, 0x72, 0x08, 0x7e, 0xda, 0x1c, 0xdb, 0xb0, + 0x36, 0x2c, 0x2a, 0x9a, 0xf5, 0x7f, 0x31, 0x6d, 0xf3, 0xd2, 0x86, 0x46, 0x36, 0xbd, 0xce, 0x4c, + 0xd7, 0x32, 0x66, 0x99, 0xa1, 0xed, 0x58, 0x5f, 0xe2, 0x2e, 0xe1, 0xab, 0x59, 0xfb, 0xd1, 0x46, + 0xa0, 0xe8, 0xf0, 0x1f, 0xdc, 0xf5, 0xf2, 0xd1, 0x5c, 0x55, 0xa7, 0xa7, 0x99, 0xaa, 0xae, 0x91, + 0x2d, 0xe6, 0x2f, 0x7d, 0x0c, 0x55, 0x19, 0xab, 0x3d, 0x19, 0x3f, 0xf6, 0xb0, 0x4b, 0xd0, 0x0c, + 0x0c, 0x3c, 0xf6, 0xb0, 0xa3, 0x61, 0xb7, 0x21, 0x4c, 0x15, 0xa6, 0xab, 0x73, 0x63, 0x33, 0x3c, + 0xdd, 0x3d, 0x0f, 0x3b, 0x5b, 0xdc, 0x4c, 0x0e, 0x8c, 0xa4, 0x2b, 0x50, 0x63, 0xee, 0xae, 0x6d, + 0x99, 0x2e, 0x46, 0xb3, 0x30, 0xe0, 0x60, 0xd7, 0xd3, 0x49, 0xe0, 0x7f, 0x3c, 0xe5, 0xcf, 0xec, + 0xe4, 0xc0, 0x4a, 0xba, 0x05, 0x43, 0x09, 0x0d, 0xfa, 0x00, 0x80, 0x68, 0x06, 0x76, 0xb3, 0x8a, + 0xb0, 0x3b, 0x33, 0x6b, 0x9a, 0x81, 0x57, 0xa9, 0xae, 0x5d, 0x7c, 0xbe, 0xd3, 0xca, 0xc9, 0x31, + 0x6b, 0xe9, 0xfb, 0x3c, 0xd4, 0xe2, 0x75, 0xa2, 0xff, 0x01, 0x72, 0x89, 0xea, 0x10, 0x85, 0x1a, + 0x11, 0xd5, 0xb0, 0x15, 0xc3, 0x0f, 0x2a, 0x4c, 0x17, 0xe4, 0x3a, 0xd5, 0xac, 0x05, 0x8a, 0x65, + 0x17, 0x4d, 0x43, 0x1d, 0x9b, 0xbd, 0xa4, 0x6d, 0x9e, 0xda, 0x0e, 0x63, 0xb3, 0x17, 0xb7, 0x3c, + 0x0f, 0x83, 0x86, 0x4a, 0xba, 0x9b, 0xd8, 0x71, 0x1b, 0x85, 0x24, 0x4e, 0xb7, 0xd5, 0x0e, 0xd6, + 0x97, 0x99, 0x52, 0x0e, 0xad, 0xd0, 0x33, 0x28, 0xc8, 0x78, 0xbd, 0xf1, 0xfb, 0xc0, 0x94, 0x30, + 0x5d, 0x9d, 0x9b, 0x88, 0x1a, 0x5a, 0xc6, 0xae, 0xab, 0x6e, 0xe0, 0x87, 0x1a, 0xd9, 0x6c, 0x7b, + 0xeb, 0x32, 0x5e, 0x6f, 0xdf, 0xf4, 0xfb, 0xda, 0xde, 0x69, 0x09, 0x2f, 0x77, 0x5a, 0x47, 0xda, + 0xde, 0xbd, 0xb1, 0x64, 0x3f, 0xa9, 0xf4, 0x83, 0x00, 0x63, 0x8b, 0x4f, 0xb1, 0x61, 0xeb, 0xaa, + 0xf3, 0x8f, 0xc0, 0x73, 0x61, 0x0f, 0x3c, 0xc7, 0xb3, 0xe0, 0x71, 0x23, 0x7c, 0xa4, 0xcf, 0x61, + 0x94, 0x96, 0xb6, 0x4a, 0x1c, 0xac, 0x1a, 0x21, 0x1b, 0xae, 0x40, 0xb5, 0xbb, 0xe9, 0x99, 0x8f, + 0x12, 0x74, 0x18, 0x0f, 0x82, 0x45, 0x64, 0x58, 0xf0, 0x8d, 0x38, 0x23, 0xe2, 0x1e, 0x37, 0x8b, + 0x83, 0xf9, 0x7a, 0x41, 0x5a, 0x85, 0xe3, 0x29, 0x00, 0xde, 0x02, 0xdb, 0x7e, 0x16, 0x00, 0xd1, + 0x76, 0x1e, 0xa8, 0xba, 0x87, 0xdd, 0x00, 0xd4, 0x49, 0x00, 0xdd, 0x97, 0x2a, 0xa6, 0x6a, 0x60, + 0x0a, 0x66, 0x45, 0xae, 0x50, 0xc9, 0x1d, 0xd5, 0xc0, 0xfb, 0x60, 0x9e, 0x3f, 0x02, 0xe6, 0x85, + 0x43, 0x31, 0x2f, 0x52, 0x92, 0x1d, 0x86, 0x39, 0x1a, 0x83, 0x92, 0xae, 0x19, 0x1a, 0x69, 0x94, + 0x68, 0x44, 0xb6, 0x90, 0x2e, 0xc2, 0x68, 0xa2, 0x2b, 0x8e, 0xd4, 0x29, 0xa8, 0xb1, 0xb6, 0x9e, + 0x50, 0x39, 0xc5, 0xaa, 0x22, 0x57, 0xf5, 0xc8, 0x54, 0xba, 0x0c, 0x27, 0x63, 0x9e, 0xa9, 0x9d, + 0xec, 0xc3, 0xff, 0x27, 0x01, 0x46, 0x6e, 0x07, 0x40, 0xb9, 0xef, 0x9a, 0xa4, 0x61, 0xf7, 0x85, + 0x58, 0xf7, 0x7f, 0x03, 0x46, 0xe9, 0x7d, 0x4e, 0x03, 0x5e, 0x35, 0xef, 0xb7, 0x05, 0xd5, 0x88, + 0x06, 0x41, 0xbb, 0x10, 0xf2, 0xc0, 0x95, 0x3e, 0x84, 0x46, 0xe4, 0x96, 0x02, 0xeb, 0x50, 0x67, + 0x04, 0xf5, 0xfb, 0x2e, 0x76, 0x56, 0x89, 0x4a, 0x02, 0xa0, 0xa4, 0xaf, 0xf3, 0x30, 0x12, 0x13, + 0xf2, 0x50, 0x67, 0x82, 0xb7, 0x48, 0xb3, 0x4c, 0xc5, 0x51, 0x09, 0xa3, 0xa4, 0x20, 0x0f, 0x85, + 0x52, 0x59, 0x25, 0xd8, 0x67, 0xad, 0xe9, 0x19, 0x0a, 0x3f, 0x08, 0x3e, 0x62, 0x45, 0xb9, 0x62, + 0x7a, 0x06, 0x63, 0xbf, 0xbf, 0x09, 0xaa, 0xad, 0x29, 0xa9, 0x48, 0x05, 0x1a, 0xa9, 0xae, 0xda, + 0xda, 0x52, 0x22, 0xd8, 0x0c, 0x8c, 0x3a, 0x9e, 0x8e, 0xd3, 0xe6, 0x45, 0x6a, 0x3e, 0xe2, 0xab, + 0x92, 0xf6, 0xa7, 0x61, 0x48, 0xed, 0x12, 0xed, 0x09, 0x0e, 0xf2, 0x97, 0x68, 0xfe, 0x1a, 0x13, + 0xf2, 0x12, 0x4e, 0xc3, 0x90, 0x6e, 0xa9, 0x3d, 0xdc, 0x53, 0x3a, 0xba, 0xd5, 0x7d, 0xe4, 0x36, + 0xca, 0xcc, 0x88, 0x09, 0xdb, 0x54, 0x26, 0x7d, 0x01, 0xa3, 0x3e, 0x04, 0x4b, 0x57, 0x93, 0x20, + 0x8c, 0xc3, 0x80, 0xe7, 0x62, 0x47, 0xd1, 0x7a, 0xfc, 0x40, 0x96, 0xfd, 0xe5, 0x52, 0x0f, 0x9d, + 0x83, 0x62, 0x4f, 0x25, 0x2a, 0x6d, 0xb8, 0x3a, 0x77, 0x32, 0xd8, 0xea, 0x3d, 0x30, 0xca, 0xd4, + 0x4c, 0xba, 0x0e, 0xc8, 0x57, 0xb9, 0xc9, 0xe8, 0x17, 0xa0, 0xe4, 0xfa, 0x02, 0x7e, 0x7f, 0x4c, + 0xc4, 0xa3, 0xa4, 0x2a, 0x91, 0x99, 0xa5, 0xf4, 0x5c, 0x00, 0x71, 0x19, 0x13, 0x47, 0xeb, 0xba, + 0xd7, 0x2c, 0x27, 0xc9, 0xac, 0x77, 0xcc, 0xfb, 0x8b, 0x50, 0x0b, 0xa8, 0xab, 0xb8, 0x98, 0x1c, + 0x7c, 0x41, 0x57, 0x03, 0xd3, 0x55, 0x4c, 0xa2, 0x13, 0x53, 0x8c, 0xdf, 0x17, 0xb7, 0xa0, 0xb5, + 0x6f, 0x27, 0x1c, 0xa0, 0x69, 0x28, 0x1b, 0xd4, 0x84, 0x23, 0x54, 0x8f, 0x3f, 0x7f, 0xbe, 0x5c, + 0xe6, 0x7a, 0xe9, 0x1e, 0x9c, 0xd9, 0x27, 0x58, 0xea, 0x84, 0xf4, 0x1f, 0xd2, 0x86, 0x13, 0x3c, + 0xe4, 0x32, 0x26, 0xaa, 0xbf, 0x8d, 0x01, 0xc2, 0x61, 0x3f, 0x42, 0xfc, 0x06, 0x98, 0x86, 0x3a, + 0xfd, 0xa1, 0xd8, 0xd8, 0x51, 0x78, 0x0e, 0x8e, 0x24, 0x95, 0xdf, 0xc5, 0x0e, 0x8b, 0x87, 0x4e, + 0x84, 0x35, 0x14, 0x18, 0xa9, 0x78, 0xc6, 0x15, 0x18, 0xdf, 0x93, 0x91, 0x97, 0xfd, 0x1e, 0x0c, + 0x1a, 0x5c, 0xc6, 0x0b, 0x6f, 0xa4, 0x0b, 0x0f, 0x7d, 0x42, 0x4b, 0xe9, 0x0f, 0x01, 0x8e, 0xa5, + 0xde, 0x3a, 0xbf, 0xcc, 0x75, 0xc7, 0x32, 0x94, 0x60, 0xd0, 0x8c, 0xb8, 0x3d, 0xec, 0xcb, 0x97, + 0xb8, 0x78, 0xa9, 0x17, 0x27, 0x7f, 0x3e, 0x41, 0x7e, 0x13, 0xca, 0xf4, 0x4a, 0x09, 0x1e, 0xe9, + 0xd1, 0xa8, 0x14, 0x0a, 0xfd, 0x5d, 0x55, 0x73, 0xda, 0xf3, 0xfe, 0xbb, 0xf7, 0x72, 0xa7, 0x75, + 0xa4, 0x19, 0x95, 0xf9, 0xcf, 0xf7, 0x54, 0x9b, 0x60, 0x47, 0xe6, 0x59, 0xd0, 0x7f, 0xa1, 0xcc, + 0x9e, 0xe6, 0x46, 0x91, 0xe6, 0x1b, 0x0a, 0x38, 0x17, 0x7f, 0xbd, 0xb9, 0x89, 0xf4, 0xad, 0x00, + 0x25, 0xd6, 0xe9, 0xbb, 0x3a, 0x08, 0x4d, 0x18, 0xc4, 0x66, 0xd7, 0xea, 0x69, 0xe6, 0x06, 0xdd, + 0xc0, 0x92, 0x1c, 0xae, 0x11, 0xe2, 0xf7, 0x82, 0xcf, 0xf4, 0x1a, 0x3f, 0xfc, 0xf3, 0x30, 0x94, + 0x60, 0x64, 0x62, 0x0a, 0x14, 0xfa, 0x99, 0x02, 0x25, 0x05, 0x6a, 0x71, 0x0d, 0x3a, 0x03, 0x45, + 0xb2, 0x65, 0xb3, 0x2b, 0x79, 0x78, 0x6e, 0x24, 0xf0, 0xa6, 0xea, 0xb5, 0x2d, 0x1b, 0xcb, 0x54, + 0xed, 0x57, 0x43, 0x87, 0x09, 0xb6, 0x7d, 0xf4, 0xb7, 0x4f, 0x5e, 0xfa, 0x92, 0x72, 0xee, 0xb1, + 0x85, 0xf4, 0x8d, 0x00, 0xc3, 0x11, 0x53, 0xae, 0x69, 0x3a, 0x7e, 0x1b, 0x44, 0x69, 0xc2, 0xe0, + 0xba, 0xa6, 0x63, 0x5a, 0x03, 0x4b, 0x17, 0xae, 0x33, 0x91, 0x3a, 0x0b, 0x68, 0x21, 0xfa, 0xd2, + 0xc8, 0x3c, 0x6e, 0xa5, 0xe0, 0xfa, 0xf8, 0x25, 0x0f, 0xa3, 0x09, 0x63, 0x7e, 0x52, 0x92, 0x0f, + 0x92, 0x90, 0x7e, 0x90, 0x3e, 0x83, 0x09, 0xa6, 0x52, 0xba, 0x96, 0x67, 0x12, 0xa5, 0xb3, 0xc5, + 0xcf, 0xaa, 0xc2, 0x91, 0xf2, 0xb7, 0x63, 0x32, 0x22, 0x74, 0x2c, 0x85, 0x7f, 0x21, 0x2f, 0x11, + 0x6c, 0xc8, 0xe3, 0x2c, 0xc2, 0x82, 0x1f, 0xa0, 0xbd, 0xc5, 0x4e, 0x1f, 0x9d, 0xd1, 0x3a, 0xd0, + 0x8a, 0xcd, 0x2a, 0x51, 0x86, 0xd8, 0x5c, 0x57, 0xe8, 0x27, 0x41, 0x33, 0x9a, 0x6e, 0x78, 0x92, + 0xf0, 0xc5, 0x47, 0x3d, 0x98, 0x4a, 0x37, 0x10, 0xcf, 0x69, 0xab, 0x9a, 0xc3, 0x8f, 0xc9, 0x21, + 0x49, 0x26, 0x12, 0x5d, 0x44, 0x03, 0x98, 0x7f, 0x7a, 0xcf, 0xde, 0x84, 0x4a, 0x48, 0x26, 0x54, + 0x81, 0xd2, 0xe2, 0xbd, 0xfb, 0xf3, 0xb7, 0xeb, 0x39, 0x34, 0x04, 0x95, 0x3b, 0x2b, 0x6b, 0x0a, + 0x5b, 0x0a, 0xe8, 0x18, 0x54, 0xe5, 0xc5, 0xeb, 0x8b, 0x9f, 0x28, 0xcb, 0xf3, 0x6b, 0x0b, 0x37, + 0xea, 0x79, 0x84, 0x60, 0x98, 0x09, 0xee, 0xac, 0x70, 0x59, 0x61, 0xee, 0xcf, 0x41, 0x18, 0x0c, + 0xd8, 0x82, 0x2e, 0x41, 0xf1, 0xae, 0xe7, 0x6e, 0xa2, 0x13, 0x51, 0x71, 0x0f, 0x1d, 0x8d, 0x60, + 0xbe, 0xd9, 0xcd, 0xf1, 0x3d, 0x72, 0xb6, 0xaf, 0x52, 0x0e, 0x2d, 0x01, 0xf8, 0xae, 0xec, 0x42, + 0x47, 0xff, 0x8a, 0x0c, 0x99, 0xa4, 0xcf, 0x30, 0xd3, 0xc2, 0x79, 0x01, 0x5d, 0x85, 0x6a, 0xec, + 0xab, 0x01, 0x65, 0x7e, 0xac, 0x36, 0x27, 0x12, 0xd2, 0xe4, 0x3b, 0x22, 0xe5, 0xce, 0x0b, 0x68, + 0x05, 0x86, 0xa9, 0x2a, 0xf8, 0x44, 0x70, 0xc3, 0xa2, 0x66, 0xb2, 0x3e, 0x9b, 0x9a, 0x93, 0xfb, + 0x68, 0xc3, 0x0e, 0x6f, 0x40, 0x35, 0x36, 0x08, 0xa3, 0x66, 0xe2, 0x56, 0x48, 0x7c, 0x2d, 0x44, + 0xc5, 0x65, 0xcc, 0xdc, 0x52, 0x0e, 0x3d, 0xe0, 0x13, 0x71, 0x7c, 0xa4, 0x3e, 0x30, 0xde, 0xa9, + 0x0c, 0x5d, 0x46, 0xcb, 0x8b, 0x00, 0xd1, 0xf0, 0x89, 0x4e, 0x26, 0x9c, 0xe2, 0xd3, 0x77, 0xb3, + 0x99, 0xa5, 0x0a, 0xcb, 0x5b, 0x85, 0x7a, 0x7a, 0x86, 0x3d, 0x28, 0xd8, 0xd4, 0x5e, 0x55, 0x46, + 0x6d, 0x6d, 0xa8, 0x84, 0xf3, 0x17, 0x6a, 0x64, 0x8c, 0x64, 0x2c, 0xd8, 0xfe, 0xc3, 0x9a, 0x94, + 0x43, 0xd7, 0xa0, 0x36, 0xaf, 0xeb, 0xfd, 0x84, 0x69, 0xc6, 0x35, 0x6e, 0x3a, 0x8e, 0x1e, 0x3e, + 0xe5, 0xe9, 0x79, 0x04, 0xfd, 0x3b, 0xbc, 0xad, 0x0f, 0x9c, 0xe3, 0x9a, 0xff, 0x39, 0xd4, 0x2e, + 0xcc, 0xf6, 0x0c, 0x26, 0x0f, 0x9c, 0x7e, 0xfa, 0xce, 0x79, 0xee, 0x10, 0xbb, 0x0c, 0xd4, 0xd7, + 0xe0, 0x58, 0x6a, 0x68, 0x41, 0x62, 0x2a, 0x4a, 0x6a, 0x7e, 0x6a, 0xb6, 0xf6, 0xd5, 0xc7, 0x4f, + 0x42, 0xec, 0xce, 0x8a, 0x98, 0xbb, 0xf7, 0x79, 0x88, 0x4e, 0x42, 0xc6, 0x6b, 0x20, 0xe5, 0xda, + 0x1f, 0x6d, 0xbf, 0x12, 0x73, 0x2f, 0x5e, 0x89, 0xb9, 0x37, 0xaf, 0x44, 0xe1, 0xab, 0x5d, 0x51, + 0xf8, 0x71, 0x57, 0x14, 0x9e, 0xef, 0x8a, 0xc2, 0xf6, 0xae, 0x28, 0xfc, 0xba, 0x2b, 0x0a, 0xbf, + 0xed, 0x8a, 0xb9, 0x37, 0xbb, 0xa2, 0xf0, 0xdd, 0x6b, 0x31, 0xb7, 0xfd, 0x5a, 0xcc, 0xbd, 0x78, + 0x2d, 0xe6, 0x3e, 0x2d, 0x77, 0x75, 0x0d, 0x9b, 0xa4, 0x53, 0xa6, 0xff, 0x76, 0xfd, 0xff, 0xaf, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xfa, 0xf2, 0x92, 0x96, 0x98, 0x13, 0x00, 0x00, } func (x MatchType) String() string { @@ -2451,6 +2572,78 @@ func (this *TimeSeriesFile) Equal(that interface{}) bool { } return true } +func (this *CardinalityRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*CardinalityRequest) + if !ok { + that2, ok := that.(CardinalityRequest) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Limit != that1.Limit { + return false + } + return true +} +func (this *CardinalityResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*CardinalityResponse) + if !ok { + that2, ok := that.(CardinalityResponse) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.NumSeries != that1.NumSeries { + return false + } + if len(this.SeriesCountByMetricName) != len(that1.SeriesCountByMetricName) { + return false + } + for i := range this.SeriesCountByMetricName { + if !this.SeriesCountByMetricName[i].Equal(that1.SeriesCountByMetricName[i]) { + return false + } + } + if len(this.LabelValueCountByLabelName) != len(that1.LabelValueCountByLabelName) { + return false + } + for i := range this.LabelValueCountByLabelName { + if !this.LabelValueCountByLabelName[i].Equal(that1.LabelValueCountByLabelName[i]) { + return false + } + } + if len(this.SeriesCountByLabelValuePair) != len(that1.SeriesCountByLabelValuePair) { + return false + } + for i := range this.SeriesCountByLabelValuePair { + if !this.SeriesCountByLabelValuePair[i].Equal(that1.SeriesCountByLabelValuePair[i]) { + return false + } + } + return true +} func (this *ReadRequest) GoString() string { if this == nil { return "nil" @@ -2804,6 +2997,35 @@ func (this *TimeSeriesFile) GoString() string { s = append(s, "}") return strings.Join(s, "") } +func (this *CardinalityRequest) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 5) + s = append(s, "&client.CardinalityRequest{") + s = append(s, "Limit: "+fmt.Sprintf("%#v", this.Limit)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func (this *CardinalityResponse) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 8) + s = append(s, "&client.CardinalityResponse{") + s = append(s, "NumSeries: "+fmt.Sprintf("%#v", this.NumSeries)+",\n") + if this.SeriesCountByMetricName != nil { + s = append(s, "SeriesCountByMetricName: "+fmt.Sprintf("%#v", this.SeriesCountByMetricName)+",\n") + } + if this.LabelValueCountByLabelName != nil { + s = append(s, "LabelValueCountByLabelName: "+fmt.Sprintf("%#v", this.LabelValueCountByLabelName)+",\n") + } + if this.SeriesCountByLabelValuePair != nil { + s = append(s, "SeriesCountByLabelValuePair: "+fmt.Sprintf("%#v", this.SeriesCountByLabelValuePair)+",\n") + } + s = append(s, "}") + return strings.Join(s, "") +} func valueToGoStringIngester(v interface{}, typ string) string { rv := reflect.ValueOf(v) if rv.IsNil() { @@ -2838,6 +3060,7 @@ type IngesterClient interface { MetricsForLabelMatchers(ctx context.Context, in *MetricsForLabelMatchersRequest, opts ...grpc.CallOption) (*MetricsForLabelMatchersResponse, error) MetricsForLabelMatchersStream(ctx context.Context, in *MetricsForLabelMatchersRequest, opts ...grpc.CallOption) (Ingester_MetricsForLabelMatchersStreamClient, error) MetricsMetadata(ctx context.Context, in *MetricsMetadataRequest, opts ...grpc.CallOption) (*MetricsMetadataResponse, error) + Cardinality(ctx context.Context, in *CardinalityRequest, opts ...grpc.CallOption) (*CardinalityResponse, error) } type ingesterClient struct { @@ -3079,6 +3302,15 @@ func (c *ingesterClient) MetricsMetadata(ctx context.Context, in *MetricsMetadat return out, nil } +func (c *ingesterClient) Cardinality(ctx context.Context, in *CardinalityRequest, opts ...grpc.CallOption) (*CardinalityResponse, error) { + out := new(CardinalityResponse) + err := c.cc.Invoke(ctx, "/cortex.Ingester/Cardinality", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // IngesterServer is the server API for Ingester service. type IngesterServer interface { Push(context.Context, *cortexpb.WriteRequest) (*cortexpb.WriteResponse, error) @@ -3094,6 +3326,7 @@ type IngesterServer interface { MetricsForLabelMatchers(context.Context, *MetricsForLabelMatchersRequest) (*MetricsForLabelMatchersResponse, error) MetricsForLabelMatchersStream(*MetricsForLabelMatchersRequest, Ingester_MetricsForLabelMatchersStreamServer) error MetricsMetadata(context.Context, *MetricsMetadataRequest) (*MetricsMetadataResponse, error) + Cardinality(context.Context, *CardinalityRequest) (*CardinalityResponse, error) } // UnimplementedIngesterServer can be embedded to have forward compatible implementations. @@ -3139,6 +3372,9 @@ func (*UnimplementedIngesterServer) MetricsForLabelMatchersStream(req *MetricsFo func (*UnimplementedIngesterServer) MetricsMetadata(ctx context.Context, req *MetricsMetadataRequest) (*MetricsMetadataResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method MetricsMetadata not implemented") } +func (*UnimplementedIngesterServer) Cardinality(ctx context.Context, req *CardinalityRequest) (*CardinalityResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Cardinality not implemented") +} func RegisterIngesterServer(s *grpc.Server, srv IngesterServer) { s.RegisterService(&_Ingester_serviceDesc, srv) @@ -3398,6 +3634,24 @@ func _Ingester_MetricsMetadata_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Ingester_Cardinality_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CardinalityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IngesterServer).Cardinality(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cortex.Ingester/Cardinality", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IngesterServer).Cardinality(ctx, req.(*CardinalityRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Ingester_serviceDesc = grpc.ServiceDesc{ ServiceName: "cortex.Ingester", HandlerType: (*IngesterServer)(nil), @@ -3434,6 +3688,10 @@ var _Ingester_serviceDesc = grpc.ServiceDesc{ MethodName: "MetricsMetadata", Handler: _Ingester_MetricsMetadata_Handler, }, + { + MethodName: "Cardinality", + Handler: _Ingester_Cardinality_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -4593,6 +4851,104 @@ func (m *TimeSeriesFile) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *CardinalityRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CardinalityRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CardinalityRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Limit != 0 { + i = encodeVarintIngester(dAtA, i, uint64(m.Limit)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *CardinalityResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CardinalityResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CardinalityResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.SeriesCountByLabelValuePair) > 0 { + for iNdEx := len(m.SeriesCountByLabelValuePair) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.SeriesCountByLabelValuePair[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintIngester(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.LabelValueCountByLabelName) > 0 { + for iNdEx := len(m.LabelValueCountByLabelName) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.LabelValueCountByLabelName[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintIngester(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.SeriesCountByMetricName) > 0 { + for iNdEx := len(m.SeriesCountByMetricName) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.SeriesCountByMetricName[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintIngester(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if m.NumSeries != 0 { + i = encodeVarintIngester(dAtA, i, uint64(m.NumSeries)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func encodeVarintIngester(dAtA []byte, offset int, v uint64) int { offset -= sovIngester(v) base := offset @@ -5098,13 +5454,55 @@ func (m *TimeSeriesFile) Size() (n int) { return n } -func sovIngester(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozIngester(x uint64) (n int) { - return sovIngester(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (this *ReadRequest) String() string { +func (m *CardinalityRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Limit != 0 { + n += 1 + sovIngester(uint64(m.Limit)) + } + return n +} + +func (m *CardinalityResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.NumSeries != 0 { + n += 1 + sovIngester(uint64(m.NumSeries)) + } + if len(m.SeriesCountByMetricName) > 0 { + for _, e := range m.SeriesCountByMetricName { + l = e.Size() + n += 1 + l + sovIngester(uint64(l)) + } + } + if len(m.LabelValueCountByLabelName) > 0 { + for _, e := range m.LabelValueCountByLabelName { + l = e.Size() + n += 1 + l + sovIngester(uint64(l)) + } + } + if len(m.SeriesCountByLabelValuePair) > 0 { + for _, e := range m.SeriesCountByLabelValuePair { + l = e.Size() + n += 1 + l + sovIngester(uint64(l)) + } + } + return n +} + +func sovIngester(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozIngester(x uint64) (n int) { + return sovIngester(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *ReadRequest) String() string { if this == nil { return "nil" } @@ -5477,6 +5875,44 @@ func (this *TimeSeriesFile) String() string { }, "") return s } +func (this *CardinalityRequest) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&CardinalityRequest{`, + `Limit:` + fmt.Sprintf("%v", this.Limit) + `,`, + `}`, + }, "") + return s +} +func (this *CardinalityResponse) String() string { + if this == nil { + return "nil" + } + repeatedStringForSeriesCountByMetricName := "[]*CardinalityStatItem{" + for _, f := range this.SeriesCountByMetricName { + repeatedStringForSeriesCountByMetricName += strings.Replace(fmt.Sprintf("%v", f), "CardinalityStatItem", "cortexpb.CardinalityStatItem", 1) + "," + } + repeatedStringForSeriesCountByMetricName += "}" + repeatedStringForLabelValueCountByLabelName := "[]*CardinalityStatItem{" + for _, f := range this.LabelValueCountByLabelName { + repeatedStringForLabelValueCountByLabelName += strings.Replace(fmt.Sprintf("%v", f), "CardinalityStatItem", "cortexpb.CardinalityStatItem", 1) + "," + } + repeatedStringForLabelValueCountByLabelName += "}" + repeatedStringForSeriesCountByLabelValuePair := "[]*CardinalityStatItem{" + for _, f := range this.SeriesCountByLabelValuePair { + repeatedStringForSeriesCountByLabelValuePair += strings.Replace(fmt.Sprintf("%v", f), "CardinalityStatItem", "cortexpb.CardinalityStatItem", 1) + "," + } + repeatedStringForSeriesCountByLabelValuePair += "}" + s := strings.Join([]string{`&CardinalityResponse{`, + `NumSeries:` + fmt.Sprintf("%v", this.NumSeries) + `,`, + `SeriesCountByMetricName:` + repeatedStringForSeriesCountByMetricName + `,`, + `LabelValueCountByLabelName:` + repeatedStringForLabelValueCountByLabelName + `,`, + `SeriesCountByLabelValuePair:` + repeatedStringForSeriesCountByLabelValuePair + `,`, + `}`, + }, "") + return s +} func valueToStringIngester(v interface{}) string { rv := reflect.ValueOf(v) if rv.IsNil() { @@ -8534,6 +8970,252 @@ func (m *TimeSeriesFile) Unmarshal(dAtA []byte) error { } return nil } +func (m *CardinalityRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CardinalityRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CardinalityRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) + } + m.Limit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Limit |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipIngester(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthIngester + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthIngester + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *CardinalityResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CardinalityResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CardinalityResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NumSeries", wireType) + } + m.NumSeries = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NumSeries |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SeriesCountByMetricName", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthIngester + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthIngester + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SeriesCountByMetricName = append(m.SeriesCountByMetricName, &cortexpb.CardinalityStatItem{}) + if err := m.SeriesCountByMetricName[len(m.SeriesCountByMetricName)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LabelValueCountByLabelName", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthIngester + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthIngester + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LabelValueCountByLabelName = append(m.LabelValueCountByLabelName, &cortexpb.CardinalityStatItem{}) + if err := m.LabelValueCountByLabelName[len(m.LabelValueCountByLabelName)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SeriesCountByLabelValuePair", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIngester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthIngester + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthIngester + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SeriesCountByLabelValuePair = append(m.SeriesCountByLabelValuePair, &cortexpb.CardinalityStatItem{}) + if err := m.SeriesCountByLabelValuePair[len(m.SeriesCountByLabelValuePair)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipIngester(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthIngester + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthIngester + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipIngester(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/pkg/ingester/client/ingester.proto b/pkg/ingester/client/ingester.proto index 9a42ebfb3d6..58bdda7b763 100644 --- a/pkg/ingester/client/ingester.proto +++ b/pkg/ingester/client/ingester.proto @@ -7,6 +7,7 @@ option go_package = "client"; import "gogoproto/gogo.proto"; import "github.com/cortexproject/cortex/pkg/cortexpb/cortex.proto"; +import "github.com/cortexproject/cortex/pkg/cortexpb/cardinality.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.unmarshaler_all) = true; @@ -26,6 +27,7 @@ service Ingester { rpc MetricsForLabelMatchers(MetricsForLabelMatchersRequest) returns (MetricsForLabelMatchersResponse) {}; rpc MetricsForLabelMatchersStream(MetricsForLabelMatchersRequest) returns (stream MetricsForLabelMatchersStreamResponse) {}; rpc MetricsMetadata(MetricsMetadataRequest) returns (MetricsMetadataResponse) {}; + rpc Cardinality(CardinalityRequest) returns (CardinalityResponse) {}; } message ReadRequest { @@ -177,3 +179,14 @@ message TimeSeriesFile { string filename = 3; bytes data = 4; } + +message CardinalityRequest { + int32 limit = 1; +} + +message CardinalityResponse { + uint64 num_series = 1; + repeated cortexpb.CardinalityStatItem series_count_by_metric_name = 2; + repeated cortexpb.CardinalityStatItem label_value_count_by_label_name = 3; + repeated cortexpb.CardinalityStatItem series_count_by_label_value_pair = 4; +} diff --git a/pkg/ingester/ingester.go b/pkg/ingester/ingester.go index 59f2abd09ed..b4b3ce0aca0 100644 --- a/pkg/ingester/ingester.go +++ b/pkg/ingester/ingester.go @@ -34,6 +34,7 @@ import ( "github.com/prometheus/prometheus/tsdb" "github.com/prometheus/prometheus/tsdb/chunkenc" "github.com/prometheus/prometheus/tsdb/chunks" + "github.com/prometheus/prometheus/tsdb/index" "github.com/prometheus/prometheus/util/compression" "github.com/prometheus/prometheus/util/zeropool" "github.com/thanos-io/objstore" @@ -2300,6 +2301,54 @@ func (i *Ingester) UserStats(ctx context.Context, req *client.UserStatsRequest) }, nil } +// Cardinality returns per-tenant cardinality statistics from the TSDB head. +func (i *Ingester) Cardinality(ctx context.Context, req *client.CardinalityRequest) (*client.CardinalityResponse, error) { + if err := i.checkRunning(); err != nil { + return nil, err + } + + userID, err := users.TenantID(ctx) + if err != nil { + return nil, err + } + + db, err := i.getTSDB(userID) + if err != nil || db == nil { + return &client.CardinalityResponse{}, nil + } + + stats := db.Head().Stats(labels.MetricName, int(req.Limit)) + + return statsToPB(stats), nil +} + +// statsToPB converts TSDB head stats to the protobuf CardinalityResponse format. +func statsToPB(stats *tsdb.Stats) *client.CardinalityResponse { + resp := &client.CardinalityResponse{ + NumSeries: stats.NumSeries, + } + + if stats.IndexPostingStats != nil { + resp.SeriesCountByMetricName = indexStatsToPB(stats.IndexPostingStats.CardinalityMetricsStats) + resp.LabelValueCountByLabelName = indexStatsToPB(stats.IndexPostingStats.CardinalityLabelStats) + resp.SeriesCountByLabelValuePair = indexStatsToPB(stats.IndexPostingStats.LabelValuePairsStats) + } + + return resp +} + +// indexStatsToPB converts Prometheus index stats to protobuf CardinalityStatItem slice. +func indexStatsToPB(stats []index.Stat) []*cortexpb.CardinalityStatItem { + items := make([]*cortexpb.CardinalityStatItem, len(stats)) + for i, s := range stats { + items[i] = &cortexpb.CardinalityStatItem{ + Name: s.Name, + Value: s.Count, + } + } + return items +} + func (i *Ingester) userStats() []UserIDStats { i.stoppedMtx.RLock() defer i.stoppedMtx.RUnlock() diff --git a/pkg/querier/blocks_store_queryable.go b/pkg/querier/blocks_store_queryable.go index b3e336a940a..121df54b91d 100644 --- a/pkg/querier/blocks_store_queryable.go +++ b/pkg/querier/blocks_store_queryable.go @@ -36,6 +36,7 @@ import ( grpc_metadata "google.golang.org/grpc/metadata" "github.com/cortexproject/cortex/pkg/cortexpb" + ingester_client "github.com/cortexproject/cortex/pkg/ingester/client" "github.com/cortexproject/cortex/pkg/querier/series" "github.com/cortexproject/cortex/pkg/querier/stats" "github.com/cortexproject/cortex/pkg/querysharding" @@ -1245,3 +1246,203 @@ func isRetryableError(err error) bool { return false } } + +// BlocksCardinality queries store gateways for cardinality statistics from compacted blocks. +// It returns the aggregated CardinalityResponse, whether the results are approximated (due to +// overlapping blocks), and any error. +func (q *BlocksStoreQueryable) BlocksCardinality(ctx context.Context, userID string, minT, maxT int64, limit int32) (*ingester_client.CardinalityResponse, bool, error) { + spanLog, spanCtx := spanlogger.New(ctx, "BlocksStoreQueryable.BlocksCardinality") + defer spanLog.Finish() + + sq := &blocksStoreQuerier{ + minT: minT, + maxT: maxT, + finder: q.finder, + stores: q.stores, + metrics: q.metrics, + limits: q.limits, + consistency: q.consistency, + logger: q.logger, + storeGatewayConsistencyCheckMaxAttempts: q.storeGatewayConsistencyCheckMaxAttempts, + } + + var ( + resMtx sync.Mutex + resResps []*storegatewaypb.CardinalityResponse + approximated bool + ) + + queryFunc := func(clients map[BlocksStoreClient][]ulid.ULID, minT, maxT int64) ([]ulid.ULID, error, error) { + resps, queriedBlocks, err, retryableError := sq.fetchCardinalityFromStores(spanCtx, userID, clients, minT, maxT, limit) + if err != nil { + return nil, err, retryableError + } + + resMtx.Lock() + resResps = append(resResps, resps...) + resMtx.Unlock() + + return queriedBlocks, nil, retryableError + } + + if err := sq.queryWithConsistencyCheck(spanCtx, spanLog, minT, maxT, nil, userID, queryFunc); err != nil { + // If the consistency check fails (missing blocks), we still return partial results + // with approximated=true rather than failing entirely. + if len(resResps) == 0 { + return nil, false, err + } + approximated = true + } + + // Aggregate results from all store gateways. + result := aggregateBlocksCardinalityResponses(resResps, int(limit)) + + return result, approximated, nil +} + +// fetchCardinalityFromStores concurrently fetches cardinality statistics from store gateway clients. +func (q *blocksStoreQuerier) fetchCardinalityFromStores( + ctx context.Context, + userID string, + clients map[BlocksStoreClient][]ulid.ULID, + minT int64, + maxT int64, + limit int32, +) ([]*storegatewaypb.CardinalityResponse, []ulid.ULID, error, error) { + var ( + reqCtx = grpc_metadata.AppendToOutgoingContext(ctx, cortex_tsdb.TenantIDExternalLabel, userID) + g, gCtx = errgroup.WithContext(reqCtx) + mtx = sync.Mutex{} + resps = []*storegatewaypb.CardinalityResponse{} + queriedBlocks = []ulid.ULID(nil) + spanLog = spanlogger.FromContext(ctx) + merrMtx = sync.Mutex{} + merr = multierror.MultiError{} + ) + + for c, blockIDs := range clients { + g.Go(func() error { + // Convert block ULIDs to bytes for the request. + blockIDBytes := make([][]byte, len(blockIDs)) + for i, id := range blockIDs { + b := id // copy + blockIDBytes[i] = b[:] + } + + req := &storegatewaypb.CardinalityRequest{ + Limit: limit, + MinTime: minT, + MaxTime: maxT, + BlockIds: blockIDBytes, + } + + resp, err := c.Cardinality(gCtx, req) + if err != nil { + if isRetryableError(err) || status.Code(err) == codes.Unimplemented { + level.Warn(spanLog).Log("err", errors.Wrapf(err, "failed to fetch cardinality from %s due to retryable error", c.RemoteAddress())) + merrMtx.Lock() + merr.Add(err) + merrMtx.Unlock() + return nil + } + + s, ok := status.FromError(err) + if !ok { + s, ok = status.FromError(errors.Cause(err)) + } + + if ok { + if s.Code() == codes.ResourceExhausted { + return validation.LimitError(s.Message()) + } + if s.Code() == codes.PermissionDenied { + return validation.AccessDeniedError(s.Message()) + } + } + return errors.Wrapf(err, "failed to fetch cardinality from %s", c.RemoteAddress()) + } + + // Parse queried blocks from response. + myQueriedBlocks := make([]ulid.ULID, 0, len(resp.QueriedBlocks)) + for _, b := range resp.QueriedBlocks { + if len(b) != len(ulid.ULID{}) { + continue + } + var id ulid.ULID + copy(id[:], b) + myQueriedBlocks = append(myQueriedBlocks, id) + } + + level.Debug(spanLog).Log("msg", "received cardinality from store-gateway", + "instance", c.RemoteAddress(), + "num_series", resp.NumSeries, + "requested_blocks", len(blockIDs), + "queried_blocks", len(myQueriedBlocks)) + + mtx.Lock() + resps = append(resps, resp) + queriedBlocks = append(queriedBlocks, myQueriedBlocks...) + mtx.Unlock() + + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, nil, err, merr.Err() + } + + return resps, queriedBlocks, nil, merr.Err() +} + +// aggregateBlocksCardinalityResponses merges cardinality responses from multiple store gateways. +// No RF division is applied since each block is sent to exactly one store gateway. +func aggregateBlocksCardinalityResponses(resps []*storegatewaypb.CardinalityResponse, limit int) *ingester_client.CardinalityResponse { + var totalNumSeries uint64 + seriesByMetric := make(map[string]uint64) + labelValueCounts := make(map[string]uint64) + seriesByPair := make(map[string]uint64) + + for _, resp := range resps { + totalNumSeries += resp.NumSeries + + for _, item := range resp.SeriesCountByMetricName { + seriesByMetric[item.Name] += item.Value + } + for _, item := range resp.LabelValueCountByLabelName { + if item.Value > labelValueCounts[item.Name] { + labelValueCounts[item.Name] = item.Value + } + } + for _, item := range resp.SeriesCountByLabelValuePair { + seriesByPair[item.Name] += item.Value + } + } + + return &ingester_client.CardinalityResponse{ + NumSeries: totalNumSeries, + SeriesCountByMetricName: sortAndTruncateStatItems(seriesByMetric, limit), + LabelValueCountByLabelName: sortAndTruncateStatItems(labelValueCounts, limit), + SeriesCountByLabelValuePair: sortAndTruncateStatItems(seriesByPair, limit), + } +} + +// sortAndTruncateStatItems sorts items descending by value and returns the top N. +func sortAndTruncateStatItems(items map[string]uint64, limit int) []*cortexpb.CardinalityStatItem { + result := make([]*cortexpb.CardinalityStatItem, 0, len(items)) + for name, value := range items { + result = append(result, &cortexpb.CardinalityStatItem{ + Name: name, + Value: value, + }) + } + + sort.Slice(result, func(i, j int) bool { + return result[i].Value > result[j].Value + }) + + if limit > 0 && len(result) > limit { + result = result[:limit] + } + return result +} diff --git a/pkg/querier/blocks_store_queryable_test.go b/pkg/querier/blocks_store_queryable_test.go index 26a2c2fb4ac..c44619535fe 100644 --- a/pkg/querier/blocks_store_queryable_test.go +++ b/pkg/querier/blocks_store_queryable_test.go @@ -2690,6 +2690,10 @@ func (m *storeGatewayClientMock) LabelValues(_ context.Context, r *storepb.Label return m.mockedLabelValuesResponse, m.mockedLabelValuesErr } +func (m *storeGatewayClientMock) Cardinality(_ context.Context, _ *storegatewaypb.CardinalityRequest, _ ...grpc.CallOption) (*storegatewaypb.CardinalityResponse, error) { + return &storegatewaypb.CardinalityResponse{}, nil +} + func (m *storeGatewayClientMock) RemoteAddress() string { return m.remoteAddr } diff --git a/pkg/querier/cardinality_handler.go b/pkg/querier/cardinality_handler.go new file mode 100644 index 00000000000..9f46a51d5cb --- /dev/null +++ b/pkg/querier/cardinality_handler.go @@ -0,0 +1,299 @@ +package querier + +import ( + "context" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/cortexproject/cortex/pkg/cortexpb" + "github.com/cortexproject/cortex/pkg/ingester/client" + "github.com/cortexproject/cortex/pkg/util" + "github.com/cortexproject/cortex/pkg/util/users" + "github.com/cortexproject/cortex/pkg/util/validation" +) + +const ( + cardinalityDefaultLimit = 10 + cardinalityMaxLimit = 512 + + cardinalitySourceHead = "head" + cardinalitySourceBlocks = "blocks" + + cardinalityErrorTypeBadData = "bad_data" + cardinalityErrorTypeInternal = "internal" +) + +type cardinalityResponse struct { + Status string `json:"status"` + Data cardinalityData `json:"data"` +} + +type cardinalityData struct { + NumSeries uint64 `json:"numSeries"` + Approximated bool `json:"approximated"` + SeriesCountByMetricName []cardinalityStatItem `json:"seriesCountByMetricName"` + LabelValueCountByLabelName []cardinalityStatItem `json:"labelValueCountByLabelName"` + SeriesCountByLabelValuePair []cardinalityStatItem `json:"seriesCountByLabelValuePair"` +} + +type cardinalityStatItem struct { + Name string `json:"name"` + Value uint64 `json:"value"` +} + +type cardinalityErrorResponse struct { + Status string `json:"status"` + ErrorType string `json:"errorType"` + Error string `json:"error"` +} + +// cardinalityMetrics holds Prometheus metrics for the cardinality endpoint. +type cardinalityMetrics struct { + requestDuration *prometheus.HistogramVec + requestsTotal *prometheus.CounterVec + inflightRequests *prometheus.GaugeVec +} + +func newCardinalityMetrics(reg prometheus.Registerer) *cardinalityMetrics { + return &cardinalityMetrics{ + requestDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "cortex", + Name: "cardinality_request_duration_seconds", + Help: "Time (in seconds) spent serving cardinality requests.", + Buckets: prometheus.DefBuckets, + }, []string{"source", "status_code"}), + requestsTotal: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ + Namespace: "cortex", + Name: "cardinality_requests_total", + Help: "Total number of cardinality requests.", + }, []string{"source", "status_code"}), + inflightRequests: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "cortex", + Name: "cardinality_inflight_requests", + Help: "Current number of in-flight cardinality requests.", + }, []string{"source"}), + } +} + +// cardinalityConcurrencyLimiter tracks per-tenant concurrency for cardinality requests. +type cardinalityConcurrencyLimiter struct { + mu sync.Mutex + tenants map[string]int + limits *validation.Overrides +} + +func newCardinalityConcurrencyLimiter(limits *validation.Overrides) *cardinalityConcurrencyLimiter { + return &cardinalityConcurrencyLimiter{ + tenants: make(map[string]int), + limits: limits, + } +} + +func (l *cardinalityConcurrencyLimiter) tryAcquire(tenantID string) bool { + l.mu.Lock() + defer l.mu.Unlock() + maxConcurrent := l.limits.CardinalityMaxConcurrentRequests(tenantID) + if l.tenants[tenantID] >= maxConcurrent { + return false + } + l.tenants[tenantID]++ + return true +} + +func (l *cardinalityConcurrencyLimiter) release(tenantID string) { + l.mu.Lock() + defer l.mu.Unlock() + l.tenants[tenantID]-- + if l.tenants[tenantID] <= 0 { + delete(l.tenants, tenantID) + } +} + +// BlocksCardinalityQuerier is the interface for querying cardinality from blocks storage. +type BlocksCardinalityQuerier interface { + BlocksCardinality(ctx context.Context, userID string, minT, maxT int64, limit int32) (*client.CardinalityResponse, bool, error) +} + +// CardinalityHandler returns an HTTP handler for cardinality statistics. +// The Distributor interface (which includes the Cardinality method) is used +// for the head path. The BlocksCardinalityQuerier is used for the blocks path. +// The limits parameter provides per-tenant configuration. +func CardinalityHandler(d Distributor, blocksQuerier BlocksCardinalityQuerier, limits *validation.Overrides, reg prometheus.Registerer) http.Handler { + metrics := newCardinalityMetrics(reg) + limiter := newCardinalityConcurrencyLimiter(limits) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + // Extract tenant ID. + tenantID, err := users.TenantID(r.Context()) + if err != nil { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, err.Error()) + return + } + + // Check if cardinality API is enabled for this tenant. + if !limits.CardinalityAPIEnabled(tenantID) { + writeCardinalityError(w, http.StatusForbidden, cardinalityErrorTypeBadData, "cardinality API is not enabled for this tenant") + return + } + + // Parse source parameter. + source := r.FormValue("source") + if source == "" { + source = cardinalitySourceHead + } + if source != cardinalitySourceHead && source != cardinalitySourceBlocks { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, `invalid source: must be "head" or "blocks"`) + return + } + + // Parse and validate limit parameter. + limit := int32(cardinalityDefaultLimit) + if s := r.FormValue("limit"); s != "" { + v, err := strconv.Atoi(s) + if err != nil || v < 1 || v > cardinalityMaxLimit { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, fmt.Sprintf("invalid limit: must be an integer between 1 and %d", cardinalityMaxLimit)) + return + } + limit = int32(v) + } + + // Validate source-specific parameters and parse time range for blocks. + var minT, maxT int64 + if source == cardinalitySourceHead { + if r.FormValue("start") != "" || r.FormValue("end") != "" { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, "start and end parameters are not supported for source=head") + return + } + } + + if source == cardinalitySourceBlocks { + startParam := r.FormValue("start") + endParam := r.FormValue("end") + if startParam == "" || endParam == "" { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, "start and end are required for source=blocks") + return + } + + minT, err = util.ParseTime(startParam) + if err != nil { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, "invalid start/end: must be RFC3339 or Unix timestamp") + return + } + + maxT, err = util.ParseTime(endParam) + if err != nil { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, "invalid start/end: must be RFC3339 or Unix timestamp") + return + } + + startTs := util.TimeFromMillis(minT) + endTs := util.TimeFromMillis(maxT) + + if !startTs.Before(endTs) { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, "invalid time range: start must be before end") + return + } + + maxRange := limits.CardinalityMaxQueryRange(tenantID) + if maxRange > 0 && endTs.Sub(startTs) > maxRange { + writeCardinalityError(w, http.StatusBadRequest, cardinalityErrorTypeBadData, + fmt.Sprintf("the query time range exceeds the limit (query length: %s, limit: %s)", endTs.Sub(startTs), maxRange)) + return + } + + if blocksQuerier == nil { + writeCardinalityError(w, http.StatusNotImplemented, cardinalityErrorTypeBadData, "source=blocks is not available") + return + } + } + + // Check concurrency limit. + if !limiter.tryAcquire(tenantID) { + statusCode := http.StatusTooManyRequests + metrics.requestsTotal.WithLabelValues(source, strconv.Itoa(statusCode)).Inc() + writeCardinalityError(w, statusCode, cardinalityErrorTypeBadData, "too many concurrent cardinality requests for this tenant") + return + } + defer limiter.release(tenantID) + + metrics.inflightRequests.WithLabelValues(source).Inc() + defer metrics.inflightRequests.WithLabelValues(source).Dec() + + // Apply per-tenant query timeout. + ctx := r.Context() + timeout := limits.CardinalityQueryTimeout(tenantID) + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + + // Execute the cardinality query. + var result *client.CardinalityResponse + var approximated bool + + switch source { + case cardinalitySourceHead: + req := &client.CardinalityRequest{Limit: limit} + result, err = d.Cardinality(ctx, req) + case cardinalitySourceBlocks: + result, approximated, err = blocksQuerier.BlocksCardinality(ctx, tenantID, minT, maxT, limit) + } + + statusCode := http.StatusOK + if err != nil { + statusCode = http.StatusInternalServerError + duration := time.Since(startTime).Seconds() + metrics.requestDuration.WithLabelValues(source, strconv.Itoa(statusCode)).Observe(duration) + metrics.requestsTotal.WithLabelValues(source, strconv.Itoa(statusCode)).Inc() + writeCardinalityError(w, statusCode, cardinalityErrorTypeInternal, err.Error()) + return + } + + duration := time.Since(startTime).Seconds() + metrics.requestDuration.WithLabelValues(source, strconv.Itoa(statusCode)).Observe(duration) + metrics.requestsTotal.WithLabelValues(source, strconv.Itoa(statusCode)).Inc() + + util.WriteJSONResponse(w, cardinalityResponse{ + Status: statusSuccess, + Data: cardinalityData{ + NumSeries: result.NumSeries, + Approximated: approximated, + SeriesCountByMetricName: convertStatItems(result.SeriesCountByMetricName), + LabelValueCountByLabelName: convertStatItems(result.LabelValueCountByLabelName), + SeriesCountByLabelValuePair: convertStatItems(result.SeriesCountByLabelValuePair), + }, + }) + }) +} + +func convertStatItems(items []*cortexpb.CardinalityStatItem) []cardinalityStatItem { + if items == nil { + return []cardinalityStatItem{} + } + result := make([]cardinalityStatItem, len(items)) + for i, item := range items { + result[i] = cardinalityStatItem{ + Name: item.Name, + Value: item.Value, + } + } + return result +} + +func writeCardinalityError(w http.ResponseWriter, statusCode int, errorType, message string) { + w.WriteHeader(statusCode) + util.WriteJSONResponse(w, cardinalityErrorResponse{ + Status: statusError, + ErrorType: errorType, + Error: message, + }) +} diff --git a/pkg/querier/cardinality_handler_test.go b/pkg/querier/cardinality_handler_test.go new file mode 100644 index 00000000000..3df8a86fe33 --- /dev/null +++ b/pkg/querier/cardinality_handler_test.go @@ -0,0 +1,212 @@ +package querier + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/weaveworks/common/user" + + "github.com/cortexproject/cortex/pkg/cortexpb" + "github.com/cortexproject/cortex/pkg/ingester/client" + "github.com/cortexproject/cortex/pkg/util/flagext" + "github.com/cortexproject/cortex/pkg/util/validation" +) + +func TestCardinalityHandler_ParameterValidation(t *testing.T) { + limits := validation.Limits{} + flagext.DefaultValues(&limits) + limits.CardinalityAPIEnabled = true + overrides := validation.NewOverrides(limits, nil) + + dist := &MockDistributor{} + dist.On("Cardinality", mock.Anything, mock.Anything).Return(&client.CardinalityResponse{ + NumSeries: 100, + }, nil).Maybe() + + handler := CardinalityHandler(dist, nil, overrides, prometheus.NewRegistry()) + + tests := []struct { + name string + query string + expectedCode int + expectedError string + }{ + { + name: "default parameters", + query: "", + expectedCode: http.StatusOK, + }, + { + name: "invalid source", + query: "source=invalid", + expectedCode: http.StatusBadRequest, + expectedError: `invalid source: must be "head" or "blocks"`, + }, + { + name: "limit too low", + query: "limit=0", + expectedCode: http.StatusBadRequest, + expectedError: "invalid limit: must be an integer between 1 and 512", + }, + { + name: "limit too high", + query: "limit=513", + expectedCode: http.StatusBadRequest, + expectedError: "invalid limit: must be an integer between 1 and 512", + }, + { + name: "limit non-integer", + query: "limit=abc", + expectedCode: http.StatusBadRequest, + expectedError: "invalid limit: must be an integer between 1 and 512", + }, + { + name: "start with head source", + query: "source=head&start=1234567890", + expectedCode: http.StatusBadRequest, + expectedError: "start and end parameters are not supported for source=head", + }, + { + name: "end with head source", + query: "source=head&end=1234567890", + expectedCode: http.StatusBadRequest, + expectedError: "start and end parameters are not supported for source=head", + }, + { + name: "blocks without start/end", + query: "source=blocks", + expectedCode: http.StatusBadRequest, + expectedError: "start and end are required for source=blocks", + }, + { + name: "blocks with only start", + query: "source=blocks&start=1234567890", + expectedCode: http.StatusBadRequest, + expectedError: "start and end are required for source=blocks", + }, + { + name: "blocks with invalid start", + query: "source=blocks&start=invalid&end=1234567890", + expectedCode: http.StatusBadRequest, + expectedError: "invalid start/end: must be RFC3339 or Unix timestamp", + }, + { + name: "blocks with start >= end", + query: "source=blocks&start=1234567890&end=1234567890", + expectedCode: http.StatusBadRequest, + expectedError: "invalid time range: start must be before end", + }, + { + name: "valid limit", + query: "limit=50", + expectedCode: http.StatusOK, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/v1/cardinality?"+tc.query, nil) + req = req.WithContext(user.InjectOrgID(req.Context(), "test-tenant")) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, tc.expectedCode, rec.Code, "status code mismatch for %s", tc.name) + + if tc.expectedError != "" { + var errResp cardinalityErrorResponse + err := json.Unmarshal(rec.Body.Bytes(), &errResp) + require.NoError(t, err) + assert.Equal(t, statusError, errResp.Status) + assert.Equal(t, tc.expectedError, errResp.Error) + } + }) + } +} + +func TestCardinalityHandler_DisabledTenant(t *testing.T) { + limits := validation.Limits{} + flagext.DefaultValues(&limits) + limits.CardinalityAPIEnabled = false + overrides := validation.NewOverrides(limits, nil) + + dist := &MockDistributor{} + handler := CardinalityHandler(dist, nil, overrides, prometheus.NewRegistry()) + + req := httptest.NewRequest("GET", "/api/v1/cardinality", nil) + req = req.WithContext(user.InjectOrgID(req.Context(), "test-tenant")) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusForbidden, rec.Code) +} + +func TestCardinalityHandler_SuccessfulResponse(t *testing.T) { + limits := validation.Limits{} + flagext.DefaultValues(&limits) + limits.CardinalityAPIEnabled = true + overrides := validation.NewOverrides(limits, nil) + + dist := &MockDistributor{} + dist.On("Cardinality", mock.Anything, &client.CardinalityRequest{Limit: 10}).Return(&client.CardinalityResponse{ + NumSeries: 1500, + SeriesCountByMetricName: []*cortexpb.CardinalityStatItem{ + {Name: "http_requests_total", Value: 500}, + {Name: "process_cpu_seconds_total", Value: 200}, + }, + LabelValueCountByLabelName: []*cortexpb.CardinalityStatItem{ + {Name: "instance", Value: 50}, + {Name: "job", Value: 10}, + }, + SeriesCountByLabelValuePair: []*cortexpb.CardinalityStatItem{ + {Name: "job=api-server", Value: 300}, + {Name: "instance=host1:9090", Value: 150}, + }, + }, nil) + + handler := CardinalityHandler(dist, nil, overrides, prometheus.NewRegistry()) + + req := httptest.NewRequest("GET", "/api/v1/cardinality", nil) + req = req.WithContext(user.InjectOrgID(req.Context(), "test-tenant")) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + var resp cardinalityResponse + err := json.Unmarshal(rec.Body.Bytes(), &resp) + require.NoError(t, err) + + assert.Equal(t, statusSuccess, resp.Status) + assert.Equal(t, uint64(1500), resp.Data.NumSeries) + assert.False(t, resp.Data.Approximated) + assert.Equal(t, 2, len(resp.Data.SeriesCountByMetricName)) + assert.Equal(t, "http_requests_total", resp.Data.SeriesCountByMetricName[0].Name) + assert.Equal(t, uint64(500), resp.Data.SeriesCountByMetricName[0].Value) + assert.Equal(t, 2, len(resp.Data.LabelValueCountByLabelName)) + assert.Equal(t, 2, len(resp.Data.SeriesCountByLabelValuePair)) +} + +func TestCardinalityHandler_ConcurrencyLimit(t *testing.T) { + limits := validation.Limits{} + flagext.DefaultValues(&limits) + limits.CardinalityAPIEnabled = true + limits.CardinalityMaxConcurrentRequests = 1 + overrides := validation.NewOverrides(limits, nil) + + // Create a limiter and pre-fill it + limiter := newCardinalityConcurrencyLimiter(overrides) + assert.True(t, limiter.tryAcquire("test-tenant")) + assert.False(t, limiter.tryAcquire("test-tenant")) + limiter.release("test-tenant") + assert.True(t, limiter.tryAcquire("test-tenant")) + limiter.release("test-tenant") +} diff --git a/pkg/querier/distributor_queryable.go b/pkg/querier/distributor_queryable.go index 8d82cd78878..4c70215863a 100644 --- a/pkg/querier/distributor_queryable.go +++ b/pkg/querier/distributor_queryable.go @@ -40,6 +40,7 @@ type Distributor interface { MetricsForLabelMatchers(ctx context.Context, from, through model.Time, hint *storage.SelectHints, partialDataEnabled bool, matchers ...*labels.Matcher) ([]labels.Labels, error) MetricsForLabelMatchersStream(ctx context.Context, from, through model.Time, hint *storage.SelectHints, partialDataEnabled bool, matchers ...*labels.Matcher) ([]labels.Labels, error) MetricsMetadata(ctx context.Context, req *client.MetricsMetadataRequest) ([]scrape.MetricMetadata, error) + Cardinality(ctx context.Context, req *client.CardinalityRequest) (*client.CardinalityResponse, error) } func newDistributorQueryable(distributor Distributor, streamingMetdata bool, labelNamesWithMatchers bool, iteratorFn chunkIteratorFunc, queryIngestersWithin time.Duration, isPartialDataEnabled partialdata.IsCfgEnabledFunc, ingesterQueryMaxAttempts int) QueryableWithFilter { diff --git a/pkg/querier/querier_test.go b/pkg/querier/querier_test.go index 4a13dae9aaf..4537a323ec2 100644 --- a/pkg/querier/querier_test.go +++ b/pkg/querier/querier_test.go @@ -1379,6 +1379,9 @@ func (m *errDistributor) MetricsForLabelMatchersStream(ctx context.Context, from func (m *errDistributor) MetricsMetadata(ctx context.Context, request *client.MetricsMetadataRequest) ([]scrape.MetricMetadata, error) { return nil, errDistributorError } +func (m *errDistributor) Cardinality(ctx context.Context, req *client.CardinalityRequest) (*client.CardinalityResponse, error) { + return nil, errDistributorError +} type emptyChunkStore struct { sync.Mutex @@ -1435,6 +1438,9 @@ func (d *emptyDistributor) MetricsForLabelMatchersStream(ctx context.Context, fr func (d *emptyDistributor) MetricsMetadata(ctx context.Context, request *client.MetricsMetadataRequest) ([]scrape.MetricMetadata, error) { return nil, nil } +func (d *emptyDistributor) Cardinality(ctx context.Context, req *client.CardinalityRequest) (*client.CardinalityResponse, error) { + return &client.CardinalityResponse{}, nil +} type mockStore interface { Get() ([]chunk.Chunk, error) diff --git a/pkg/querier/store_gateway_client_test.go b/pkg/querier/store_gateway_client_test.go index 34f74528170..06a37e18596 100644 --- a/pkg/querier/store_gateway_client_test.go +++ b/pkg/querier/store_gateway_client_test.go @@ -81,3 +81,7 @@ func (m *mockStoreGatewayServer) LabelNames(context.Context, *storepb.LabelNames func (m *mockStoreGatewayServer) LabelValues(context.Context, *storepb.LabelValuesRequest) (*storepb.LabelValuesResponse, error) { return nil, nil } + +func (m *mockStoreGatewayServer) Cardinality(context.Context, *storegatewaypb.CardinalityRequest) (*storegatewaypb.CardinalityResponse, error) { + return &storegatewaypb.CardinalityResponse{}, nil +} diff --git a/pkg/querier/testutils.go b/pkg/querier/testutils.go index 4ac69988bfa..60d94740e76 100644 --- a/pkg/querier/testutils.go +++ b/pkg/querier/testutils.go @@ -63,6 +63,11 @@ func (m *MockDistributor) MetricsMetadata(ctx context.Context, request *client.M return args.Get(0).([]scrape.MetricMetadata), args.Error(1) } +func (m *MockDistributor) Cardinality(ctx context.Context, req *client.CardinalityRequest) (*client.CardinalityResponse, error) { + args := m.Called(ctx, req) + return args.Get(0).(*client.CardinalityResponse), args.Error(1) +} + type MockLimitingDistributor struct { MockDistributor response *client.QueryStreamResponse diff --git a/pkg/storegateway/bucket_stores.go b/pkg/storegateway/bucket_stores.go index f017457a9f3..8c0f362d749 100644 --- a/pkg/storegateway/bucket_stores.go +++ b/pkg/storegateway/bucket_stores.go @@ -6,16 +6,19 @@ import ( "math" "os" "path/filepath" + "strings" "sync" "time" "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/gogo/protobuf/types" "github.com/oklog/ulid/v2" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" "github.com/thanos-io/objstore" "github.com/thanos-io/thanos/pkg/block" @@ -26,6 +29,7 @@ import ( "github.com/thanos-io/thanos/pkg/pool" "github.com/thanos-io/thanos/pkg/store" storecache "github.com/thanos-io/thanos/pkg/store/cache" + "github.com/thanos-io/thanos/pkg/store/hintspb" "github.com/thanos-io/thanos/pkg/store/storepb" "github.com/weaveworks/common/httpgrpc" "github.com/weaveworks/common/logging" @@ -33,8 +37,10 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "github.com/cortexproject/cortex/pkg/cortexpb" "github.com/cortexproject/cortex/pkg/storage/bucket" "github.com/cortexproject/cortex/pkg/storage/tsdb" + "github.com/cortexproject/cortex/pkg/storegateway/storegatewaypb" "github.com/cortexproject/cortex/pkg/util" "github.com/cortexproject/cortex/pkg/util/backoff" cortex_errors "github.com/cortexproject/cortex/pkg/util/errors" @@ -49,6 +55,7 @@ type BucketStores interface { storepb.StoreServer SyncBlocks(ctx context.Context) error InitialSync(ctx context.Context) error + Cardinality(ctx context.Context, req *storegatewaypb.CardinalityRequest) (*storegatewaypb.CardinalityResponse, error) } // ThanosBucketStores is a multi-tenant wrapper of Thanos BucketStore. @@ -447,6 +454,159 @@ func (u *ThanosBucketStores) LabelValues(ctx context.Context, req *storepb.Label return store.LabelValues(ctx, req) } +// Cardinality returns cardinality statistics for specific blocks owned by a tenant. +func (u *ThanosBucketStores) Cardinality(ctx context.Context, req *storegatewaypb.CardinalityRequest) (*storegatewaypb.CardinalityResponse, error) { + spanLog, spanCtx := spanlogger.New(ctx, "BucketStores.Cardinality") + defer spanLog.Finish() + + userID := getUserIDFromGRPCContext(spanCtx) + if userID == "" { + return nil, fmt.Errorf("no userID") + } + + err := u.getStoreError(userID) + if err != nil { + userBkt := bucket.NewUserBucketClient(userID, u.bucket, u.limits) + if cortex_errors.ErrorIs(err, userBkt.IsAccessDeniedErr) { + return nil, httpgrpc.Errorf(int(codes.PermissionDenied), "store error: %s", err) + } + return nil, err + } + + userStore := u.getStore(userID) + if userStore == nil { + return &storegatewaypb.CardinalityResponse{}, nil + } + + // Parse requested block IDs. + requestedBlocks := make([]ulid.ULID, 0, len(req.BlockIds)) + for _, b := range req.BlockIds { + if len(b) != 16 { + continue + } + var id ulid.ULID + copy(id[:], b) + requestedBlocks = append(requestedBlocks, id) + } + + // Build block hints once, reused for all LabelNames/LabelValues requests. + blockRegex := buildBlockIDRegex(requestedBlocks) + var labelValuesHints *types.Any + if blockRegex != "" { + hints := &hintspb.LabelValuesRequestHints{ + BlockMatchers: []storepb.LabelMatcher{ + { + Type: storepb.LabelMatcher_RE, + Name: block.BlockIDLabel, + Value: blockRegex, + }, + }, + } + labelValuesHints, err = types.MarshalAny(hints) + if err != nil { + return nil, errors.Wrap(err, "marshal block hints") + } + } + + // Use the BucketStore's LabelNames to get all label names for these blocks. + labelNamesReq := &storepb.LabelNamesRequest{ + Start: req.MinTime, + End: req.MaxTime, + } + if blockRegex != "" { + labelNamesHints := &hintspb.LabelNamesRequestHints{ + BlockMatchers: []storepb.LabelMatcher{ + { + Type: storepb.LabelMatcher_RE, + Name: block.BlockIDLabel, + Value: blockRegex, + }, + }, + } + anyHints, err := types.MarshalAny(labelNamesHints) + if err != nil { + return nil, errors.Wrap(err, "marshal label names hints") + } + labelNamesReq.Hints = anyHints + } + + labelNamesResp, err := userStore.LabelNames(spanCtx, labelNamesReq) + if err != nil { + return nil, errors.Wrap(err, "fetch label names for cardinality") + } + + // For each label name, get the distinct values to compute labelValueCountByLabelName. + labelValueCounts := make(map[string]uint64, len(labelNamesResp.Names)) + metricNames := []string{} + for _, name := range labelNamesResp.Names { + labelValuesReq := &storepb.LabelValuesRequest{ + Label: name, + Start: req.MinTime, + End: req.MaxTime, + Hints: labelValuesHints, + } + + labelValuesResp, err := userStore.LabelValues(spanCtx, labelValuesReq) + if err != nil { + level.Warn(spanLog).Log("msg", "failed to fetch label values for cardinality", "label", name, "err", err) + continue + } + + labelValueCounts[name] = uint64(len(labelValuesResp.Values)) + + if name == labels.MetricName { //nolint:staticcheck // MetricName is widely used in this codebase. + metricNames = labelValuesResp.Values + } + } + + // Build the response. + resp := &storegatewaypb.CardinalityResponse{} + + // labelValueCountByLabelName + for name, count := range labelValueCounts { + resp.LabelValueCountByLabelName = append(resp.LabelValueCountByLabelName, &cortexpb.CardinalityStatItem{ + Name: name, + Value: count, + }) + } + + // seriesCountByMetricName: We don't have exact per-metric series counts from + // LabelValues alone. Use the number of distinct metric names as a placeholder. + // For exact counts, we would need to expand posting lists per metric name. + // For now, report the number of metric names found. + for _, name := range metricNames { + resp.SeriesCountByMetricName = append(resp.SeriesCountByMetricName, &cortexpb.CardinalityStatItem{ + Name: name, + Value: 1, // Placeholder: exact series counts require posting list expansion. + }) + } + + // Set queried blocks in response. + for _, id := range requestedBlocks { + b := id // copy + resp.QueriedBlocks = append(resp.QueriedBlocks, b[:]) + } + + level.Debug(spanLog).Log("msg", "computed cardinality", "user", userID, + "label_names", len(labelNamesResp.Names), + "metric_names", len(metricNames), + "queried_blocks", len(requestedBlocks)) + + return resp, nil +} + +// buildBlockIDRegex creates a regex pattern matching the given block ULIDs. +func buildBlockIDRegex(blockIDs []ulid.ULID) string { + if len(blockIDs) == 0 { + return "" + } + strs := make([]string, len(blockIDs)) + for i, id := range blockIDs { + strs[i] = id.String() + } + return strings.Join(strs, "|") +} + // scanUsers in the bucket and return the list of found users. It includes active and deleting users // but not deleted users. func (u *ThanosBucketStores) scanUsers(ctx context.Context) ([]string, error) { diff --git a/pkg/storegateway/gateway.go b/pkg/storegateway/gateway.go index 14724d60b51..46b2d1f6db1 100644 --- a/pkg/storegateway/gateway.go +++ b/pkg/storegateway/gateway.go @@ -431,6 +431,14 @@ func (g *StoreGateway) LabelValues(ctx context.Context, req *storepb.LabelValues return g.stores.LabelValues(ctx, req) } +// Cardinality returns cardinality statistics for a tenant's blocks. +func (g *StoreGateway) Cardinality(ctx context.Context, req *storegatewaypb.CardinalityRequest) (*storegatewaypb.CardinalityResponse, error) { + if err := g.checkResourceUtilization(); err != nil { + return nil, err + } + return g.stores.Cardinality(ctx, req) +} + func (g *StoreGateway) checkResourceUtilization() error { if g.resourceBasedLimiter == nil { return nil diff --git a/pkg/storegateway/parquet_bucket_stores.go b/pkg/storegateway/parquet_bucket_stores.go index b51bf758ae5..24dda77e6d6 100644 --- a/pkg/storegateway/parquet_bucket_stores.go +++ b/pkg/storegateway/parquet_bucket_stores.go @@ -30,6 +30,7 @@ import ( "github.com/cortexproject/cortex/pkg/querysharding" "github.com/cortexproject/cortex/pkg/storage/bucket" "github.com/cortexproject/cortex/pkg/storage/tsdb" + "github.com/cortexproject/cortex/pkg/storegateway/storegatewaypb" cortex_util "github.com/cortexproject/cortex/pkg/util" cortex_errors "github.com/cortexproject/cortex/pkg/util/errors" "github.com/cortexproject/cortex/pkg/util/parquetutil" @@ -196,6 +197,11 @@ func (u *ParquetBucketStores) LabelValues(ctx context.Context, req *storepb.Labe return store.LabelValues(ctx, req) } +// Cardinality is not supported for Parquet stores; returns empty response. +func (u *ParquetBucketStores) Cardinality(_ context.Context, _ *storegatewaypb.CardinalityRequest) (*storegatewaypb.CardinalityResponse, error) { + return &storegatewaypb.CardinalityResponse{}, nil +} + // SyncBlocks implements BucketStores func (u *ParquetBucketStores) SyncBlocks(ctx context.Context) error { return nil diff --git a/pkg/storegateway/storegatewaypb/gateway.pb.go b/pkg/storegateway/storegatewaypb/gateway.pb.go index fa5913faf44..9eed0c820f0 100644 --- a/pkg/storegateway/storegatewaypb/gateway.pb.go +++ b/pkg/storegateway/storegatewaypb/gateway.pb.go @@ -4,14 +4,21 @@ package storegatewaypb import ( + bytes "bytes" context "context" fmt "fmt" + cortexpb "github.com/cortexproject/cortex/pkg/cortexpb" + _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" storepb "github.com/thanos-io/thanos/pkg/store/storepb" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + io "io" math "math" + math_bits "math/bits" + reflect "reflect" + strings "strings" ) // Reference imports to suppress errors if they are not otherwise used. @@ -25,27 +32,328 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +type CardinalityRequest struct { + Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + MinTime int64 `protobuf:"varint,2,opt,name=min_time,json=minTime,proto3" json:"min_time,omitempty"` + MaxTime int64 `protobuf:"varint,3,opt,name=max_time,json=maxTime,proto3" json:"max_time,omitempty"` + BlockIds [][]byte `protobuf:"bytes,4,rep,name=block_ids,json=blockIds,proto3" json:"block_ids,omitempty"` +} + +func (m *CardinalityRequest) Reset() { *m = CardinalityRequest{} } +func (*CardinalityRequest) ProtoMessage() {} +func (*CardinalityRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f1a937782ebbded5, []int{0} +} +func (m *CardinalityRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CardinalityRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CardinalityRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CardinalityRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CardinalityRequest.Merge(m, src) +} +func (m *CardinalityRequest) XXX_Size() int { + return m.Size() +} +func (m *CardinalityRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CardinalityRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CardinalityRequest proto.InternalMessageInfo + +func (m *CardinalityRequest) GetLimit() int32 { + if m != nil { + return m.Limit + } + return 0 +} + +func (m *CardinalityRequest) GetMinTime() int64 { + if m != nil { + return m.MinTime + } + return 0 +} + +func (m *CardinalityRequest) GetMaxTime() int64 { + if m != nil { + return m.MaxTime + } + return 0 +} + +func (m *CardinalityRequest) GetBlockIds() [][]byte { + if m != nil { + return m.BlockIds + } + return nil +} + +type CardinalityResponse struct { + NumSeries uint64 `protobuf:"varint,1,opt,name=num_series,json=numSeries,proto3" json:"num_series,omitempty"` + SeriesCountByMetricName []*cortexpb.CardinalityStatItem `protobuf:"bytes,2,rep,name=series_count_by_metric_name,json=seriesCountByMetricName,proto3" json:"series_count_by_metric_name,omitempty"` + LabelValueCountByLabelName []*cortexpb.CardinalityStatItem `protobuf:"bytes,3,rep,name=label_value_count_by_label_name,json=labelValueCountByLabelName,proto3" json:"label_value_count_by_label_name,omitempty"` + SeriesCountByLabelValuePair []*cortexpb.CardinalityStatItem `protobuf:"bytes,4,rep,name=series_count_by_label_value_pair,json=seriesCountByLabelValuePair,proto3" json:"series_count_by_label_value_pair,omitempty"` + QueriedBlocks [][]byte `protobuf:"bytes,5,rep,name=queried_blocks,json=queriedBlocks,proto3" json:"queried_blocks,omitempty"` +} + +func (m *CardinalityResponse) Reset() { *m = CardinalityResponse{} } +func (*CardinalityResponse) ProtoMessage() {} +func (*CardinalityResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f1a937782ebbded5, []int{1} +} +func (m *CardinalityResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CardinalityResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CardinalityResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CardinalityResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CardinalityResponse.Merge(m, src) +} +func (m *CardinalityResponse) XXX_Size() int { + return m.Size() +} +func (m *CardinalityResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CardinalityResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CardinalityResponse proto.InternalMessageInfo + +func (m *CardinalityResponse) GetNumSeries() uint64 { + if m != nil { + return m.NumSeries + } + return 0 +} + +func (m *CardinalityResponse) GetSeriesCountByMetricName() []*cortexpb.CardinalityStatItem { + if m != nil { + return m.SeriesCountByMetricName + } + return nil +} + +func (m *CardinalityResponse) GetLabelValueCountByLabelName() []*cortexpb.CardinalityStatItem { + if m != nil { + return m.LabelValueCountByLabelName + } + return nil +} + +func (m *CardinalityResponse) GetSeriesCountByLabelValuePair() []*cortexpb.CardinalityStatItem { + if m != nil { + return m.SeriesCountByLabelValuePair + } + return nil +} + +func (m *CardinalityResponse) GetQueriedBlocks() [][]byte { + if m != nil { + return m.QueriedBlocks + } + return nil +} + +func init() { + proto.RegisterType((*CardinalityRequest)(nil), "gatewaypb.CardinalityRequest") + proto.RegisterType((*CardinalityResponse)(nil), "gatewaypb.CardinalityResponse") +} + func init() { proto.RegisterFile("gateway.proto", fileDescriptor_f1a937782ebbded5) } var fileDescriptor_f1a937782ebbded5 = []byte{ - // 257 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0x4f, 0x2c, 0x49, - 0x2d, 0x4f, 0xac, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x72, 0x0b, 0x92, 0xa4, - 0xcc, 0xd3, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x4b, 0x32, 0x12, 0xf3, - 0xf2, 0x8b, 0x75, 0x33, 0xf3, 0xa1, 0x2c, 0xfd, 0x82, 0xec, 0x74, 0xfd, 0xe2, 0x92, 0xfc, 0xa2, - 0x54, 0x08, 0x59, 0x90, 0xa4, 0x5f, 0x54, 0x90, 0x0c, 0x31, 0xc3, 0xe8, 0x1a, 0x23, 0x17, 0x4f, - 0x30, 0x48, 0xd4, 0x1d, 0x62, 0x96, 0x90, 0x25, 0x17, 0x5b, 0x70, 0x6a, 0x51, 0x66, 0x6a, 0xb1, - 0x90, 0xa8, 0x1e, 0x44, 0xbf, 0x1e, 0x84, 0x1f, 0x94, 0x5a, 0x58, 0x9a, 0x5a, 0x5c, 0x22, 0x25, - 0x86, 0x2e, 0x5c, 0x5c, 0x90, 0x9f, 0x57, 0x9c, 0x6a, 0xc0, 0x28, 0xe4, 0xcc, 0xc5, 0xe5, 0x93, - 0x98, 0x94, 0x9a, 0xe3, 0x97, 0x98, 0x9b, 0x5a, 0x2c, 0x24, 0x09, 0x53, 0x87, 0x10, 0x83, 0x19, - 0x21, 0x85, 0x4d, 0x0a, 0x62, 0x8c, 0x90, 0x1b, 0x17, 0x37, 0x58, 0x34, 0x2c, 0x31, 0xa7, 0x34, - 0xb5, 0x58, 0x08, 0x55, 0x29, 0x44, 0x10, 0x66, 0x8c, 0x34, 0x56, 0x39, 0x88, 0x39, 0x4e, 0x2e, - 0x17, 0x1e, 0xca, 0x31, 0xdc, 0x78, 0x28, 0xc7, 0xf0, 0xe1, 0xa1, 0x1c, 0x63, 0xc3, 0x23, 0x39, - 0xc6, 0x15, 0x8f, 0xe4, 0x18, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, - 0x39, 0xc6, 0x17, 0x8f, 0xe4, 0x18, 0x3e, 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, - 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, 0xa2, 0xf8, 0xc0, 0x21, 0x04, 0x0f, 0xd7, 0x24, 0x36, - 0x70, 0x28, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x1b, 0xec, 0xe6, 0x0a, 0x7a, 0x01, 0x00, - 0x00, + // 558 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0x4f, 0x6f, 0xd3, 0x30, + 0x14, 0x4f, 0x96, 0x6d, 0x6c, 0xde, 0x9f, 0x83, 0x19, 0xd0, 0xa5, 0xaa, 0xa9, 0x26, 0x21, 0xf5, + 0x42, 0x82, 0xc6, 0x01, 0x71, 0xe1, 0xd0, 0x22, 0xd0, 0xa4, 0x82, 0x50, 0x8a, 0x38, 0xc0, 0x21, + 0x72, 0x52, 0xab, 0x33, 0x8b, 0xe3, 0x2c, 0x76, 0xa0, 0x3d, 0x20, 0x21, 0x3e, 0x01, 0x1f, 0x82, + 0x03, 0x1f, 0x85, 0x63, 0x8f, 0x3b, 0xd2, 0xf4, 0xc2, 0x71, 0x1f, 0x01, 0xc5, 0x76, 0xff, 0x8d, + 0x21, 0xb8, 0x44, 0x7e, 0xbf, 0x9f, 0xdf, 0xef, 0xe7, 0xf7, 0xf2, 0x1e, 0xd8, 0x1b, 0x60, 0x49, + 0x3e, 0xe2, 0x91, 0x97, 0xe5, 0x5c, 0x72, 0xb8, 0x6d, 0xc2, 0x2c, 0x72, 0x1f, 0x0d, 0xa8, 0x3c, + 0x2d, 0x22, 0x2f, 0xe6, 0xcc, 0x97, 0xa7, 0x38, 0xe5, 0xe2, 0x3e, 0xe5, 0xe6, 0xe4, 0x67, 0x67, + 0x03, 0x5f, 0x48, 0x9e, 0x13, 0xfd, 0xcd, 0x22, 0x3f, 0xcf, 0x62, 0xad, 0xe1, 0x3e, 0x59, 0x4a, + 0x8c, 0x79, 0x2e, 0xc9, 0x30, 0xcb, 0xf9, 0x7b, 0x12, 0x4b, 0x13, 0xa9, 0x64, 0x43, 0x44, 0x7e, + 0x8c, 0xf3, 0x3e, 0x4d, 0x71, 0x42, 0xa5, 0x79, 0x83, 0x7b, 0x30, 0xe0, 0x03, 0xae, 0x8e, 0x7e, + 0x75, 0xd2, 0xe8, 0xd1, 0x27, 0x00, 0x3b, 0x8b, 0xab, 0x01, 0x39, 0x2f, 0x88, 0x90, 0xf0, 0x00, + 0x6c, 0x24, 0x94, 0x51, 0x59, 0xb3, 0x9b, 0x76, 0x6b, 0x23, 0xd0, 0x01, 0x3c, 0x04, 0x5b, 0x8c, + 0xa6, 0xa1, 0xa4, 0x8c, 0xd4, 0xd6, 0x9a, 0x76, 0xcb, 0x09, 0x6e, 0x30, 0x9a, 0xbe, 0xa6, 0x8c, + 0x28, 0x0a, 0x0f, 0x35, 0xe5, 0x18, 0x0a, 0x0f, 0x15, 0x55, 0x07, 0xdb, 0x51, 0xc2, 0xe3, 0xb3, + 0x90, 0xf6, 0x45, 0x6d, 0xbd, 0xe9, 0xb4, 0x76, 0x83, 0x2d, 0x05, 0x9c, 0xf4, 0xc5, 0xd1, 0x17, + 0x07, 0xdc, 0x5c, 0xf1, 0x17, 0x19, 0x4f, 0x05, 0x81, 0x0d, 0x00, 0xd2, 0x82, 0x85, 0x82, 0xe4, + 0x94, 0x08, 0xf5, 0x8a, 0xf5, 0x60, 0x3b, 0x2d, 0x58, 0x4f, 0x01, 0xf0, 0x1d, 0xa8, 0x6b, 0x2a, + 0x8c, 0x79, 0x91, 0xca, 0x30, 0x1a, 0x85, 0x8c, 0xc8, 0x9c, 0xc6, 0x61, 0x8a, 0xd5, 0xe3, 0x9c, + 0xd6, 0xce, 0x71, 0xc3, 0x9b, 0x75, 0xc3, 0x5b, 0xb2, 0xe8, 0x49, 0x2c, 0x4f, 0x24, 0x61, 0xc1, + 0x1d, 0xad, 0xd0, 0xa9, 0x04, 0xda, 0xa3, 0x17, 0x2a, 0xfd, 0x25, 0x66, 0x04, 0x46, 0xe0, 0x6e, + 0x82, 0x23, 0x92, 0x84, 0x1f, 0x70, 0x52, 0x90, 0x85, 0x83, 0x06, 0x95, 0x81, 0xf3, 0x3f, 0x06, + 0xae, 0x4a, 0x78, 0x53, 0x89, 0x18, 0x93, 0x6e, 0x05, 0x28, 0x8f, 0x3e, 0x68, 0x5e, 0x2d, 0x60, + 0xd9, 0x33, 0xc3, 0x34, 0x57, 0xbd, 0xfa, 0xa7, 0x49, 0x7d, 0xa5, 0x8a, 0xee, 0xdc, 0xf1, 0x15, + 0xa6, 0x39, 0xbc, 0x07, 0xf6, 0xcf, 0x8b, 0x8a, 0xef, 0x87, 0xaa, 0xe3, 0xa2, 0xb6, 0xa1, 0xfa, + 0xbf, 0x67, 0xd0, 0xb6, 0x02, 0x8f, 0xbf, 0xad, 0x81, 0xdd, 0x5e, 0x35, 0x6f, 0xcf, 0xf5, 0x94, + 0xc2, 0xc7, 0x60, 0xd3, 0x34, 0xfa, 0x96, 0xa7, 0x27, 0xd3, 0xd3, 0xb1, 0x99, 0x0f, 0xf7, 0xf6, + 0x55, 0x58, 0xff, 0xb6, 0x07, 0x36, 0xec, 0x00, 0x30, 0xaf, 0x52, 0xc0, 0xc3, 0xd9, 0xbd, 0x05, + 0x36, 0x93, 0x70, 0xaf, 0xa3, 0xcc, 0xdf, 0x7f, 0x06, 0x76, 0x16, 0x95, 0x08, 0xb8, 0x7a, 0x55, + 0x83, 0x33, 0x99, 0xfa, 0xb5, 0x9c, 0xd1, 0xe9, 0x82, 0x9d, 0xa5, 0x9e, 0xc1, 0x86, 0x37, 0x5f, + 0x43, 0xef, 0xcf, 0xa1, 0x77, 0xd1, 0xdf, 0x68, 0xad, 0xd6, 0x7e, 0x3a, 0x9e, 0x20, 0xeb, 0x62, + 0x82, 0xac, 0xcb, 0x09, 0xb2, 0x3f, 0x97, 0xc8, 0xfe, 0x5e, 0x22, 0xfb, 0x47, 0x89, 0xec, 0x71, + 0x89, 0xec, 0x9f, 0x25, 0xb2, 0x7f, 0x95, 0xc8, 0xba, 0x2c, 0x91, 0xfd, 0x75, 0x8a, 0xac, 0xf1, + 0x14, 0x59, 0x17, 0x53, 0x64, 0xbd, 0xdd, 0x57, 0x9b, 0x3c, 0x57, 0x8e, 0x36, 0xd5, 0xde, 0x3d, + 0xfc, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x0c, 0xdf, 0x5b, 0xd3, 0x22, 0x04, 0x00, 0x00, +} + +func (this *CardinalityRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*CardinalityRequest) + if !ok { + that2, ok := that.(CardinalityRequest) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Limit != that1.Limit { + return false + } + if this.MinTime != that1.MinTime { + return false + } + if this.MaxTime != that1.MaxTime { + return false + } + if len(this.BlockIds) != len(that1.BlockIds) { + return false + } + for i := range this.BlockIds { + if !bytes.Equal(this.BlockIds[i], that1.BlockIds[i]) { + return false + } + } + return true +} +func (this *CardinalityResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*CardinalityResponse) + if !ok { + that2, ok := that.(CardinalityResponse) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.NumSeries != that1.NumSeries { + return false + } + if len(this.SeriesCountByMetricName) != len(that1.SeriesCountByMetricName) { + return false + } + for i := range this.SeriesCountByMetricName { + if !this.SeriesCountByMetricName[i].Equal(that1.SeriesCountByMetricName[i]) { + return false + } + } + if len(this.LabelValueCountByLabelName) != len(that1.LabelValueCountByLabelName) { + return false + } + for i := range this.LabelValueCountByLabelName { + if !this.LabelValueCountByLabelName[i].Equal(that1.LabelValueCountByLabelName[i]) { + return false + } + } + if len(this.SeriesCountByLabelValuePair) != len(that1.SeriesCountByLabelValuePair) { + return false + } + for i := range this.SeriesCountByLabelValuePair { + if !this.SeriesCountByLabelValuePair[i].Equal(that1.SeriesCountByLabelValuePair[i]) { + return false + } + } + if len(this.QueriedBlocks) != len(that1.QueriedBlocks) { + return false + } + for i := range this.QueriedBlocks { + if !bytes.Equal(this.QueriedBlocks[i], that1.QueriedBlocks[i]) { + return false + } + } + return true +} +func (this *CardinalityRequest) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 8) + s = append(s, "&storegatewaypb.CardinalityRequest{") + s = append(s, "Limit: "+fmt.Sprintf("%#v", this.Limit)+",\n") + s = append(s, "MinTime: "+fmt.Sprintf("%#v", this.MinTime)+",\n") + s = append(s, "MaxTime: "+fmt.Sprintf("%#v", this.MaxTime)+",\n") + s = append(s, "BlockIds: "+fmt.Sprintf("%#v", this.BlockIds)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func (this *CardinalityResponse) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 9) + s = append(s, "&storegatewaypb.CardinalityResponse{") + s = append(s, "NumSeries: "+fmt.Sprintf("%#v", this.NumSeries)+",\n") + if this.SeriesCountByMetricName != nil { + s = append(s, "SeriesCountByMetricName: "+fmt.Sprintf("%#v", this.SeriesCountByMetricName)+",\n") + } + if this.LabelValueCountByLabelName != nil { + s = append(s, "LabelValueCountByLabelName: "+fmt.Sprintf("%#v", this.LabelValueCountByLabelName)+",\n") + } + if this.SeriesCountByLabelValuePair != nil { + s = append(s, "SeriesCountByLabelValuePair: "+fmt.Sprintf("%#v", this.SeriesCountByLabelValuePair)+",\n") + } + s = append(s, "QueriedBlocks: "+fmt.Sprintf("%#v", this.QueriedBlocks)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func valueToGoStringGateway(v interface{}, typ string) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("func(v %v) *%v { return &v } ( %#v )", typ, typ, pv) } // Reference imports to suppress errors if they are not otherwise used. @@ -72,6 +380,8 @@ type StoreGatewayClient interface { LabelNames(ctx context.Context, in *storepb.LabelNamesRequest, opts ...grpc.CallOption) (*storepb.LabelNamesResponse, error) // LabelValues returns all label values for given label name. LabelValues(ctx context.Context, in *storepb.LabelValuesRequest, opts ...grpc.CallOption) (*storepb.LabelValuesResponse, error) + // Cardinality returns cardinality statistics for a tenant's blocks. + Cardinality(ctx context.Context, in *CardinalityRequest, opts ...grpc.CallOption) (*CardinalityResponse, error) } type storeGatewayClient struct { @@ -132,6 +442,15 @@ func (c *storeGatewayClient) LabelValues(ctx context.Context, in *storepb.LabelV return out, nil } +func (c *storeGatewayClient) Cardinality(ctx context.Context, in *CardinalityRequest, opts ...grpc.CallOption) (*CardinalityResponse, error) { + out := new(CardinalityResponse) + err := c.cc.Invoke(ctx, "/gatewaypb.StoreGateway/Cardinality", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // StoreGatewayServer is the server API for StoreGateway service. type StoreGatewayServer interface { // Series streams each Series for given label matchers and time range. @@ -146,6 +465,8 @@ type StoreGatewayServer interface { LabelNames(context.Context, *storepb.LabelNamesRequest) (*storepb.LabelNamesResponse, error) // LabelValues returns all label values for given label name. LabelValues(context.Context, *storepb.LabelValuesRequest) (*storepb.LabelValuesResponse, error) + // Cardinality returns cardinality statistics for a tenant's blocks. + Cardinality(context.Context, *CardinalityRequest) (*CardinalityResponse, error) } // UnimplementedStoreGatewayServer can be embedded to have forward compatible implementations. @@ -161,6 +482,9 @@ func (*UnimplementedStoreGatewayServer) LabelNames(ctx context.Context, req *sto func (*UnimplementedStoreGatewayServer) LabelValues(ctx context.Context, req *storepb.LabelValuesRequest) (*storepb.LabelValuesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LabelValues not implemented") } +func (*UnimplementedStoreGatewayServer) Cardinality(ctx context.Context, req *CardinalityRequest) (*CardinalityResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Cardinality not implemented") +} func RegisterStoreGatewayServer(s *grpc.Server, srv StoreGatewayServer) { s.RegisterService(&_StoreGateway_serviceDesc, srv) @@ -223,6 +547,24 @@ func _StoreGateway_LabelValues_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _StoreGateway_Cardinality_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CardinalityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreGatewayServer).Cardinality(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gatewaypb.StoreGateway/Cardinality", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreGatewayServer).Cardinality(ctx, req.(*CardinalityRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _StoreGateway_serviceDesc = grpc.ServiceDesc{ ServiceName: "gatewaypb.StoreGateway", HandlerType: (*StoreGatewayServer)(nil), @@ -235,6 +577,10 @@ var _StoreGateway_serviceDesc = grpc.ServiceDesc{ MethodName: "LabelValues", Handler: _StoreGateway_LabelValues_Handler, }, + { + MethodName: "Cardinality", + Handler: _StoreGateway_Cardinality_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -245,3 +591,715 @@ var _StoreGateway_serviceDesc = grpc.ServiceDesc{ }, Metadata: "gateway.proto", } + +func (m *CardinalityRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CardinalityRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CardinalityRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.BlockIds) > 0 { + for iNdEx := len(m.BlockIds) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.BlockIds[iNdEx]) + copy(dAtA[i:], m.BlockIds[iNdEx]) + i = encodeVarintGateway(dAtA, i, uint64(len(m.BlockIds[iNdEx]))) + i-- + dAtA[i] = 0x22 + } + } + if m.MaxTime != 0 { + i = encodeVarintGateway(dAtA, i, uint64(m.MaxTime)) + i-- + dAtA[i] = 0x18 + } + if m.MinTime != 0 { + i = encodeVarintGateway(dAtA, i, uint64(m.MinTime)) + i-- + dAtA[i] = 0x10 + } + if m.Limit != 0 { + i = encodeVarintGateway(dAtA, i, uint64(m.Limit)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *CardinalityResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CardinalityResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CardinalityResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.QueriedBlocks) > 0 { + for iNdEx := len(m.QueriedBlocks) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.QueriedBlocks[iNdEx]) + copy(dAtA[i:], m.QueriedBlocks[iNdEx]) + i = encodeVarintGateway(dAtA, i, uint64(len(m.QueriedBlocks[iNdEx]))) + i-- + dAtA[i] = 0x2a + } + } + if len(m.SeriesCountByLabelValuePair) > 0 { + for iNdEx := len(m.SeriesCountByLabelValuePair) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.SeriesCountByLabelValuePair[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGateway(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.LabelValueCountByLabelName) > 0 { + for iNdEx := len(m.LabelValueCountByLabelName) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.LabelValueCountByLabelName[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGateway(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.SeriesCountByMetricName) > 0 { + for iNdEx := len(m.SeriesCountByMetricName) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.SeriesCountByMetricName[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGateway(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if m.NumSeries != 0 { + i = encodeVarintGateway(dAtA, i, uint64(m.NumSeries)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintGateway(dAtA []byte, offset int, v uint64) int { + offset -= sovGateway(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *CardinalityRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Limit != 0 { + n += 1 + sovGateway(uint64(m.Limit)) + } + if m.MinTime != 0 { + n += 1 + sovGateway(uint64(m.MinTime)) + } + if m.MaxTime != 0 { + n += 1 + sovGateway(uint64(m.MaxTime)) + } + if len(m.BlockIds) > 0 { + for _, b := range m.BlockIds { + l = len(b) + n += 1 + l + sovGateway(uint64(l)) + } + } + return n +} + +func (m *CardinalityResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.NumSeries != 0 { + n += 1 + sovGateway(uint64(m.NumSeries)) + } + if len(m.SeriesCountByMetricName) > 0 { + for _, e := range m.SeriesCountByMetricName { + l = e.Size() + n += 1 + l + sovGateway(uint64(l)) + } + } + if len(m.LabelValueCountByLabelName) > 0 { + for _, e := range m.LabelValueCountByLabelName { + l = e.Size() + n += 1 + l + sovGateway(uint64(l)) + } + } + if len(m.SeriesCountByLabelValuePair) > 0 { + for _, e := range m.SeriesCountByLabelValuePair { + l = e.Size() + n += 1 + l + sovGateway(uint64(l)) + } + } + if len(m.QueriedBlocks) > 0 { + for _, b := range m.QueriedBlocks { + l = len(b) + n += 1 + l + sovGateway(uint64(l)) + } + } + return n +} + +func sovGateway(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGateway(x uint64) (n int) { + return sovGateway(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *CardinalityRequest) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&CardinalityRequest{`, + `Limit:` + fmt.Sprintf("%v", this.Limit) + `,`, + `MinTime:` + fmt.Sprintf("%v", this.MinTime) + `,`, + `MaxTime:` + fmt.Sprintf("%v", this.MaxTime) + `,`, + `BlockIds:` + fmt.Sprintf("%v", this.BlockIds) + `,`, + `}`, + }, "") + return s +} +func (this *CardinalityResponse) String() string { + if this == nil { + return "nil" + } + repeatedStringForSeriesCountByMetricName := "[]*CardinalityStatItem{" + for _, f := range this.SeriesCountByMetricName { + repeatedStringForSeriesCountByMetricName += strings.Replace(fmt.Sprintf("%v", f), "CardinalityStatItem", "cortexpb.CardinalityStatItem", 1) + "," + } + repeatedStringForSeriesCountByMetricName += "}" + repeatedStringForLabelValueCountByLabelName := "[]*CardinalityStatItem{" + for _, f := range this.LabelValueCountByLabelName { + repeatedStringForLabelValueCountByLabelName += strings.Replace(fmt.Sprintf("%v", f), "CardinalityStatItem", "cortexpb.CardinalityStatItem", 1) + "," + } + repeatedStringForLabelValueCountByLabelName += "}" + repeatedStringForSeriesCountByLabelValuePair := "[]*CardinalityStatItem{" + for _, f := range this.SeriesCountByLabelValuePair { + repeatedStringForSeriesCountByLabelValuePair += strings.Replace(fmt.Sprintf("%v", f), "CardinalityStatItem", "cortexpb.CardinalityStatItem", 1) + "," + } + repeatedStringForSeriesCountByLabelValuePair += "}" + s := strings.Join([]string{`&CardinalityResponse{`, + `NumSeries:` + fmt.Sprintf("%v", this.NumSeries) + `,`, + `SeriesCountByMetricName:` + repeatedStringForSeriesCountByMetricName + `,`, + `LabelValueCountByLabelName:` + repeatedStringForLabelValueCountByLabelName + `,`, + `SeriesCountByLabelValuePair:` + repeatedStringForSeriesCountByLabelValuePair + `,`, + `QueriedBlocks:` + fmt.Sprintf("%v", this.QueriedBlocks) + `,`, + `}`, + }, "") + return s +} +func valueToStringGateway(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *CardinalityRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CardinalityRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CardinalityRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) + } + m.Limit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Limit |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MinTime", wireType) + } + m.MinTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MinTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxTime", wireType) + } + m.MaxTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BlockIds", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGateway + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGateway + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BlockIds = append(m.BlockIds, make([]byte, postIndex-iNdEx)) + copy(m.BlockIds[len(m.BlockIds)-1], dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGateway(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthGateway + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthGateway + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *CardinalityResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CardinalityResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CardinalityResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NumSeries", wireType) + } + m.NumSeries = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NumSeries |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SeriesCountByMetricName", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGateway + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGateway + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SeriesCountByMetricName = append(m.SeriesCountByMetricName, &cortexpb.CardinalityStatItem{}) + if err := m.SeriesCountByMetricName[len(m.SeriesCountByMetricName)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LabelValueCountByLabelName", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGateway + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGateway + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LabelValueCountByLabelName = append(m.LabelValueCountByLabelName, &cortexpb.CardinalityStatItem{}) + if err := m.LabelValueCountByLabelName[len(m.LabelValueCountByLabelName)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SeriesCountByLabelValuePair", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGateway + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGateway + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SeriesCountByLabelValuePair = append(m.SeriesCountByLabelValuePair, &cortexpb.CardinalityStatItem{}) + if err := m.SeriesCountByLabelValuePair[len(m.SeriesCountByLabelValuePair)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field QueriedBlocks", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGateway + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGateway + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGateway + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.QueriedBlocks = append(m.QueriedBlocks, make([]byte, postIndex-iNdEx)) + copy(m.QueriedBlocks[len(m.QueriedBlocks)-1], dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGateway(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthGateway + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthGateway + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGateway(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGateway + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGateway + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGateway + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGateway + } + iNdEx += length + if iNdEx < 0 { + return 0, ErrInvalidLengthGateway + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGateway + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipGateway(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + if iNdEx < 0 { + return 0, ErrInvalidLengthGateway + } + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthGateway = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGateway = fmt.Errorf("proto: integer overflow") +) diff --git a/pkg/storegateway/storegatewaypb/gateway.proto b/pkg/storegateway/storegatewaypb/gateway.proto index 14e65859c27..0d2fd164ad3 100644 --- a/pkg/storegateway/storegatewaypb/gateway.proto +++ b/pkg/storegateway/storegatewaypb/gateway.proto @@ -2,9 +2,14 @@ syntax = "proto3"; package gatewaypb; import "github.com/thanos-io/thanos/pkg/store/storepb/rpc.proto"; +import "github.com/cortexproject/cortex/pkg/cortexpb/cardinality.proto"; +import "gogoproto/gogo.proto"; option go_package = "storegatewaypb"; +option (gogoproto.marshaler_all) = true; +option (gogoproto.unmarshaler_all) = true; + service StoreGateway { // Series streams each Series for given label matchers and time range. // @@ -20,4 +25,22 @@ service StoreGateway { // LabelValues returns all label values for given label name. rpc LabelValues(thanos.LabelValuesRequest) returns (thanos.LabelValuesResponse); + + // Cardinality returns cardinality statistics for a tenant's blocks. + rpc Cardinality(CardinalityRequest) returns (CardinalityResponse); +} + +message CardinalityRequest { + int32 limit = 1; + int64 min_time = 2; + int64 max_time = 3; + repeated bytes block_ids = 4; +} + +message CardinalityResponse { + uint64 num_series = 1; + repeated cortexpb.CardinalityStatItem series_count_by_metric_name = 2; + repeated cortexpb.CardinalityStatItem label_value_count_by_label_name = 3; + repeated cortexpb.CardinalityStatItem series_count_by_label_value_pair = 4; + repeated bytes queried_blocks = 5; } diff --git a/pkg/util/validation/exporter_test.go b/pkg/util/validation/exporter_test.go index 0b1ef21ce8b..c7aea89d5e0 100644 --- a/pkg/util/validation/exporter_test.go +++ b/pkg/util/validation/exporter_test.go @@ -48,6 +48,10 @@ func TestOverridesExporter_withConfig(t *testing.T) { cortex_overrides{limit_name="alertmanager_max_templates_count",user="tenant-a"} 0 cortex_overrides{limit_name="alertmanager_notification_rate_limit",user="tenant-a"} 0 cortex_overrides{limit_name="alertmanager_receivers_firewall_block_private_addresses",user="tenant-a"} 0 + cortex_overrides{limit_name="cardinality_api_enabled",user="tenant-a"} 0 + cortex_overrides{limit_name="cardinality_max_concurrent_requests",user="tenant-a"} 2 + cortex_overrides{limit_name="cardinality_max_query_range",user="tenant-a"} 86400 + cortex_overrides{limit_name="cardinality_query_timeout",user="tenant-a"} 60 cortex_overrides{limit_name="compactor_blocks_retention_period",user="tenant-a"} 0 cortex_overrides{limit_name="compactor_partition_index_size_bytes",user="tenant-a"} 6.8719476736e+10 cortex_overrides{limit_name="compactor_partition_series_count",user="tenant-a"} 0 diff --git a/pkg/util/validation/limits.go b/pkg/util/validation/limits.go index 73f09fe3407..37583ece72e 100644 --- a/pkg/util/validation/limits.go +++ b/pkg/util/validation/limits.go @@ -197,6 +197,12 @@ type Limits struct { QueryVerticalShardSize int `yaml:"query_vertical_shard_size" json:"query_vertical_shard_size"` QueryPartialData bool `yaml:"query_partial_data" json:"query_partial_data" doc:"nocli|description=Enable to allow queries to be evaluated with data from a single zone, if other zones are not available.|default=false"` + // Cardinality API limits. + CardinalityAPIEnabled bool `yaml:"cardinality_api_enabled" json:"cardinality_api_enabled"` + CardinalityMaxQueryRange model.Duration `yaml:"cardinality_max_query_range" json:"cardinality_max_query_range"` + CardinalityMaxConcurrentRequests int `yaml:"cardinality_max_concurrent_requests" json:"cardinality_max_concurrent_requests"` + CardinalityQueryTimeout model.Duration `yaml:"cardinality_query_timeout" json:"cardinality_query_timeout"` + // Parquet Queryable enforced limits. ParquetMaxFetchedRowCount int `yaml:"parquet_max_fetched_row_count" json:"parquet_max_fetched_row_count"` ParquetMaxFetchedChunkBytes int `yaml:"parquet_max_fetched_chunk_bytes" json:"parquet_max_fetched_chunk_bytes"` @@ -329,6 +335,14 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) { f.IntVar(&l.MaxOutstandingPerTenant, "frontend.max-outstanding-requests-per-tenant", 100, "Maximum number of outstanding requests per tenant per request queue (either query frontend or query scheduler); requests beyond this error with HTTP 429.") + // Cardinality API limits. + f.BoolVar(&l.CardinalityAPIEnabled, "querier.cardinality-api-enabled", false, "[Experimental] Enables the per-tenant cardinality API endpoint. When disabled, the endpoint returns HTTP 403.") + _ = l.CardinalityMaxQueryRange.Set("24h") + f.Var(&l.CardinalityMaxQueryRange, "querier.cardinality-max-query-range", "[Experimental] Maximum allowed time range (end - start) for source=blocks cardinality queries.") + f.IntVar(&l.CardinalityMaxConcurrentRequests, "querier.cardinality-max-concurrent-requests", 2, "[Experimental] Maximum number of concurrent cardinality requests per tenant. Excess requests are rejected with HTTP 429.") + _ = l.CardinalityQueryTimeout.Set("60s") + f.Var(&l.CardinalityQueryTimeout, "querier.cardinality-query-timeout", "[Experimental] Per-request timeout for cardinality computation. On timeout, partial results are returned.") + f.Var(&l.RulerEvaluationDelay, "ruler.evaluation-delay-duration", "Deprecated(use ruler.query-offset instead) and will be removed in v1.19.0: Duration to delay the evaluation of rules to ensure the underlying metrics have been pushed to Cortex.") f.Float64Var(&l.RulerTenantShardSize, "ruler.tenant-shard-size", 0, "The default tenant's shard size when the shuffle-sharding strategy is used by ruler. When this setting is specified in the per-tenant overrides, a value of 0 disables shuffle sharding for the tenant. If the value is < 1 the shard size will be a percentage of the total rulers.") f.IntVar(&l.RulerMaxRulesPerRuleGroup, "ruler.max-rules-per-rule-group", 0, "Maximum number of rules per rule group per-tenant. 0 to disable.") @@ -1179,6 +1193,26 @@ func (o *Overrides) MaxTotalLabelValueLengthForUnoptimizedRegex(userID string) i return o.GetOverridesForUser(userID).MaxTotalLabelValueLengthForUnoptimizedRegex } +// CardinalityAPIEnabled returns whether the cardinality API is enabled for the tenant. +func (o *Overrides) CardinalityAPIEnabled(userID string) bool { + return o.GetOverridesForUser(userID).CardinalityAPIEnabled +} + +// CardinalityMaxQueryRange returns the maximum allowed time range for source=blocks cardinality queries. +func (o *Overrides) CardinalityMaxQueryRange(userID string) time.Duration { + return time.Duration(o.GetOverridesForUser(userID).CardinalityMaxQueryRange) +} + +// CardinalityMaxConcurrentRequests returns the maximum number of concurrent cardinality requests per tenant. +func (o *Overrides) CardinalityMaxConcurrentRequests(userID string) int { + return o.GetOverridesForUser(userID).CardinalityMaxConcurrentRequests +} + +// CardinalityQueryTimeout returns the per-request timeout for cardinality computation. +func (o *Overrides) CardinalityQueryTimeout(userID string) time.Duration { + return time.Duration(o.GetOverridesForUser(userID).CardinalityQueryTimeout) +} + // GetOverridesForUser returns the per-tenant limits with overrides. func (o *Overrides) GetOverridesForUser(userID string) *Limits { if o.tenantLimits != nil { diff --git a/schemas/cortex-config-schema.json b/schemas/cortex-config-schema.json index 20cf970c35b..91a4a5c5afd 100644 --- a/schemas/cortex-config-schema.json +++ b/schemas/cortex-config-schema.json @@ -5022,6 +5022,32 @@ "type": "boolean", "x-cli-flag": "alertmanager.receivers-firewall-block-private-addresses" }, + "cardinality_api_enabled": { + "default": false, + "description": "[Experimental] Enables the per-tenant cardinality API endpoint. When disabled, the endpoint returns HTTP 403.", + "type": "boolean", + "x-cli-flag": "querier.cardinality-api-enabled" + }, + "cardinality_max_concurrent_requests": { + "default": 2, + "description": "[Experimental] Maximum number of concurrent cardinality requests per tenant. Excess requests are rejected with HTTP 429.", + "type": "number", + "x-cli-flag": "querier.cardinality-max-concurrent-requests" + }, + "cardinality_max_query_range": { + "default": "1d", + "description": "[Experimental] Maximum allowed time range (end - start) for source=blocks cardinality queries.", + "type": "string", + "x-cli-flag": "querier.cardinality-max-query-range", + "x-format": "duration" + }, + "cardinality_query_timeout": { + "default": "1m", + "description": "[Experimental] Per-request timeout for cardinality computation. On timeout, partial results are returned.", + "type": "string", + "x-cli-flag": "querier.cardinality-query-timeout", + "x-format": "duration" + }, "compactor_blocks_retention_period": { "default": "0s", "description": "Delete blocks containing samples older than the specified retention period. 0 to disable.",