Repository: cortexproject/cortex
Pull request #2023 (2020-01-22 15:30:48)

TSDB: compactor's syncer metrics
This PR exposes metrics from TSDB's compactor "syncer" component. In Cortex, we instantiate multiple syncers, new one for each sync operation. Because of this, approach used to gather metrics is different from previous TSDB metrics work.

Here we register fixed set of metrics that are exported, and we update them when syncer has done its job, via `defer c.syncerMetrics.gatherThanosSyncerMetrics(reg)` in `compactUser` method.

Since tsdb.Compactor is reused between `compactUser` calls, we don't need to handle those metrics (they are expored with their original `prometheus_tsdb_compactions` prefix)

**Checklist**
- [x] Tests updated
- [x] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d172a9d877..3e9552ab2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ instructions below to upgrade your Postgres.
   * `--experimental.tsdb.max-tsdb-opening-concurrency-on-startup`
 * [ENHANCEMENT] Experimental TSDB: Added `cortex_ingester_shipper_dir_syncs_total`, `cortex_ingester_shipper_dir_sync_failures_total`, `cortex_ingester_shipper_uploads_total` and `cortex_ingester_shipper_upload_failures_total` metrics from TSDB shipper component. #1983
 * [ENHANCEMENT] Experimental TSDB: Querier now exports aggregate metrics from Thanos bucket store and in memory index cache (many metrics to list, but all have `cortex_querier_bucket_store_` or `cortex_querier_blocks_index_cache_` prefix). #1996 
+* [ENHANCEMENT] Experimental TSDB: Export TSDB Syncer metrics from Compactor component, they are prefixed with `cortex_compactor_`. #2023
 * [ENHANCEMENT] Experimental TSDB: Improved multi-tenant bucket store. #1991
   * Allowed to configure the blocks sync interval via `-experimental.tsdb.bucket-store.sync-interval` (0 disables the sync)
   * Limited the number of tenants concurrently synched by `-experimental.tsdb.bucket-store.block-sync-concurrency`
diff --git a/pkg/compactor/compactor.go b/pkg/compactor/compactor.go
index d2eb7b8cd9..c6de92f45e 100644
--- a/pkg/compactor/compactor.go
+++ b/pkg/compactor/compactor.go
@@ -76,6 +76,9 @@ type Compactor struct {
 	compactionRunsStarted   prometheus.Counter
 	compactionRunsCompleted prometheus.Counter
 	compactionRunsFailed    prometheus.Counter
+
+	// TSDB syncer metrics
+	syncerMetrics *syncerMetrics
 }
 
 // NewCompactor makes a new Compactor.
