github.com/thanos-io/thanos@v0.32.5/pkg/block/metadata/meta.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package metadata
     5  
     6  // metadata package implements writing and reading wrapped meta.json where Thanos puts its metadata.
     7  // Those metadata contains external labels, downsampling resolution and source type.
     8  // This package is minimal and separated because it used by testutils which limits test helpers we can use in
     9  // this package.
    10  
    11  import (
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"os"
    16  	"path/filepath"
    17  
    18  	"github.com/go-kit/log"
    19  	"github.com/oklog/ulid"
    20  	"github.com/pkg/errors"
    21  	"github.com/prometheus/prometheus/model/labels"
    22  	"github.com/prometheus/prometheus/model/relabel"
    23  	"github.com/prometheus/prometheus/promql/parser"
    24  	"github.com/prometheus/prometheus/tsdb"
    25  	"github.com/prometheus/prometheus/tsdb/fileutil"
    26  	"github.com/prometheus/prometheus/tsdb/tombstones"
    27  	"gopkg.in/yaml.v3"
    28  
    29  	"github.com/thanos-io/thanos/pkg/runutil"
    30  )
    31  
    32  type SourceType string
    33  
    34  const (
    35  	// TODO(bwplotka): Merge with pkg/component package.
    36  	UnknownSource         SourceType = ""
    37  	SidecarSource         SourceType = "sidecar"
    38  	ReceiveSource         SourceType = "receive"
    39  	CompactorSource       SourceType = "compactor"
    40  	CompactorRepairSource SourceType = "compactor.repair"
    41  	RulerSource           SourceType = "ruler"
    42  	BucketRepairSource    SourceType = "bucket.repair"
    43  	BucketRewriteSource   SourceType = "bucket.rewrite"
    44  	TestSource            SourceType = "test"
    45  )
    46  
    47  const (
    48  	// MetaFilename is the known JSON filename for meta information.
    49  	MetaFilename = "meta.json"
    50  	// TSDBVersion1 is a enumeration of TSDB meta versions supported by Thanos.
    51  	TSDBVersion1 = 1
    52  	// ThanosVersion1 is a enumeration of Thanos section of TSDB meta supported by Thanos.
    53  	ThanosVersion1 = 1
    54  )
    55  
    56  // Meta describes the a block's meta. It wraps the known TSDB meta structure and
    57  // extends it by Thanos-specific fields.
    58  type Meta struct {
    59  	tsdb.BlockMeta
    60  
    61  	Thanos Thanos `json:"thanos"`
    62  }
    63  
    64  func (m *Meta) String() string {
    65  	return fmt.Sprintf("%s (min time: %d, max time: %d)", m.ULID, m.MinTime, m.MaxTime)
    66  }
    67  
    68  // Thanos holds block meta information specific to Thanos.
    69  type Thanos struct {
    70  	// Version of Thanos meta file. If none specified, 1 is assumed (since first version did not have explicit version specified).
    71  	Version int `json:"version,omitempty"`
    72  
    73  	// Labels are the external labels identifying the producer as well as tenant.
    74  	// See https://thanos.io/tip/thanos/storage.md#external-labels for details.
    75  	Labels     map[string]string `json:"labels"`
    76  	Downsample ThanosDownsample  `json:"downsample"`
    77  
    78  	// Source is a real upload source of the block.
    79  	Source SourceType `json:"source"`
    80  
    81  	// List of segment files (in chunks directory), in sorted order. Optional.
    82  	// Deprecated. Use Files instead.
    83  	SegmentFiles []string `json:"segment_files,omitempty"`
    84  
    85  	// File is a sorted (by rel path) list of all files in block directory of this block known to TSDB.
    86  	// Sorted by relative path.
    87  	// Useful to avoid API call to get size of each file, as well as for debugging purposes.
    88  	// Optional, added in v0.17.0.
    89  	Files []File `json:"files,omitempty"`
    90  
    91  	// Rewrites is present when any rewrite (deletion, relabel etc) were applied to this block. Optional.
    92  	Rewrites []Rewrite `json:"rewrites,omitempty"`
    93  
    94  	// IndexStats contains stats info related to block index.
    95  	IndexStats IndexStats `json:"index_stats,omitempty"`
    96  
    97  	// Extensions are used for plugin any arbitrary additional information for block. Optional.
    98  	Extensions any `json:"extensions,omitempty"`
    99  }
   100  
   101  type IndexStats struct {
   102  	SeriesMaxSize int64 `json:"series_max_size,omitempty"`
   103  	ChunkMaxSize  int64 `json:"chunk_max_size,omitempty"`
   104  }
   105  
   106  func (m *Thanos) ParseExtensions(v any) (any, error) {
   107  	return ConvertExtensions(m.Extensions, v)
   108  }
   109  
   110  // ConvertExtensions converts extensions with `any` type into specific type `v`
   111  // that the caller expects.
   112  func ConvertExtensions(extensions any, v any) (any, error) {
   113  	if extensions == nil {
   114  		return nil, nil
   115  	}
   116  	extensionsContent, err := json.Marshal(extensions)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	if err = json.Unmarshal(extensionsContent, v); err != nil {
   121  		return nil, err
   122  	}
   123  	return v, nil
   124  }
   125  
   126  type Rewrite struct {
   127  	// ULIDs of all source head blocks that went into the block.
   128  	Sources []ulid.ULID `json:"sources,omitempty"`
   129  	// Deletions if applied (in order).
   130  	DeletionsApplied []DeletionRequest `json:"deletions_applied,omitempty"`
   131  	// Relabels if applied.
   132  	RelabelsApplied []*relabel.Config `json:"relabels_applied,omitempty"`
   133  }
   134  
   135  type Matchers []*labels.Matcher
   136  
   137  func (m *Matchers) UnmarshalYAML(value *yaml.Node) (err error) {
   138  	*m, err = parser.ParseMetricSelector(value.Value)
   139  	if err != nil {
   140  		return errors.Wrapf(err, "parse metric selector %v", value.Value)
   141  	}
   142  	return nil
   143  }
   144  
   145  type DeletionRequest struct {
   146  	Matchers  Matchers             `json:"matchers" yaml:"matchers"`
   147  	Intervals tombstones.Intervals `json:"intervals,omitempty" yaml:"intervals,omitempty"`
   148  	RequestID string               `json:"request_id,omitempty" yaml:"request_id,omitempty"`
   149  }
   150  
   151  type File struct {
   152  	RelPath string `json:"rel_path"`
   153  	// SizeBytes is optional (e.g meta.json does not show size).
   154  	SizeBytes int64 `json:"size_bytes,omitempty"`
   155  
   156  	// Hash is an optional hash of this file. Used for potentially avoiding an extra download.
   157  	Hash *ObjectHash `json:"hash,omitempty"`
   158  }
   159  
   160  type ThanosDownsample struct {
   161  	Resolution int64 `json:"resolution"`
   162  }
   163  
   164  // InjectThanos sets Thanos meta to the block meta JSON and saves it to the disk.
   165  // NOTE: It should be used after writing any block by any Thanos component, otherwise we will miss crucial metadata.
   166  func InjectThanos(logger log.Logger, bdir string, meta Thanos, downsampledMeta *tsdb.BlockMeta) (*Meta, error) {
   167  	newMeta, err := ReadFromDir(bdir)
   168  	if err != nil {
   169  		return nil, errors.Wrap(err, "read new meta")
   170  	}
   171  	newMeta.Thanos = meta
   172  
   173  	// While downsampling we need to copy original compaction.
   174  	if downsampledMeta != nil {
   175  		newMeta.Compaction = downsampledMeta.Compaction
   176  	}
   177  
   178  	if err := newMeta.WriteToDir(logger, bdir); err != nil {
   179  		return nil, errors.Wrap(err, "write new meta")
   180  	}
   181  
   182  	return newMeta, nil
   183  }
   184  
   185  // GroupKey returns a unique identifier for the compaction group the block belongs to.
   186  // It considers the downsampling resolution and the block's labels.
   187  func (m *Thanos) GroupKey() string {
   188  	return fmt.Sprintf("%d@%v", m.Downsample.Resolution, labels.FromMap(m.Labels).Hash())
   189  }
   190  
   191  // ResolutionString returns a the block's resolution as a string.
   192  func (m *Thanos) ResolutionString() string {
   193  	return fmt.Sprintf("%d", m.Downsample.Resolution)
   194  }
   195  
   196  // WriteToDir writes the encoded meta into <dir>/meta.json.
   197  func (m Meta) WriteToDir(logger log.Logger, dir string) error {
   198  	// Make any changes to the file appear atomic.
   199  	path := filepath.Join(dir, MetaFilename)
   200  	tmp := path + ".tmp"
   201  
   202  	f, err := os.Create(tmp)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if err := m.Write(f); err != nil {
   208  		runutil.CloseWithLogOnErr(logger, f, "close meta")
   209  		return err
   210  	}
   211  	if err := f.Close(); err != nil {
   212  		return err
   213  	}
   214  	return renameFile(logger, tmp, path)
   215  }
   216  
   217  // Write writes the given encoded meta to writer.
   218  func (m Meta) Write(w io.Writer) error {
   219  	enc := json.NewEncoder(w)
   220  	enc.SetIndent("", "\t")
   221  	return enc.Encode(&m)
   222  }
   223  
   224  func renameFile(logger log.Logger, from, to string) error {
   225  	if err := os.RemoveAll(to); err != nil {
   226  		return err
   227  	}
   228  	if err := os.Rename(from, to); err != nil {
   229  		return err
   230  	}
   231  
   232  	// Directory was renamed; sync parent dir to persist rename.
   233  	pdir, err := fileutil.OpenDir(filepath.Dir(to))
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	if err = fileutil.Fdatasync(pdir); err != nil {
   239  		runutil.CloseWithLogOnErr(logger, pdir, "close dir")
   240  		return err
   241  	}
   242  	return pdir.Close()
   243  }
   244  
   245  // ReadFromDir reads the given meta from <dir>/meta.json.
   246  func ReadFromDir(dir string) (*Meta, error) {
   247  	f, err := os.Open(filepath.Join(dir, filepath.Clean(MetaFilename)))
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	return Read(f)
   252  }
   253  
   254  // Read the block meta from the given reader.
   255  func Read(rc io.ReadCloser) (_ *Meta, err error) {
   256  	defer runutil.ExhaustCloseWithErrCapture(&err, rc, "close meta JSON")
   257  
   258  	var m Meta
   259  	if err = json.NewDecoder(rc).Decode(&m); err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	if m.Version != TSDBVersion1 {
   264  		return nil, errors.Errorf("unexpected meta file version %d", m.Version)
   265  	}
   266  
   267  	version := m.Thanos.Version
   268  	if version == 0 {
   269  		// For compatibility.
   270  		version = ThanosVersion1
   271  	}
   272  
   273  	if version != ThanosVersion1 {
   274  		return nil, errors.Errorf("unexpected meta file Thanos section version %d", m.Version)
   275  	}
   276  
   277  	if m.Thanos.Labels == nil {
   278  		// To avoid extra nil checks, allocate map here if empty.
   279  		m.Thanos.Labels = make(map[string]string)
   280  	}
   281  	return &m, nil
   282  }