Ctrl K
ComunidadGuías…

prom/clickhouse_querier.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package prom

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"

	chgo "github.com/ClickHouse/ch-go"
	"github.com/ClickHouse/ch-go/proto"
	"github.com/coroot/coroot/ch"
	promModel "github.com/prometheus/common/model"
	"github.com/prometheus/prometheus/model/histogram"
	"github.com/prometheus/prometheus/model/labels"
	"github.com/prometheus/prometheus/storage"
	"github.com/prometheus/prometheus/tsdb/chunkenc"
	"github.com/prometheus/prometheus/tsdb/chunks"
	"github.com/prometheus/prometheus/util/annotations"
)

type clickhouseQuerier struct {
	ch         *ch.LowLevelClient
	mint, maxt int64
}

func (q *clickhouseQuerier) Select(ctx context.Context, _ bool, hints *storage.SelectHints, matchers ...*labels.Matcher) storage.SeriesSet {
	query := fmt.Sprintf(`
		SELECT
		    MetricName,
		    Labels,
		    groupArray(toUnixTimestamp(Timestamp)) AS Timestamps,
			groupArray(Value) AS Values
		FROM @@table_metrics@@
		WHERE
		 Timestamp >= toDateTime(%d) AND Timestamp <= toDateTime(%d) %s
		GROUP BY MetricName, Labels
		ORDER BY MetricName, Labels
	`, q.mint/1000, q.maxt/1000, q.buildWhere(matchers))

	metricName := (&proto.ColStr{}).LowCardinality()
	metricLabels := proto.NewMap[string, string]((&proto.ColStr{}).LowCardinality(), &proto.ColStr{})
	timestamps := proto.NewArray[uint32](&proto.ColUInt32{})
	values := proto.NewArray[float64](&proto.ColFloat64{})

	ss := &seriesSet{}
	ss.err = q.ch.Do(ctx, chgo.Query{
		Body: query,
		Result: proto.Results{
			{Name: "MetricName", Data: metricName},
			{Name: "Labels", Data: metricLabels},
			{Name: "Timestamps", Data: timestamps},
			{Name: "Values", Data: values},
		},
		OnResult: func(ctx context.Context, block proto.Block) error {
			for i := range block.Rows {
				lsMap := metricLabels.Row(i)
				lb := labels.NewScratchBuilder(len(lsMap) + 1)
				lb.Add(promModel.MetricNameLabel, metricName.Row(i))
				for k, v := range lsMap {
					lb.Add(k, v)
				}
				lb.Sort()
				ls := lb.Labels()
				tss := timestamps.Row(i)
				vals := values.Row(i)
				samples := make([]chunks.Sample, 0, len(tss))
				for j, ts := range tss {
					samples = append(samples, sample{t: (int64(ts) * 1000) * hints.Step / hints.Step, f: vals[j]})
				}
				ss.series = append(ss.series, storage.NewListSeries(ls, sortedAndDeduplicatedSamples(samples)))
			}
			return nil
		},
	})
	return ss
}

func sortedAndDeduplicatedSamples(samples []chunks.Sample) []chunks.Sample {
	sort.Slice(samples, func(i, j int) bool {
		return samples[i].T() < samples[j].T()
	})
	deduped := samples[:0]
	for i := 0; i < len(samples); {
		t := samples[i].T()
		j := i + 1
		for j < len(samples) && samples[j].T() == t {
			j++
		}
		deduped = append(deduped, samples[j-1])
		i = j
	}
	return deduped
}

func (q *clickhouseQuerier) LabelValues(ctx context.Context, name string, _ *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) {
	var res []string
	if name == promModel.MetricNameLabel {
		value := (&proto.ColStr{}).LowCardinality()
		err := q.ch.Do(ctx, chgo.Query{
			Body: fmt.Sprintf(`
				SELECT DISTINCT MetricName as LabelValue 
				FROM @@table_metrics@@ 
				WHERE Timestamp >= toDateTime(%d) AND Timestamp <= toDateTime(%d) %s`,
				q.mint/1000, q.maxt/1000, q.buildWhere(matchers)),
			Result: proto.Results{{Name: "LabelValue", Data: value}},
			OnResult: func(ctx context.Context, block proto.Block) error {
				for i := range block.Rows {
					res = append(res, value.Row(i))
				}
				return nil
			},
		})
		return res, nil, err
	}

	value := &proto.ColStr{}
	err := q.ch.Do(ctx, chgo.Query{
		Body: fmt.Sprintf(`
			SELECT DISTINCT %s as LabelValue 
			FROM @@table_metrics@@ 
			WHERE Timestamp >= toDateTime(%d) AND Timestamp <= toDateTime(%d) %s`,
			"Labels['"+escapeString(name)+"']", q.mint/1000, q.maxt/1000, q.buildWhere(matchers)),
		Result: proto.Results{{Name: "LabelValue", Data: value}},
		OnResult: func(ctx context.Context, block proto.Block) error {
			for i := range block.Rows {
				res = append(res, value.Row(i))
			}
			return nil
		},
	})
	return res, nil, err
}

func (q *clickhouseQuerier) LabelNames(ctx context.Context, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
	return nil, nil, errors.New("not yet implemented")
}

func (q *clickhouseQuerier) Close() error {
	return nil
}

func (q *clickhouseQuerier) buildWhere(ms []*labels.Matcher) string {
	conds := make([]string, 0, len(ms))
	for _, m := range ms {
		if c := q.condition(m); c != "" {
			conds = append(conds, c)
		}
	}
	if len(conds) > 0 {
		return "AND " + strings.Join(conds, " AND ")
	}
	return ""
}

func (q *clickhouseQuerier) condition(matcher *labels.Matcher) string {
	var column string
	if matcher.Name == labels.MetricName {
		column = "MetricName"
	} else {
		column = "Labels['" + escapeString(matcher.Name) + "']"
	}
	switch matcher.Type {
	case labels.MatchEqual:
		return fmt.Sprintf("%s = '%s'", column, escapeString(matcher.Value))
	case labels.MatchNotEqual:
		return fmt.Sprintf("%s != '%s'", column, escapeString(matcher.Value))
	case labels.MatchRegexp:
		return fmt.Sprintf("match(%s, '%s')", column, escapeString(matcher.Value))
	case labels.MatchNotRegexp:
		return fmt.Sprintf("NOT match(%s, '%s')", column, escapeString(matcher.Value))
	}
	return ""
}

type seriesSet struct {
	err    error
	cur    int
	series []storage.Series
}

func (ss *seriesSet) Next() bool {
	ss.cur++
	return ss.cur-1 < len(ss.series)
}

func (ss *seriesSet) At() storage.Series {
	return ss.series[ss.cur-1]
}

func (ss *seriesSet) Err() error {
	return ss.err
}

func (ss *seriesSet) Warnings() annotations.Annotations { return nil }

type sample struct {
	t int64
	f float64
}

func (s sample) T() int64 {
	return s.t
}

func (s sample) ST() int64 {
	return 0
}

func (s sample) F() float64 {
	return s.f
}

func (s sample) H() *histogram.Histogram {
	return nil
}

func (s sample) FH() *histogram.FloatHistogram {
	return nil
}

func (s sample) Type() chunkenc.ValueType {
	return chunkenc.ValFloat
}

func (s sample) Copy() chunks.Sample {
	return sample{t: s.t, f: s.f}
}

func escapeString(s string) string {
	return strings.ReplaceAll(s, "'", "''")
}