@@ -136,6 +139,7 @@ func newCompactor(
 	// Register metrics.
 	if registerer != nil {
 		registerer.MustRegister(c.compactionRunsStarted, c.compactionRunsCompleted, c.compactionRunsFailed)
+		c.syncerMetrics = newSyncerMetrics(registerer)
 	}
 
 	// Start the compactor loop.
@@ -224,9 +228,12 @@ func (c *Compactor) compactUsers(ctx context.Context) bool {
 func (c *Compactor) compactUser(ctx context.Context, userID string) error {
 	bucket := cortex_tsdb.NewUserBucketClient(userID, c.bucketClient)
 
+	reg := prometheus.NewRegistry()
+	defer c.syncerMetrics.gatherThanosSyncerMetrics(reg)
+
 	syncer, err := compact.NewSyncer(
 		c.logger,
-		nil, // TODO(pracucci) we should pass the prometheus registerer, but we would need to inject the user label to each metric, otherwise we have clashing metrics
+		reg,
 		bucket,
 		c.compactorCfg.ConsistencyDelay,
 		c.compactorCfg.BlockSyncConcurrency,
diff --git a/pkg/compactor/syncer_metrics.go b/pkg/compactor/syncer_metrics.go
new file mode 100644
index 0000000000..dc6a287b94
--- /dev/null
+++ b/pkg/compactor/syncer_metrics.go
@@ -0,0 +1,132 @@
+package compactor
+
+import (
+	"github.com/cortexproject/cortex/pkg/util"
+	"github.com/go-kit/kit/log/level"
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+// Copied from Thanos, pkg/compact/compact.go.
+// Here we aggregate metrics from all finished syncers.
+type syncerMetrics struct {
+	syncMetas                 prometheus.Counter
+	syncMetaFailures          prometheus.Counter
+	syncMetaDuration          *util.HistogramDataCollector // was prometheus.Histogram before
+	garbageCollectedBlocks    prometheus.Counter
+	garbageCollections        prometheus.Counter
+	garbageCollectionFailures prometheus.Counter
+	garbageCollectionDuration *util.HistogramDataCollector // was prometheus.Histogram before
+	compactions               prometheus.Counter
+	compactionRunsStarted     prometheus.Counter
+	compactionRunsCompleted   prometheus.Counter
+	compactionFailures        prometheus.Counter
+	verticalCompactions       prometheus.Counter
+}
+
+// Copied (and modified with Cortex prefix) from Thanos, pkg/compact/compact.go
+// We also ignore "group" label, since we only use a single group.
+func newSyncerMetrics(reg prometheus.Registerer) *syncerMetrics {
+	var m syncerMetrics
+
+	m.syncMetas = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_sync_meta_total",
+		Help: "TSDB Syncer: Total number of sync meta operations.",
+	})
+	m.syncMetaFailures = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_sync_meta_failures_total",
+		Help: "TSDB Syncer: Total number of failed sync meta operations.",
+	})
+	m.syncMetaDuration = util.NewHistogramDataCollector(prometheus.NewDesc(
+		"cortex_compactor_sync_meta_duration_seconds",
+		"TSDB Syncer: Time it took to sync meta files.",
+		nil, nil))
+
+	m.garbageCollectedBlocks = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_garbage_collected_blocks_total",
+		Help: "TSDB Syncer: Total number of deleted blocks by compactor.",
+	})
+	m.garbageCollections = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_garbage_collection_total",
+		Help: "TSDB Syncer: Total number of garbage collection operations.",
+	})
+	m.garbageCollectionFailures = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_garbage_collection_failures_total",
+		Help: "TSDB Syncer: Total number of failed garbage collection operations.",
+	})
+	m.garbageCollectionDuration = util.NewHistogramDataCollector(prometheus.NewDesc(
+		"cortex_compactor_garbage_collection_duration_seconds",
+		"TSDB Syncer: Time it took to perform garbage collection iteration.",
+		nil, nil))
+
+	m.compactions = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_group_compactions_total",
+		Help: "TSDB Syncer: Total number of group compaction attempts that resulted in a new block.",
+	})
+	m.compactionRunsStarted = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_group_compaction_runs_started_total",
+		Help: "TSDB Syncer: Total number of group compaction attempts.",
+	})
+	m.compactionRunsCompleted = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_group_compaction_runs_completed_total",
+		Help: "TSDB Syncer: Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.",
+	})
+	m.compactionFailures = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_group_compactions_failures_total",
+		Help: "TSDB Syncer: Total number of failed group compactions.",
+	})
+	m.verticalCompactions = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "cortex_compactor_group_vertical_compactions_total",
+		Help: "TSDB Syncer: Total number of group compaction attempts that resulted in a new block based on overlapping blocks.",
+	})
+
+	if reg != nil {
+		reg.MustRegister(
+			m.syncMetas,
+			m.syncMetaFailures,
+			m.syncMetaDuration,
+			m.garbageCollectedBlocks,
+			m.garbageCollections,
+			m.garbageCollectionFailures,
+			m.garbageCollectionDuration,
+			m.compactions,
+			m.compactionRunsStarted,
+			m.compactionRunsCompleted,
+			m.compactionFailures,
+			m.verticalCompactions,
+		)
+	}
+	return &m
+}
+
+func (m *syncerMetrics) gatherThanosSyncerMetrics(reg *prometheus.Registry) {
+	if m == nil {
+		return
+	}
+
+	mf, err := reg.Gather()
+	if err != nil {
+		level.Warn(util.Logger).Log("msg", "failed to gather metrics from syncer registry after compaction", "err", err)
+		return
+	}
+
+	mfm, err := util.NewMetricFamilyMap(mf)
+	if err != nil {
+		level.Warn(util.Logger).Log("msg", "failed to gather metrics from syncer registry after compaction", "err", err)
+		return
+	}
+
+	m.syncMetas.Add(mfm.SumCounters("thanos_compact_sync_meta_total"))
+	m.syncMetaFailures.Add(mfm.SumCounters("thanos_compact_sync_meta_failures_total"))
+	m.syncMetaDuration.Add(mfm.SumHistograms("thanos_compact_sync_meta_duration_seconds"))
+	m.garbageCollectedBlocks.Add(mfm.SumCounters("thanos_compact_garbage_collected_blocks_total"))
+	m.garbageCollections.Add(mfm.SumCounters("thanos_compact_garbage_collection_total"))
+	m.garbageCollectionFailures.Add(mfm.SumCounters("thanos_compact_garbage_collection_failures_total"))
+	m.garbageCollectionDuration.Add(mfm.SumHistograms("thanos_compact_garbage_collection_duration_seconds"))
+
+	// These metrics have "group" label, but we sum them all together.
+	m.compactions.Add(mfm.SumCounters("thanos_compact_group_compactions_total"))
+	m.compactionRunsStarted.Add(mfm.SumCounters("thanos_compact_group_compaction_runs_started_total"))
+	m.compactionRunsCompleted.Add(mfm.SumCounters("thanos_compact_group_compaction_runs_completed_total"))
+	m.compactionFailures.Add(mfm.SumCounters("thanos_compact_group_compactions_failures_total"))
+	m.verticalCompactions.Add(mfm.SumCounters("thanos_compact_group_vertical_compactions_total"))
+}
diff --git a/pkg/util/metrics_helper.go b/pkg/util/metrics_helper.go
index 4957c40ded..234b154ebd 100644
--- a/pkg/util/metrics_helper.go
+++ b/pkg/util/metrics_helper.go
@@ -4,77 +4,147 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
+	"sync"
 
 	"github.com/go-kit/kit/log/level"
 	"github.com/prometheus/client_golang/prometheus"
 	dto "github.com/prometheus/client_model/go"
 )
 
-// MetricFamiliesPerUser is a collection of metrics gathered via calling Gatherer.Gather() method on different
-// gatherers, one per user.
-// First key = userID, second key = metric name.
-// Value = slice of gathered values with the same metric name.
-type MetricFamiliesPerUser map[string]map[string]*dto.MetricFamily
+// Data for single value (counter/gauge) with labels.
+type singleValueWithLabels struct {
+	Value       float64
+	LabelValues []string
+}
 
