github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/cli/server.go (about)

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"time"
     8  
     9  	"github.com/davecgh/go-spew/spew"
    10  	"github.com/prometheus/client_golang/prometheus"
    11  	"github.com/pyroscope-io/client/pyroscope"
    12  	"github.com/sirupsen/logrus"
    13  	"golang.org/x/sync/errgroup"
    14  	"gopkg.in/yaml.v2"
    15  
    16  	// revive:disable:blank-imports register discoverer
    17  	"github.com/pyroscope-io/pyroscope/pkg/baseurl"
    18  	"github.com/pyroscope-io/pyroscope/pkg/remotewrite"
    19  	_ "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/aws"
    20  	_ "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/consul"
    21  	_ "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/file"
    22  	_ "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/http"
    23  	_ "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/kubernetes"
    24  
    25  	"github.com/pyroscope-io/pyroscope/pkg/admin"
    26  	"github.com/pyroscope-io/pyroscope/pkg/analytics"
    27  	"github.com/pyroscope-io/pyroscope/pkg/config"
    28  	"github.com/pyroscope-io/pyroscope/pkg/exporter"
    29  	"github.com/pyroscope-io/pyroscope/pkg/health"
    30  	"github.com/pyroscope-io/pyroscope/pkg/ingestion"
    31  	"github.com/pyroscope-io/pyroscope/pkg/parser"
    32  	"github.com/pyroscope-io/pyroscope/pkg/scrape"
    33  	sc "github.com/pyroscope-io/pyroscope/pkg/scrape/config"
    34  	"github.com/pyroscope-io/pyroscope/pkg/scrape/discovery"
    35  	"github.com/pyroscope-io/pyroscope/pkg/selfprofiling"
    36  	"github.com/pyroscope-io/pyroscope/pkg/server"
    37  	"github.com/pyroscope-io/pyroscope/pkg/service"
    38  	"github.com/pyroscope-io/pyroscope/pkg/sqlstore"
    39  	"github.com/pyroscope-io/pyroscope/pkg/storage"
    40  	"github.com/pyroscope-io/pyroscope/pkg/util/debug"
    41  )
    42  
    43  type Server struct {
    44  	svc *serverService
    45  }
    46  
    47  type serverService struct {
    48  	config     *config.Server
    49  	logger     *logrus.Logger
    50  	controller *server.Controller
    51  	storage    *storage.Storage
    52  	// queue used to ingest data into the storage
    53  	storageQueue     *storage.IngestionQueue
    54  	analyticsService *analytics.Service
    55  	selfProfiling    *pyroscope.Session
    56  	debugReporter    *debug.Reporter
    57  	healthController *health.Controller
    58  	adminServer      *admin.Server
    59  	discoveryManager *discovery.Manager
    60  	scrapeManager    *scrape.Manager
    61  	database         *sqlstore.SQLStore
    62  	remoteWriteQueue []*remotewrite.IngestionQueue
    63  
    64  	stopped chan struct{}
    65  	done    chan struct{}
    66  	group   *errgroup.Group
    67  }
    68  
    69  func newServerService(c *config.Server) (*serverService, error) {
    70  	logLevel, err := logrus.ParseLevel(c.LogLevel)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	logger := logrus.StandardLogger()
    76  	logger.SetLevel(logLevel)
    77  
    78  	if err = loadScrapeConfigsFromFile(c); err != nil {
    79  		return nil, fmt.Errorf("could not load scrape config: %w", err)
    80  	}
    81  
    82  	svc := serverService{
    83  		config:  c,
    84  		logger:  logger,
    85  		stopped: make(chan struct{}),
    86  		done:    make(chan struct{}),
    87  	}
    88  
    89  	diskPressure := health.DiskPressure{
    90  		Threshold: c.MinFreeSpacePercentage,
    91  		Path:      c.StoragePath,
    92  	}
    93  
    94  	svc.database, err = sqlstore.Open(c)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("can't open database %q: %w", c.Database.URL, err)
    97  	}
    98  
    99  	svc.healthController = health.NewController(svc.logger, time.Minute, diskPressure)
   100  
   101  	var appMetadataSaver storage.ApplicationMetadataSaver = service.NewApplicationMetadataService(svc.database.DB())
   102  	appMetadataSaver = service.NewApplicationMetadataCacheService(service.ApplicationMetadataCacheServiceConfig{}, appMetadataSaver)
   103  
   104  	storageConfig := storage.NewConfig(svc.config)
   105  	svc.storage, err = storage.New(storageConfig, svc.logger, prometheus.DefaultRegisterer, svc.healthController, appMetadataSaver)
   106  	if err != nil {
   107  		return nil, fmt.Errorf("new storage: %w", err)
   108  	}
   109  
   110  	svc.debugReporter = debug.NewReporter(svc.logger, svc.storage, prometheus.DefaultRegisterer)
   111  
   112  	if svc.config.Auth.JWTSecret == "" {
   113  		if svc.config.Auth.JWTSecret, err = svc.storage.JWT(); err != nil {
   114  			return nil, err
   115  		}
   116  	}
   117  
   118  	appMetadataSvc := service.NewApplicationMetadataService(svc.database.DB())
   119  	migrator := NewAppMetadataMigrator(logger, svc.storage, appMetadataSvc)
   120  	err = migrator.Migrate()
   121  	if err != nil {
   122  		svc.logger.Error(err)
   123  	}
   124  	appSvc := service.NewApplicationService(appMetadataSvc, svc.storage)
   125  
   126  	// this needs to happen after storage is initiated!
   127  	if svc.config.EnableExperimentalAdmin {
   128  		socketPath := svc.config.AdminSocketPath
   129  		userService := service.NewUserService(svc.database.DB())
   130  		adminController := admin.NewController(svc.logger, appSvc, userService, svc.storage)
   131  		httpClient, err := admin.NewHTTPOverUDSClient(socketPath)
   132  		if err != nil {
   133  			return nil, fmt.Errorf("admin: %w", err)
   134  		}
   135  
   136  		adminHTTPOverUDS, err := admin.NewUdsHTTPServer(socketPath, httpClient)
   137  		if err != nil {
   138  			return nil, fmt.Errorf("admin: %w", err)
   139  		}
   140  
   141  		svc.adminServer, err = admin.NewServer(
   142  			svc.logger,
   143  			adminController,
   144  			adminHTTPOverUDS,
   145  		)
   146  		if err != nil {
   147  			return nil, fmt.Errorf("admin: %w", err)
   148  		}
   149  	}
   150  
   151  	exportedMetricsRegistry := prometheus.NewRegistry()
   152  	metricsExporter, err := exporter.NewExporter(svc.config.MetricsExportRules, exportedMetricsRegistry)
   153  	if err != nil {
   154  		return nil, fmt.Errorf("new metric exporter: %w", err)
   155  	}
   156  
   157  	svc.storageQueue = storage.NewIngestionQueue(svc.logger, svc.storage, prometheus.DefaultRegisterer, storageConfig)
   158  
   159  	defaultMetricsRegistry := prometheus.DefaultRegisterer
   160  
   161  	var ingester ingestion.Ingester
   162  	if !svc.config.RemoteWrite.Enabled || !svc.config.RemoteWrite.DisableLocalWrites {
   163  		ingester = parser.New(svc.logger, svc.storageQueue, metricsExporter)
   164  	} else {
   165  		ingester = ingestion.NewNoopIngester()
   166  	}
   167  
   168  	// If remote write is available, let's write to both local storage and to the remote server
   169  	if svc.config.RemoteWrite.Enabled {
   170  		err = loadRemoteWriteTargetConfigsFromFile(svc.config)
   171  		if err != nil {
   172  			return nil, err
   173  		}
   174  
   175  		if len(svc.config.RemoteWrite.Targets) <= 0 {
   176  			return nil, fmt.Errorf("remote write is enabled but no targets are set up")
   177  		}
   178  
   179  		remoteClients := make([]ingestion.Ingester, len(svc.config.RemoteWrite.Targets))
   180  		svc.remoteWriteQueue = make([]*remotewrite.IngestionQueue, len(svc.config.RemoteWrite.Targets))
   181  
   182  		i := 0
   183  		for targetName, t := range svc.config.RemoteWrite.Targets {
   184  			targetLogger := logger.WithField("remote_target", targetName)
   185  			targetLogger.Debug("Initializing remote write target")
   186  
   187  			remoteClient := remotewrite.NewClient(targetLogger, defaultMetricsRegistry, targetName, t)
   188  			q := remotewrite.NewIngestionQueue(targetLogger, defaultMetricsRegistry, remoteClient, targetName, t)
   189  
   190  			remoteClients[i] = q
   191  			svc.remoteWriteQueue[i] = q
   192  			i++
   193  		}
   194  
   195  		ingesters := append([]ingestion.Ingester{ingester}, remoteClients...)
   196  		ingester = ingestion.NewParallelizer(svc.logger, ingesters...)
   197  	}
   198  	if !svc.config.NoSelfProfiling {
   199  		svc.selfProfiling = selfprofiling.NewSession(svc.logger, ingester, "pyroscope.server", svc.config.SelfProfilingTags)
   200  	}
   201  
   202  	svc.scrapeManager = scrape.NewManager(
   203  		svc.logger.WithField("component", "scrape-manager"),
   204  		ingester,
   205  		defaultMetricsRegistry)
   206  
   207  	svc.controller, err = server.New(server.Config{
   208  		Configuration:           svc.config,
   209  		Storage:                 svc.storage,
   210  		Ingester:                ingester,
   211  		Notifier:                svc.healthController,
   212  		Logger:                  svc.logger,
   213  		MetricsRegisterer:       defaultMetricsRegistry,
   214  		ExportedMetricsRegistry: exportedMetricsRegistry,
   215  		ScrapeManager:           svc.scrapeManager,
   216  		DB:                      svc.database.DB(),
   217  	})
   218  	if err != nil {
   219  		return nil, fmt.Errorf("new server: %w", err)
   220  	}
   221  
   222  	svc.discoveryManager = discovery.NewManager(
   223  		svc.logger.WithField("component", "discovery-manager"))
   224  
   225  	if !c.AnalyticsOptOut {
   226  		svc.analyticsService = analytics.NewService(c, svc.storage, svc.controller)
   227  	}
   228  
   229  	if os.Getenv("PYROSCOPE_CONFIG_DEBUG") != "" {
   230  		fmt.Println("parsed config:")
   231  		spew.Dump(svc.config)
   232  	}
   233  
   234  	return &svc, nil
   235  }
   236  
   237  func (svc *serverService) Start() error {
   238  	g, ctx := errgroup.WithContext(context.Background())
   239  	svc.group = g
   240  	g.Go(func() error {
   241  		// if you ever change this line, make sure to update this homebrew test:
   242  		// https://github.com/pyroscope-io/homebrew-brew/blob/main/Formula/pyroscope.rb#L94
   243  		svc.logger.Info("starting HTTP server")
   244  		return svc.controller.Start()
   245  	})
   246  	if svc.config.EnableExperimentalAdmin {
   247  		g.Go(func() error {
   248  			svc.logger.Info("starting admin server")
   249  			return svc.adminServer.Start()
   250  		})
   251  	}
   252  
   253  	if svc.config.BaseURLBindAddr != "" {
   254  		go baseurl.Start(svc.config)
   255  	}
   256  	go svc.debugReporter.Start()
   257  	if svc.analyticsService != nil {
   258  		go svc.analyticsService.Start()
   259  	}
   260  
   261  	svc.healthController.Start()
   262  	if !svc.config.NoSelfProfiling {
   263  		if err := svc.selfProfiling.Start(); err != nil {
   264  			svc.logger.WithError(err).Error("failed to start self-profiling")
   265  		}
   266  	}
   267  
   268  	// Scrape and Discovery managers have to be initialized
   269  	// with ApplyConfig before starting running.
   270  	if err := svc.applyScrapeConfigs(svc.config); err != nil {
   271  		return err
   272  	}
   273  	g.Go(func() error {
   274  		svc.logger.Debug("starting discovery manager")
   275  		return svc.discoveryManager.Run()
   276  	})
   277  	g.Go(func() error {
   278  		svc.logger.Debug("starting scrape manager")
   279  		return svc.scrapeManager.Run(svc.discoveryManager.SyncCh())
   280  	})
   281  
   282  	defer close(svc.done)
   283  	select {
   284  	case <-svc.stopped:
   285  	case <-ctx.Done():
   286  		// The context is canceled the first time a function passed to Go
   287  		// returns a non-nil error.
   288  	}
   289  	// N.B. internal components are de-initialized/disposed (if applicable)
   290  	// regardless of the exit reason. Once server is stopped, wait for all
   291  	// Go goroutines to finish.
   292  	svc.stop()
   293  	return svc.group.Wait()
   294  }
   295  
   296  func (svc *serverService) Stop() {
   297  	close(svc.stopped)
   298  	<-svc.done
   299  }
   300  
   301  //revive:disable-next-line:confusing-naming methods are different
   302  func (svc *serverService) stop() {
   303  	if svc.config.EnableExperimentalAdmin {
   304  		svc.logger.Debug("stopping admin server")
   305  		if err := svc.adminServer.Stop(); err != nil {
   306  			svc.logger.WithError(err).Error("admin server stop")
   307  		}
   308  	}
   309  	svc.controller.Drain()
   310  	svc.logger.Debug("stopping discovery manager")
   311  	svc.discoveryManager.Stop()
   312  	svc.logger.Debug("stopping scrape manager")
   313  	svc.scrapeManager.Stop()
   314  	svc.logger.Debug("stopping debug reporter")
   315  	svc.debugReporter.Stop()
   316  	svc.healthController.Stop()
   317  	if svc.analyticsService != nil {
   318  		svc.logger.Debug("stopping analytics service")
   319  		svc.analyticsService.Stop()
   320  	}
   321  
   322  	if !svc.config.NoSelfProfiling {
   323  		svc.logger.Debug("stopping self profiling")
   324  		svc.selfProfiling.Stop()
   325  	}
   326  
   327  	if svc.config.RemoteWrite.Enabled {
   328  		svc.logger.Debug("stopping remote queues")
   329  		for _, q := range svc.remoteWriteQueue {
   330  			q.Stop()
   331  		}
   332  	}
   333  
   334  	svc.logger.Debug("stopping ingestion queue")
   335  	svc.storageQueue.Stop()
   336  	svc.logger.Debug("stopping storage")
   337  	if err := svc.storage.Close(); err != nil {
   338  		svc.logger.WithError(err).Error("storage close")
   339  	}
   340  	svc.logger.Debug("closing database")
   341  	if err := svc.database.Close(); err != nil {
   342  		svc.logger.WithError(err).Error("database close")
   343  	}
   344  	// we stop the http server as the last thing due to:
   345  	// 1. we may still want to bserve metric values while storage is closing
   346  	// 2. we want the /healthz endpoint to still be responding while server is shutting down
   347  	// (we are thinking in a k8s context here, but maybe 'terminationGracePeriodSeconds' makes this unnecessary)
   348  	svc.logger.Debug("stopping http server")
   349  	if err := svc.controller.Stop(); err != nil {
   350  		svc.logger.WithError(err).Error("controller stop")
   351  	}
   352  }
   353  
   354  func (svc *serverService) ApplyConfig(c *config.Server) error {
   355  	return svc.applyScrapeConfigs(c)
   356  }
   357  
   358  func (svc *serverService) applyScrapeConfigs(c *config.Server) error {
   359  	if err := loadScrapeConfigsFromFile(c); err != nil {
   360  		return fmt.Errorf("could not load scrape configs from %s: %w", c.Config, err)
   361  	}
   362  	if err := svc.discoveryManager.ApplyConfig(discoveryConfigs(c.ScrapeConfigs)); err != nil {
   363  		// discoveryManager.ApplyConfig never return errors.
   364  		return err
   365  	}
   366  	return svc.scrapeManager.ApplyConfig(c.ScrapeConfigs)
   367  }
   368  
   369  func discoveryConfigs(cfg []*sc.Config) map[string]discovery.Configs {
   370  	c := make(map[string]discovery.Configs)
   371  	for _, x := range cfg {
   372  		c[x.JobName] = x.ServiceDiscoveryConfigs
   373  	}
   374  	return c
   375  }
   376  
   377  func loadScrapeConfigsFromFile(c *config.Server) error {
   378  	b, err := os.ReadFile(c.Config)
   379  	switch {
   380  	case err == nil:
   381  	case os.IsNotExist(err):
   382  		return nil
   383  	default:
   384  		return err
   385  	}
   386  	type scrapeConfig struct {
   387  		ScrapeConfigs []*sc.Config `yaml:"scrape-configs" mapstructure:"-"`
   388  	}
   389  	var s scrapeConfig
   390  	if err = yaml.Unmarshal([]byte(performSubstitutions(b)), &s); err != nil {
   391  		return err
   392  	}
   393  	// Populate scrape configs.
   394  	c.ScrapeConfigs = s.ScrapeConfigs
   395  	return nil
   396  }
   397  
   398  func loadRemoteWriteTargetConfigsFromFile(c *config.Server) error {
   399  	b, err := os.ReadFile(c.Config)
   400  	switch {
   401  	case err == nil:
   402  	case os.IsNotExist(err):
   403  		return nil
   404  	default:
   405  		return err
   406  	}
   407  
   408  	type cfg struct {
   409  		RemoteWrite struct {
   410  			Targets map[string]config.RemoteWriteTarget `yaml:"targets" mapstructure:"-"`
   411  		} `yaml:"remote-write"`
   412  	}
   413  
   414  	var s cfg
   415  	if err = yaml.Unmarshal([]byte(performSubstitutions(b)), &s); err != nil {
   416  		return err
   417  	}
   418  
   419  	c.RemoteWrite.Targets = s.RemoteWrite.Targets
   420  
   421  	return nil
   422  }