github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/integration/cluster/cluster.go (about)

     1  package cluster
     2  
     3  import (
     4  	"errors"
     5  	"flag"
     6  	"fmt"
     7  	"net"
     8  	"net/http/httptest"
     9  	"net/url"
    10  	"os"
    11  	"sync"
    12  	"text/template"
    13  	"time"
    14  
    15  	"github.com/grafana/dskit/multierror"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  
    18  	"github.com/grafana/loki/pkg/loki"
    19  	"github.com/grafana/loki/pkg/util/cfg"
    20  )
    21  
    22  var (
    23  	wrapRegistryOnce sync.Once
    24  
    25  	configTemplate = template.Must(template.New("").Parse(`
    26  auth_enabled: true
    27  
    28  server:
    29    http_listen_port: {{.httpPort}}
    30    grpc_listen_port: {{.grpcPort}}
    31  
    32  common:
    33    path_prefix: {{.dataPath}}
    34    storage:
    35      filesystem:
    36        chunks_directory: {{.sharedDataPath}}/chunks
    37        rules_directory: {{.sharedDataPath}}/rules
    38    replication_factor: 1
    39    ring:
    40      instance_addr: 127.0.0.1
    41      kvstore:
    42        store: inmemory
    43  
    44  storage_config:
    45    boltdb_shipper:
    46      shared_store: filesystem
    47      active_index_directory: {{.dataPath}}/index
    48      cache_location: {{.dataPath}}/boltdb-cache
    49  
    50  schema_config:
    51    configs:
    52      - from: 2020-10-24
    53        store: boltdb-shipper
    54        object_store: filesystem
    55        schema: v11
    56        index:
    57          prefix: index_
    58          period: 24h
    59  
    60  compactor:
    61    working_directory: {{.dataPath}}/retention
    62    shared_store: filesystem
    63    retention_enabled: true
    64  
    65  analytics:
    66    reporting_enabled: false
    67  
    68  ingester:
    69    lifecycler:
    70      min_ready_duration: 0s
    71  
    72  frontend_worker:
    73    scheduler_address: localhost:{{.schedulerPort}}
    74  
    75  frontend:
    76    scheduler_address: localhost:{{.schedulerPort}}
    77  `))
    78  )
    79  
    80  func wrapRegistry() {
    81  	wrapRegistryOnce.Do(func() {
    82  		prometheus.DefaultRegisterer = &wrappedRegisterer{Registerer: prometheus.DefaultRegisterer}
    83  	})
    84  }
    85  
    86  type wrappedRegisterer struct {
    87  	prometheus.Registerer
    88  }
    89  
    90  func (w *wrappedRegisterer) Register(collector prometheus.Collector) error {
    91  	if err := w.Registerer.Register(collector); err != nil {
    92  		var aErr prometheus.AlreadyRegisteredError
    93  		if errors.As(err, &aErr) {
    94  			return nil
    95  		}
    96  		return err
    97  	}
    98  	return nil
    99  }
   100  
   101  func (w *wrappedRegisterer) MustRegister(collectors ...prometheus.Collector) {
   102  	for _, c := range collectors {
   103  		if err := w.Register(c); err != nil {
   104  			panic(err.Error())
   105  		}
   106  	}
   107  }
   108  
   109  type Cluster struct {
   110  	sharedPath string
   111  	components []*Component
   112  	waitGroup  sync.WaitGroup
   113  }
   114  
   115  func New() *Cluster {
   116  	wrapRegistry()
   117  	sharedPath, err := os.MkdirTemp("", "loki-shared-data")
   118  	if err != nil {
   119  		panic(err.Error())
   120  	}
   121  
   122  	return &Cluster{
   123  		sharedPath: sharedPath,
   124  	}
   125  }
   126  
   127  func (c *Cluster) Run() error {
   128  	for _, component := range c.components {
   129  		if err := component.run(); err != nil {
   130  			return err
   131  		}
   132  	}
   133  	return nil
   134  }
   135  func (c *Cluster) Cleanup() error {
   136  	var (
   137  		files []string
   138  		dirs  []string
   139  	)
   140  	if c.sharedPath != "" {
   141  		dirs = append(dirs, c.sharedPath)
   142  	}
   143  
   144  	// call all components cleanup
   145  	errs := multierror.New()
   146  	for _, component := range c.components {
   147  		f, d := component.cleanup()
   148  		files = append(files, f...)
   149  		dirs = append(dirs, d...)
   150  	}
   151  	if err := errs.Err(); err != nil {
   152  		return err
   153  	}
   154  
   155  	// wait for all process to close
   156  	c.waitGroup.Wait()
   157  
   158  	// cleanup dirs/files
   159  	for _, d := range dirs {
   160  		errs.Add(os.RemoveAll(d))
   161  	}
   162  	for _, f := range files {
   163  		errs.Add(os.Remove(f))
   164  	}
   165  
   166  	return errs.Err()
   167  }
   168  
   169  func (c *Cluster) AddComponent(name string, flags ...string) *Component {
   170  	component := &Component{
   171  		name:    name,
   172  		cluster: c,
   173  		flags:   flags,
   174  	}
   175  
   176  	var err error
   177  	component.httpPort, err = getFreePort()
   178  	if err != nil {
   179  		panic(fmt.Errorf("error allocating HTTP port: %w", err))
   180  	}
   181  
   182  	component.grpcPort, err = getFreePort()
   183  	if err != nil {
   184  		panic(fmt.Errorf("error allocating GRPC port: %w", err))
   185  	}
   186  
   187  	c.components = append(c.components, component)
   188  	return component
   189  }
   190  
   191  type Component struct {
   192  	loki    *loki.Loki
   193  	name    string
   194  	cluster *Cluster
   195  	flags   []string
   196  
   197  	httpPort int
   198  	grpcPort int
   199  
   200  	configFile string
   201  	dataPath   string
   202  }
   203  
   204  func (c *Component) HTTPURL() *url.URL {
   205  	return &url.URL{
   206  		Host:   fmt.Sprintf("localhost:%d", c.httpPort),
   207  		Scheme: "http",
   208  	}
   209  }
   210  
   211  func (c *Component) GRPCURL() *url.URL {
   212  	return &url.URL{
   213  		Host:   fmt.Sprintf("localhost:%d", c.grpcPort),
   214  		Scheme: "grpc",
   215  	}
   216  }
   217  
   218  func (c *Component) writeConfig() error {
   219  	var err error
   220  
   221  	configFile, err := os.CreateTemp("", "loki-config")
   222  	if err != nil {
   223  		return fmt.Errorf("error creating config file: %w", err)
   224  	}
   225  
   226  	c.dataPath, err = os.MkdirTemp("", "loki-data")
   227  	if err != nil {
   228  		return fmt.Errorf("error creating data path: %w", err)
   229  	}
   230  
   231  	if err := configTemplate.Execute(configFile, map[string]interface{}{
   232  		"dataPath":       c.dataPath,
   233  		"sharedDataPath": c.cluster.sharedPath,
   234  		"grpcPort":       c.grpcPort,
   235  		"httpPort":       c.httpPort,
   236  		"schedulerPort":  c.grpcPort,
   237  	}); err != nil {
   238  		return fmt.Errorf("error writing config file: %w", err)
   239  	}
   240  
   241  	if err := configFile.Close(); err != nil {
   242  		return fmt.Errorf("error closing config file: %w", err)
   243  	}
   244  	c.configFile = configFile.Name()
   245  
   246  	return nil
   247  }
   248  
   249  func (c *Component) run() error {
   250  	if err := c.writeConfig(); err != nil {
   251  		return err
   252  	}
   253  
   254  	var config loki.ConfigWrapper
   255  
   256  	var flagset = flag.NewFlagSet("test-flags", flag.ExitOnError)
   257  
   258  	if err := cfg.DynamicUnmarshal(&config, append(
   259  		c.flags,
   260  		"-config.file",
   261  		c.configFile,
   262  	), flagset); err != nil {
   263  		return err
   264  	}
   265  
   266  	if err := config.Validate(); err != nil {
   267  		return err
   268  	}
   269  
   270  	var err error
   271  	c.loki, err = loki.New(config.Config)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	var (
   277  		readyCh = make(chan struct{})
   278  		errCh   = make(chan error, 1)
   279  	)
   280  
   281  	go func() {
   282  		for {
   283  			time.Sleep(time.Millisecond * 200)
   284  			if c.loki.Server.HTTP == nil {
   285  				continue
   286  			}
   287  
   288  			req := httptest.NewRequest("GET", "http://localhost/ready", nil)
   289  			w := httptest.NewRecorder()
   290  			c.loki.Server.HTTP.ServeHTTP(w, req)
   291  
   292  			if w.Code == 200 {
   293  				close(readyCh)
   294  				return
   295  			}
   296  		}
   297  	}()
   298  
   299  	c.cluster.waitGroup.Add(1)
   300  	go func() {
   301  		defer c.cluster.waitGroup.Done()
   302  		err := c.loki.Run(loki.RunOpts{})
   303  		if err != nil {
   304  			newErr := fmt.Errorf("error starting component %v: %w", c.name, err)
   305  			errCh <- newErr
   306  		}
   307  	}()
   308  
   309  	select {
   310  	case <-readyCh:
   311  		break
   312  	case err := <-errCh:
   313  		return err
   314  	}
   315  
   316  	return nil
   317  }
   318  
   319  // cleanup calls the stop handler and returns files and directories to be cleaned up
   320  func (c *Component) cleanup() (files []string, dirs []string) {
   321  	if c.loki != nil {
   322  		c.loki.SignalHandler.Stop()
   323  	}
   324  	if c.configFile != "" {
   325  		files = append(files, c.configFile)
   326  	}
   327  	if c.dataPath != "" {
   328  		dirs = append(dirs, c.dataPath)
   329  	}
   330  	return files, dirs
   331  }
   332  
   333  func getFreePort() (port int, err error) {
   334  	var a *net.TCPAddr
   335  	if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil {
   336  		var l *net.TCPListener
   337  		if l, err = net.ListenTCP("tcp", a); err == nil {
   338  			defer l.Close()
   339  			return l.Addr().(*net.TCPAddr).Port, nil
   340  		}
   341  	}
   342  	return
   343  }