-func BuildMetricFamiliesPerUserFromUserRegistries(regs map[string]*prometheus.Registry) MetricFamiliesPerUser {
-	data := MetricFamiliesPerUser{}
-	for userID, r := range regs {
-		m, err := r.Gather()
-		if err == nil {
-			err = data.AddGatheredDataForUser(userID, m)
-		}
+// Key to this map is value unique to label values (generated by getLabelsString function)
+type singleValueWithLabelsMap map[string]singleValueWithLabels
 
-		if err != nil {
-			level.Warn(Logger).Log("msg", "failed to gather metrics from registry", "user", userID, "err", err)
-			continue
-		}
+// This function is used to aggregate results with different labels into a map. Values for same labels are added together.
+func (m singleValueWithLabelsMap) aggregateFn(labelsKey string, labelValues []string, value float64) {
+	r := m[labelsKey]
+	if r.LabelValues == nil {
+		r.LabelValues = labelValues
+	}
+
+	r.Value += value
+	m[labelsKey] = r
+}
+
+func (m singleValueWithLabelsMap) WriteToMetricChannel(out chan<- prometheus.Metric, desc *prometheus.Desc, valueType prometheus.ValueType) {
+	for _, cr := range m {
+		out <- prometheus.MustNewConstMetric(desc, valueType, cr.Value, cr.LabelValues...)
 	}
-	return data
 }
 
-// AddGatheredDataForUser adds user-specific output of Gatherer.Gather method.
+// MetricFamilyMap is a map of metric names to their family (metrics with same name, but different labels)
+// Keeping map of metric name to its family makes it easier to do searches later.
+type MetricFamilyMap map[string]*dto.MetricFamily
+
+// NewMetricFamilyMap sorts output from Gatherer.Gather method into a map.
 // Gatherer.Gather specifies that there metric families are uniquely named, and we use that fact here.
 // If they are not, this method returns error.
-func (d MetricFamiliesPerUser) AddGatheredDataForUser(userID string, metrics []*dto.MetricFamily) error {
-	// Keeping map of metric name to its family makes it easier to do searches later.
-	perMetricName := map[string]*dto.MetricFamily{}
+func NewMetricFamilyMap(metrics []*dto.MetricFamily) (MetricFamilyMap, error) {
+	perMetricName := MetricFamilyMap{}
 
 	for _, m := range metrics {
 		name := m.GetName()
 		// these errors should never happen when passing Gatherer.Gather() output.
 		if name == "" {
-			return errors.New("empty name for metric family")
+			return nil, errors.New("empty name for metric family")
 		}
 		if perMetricName[name] != nil {
-			return fmt.Errorf("non-unique name for metric family: %q", name)
+			return nil, fmt.Errorf("non-unique name for metric family: %q", name)
 		}
 
 		perMetricName[name] = m
 	}
 
-	d[userID] = perMetricName
-	return nil
+	return perMetricName, nil
+}
+
+func (mfm MetricFamilyMap) SumCounters(name string) float64 {
+	return sum(mfm[name], counterValue)
+}
+
+func (mfm MetricFamilyMap) SumGauges(name string) float64 {
+	return sum(mfm[name], gaugeValue)
+}
+
+func (mfm MetricFamilyMap) SumHistograms(name string) HistogramData {
+	hd := HistogramData{}
+	mfm.SumHistogramsTo(name, &hd)
+	return hd
+}
+
+func (mfm MetricFamilyMap) SumHistogramsTo(name string, output *HistogramData) {
+	for _, m := range mfm[name].GetMetric() {
+		output.AddHistogram(m.GetHistogram())
+	}
+}
+
+func (mfm MetricFamilyMap) SumSummaries(name string) SummaryData {
+	sd := SummaryData{}
+	mfm.SumSummariesTo(name, &sd)
+	return sd
+}
+
+func (mfm MetricFamilyMap) SumSummariesTo(name string, output *SummaryData) {
+	for _, m := range mfm[name].GetMetric() {
+		output.AddSummary(m.GetSummary())
+	}
+}
+
+func (mfm MetricFamilyMap) sumOfSingleValuesWithLabels(metric string, labelNames []string, extractFn func(*dto.Metric) float64, aggregateFn func(labelsKey string, labelValues []string, value float64)) {
+	metricsPerLabelValue := getMetricsWithLabelNames(mfm[metric], labelNames)
+
+	for key, mlv := range metricsPerLabelValue {
+		for _, m := range mlv.metrics {
+			val := extractFn(m)
+			aggregateFn(key, mlv.labelValues, val)
+		}
+	}
+}
+
+// MetricFamiliesPerUser is a collection of metrics gathered via calling Gatherer.Gather() method on different
+// gatherers, one per user.
+type MetricFamiliesPerUser map[string]MetricFamilyMap
+
+func BuildMetricFamiliesPerUserFromUserRegistries(regs map[string]*prometheus.Registry) MetricFamiliesPerUser {
+	data := MetricFamiliesPerUser{}
+	for userID, r := range regs {
+		m, err := r.Gather()
+		if err == nil {
+			var mfm MetricFamilyMap // := would shadow err from outer block, and single err check will not work
+			mfm, err = NewMetricFamilyMap(m)
+			if err == nil {
+				data[userID] = mfm
+			}
+		}
+
+		if err != nil {
+			level.Warn(Logger).Log("msg", "failed to gather metrics from registry", "user", userID, "err", err)
+			continue
+		}
+	}
+	return data
 }
 
 func (d MetricFamiliesPerUser) SendSumOfCounters(out chan<- prometheus.Metric, desc *prometheus.Desc, counter string) {
 	result := float64(0)
-	for _, perMetric := range d {
-		result += sum(perMetric[counter], counterValue)
+	for _, userMetrics := range d {
+		result += userMetrics.SumCounters(counter)
 	}
-
 	out <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, result)
 }
 
 func (d MetricFamiliesPerUser) SendSumOfCountersWithLabels(out chan<- prometheus.Metric, desc *prometheus.Desc, counter string, labelNames ...string) {
-	result := d.sumOfSingleValuesWithLabels(counter, counterValue, labelNames)
-	for _, cr := range result {
-		out <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, cr.value, cr.labelValues...)
-	}
+	d.sumOfSingleValuesWithLabels(counter, counterValue, labelNames).WriteToMetricChannel(out, desc, prometheus.CounterValue)
 }
 
 func (d MetricFamiliesPerUser) SendSumOfCountersPerUser(out chan<- prometheus.Metric, desc *prometheus.Desc, counter string) {
-	for user, perMetric := range d {
-		v := sum(perMetric[counter], counterValue)
+	for user, userMetrics := range d {
+		v := userMetrics.SumCounters(counter)
 
 		out <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, v, user)
 	}
@@ -82,70 +152,35 @@ func (d MetricFamiliesPerUser) SendSumOfCountersPerUser(out chan<- prometheus.Me
 
 func (d MetricFamiliesPerUser) SendSumOfGauges(out chan<- prometheus.Metric, desc *prometheus.Desc, gauge string) {
 	result := float64(0)
-	for _, perMetric := range d {
-		result += sum(perMetric[gauge], gaugeValue)
+	for _, userMetrics := range d {
+		result += userMetrics.SumGauges(gauge)
 	}
 	out <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, result)
 }
 
 func (d MetricFamiliesPerUser) SendSumOfGaugesWithLabels(out chan<- prometheus.Metric, desc *prometheus.Desc, gauge string, labelNames ...string) {
-	result := d.sumOfSingleValuesWithLabels(gauge, gaugeValue, labelNames)
-	for _, cr := range result {
-		out <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, cr.value, cr.labelValues...)
-	}
+	d.sumOfSingleValuesWithLabels(gauge, gaugeValue, labelNames).WriteToMetricChannel(out, desc, prometheus.GaugeValue)
 }
 
-type singleResult struct {
-	value       float64
-	labelValues []string
-}
-
-func (d MetricFamiliesPerUser) sumOfSingleValuesWithLabels(metric string, fn func(*dto.Metric) float64, labelNames []string) map[string]singleResult {
-	result := map[string]singleResult{}
-
+func (d MetricFamiliesPerUser) sumOfSingleValuesWithLabels(metric string, fn func(*dto.Metric) float64, labelNames []string) singleValueWithLabelsMap {
+	result := singleValueWithLabelsMap{}
 	for _, userMetrics := range d {
-		metricsPerLabelValue := getMetricsWithLabelNames(userMetrics[metric], labelNames)
-
-		for key, mlv := range metricsPerLabelValue {
-			for _, m := range mlv.metrics {
-				r := result[key]
-				if r.labelValues == nil {
-					r.labelValues = mlv.labelValues
-				}
-
-				r.value += fn(m)
-				result[key] = r
-			}
-		}
+		userMetrics.sumOfSingleValuesWithLabels(metric, labelNames, fn, result.aggregateFn)
 	}
-
 	return result
 }
 
 func (d MetricFamiliesPerUser) SendSumOfSummaries(out chan<- prometheus.Metric, desc *prometheus.Desc, summaryName string) {
-	var (
-		sampleCount uint64
-		sampleSum   float64
-		quantiles   map[float64]float64
-	)
-
+	summaryData := SummaryData{}
 	for _, userMetrics := range d {
-		for _, m := range userMetrics[summaryName].GetMetric() {
-			summary := m.GetSummary()
-			sampleCount += summary.GetSampleCount()
-			sampleSum += summary.GetSampleSum()
-			quantiles = mergeSummaryQuantiles(quantiles, summary.GetQuantile())
-		}
+		userMetrics.SumSummariesTo(summaryName, &summaryData)
 	}
-
-	out <- prometheus.MustNewConstSummary(desc, sampleCount, sampleSum, quantiles)
+	out <- summaryData.Metric(desc)
 }
 
 func (d MetricFamiliesPerUser) SendSumOfSummariesWithLabels(out chan<- prometheus.Metric, desc *prometheus.Desc, summaryName string, labelNames ...string) {
 	type summaryResult struct {
-		sampleCount uint64
-		sampleSum   float64
-		quantiles   map[float64]float64
+		data        SummaryData
 		labelValues []string
 	}
 
@@ -161,74 +196,26 @@ func (d MetricFamiliesPerUser) SendSumOfSummariesWithLabels(out chan<- prometheu
 					r.labelValues = mwl.labelValues
 				}
 
-				summary := m.GetSummary()
-				r.sampleCount += summary.GetSampleCount()
-				r.sampleSum += summary.GetSampleSum()
-				r.quantiles = mergeSummaryQuantiles(r.quantiles, summary.GetQuantile())
-
+				r.data.AddSummary(m.GetSummary())
 				result[key] = r
 			}
 		}
 	}
 
 	for _, sr := range result {
-		out <- prometheus.MustNewConstSummary(desc, sr.sampleCount, sr.sampleSum, sr.quantiles, sr.labelValues...)
+		out <- sr.data.Metric(desc, sr.labelValues...)
 	}
 }
 
 func (d MetricFamiliesPerUser) SendSumOfHistograms(out chan<- prometheus.Metric, desc *prometheus.Desc, histogramName string) {
-	var (
-		sampleCount uint64
-		sampleSum   float64
-		buckets     map[float64]uint64
-	)
-
+	hd := HistogramData{}
 	for _, userMetrics := range d {
-		for _, m := range userMetrics[histogramName].GetMetric() {
-			histo := m.GetHistogram()
-			sampleCount += histo.GetSampleCount()
-			sampleSum += histo.GetSampleSum()
-			buckets = mergeHistogramBuckets(buckets, histo.GetBucket())
-		}
+		userMetrics.SumHistogramsTo(histogramName, &hd)
 	}
-
-	out <- prometheus.MustNewConstHistogram(desc, sampleCount, sampleSum, buckets)
-}
-
-func mergeSummaryQuantiles(quantiles map[float64]float64, summaryQuantiles []*dto.Quantile) map[float64]float64 {
-	if len(summaryQuantiles) == 0 {
-		return quantiles
-	}
-
-	out := quantiles
-	if out == nil {
-		out = map[float64]float64{}
-	}
-
-	for _, q := range summaryQuantiles {
-		// we assume that all summaries have same quantiles
-		out[q.GetQuantile()] += q.GetValue()
-	}
-	return out
-}
-
-func mergeHistogramBuckets(buckets map[float64]uint64, histogramBuckets []*dto.Bucket) map[float64]uint64 {
-	if len(histogramBuckets) == 0 {
-		return buckets
-	}
-
-	out := buckets
-	if out == nil {
-		out = map[float64]uint64{}
-	}
-
-	for _, q := range histogramBuckets {
-		// we assume that all histograms have same buckets
-		out[q.GetUpperBound()] += q.GetCumulativeCount()
-	}
-	return out
+	out <- hd.Metric(desc)
 }
 
+// struct for holding metrics with same label values
 type metricsWithLabels struct {
 	labelValues []string
 	metrics     []*dto.Metric
@@ -294,3 +281,107 @@ func sum(mf *dto.MetricFamily, fn func(*dto.Metric) float64) float64 {
 // This works even if m is nil, m.Counter is nil or m.Counter.Value is nil (it returns 0 in those cases)
 func counterValue(m *dto.Metric) float64 { return m.GetCounter().GetValue() }
 func gaugeValue(m *dto.Metric) float64   { return m.GetGauge().GetValue() }
+
+// SummaryData keeps all data needed to create summary metric
+type SummaryData struct {
+	sampleCount uint64
+	sampleSum   float64
+	quantiles   map[float64]float64
+}
+
+func (s *SummaryData) AddSummary(sum *dto.Summary) {
+	s.sampleCount += sum.GetSampleCount()
+	s.sampleSum += sum.GetSampleSum()
+
+	qs := sum.GetQuantile()
+	if len(qs) > 0 && s.quantiles == nil {
+		s.quantiles = map[float64]float64{}
+	}
+
+	for _, q := range qs {
+		// we assume that all summaries have same quantiles
+		s.quantiles[q.GetQuantile()] += q.GetValue()
+	}
+}
+
+func (s *SummaryData) Metric(desc *prometheus.Desc, labelValues ...string) prometheus.Metric {
+	return prometheus.MustNewConstSummary(desc, s.sampleCount, s.sampleSum, s.quantiles, labelValues...)
+}
+
+// HistogramData keeps data required to build histogram Metric
+type HistogramData struct {
+	sampleCount uint64
+	sampleSum   float64
+	buckets     map[float64]uint64
+}
+
+// Adds histogram from gathered metrics to this histogram data.
+func (d *HistogramData) AddHistogram(histo *dto.Histogram) {
+	d.sampleCount += histo.GetSampleCount()
+	d.sampleSum += histo.GetSampleSum()
+
+	histoBuckets := histo.GetBucket()
+	if len(histoBuckets) > 0 && d.buckets == nil {
+		d.buckets = map[float64]uint64{}
+	}
+
+	for _, b := range histoBuckets {
+		// we assume that all histograms have same buckets
+		d.buckets[b.GetUpperBound()] += b.GetCumulativeCount()
+	}
+}
+
+// Merged another histogram data into this one.
+func (d *HistogramData) AddHistogramData(histo HistogramData) {
+	d.sampleCount += histo.sampleCount
+	d.sampleSum += histo.sampleSum
+
+	if len(histo.buckets) > 0 && d.buckets == nil {
+		d.buckets = map[float64]uint64{}
+	}
+
+	for bound, count := range histo.buckets {
+		// we assume that all histograms have same buckets
+		d.buckets[bound] += count
+	}
+}
+
+// Return prometheus metric from this histogram data.
+func (d *HistogramData) Metric(desc *prometheus.Desc) prometheus.Metric {
+	return prometheus.MustNewConstHistogram(desc, d.sampleCount, d.sampleSum, d.buckets)
+}
+
+// Creates new histogram data collector.
+func NewHistogramDataCollector(desc *prometheus.Desc) *HistogramDataCollector {
+	return &HistogramDataCollector{
+		desc: desc,
+		data: &HistogramData{},
+	}
+}
+
+// HistogramDataCollector combines histogram data, with prometheus descriptor. It can be registered
+// into prometheus to report histogram with stored data. Data can be updated via Add method.
+type HistogramDataCollector struct {
+	desc *prometheus.Desc
+
+	dataMu sync.RWMutex
+	data   *HistogramData
+}
+
+func (h *HistogramDataCollector) Describe(out chan<- *prometheus.Desc) {
+	out <- h.desc
+}
+
+func (h *HistogramDataCollector) Collect(out chan<- prometheus.Metric) {
+	h.dataMu.RLock()
+	defer h.dataMu.RUnlock()
+
+	out <- h.data.Metric(h.desc)
+}
+
+func (h *HistogramDataCollector) Add(hd HistogramData) {
+	h.dataMu.Lock()
+	defer h.dataMu.Unlock()
+
+	h.data.AddHistogramData(hd)
+}


Test patch:
diff --git a/pkg/compactor/compactor_test.go b/pkg/compactor/compactor_test.go
index 59e14a8e2c..99bf38f51c 100644
--- a/pkg/compactor/compactor_test.go
+++ b/pkg/compactor/compactor_test.go
@@ -99,6 +99,58 @@ func TestCompactor_ShouldDoNothingOnNoUserBlocks(t *testing.T) {
 		# TYPE cortex_compactor_runs_failed_total counter
 		# HELP cortex_compactor_runs_failed_total Total number of compaction runs failed.
 		cortex_compactor_runs_failed_total 0
+
+		# HELP cortex_compactor_garbage_collected_blocks_total TSDB Syncer: Total number of deleted blocks by compactor.
+		# TYPE cortex_compactor_garbage_collected_blocks_total counter
+		cortex_compactor_garbage_collected_blocks_total 0
+
+		# HELP cortex_compactor_garbage_collection_duration_seconds TSDB Syncer: Time it took to perform garbage collection iteration.
+		# TYPE cortex_compactor_garbage_collection_duration_seconds histogram
+		cortex_compactor_garbage_collection_duration_seconds_bucket{le="+Inf"} 0
+		cortex_compactor_garbage_collection_duration_seconds_sum 0
+		cortex_compactor_garbage_collection_duration_seconds_count 0
+
+		# HELP cortex_compactor_garbage_collection_failures_total TSDB Syncer: Total number of failed garbage collection operations.
+		# TYPE cortex_compactor_garbage_collection_failures_total counter
+		cortex_compactor_garbage_collection_failures_total 0
+
+		# HELP cortex_compactor_garbage_collection_total TSDB Syncer: Total number of garbage collection operations.
+		# TYPE cortex_compactor_garbage_collection_total counter
+		cortex_compactor_garbage_collection_total 0
+
+		# HELP cortex_compactor_sync_meta_duration_seconds TSDB Syncer: Time it took to sync meta files.
+		# TYPE cortex_compactor_sync_meta_duration_seconds histogram
+		cortex_compactor_sync_meta_duration_seconds_bucket{le="+Inf"} 0
+		cortex_compactor_sync_meta_duration_seconds_sum 0
+		cortex_compactor_sync_meta_duration_seconds_count 0
+
+		# HELP cortex_compactor_sync_meta_failures_total TSDB Syncer: Total number of failed sync meta operations.
+		# TYPE cortex_compactor_sync_meta_failures_total counter
+		cortex_compactor_sync_meta_failures_total 0
+
+		# HELP cortex_compactor_sync_meta_total TSDB Syncer: Total number of sync meta operations.
+		# TYPE cortex_compactor_sync_meta_total counter
+		cortex_compactor_sync_meta_total 0
+
+		# HELP cortex_compactor_group_compaction_runs_completed_total TSDB Syncer: Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
+		# TYPE cortex_compactor_group_compaction_runs_completed_total counter
+		cortex_compactor_group_compaction_runs_completed_total 0
+
+		# HELP cortex_compactor_group_compaction_runs_started_total TSDB Syncer: Total number of group compaction attempts.
+		# TYPE cortex_compactor_group_compaction_runs_started_total counter
+		cortex_compactor_group_compaction_runs_started_total 0
+
+		# HELP cortex_compactor_group_compactions_failures_total TSDB Syncer: Total number of failed group compactions.
+		# TYPE cortex_compactor_group_compactions_failures_total counter
+		cortex_compactor_group_compactions_failures_total 0
+
+		# HELP cortex_compactor_group_compactions_total TSDB Syncer: Total number of group compaction attempts that resulted in a new block.
+		# TYPE cortex_compactor_group_compactions_total counter
+		cortex_compactor_group_compactions_total 0
+
+		# HELP cortex_compactor_group_vertical_compactions_total TSDB Syncer: Total number of group compaction attempts that resulted in a new block based on overlapping blocks.
+		# TYPE cortex_compactor_group_vertical_compactions_total counter
+		cortex_compactor_group_vertical_compactions_total 0
 	`)))
 }
 
@@ -142,6 +194,58 @@ func TestCompactor_ShouldRetryOnFailureWhileDiscoveringUsersFromBucket(t *testin
 		# TYPE cortex_compactor_runs_failed_total counter
 		# HELP cortex_compactor_runs_failed_total Total number of compaction runs failed.
 		cortex_compactor_runs_failed_total 1
+
+		# HELP cortex_compactor_garbage_collected_blocks_total TSDB Syncer: Total number of deleted blocks by compactor.
+		# TYPE cortex_compactor_garbage_collected_blocks_total counter
+		cortex_compactor_garbage_collected_blocks_total 0
+
+		# HELP cortex_compactor_garbage_collection_duration_seconds TSDB Syncer: Time it took to perform garbage collection iteration.
+		# TYPE cortex_compactor_garbage_collection_duration_seconds histogram
+		cortex_compactor_garbage_collection_duration_seconds_bucket{le="+Inf"} 0
+		cortex_compactor_garbage_collection_duration_seconds_sum 0
+		cortex_compactor_garbage_collection_duration_seconds_count 0
+
+		# HELP cortex_compactor_garbage_collection_failures_total TSDB Syncer: Total number of failed garbage collection operations.
+		# TYPE cortex_compactor_garbage_collection_failures_total counter
+		cortex_compactor_garbage_collection_failures_total 0
+
+		# HELP cortex_compactor_garbage_collection_total TSDB Syncer: Total number of garbage collection operations.
+		# TYPE cortex_compactor_garbage_collection_total counter
+		cortex_compactor_garbage_collection_total 0
+
+		# HELP cortex_compactor_sync_meta_duration_seconds TSDB Syncer: Time it took to sync meta files.
+		# TYPE cortex_compactor_sync_meta_duration_seconds histogram
+		cortex_compactor_sync_meta_duration_seconds_bucket{le="+Inf"} 0
+		cortex_compactor_sync_meta_duration_seconds_sum 0
+		cortex_compactor_sync_meta_duration_seconds_count 0
+
+		# HELP cortex_compactor_sync_meta_failures_total TSDB Syncer: Total number of failed sync meta operations.
+		# TYPE cortex_compactor_sync_meta_failures_total counter
+		cortex_compactor_sync_meta_failures_total 0
+
+		# HELP cortex_compactor_sync_meta_total TSDB Syncer: Total number of sync meta operations.
+		# TYPE cortex_compactor_sync_meta_total counter
+		cortex_compactor_sync_meta_total 0
+
+		# HELP cortex_compactor_group_compaction_runs_completed_total TSDB Syncer: Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
+		# TYPE cortex_compactor_group_compaction_runs_completed_total counter
+		cortex_compactor_group_compaction_runs_completed_total 0
+
+		# HELP cortex_compactor_group_compaction_runs_started_total TSDB Syncer: Total number of group compaction attempts.
+		# TYPE cortex_compactor_group_compaction_runs_started_total counter
+		cortex_compactor_group_compaction_runs_started_total 0
+
+		# HELP cortex_compactor_group_compactions_failures_total TSDB Syncer: Total number of failed group compactions.
+		# TYPE cortex_compactor_group_compactions_failures_total counter
+		cortex_compactor_group_compactions_failures_total 0
+
+		# HELP cortex_compactor_group_compactions_total TSDB Syncer: Total number of group compaction attempts that resulted in a new block.
+		# TYPE cortex_compactor_group_compactions_total counter
+		cortex_compactor_group_compactions_total 0
+
+		# HELP cortex_compactor_group_vertical_compactions_total TSDB Syncer: Total number of group compaction attempts that resulted in a new block based on overlapping blocks.
+		# TYPE cortex_compactor_group_vertical_compactions_total counter
+		cortex_compactor_group_vertical_compactions_total 0
 	`)))
 }
 
@@ -191,6 +295,9 @@ func TestCompactor_ShouldIterateOverUsersAndRunCompaction(t *testing.T) {
 		`level=info msg="successfully compacted user blocks" user=user-2`,
 	}, strings.Split(strings.TrimSpace(logs.String()), "\n"))
 
+	// Instead of testing for shipper metrics, we only check our metrics here.
+	// Real shipper metrics are too variable to embed into a test.
+	testedMetrics := []string{"cortex_compactor_runs_started_total", "cortex_compactor_runs_completed_total", "cortex_compactor_runs_failed_total"}
 	assert.NoError(t, prom_testutil.GatherAndCompare(registry, strings.NewReader(`
 		# TYPE cortex_compactor_runs_started_total counter
 		# HELP cortex_compactor_runs_started_total Total number of compaction runs started.
@@ -203,7 +310,7 @@ func TestCompactor_ShouldIterateOverUsersAndRunCompaction(t *testing.T) {
 		# TYPE cortex_compactor_runs_failed_total counter
 		# HELP cortex_compactor_runs_failed_total Total number of compaction runs failed.
 		cortex_compactor_runs_failed_total 0
-	`)))
+	`), testedMetrics...))
 }
 
 func prepare(t *testing.T, bucketClient *cortex_tsdb.BucketClientMock) (*Compactor, *tsdbCompactorMock, *bytes.Buffer, prometheus.Gatherer) {
diff --git a/pkg/compactor/syncer_metrics_test.go b/pkg/compactor/syncer_metrics_test.go
new file mode 100644
index 0000000000..4103bbb610
--- /dev/null
+++ b/pkg/compactor/syncer_metrics_test.go
@@ -0,0 +1,232 @@
+package compactor
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/testutil"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSyncerMetrics(t *testing.T) {
+	reg := prometheus.NewPedanticRegistry()
+
+	sm := newSyncerMetrics(reg)
+	sm.gatherThanosSyncerMetrics(generateTestData(12345))
+	sm.gatherThanosSyncerMetrics(generateTestData(76543))
+	sm.gatherThanosSyncerMetrics(generateTestData(22222))
+	// total base = 111110
+
+	err := testutil.GatherAndCompare(reg, bytes.NewBufferString(`
+			# HELP cortex_compactor_sync_meta_total TSDB Syncer: Total number of sync meta operations.
+			# TYPE cortex_compactor_sync_meta_total counter
+			cortex_compactor_sync_meta_total 111110
+
+			# HELP cortex_compactor_sync_meta_failures_total TSDB Syncer: Total number of failed sync meta operations.
+			# TYPE cortex_compactor_sync_meta_failures_total counter
+			cortex_compactor_sync_meta_failures_total 222220
+
+			# HELP cortex_compactor_sync_meta_duration_seconds TSDB Syncer: Time it took to sync meta files.
+			# TYPE cortex_compactor_sync_meta_duration_seconds histogram
+			# Observed values: 3.7035, 22.9629, 6.6666 (seconds)
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="0.01"} 0
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="0.1"} 0
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="0.3"} 0
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="0.6"} 0
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="1"} 0
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="3"} 0
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="6"} 1
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="9"} 2
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="20"} 2
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="30"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="60"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="90"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="120"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="240"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="360"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="720"} 3
+			cortex_compactor_sync_meta_duration_seconds_bucket{le="+Inf"} 3
+			# rounding error
+			cortex_compactor_sync_meta_duration_seconds_sum 33.333000000000006
+			cortex_compactor_sync_meta_duration_seconds_count 3
+
+			# HELP cortex_compactor_garbage_collected_blocks_total TSDB Syncer: Total number of deleted blocks by compactor.
+			# TYPE cortex_compactor_garbage_collected_blocks_total counter
+			cortex_compactor_garbage_collected_blocks_total 444440
+
+			# HELP cortex_compactor_garbage_collection_total TSDB Syncer: Total number of garbage collection operations.
+			# TYPE cortex_compactor_garbage_collection_total counter
+			cortex_compactor_garbage_collection_total 555550
+
+			# HELP cortex_compactor_garbage_collection_failures_total TSDB Syncer: Total number of failed garbage collection operations.
+			# TYPE cortex_compactor_garbage_collection_failures_total counter
+			cortex_compactor_garbage_collection_failures_total 666660
+
+			# HELP cortex_compactor_garbage_collection_duration_seconds TSDB Syncer: Time it took to perform garbage collection iteration.
+			# TYPE cortex_compactor_garbage_collection_duration_seconds histogram
+			# Observed values: 8.6415, 53.5801, 15.5554
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="0.01"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="0.1"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="0.3"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="0.6"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="1"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="3"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="6"} 0
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="9"} 1
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="20"} 2
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="30"} 2
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="60"} 3
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="90"} 3
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="120"} 3
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="240"} 3
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="360"} 3
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="720"} 3
+			cortex_compactor_garbage_collection_duration_seconds_bucket{le="+Inf"} 3
+			cortex_compactor_garbage_collection_duration_seconds_sum 77.777
+			cortex_compactor_garbage_collection_duration_seconds_count 3
+
+			# HELP cortex_compactor_group_compactions_total TSDB Syncer: Total number of group compaction attempts that resulted in a new block.
+			# TYPE cortex_compactor_group_compactions_total counter
+			# Sum across all groups
+			cortex_compactor_group_compactions_total 2999970
+
+			# HELP cortex_compactor_group_compaction_runs_started_total TSDB Syncer: Total number of group compaction attempts.
+			# TYPE cortex_compactor_group_compaction_runs_started_total counter
+			# Sum across all groups
+			cortex_compactor_group_compaction_runs_started_total 3999960
+
+			# HELP cortex_compactor_group_compaction_runs_completed_total TSDB Syncer: Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.
+			# TYPE cortex_compactor_group_compaction_runs_completed_total counter
+			# Sum across all groups
+			cortex_compactor_group_compaction_runs_completed_total 4999950
+
+			# HELP cortex_compactor_group_compactions_failures_total TSDB Syncer: Total number of failed group compactions.
+			# TYPE cortex_compactor_group_compactions_failures_total counter
+			cortex_compactor_group_compactions_failures_total 5999940
+
+			# HELP cortex_compactor_group_vertical_compactions_total TSDB Syncer: Total number of group compaction attempts that resulted in a new block based on overlapping blocks.
+			# TYPE cortex_compactor_group_vertical_compactions_total counter
+			cortex_compactor_group_vertical_compactions_total 6999930
+	`))
+	require.NoError(t, err)
+}
+
+func generateTestData(base float64) *prometheus.Registry {
+	r := prometheus.NewRegistry()
+	m := newTestSyncerMetrics(r)
+	m.syncMetas.Add(1 * base)
+	m.syncMetaFailures.Add(2 * base)
+	m.syncMetaDuration.Observe(3 * base / 10000)
+	m.garbageCollectedBlocks.Add(4 * base)
+	m.garbageCollections.Add(5 * base)
+	m.garbageCollectionFailures.Add(6 * base)
+	m.garbageCollectionDuration.Observe(7 * base / 10000)
+	m.compactions.WithLabelValues("aaa").Add(8 * base)
+	m.compactions.WithLabelValues("bbb").Add(9 * base)
+	m.compactions.WithLabelValues("ccc").Add(10 * base)
+	m.compactionRunsStarted.WithLabelValues("aaa").Add(11 * base)
+	m.compactionRunsStarted.WithLabelValues("bbb").Add(12 * base)
+	m.compactionRunsStarted.WithLabelValues("ccc").Add(13 * base)
+	m.compactionRunsCompleted.WithLabelValues("aaa").Add(14 * base)
+	m.compactionRunsCompleted.WithLabelValues("bbb").Add(15 * base)
+	m.compactionRunsCompleted.WithLabelValues("ccc").Add(16 * base)
+	m.compactionFailures.WithLabelValues("aaa").Add(17 * base)
+	m.compactionFailures.WithLabelValues("bbb").Add(18 * base)
+	m.compactionFailures.WithLabelValues("ccc").Add(19 * base)
+	m.verticalCompactions.WithLabelValues("aaa").Add(20 * base)
+	m.verticalCompactions.WithLabelValues("bbb").Add(21 * base)
+	m.verticalCompactions.WithLabelValues("ccc").Add(22 * base)
+	return r
+}
+
+// directly copied from Thanos (and renamed syncerMetrics to testSyncerMetrics to avoid conflict)
+type testSyncerMetrics struct {
+	syncMetas                 prometheus.Counter
+	syncMetaFailures          prometheus.Counter
+	syncMetaDuration          prometheus.Histogram
+	garbageCollectedBlocks    prometheus.Counter
+	garbageCollections        prometheus.Counter
+	garbageCollectionFailures prometheus.Counter
+	garbageCollectionDuration prometheus.Histogram
+	compactions               *prometheus.CounterVec
+	compactionRunsStarted     *prometheus.CounterVec
+	compactionRunsCompleted   *prometheus.CounterVec
+	compactionFailures        *prometheus.CounterVec
+	verticalCompactions       *prometheus.CounterVec
+}
+
+func newTestSyncerMetrics(reg prometheus.Registerer) *testSyncerMetrics {
+	var m testSyncerMetrics
+
+	m.syncMetas = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "thanos_compact_sync_meta_total",
+		Help: "Total number of sync meta operations.",
+	})
+	m.syncMetaFailures = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "thanos_compact_sync_meta_failures_total",
+		Help: "Total number of failed sync meta operations.",
+	})
+	m.syncMetaDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
+		Name:    "thanos_compact_sync_meta_duration_seconds",
+		Help:    "Time it took to sync meta files.",
+		Buckets: []float64{0.01, 0.1, 0.3, 0.6, 1, 3, 6, 9, 20, 30, 60, 90, 120, 240, 360, 720},
+	})
+
+	m.garbageCollectedBlocks = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "thanos_compact_garbage_collected_blocks_total",
+		Help: "Total number of deleted blocks by compactor.",
+	})
+	m.garbageCollections = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "thanos_compact_garbage_collection_total",
+		Help: "Total number of garbage collection operations.",
+	})
+	m.garbageCollectionFailures = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "thanos_compact_garbage_collection_failures_total",
+		Help: "Total number of failed garbage collection operations.",
+	})
+	m.garbageCollectionDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
+		Name:    "thanos_compact_garbage_collection_duration_seconds",
+		Help:    "Time it took to perform garbage collection iteration.",
+		Buckets: []float64{0.01, 0.1, 0.3, 0.6, 1, 3, 6, 9, 20, 30, 60, 90, 120, 240, 360, 720},
+	})
+
+	m.compactions = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "thanos_compact_group_compactions_total",
+		Help: "Total number of group compaction attempts that resulted in a new block.",
+	}, []string{"group"})
+	m.compactionRunsStarted = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "thanos_compact_group_compaction_runs_started_total",
+		Help: "Total number of group compaction attempts.",
+	}, []string{"group"})
+	m.compactionRunsCompleted = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "thanos_compact_group_compaction_runs_completed_total",
+		Help: "Total number of group completed compaction runs. This also includes compactor group runs that resulted with no compaction.",
+	}, []string{"group"})
+	m.compactionFailures = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "thanos_compact_group_compactions_failures_total",
+		Help: "Total number of failed group compactions.",
+	}, []string{"group"})
+	m.verticalCompactions = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "thanos_compact_group_vertical_compactions_total",
+		Help: "Total number of group compaction attempts that resulted in a new block based on overlapping blocks.",
+	}, []string{"group"})
+
+	if reg != nil {
+		reg.MustRegister(
+			m.syncMetas,
+			m.syncMetaFailures,
+			m.syncMetaDuration,
+			m.garbageCollectedBlocks,
+			m.garbageCollections,
+			m.garbageCollectionFailures,
+			m.garbageCollectionDuration,
+			m.compactions,
+			m.compactionRunsStarted,
+			m.compactionRunsCompleted,
+			m.compactionFailures,
+			m.verticalCompactions,
+		)
+	}
+	return &m
+}