mirror of https://github.com/grafana/grafana
feat: add ability to launch targeted dskit modules in the grafana server CLI command (#74188)
* feat: add ability to launch targeted dskit modules in the grafana server CLI command This commit adds a ModuleServer and ModuleRunner suitable for launching dskit services and updates the server cli command to use this instead of the full Server. The default behavior is unchanged and will launch the full Grafana server. Individual services are targeted by setting target=comma,seperated,list in the config file. * require dev mode to target dskit modules * remove unused type * replace setting.CommandLineArgs w/setting.Cfg; the caller can deal with calling setting.NewCfg * Update pkg/server/module_server.go Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com> --------- Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com>pull/74274/head
parent
94dd17a936
commit
0de2c9eb96
@ -0,0 +1,20 @@ |
||||
// Server defines the main entrypoints to Grafana and the Grafana CLI, as well
|
||||
// as test environments. OSS and Enterprise-specific build tags are used in this
|
||||
// package to control wire dependencies for each build.
|
||||
package server |
||||
|
||||
// Notes about wiresets:
|
||||
//
|
||||
// wire.go contains wire sets used by both OSS and Enterprise builds. These are
|
||||
// generally base wiresets imported by the OSS- or Enterprise-specific sets.
|
||||
//
|
||||
// wireexts_oss.go contains the "extensions" wiresets, used only by OSS builds.
|
||||
// wireexts_enterprise.go contains wiresets used only by Enterprise builds. This
|
||||
// file is located in the grafana-enterprise repo.
|
||||
//
|
||||
// NOTE WELL: The extensions sets can import wiresets from wire.go, but sets in
|
||||
// wire.go cannot include a build-specific wireset. The extension set must be built in wire.go.
|
||||
//
|
||||
// We use go build tags during build to configure which wiresets are used in a
|
||||
// given build. We do not commit generated wire sets (wire_gen.go) into the
|
||||
// repo.
|
||||
@ -0,0 +1,25 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
// ModuleRunner is a simplified version of Runner that is used in the grafana
|
||||
// server target command. It has a minimal set of dependencies required to
|
||||
// launch background/dskit services.
|
||||
type ModuleRunner struct { |
||||
Cfg *setting.Cfg |
||||
SettingsProvider setting.Provider |
||||
Features featuremgmt.FeatureToggles |
||||
} |
||||
|
||||
func NewModuleRunner(cfg *setting.Cfg, settingsProvider setting.Provider, |
||||
features featuremgmt.FeatureToggles, |
||||
) ModuleRunner { |
||||
return ModuleRunner{ |
||||
Cfg: cfg, |
||||
SettingsProvider: settingsProvider, |
||||
Features: features, |
||||
} |
||||
} |
||||
@ -0,0 +1,205 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net" |
||||
"os" |
||||
"path" |
||||
"path/filepath" |
||||
"strconv" |
||||
"sync" |
||||
|
||||
"github.com/grafana/dskit/services" |
||||
|
||||
"github.com/grafana/grafana/pkg/api" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/modules" |
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
// NewModule returns an instance of a ModuleServer, responsible for managing
|
||||
// dskit modules (services).
|
||||
func NewModule(opts Options, apiOpts api.ServerOptions, cfg *setting.Cfg) (*ModuleServer, error) { |
||||
s, err := newModuleServer(opts, apiOpts, cfg) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err := s.init(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
func newModuleServer(opts Options, apiOpts api.ServerOptions, cfg *setting.Cfg) (*ModuleServer, error) { |
||||
rootCtx, shutdownFn := context.WithCancel(context.Background()) |
||||
|
||||
s := &ModuleServer{ |
||||
opts: opts, |
||||
apiOpts: apiOpts, |
||||
context: rootCtx, |
||||
shutdownFn: shutdownFn, |
||||
shutdownFinished: make(chan struct{}), |
||||
log: log.New("base-server"), |
||||
cfg: cfg, |
||||
pidFile: opts.PidFile, |
||||
version: opts.Version, |
||||
commit: opts.Commit, |
||||
buildBranch: opts.BuildBranch, |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// ModuleServer is responsible for managing the lifecycle of dskit services. The
|
||||
// ModuleServer has the minimal set of dependencies to launch dskit services,
|
||||
// but it can be used to launch the entire Grafana server.
|
||||
type ModuleServer struct { |
||||
opts Options |
||||
apiOpts api.ServerOptions |
||||
|
||||
context context.Context |
||||
shutdownFn context.CancelFunc |
||||
log log.Logger |
||||
cfg *setting.Cfg |
||||
shutdownOnce sync.Once |
||||
shutdownFinished chan struct{} |
||||
isInitialized bool |
||||
mtx sync.Mutex |
||||
|
||||
pidFile string |
||||
version string |
||||
commit string |
||||
buildBranch string |
||||
} |
||||
|
||||
// init initializes the server and its services.
|
||||
func (s *ModuleServer) init() error { |
||||
s.mtx.Lock() |
||||
defer s.mtx.Unlock() |
||||
|
||||
if s.isInitialized { |
||||
return nil |
||||
} |
||||
s.isInitialized = true |
||||
|
||||
if err := s.writePIDFile(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Run initializes and starts services. This will block until all services have
|
||||
// exited. To initiate shutdown, call the Shutdown method in another goroutine.
|
||||
func (s *ModuleServer) Run() error { |
||||
defer close(s.shutdownFinished) |
||||
|
||||
if err := s.init(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.notifySystemd("READY=1") |
||||
s.log.Debug("Waiting on services...") |
||||
|
||||
// Only allow individual dskit modules to run in dev mode.
|
||||
if s.cfg.Env != "dev" { |
||||
if len(s.cfg.Target) > 1 || s.cfg.Target[0] != "all" { |
||||
s.log.Error("dskit module targeting is only supported in dev mode. Falling back to 'all'") |
||||
s.cfg.Target = []string{"all"} |
||||
} |
||||
} |
||||
|
||||
m := modules.New(s.cfg.Target) |
||||
|
||||
m.RegisterModule(modules.Core, func() (services.Service, error) { |
||||
return NewService(s.cfg, s.opts, s.apiOpts) |
||||
}) |
||||
|
||||
m.RegisterModule(modules.GrafanaAPIServer, func() (services.Service, error) { |
||||
return grafanaapiserver.New(path.Join(s.cfg.DataPath, "k8s")) |
||||
}) |
||||
|
||||
m.RegisterModule(modules.All, nil) |
||||
|
||||
return m.Run(s.context) |
||||
} |
||||
|
||||
// Shutdown initiates Grafana graceful shutdown. This shuts down all
|
||||
// running background services. Since Run blocks Shutdown supposed to
|
||||
// be run from a separate goroutine.
|
||||
func (s *ModuleServer) Shutdown(ctx context.Context, reason string) error { |
||||
var err error |
||||
s.shutdownOnce.Do(func() { |
||||
s.log.Info("Shutdown started", "reason", reason) |
||||
// Call cancel func to stop background services.
|
||||
s.shutdownFn() |
||||
// Wait for server to shut down
|
||||
select { |
||||
case <-s.shutdownFinished: |
||||
s.log.Debug("Finished waiting for server to shut down") |
||||
case <-ctx.Done(): |
||||
s.log.Warn("Timed out while waiting for server to shut down") |
||||
err = fmt.Errorf("timeout waiting for shutdown") |
||||
} |
||||
}) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// writePIDFile retrieves the current process ID and writes it to file.
|
||||
func (s *ModuleServer) writePIDFile() error { |
||||
if s.pidFile == "" { |
||||
return nil |
||||
} |
||||
|
||||
// Ensure the required directory structure exists.
|
||||
err := os.MkdirAll(filepath.Dir(s.pidFile), 0700) |
||||
if err != nil { |
||||
s.log.Error("Failed to verify pid directory", "error", err) |
||||
return fmt.Errorf("failed to verify pid directory: %s", err) |
||||
} |
||||
|
||||
// Retrieve the PID and write it to file.
|
||||
pid := strconv.Itoa(os.Getpid()) |
||||
if err := os.WriteFile(s.pidFile, []byte(pid), 0644); err != nil { |
||||
s.log.Error("Failed to write pidfile", "error", err) |
||||
return fmt.Errorf("failed to write pidfile: %s", err) |
||||
} |
||||
|
||||
s.log.Info("Writing PID file", "path", s.pidFile, "pid", pid) |
||||
return nil |
||||
} |
||||
|
||||
// notifySystemd sends state notifications to systemd.
|
||||
func (s *ModuleServer) notifySystemd(state string) { |
||||
notifySocket := os.Getenv("NOTIFY_SOCKET") |
||||
if notifySocket == "" { |
||||
s.log.Debug( |
||||
"NOTIFY_SOCKET environment variable empty or unset, can't send systemd notification") |
||||
return |
||||
} |
||||
|
||||
socketAddr := &net.UnixAddr{ |
||||
Name: notifySocket, |
||||
Net: "unixgram", |
||||
} |
||||
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) |
||||
if err != nil { |
||||
s.log.Warn("Failed to connect to systemd", "err", err, "socket", notifySocket) |
||||
return |
||||
} |
||||
defer func() { |
||||
if err := conn.Close(); err != nil { |
||||
s.log.Warn("Failed to close connection", "err", err) |
||||
} |
||||
}() |
||||
|
||||
_, err = conn.Write([]byte(state)) |
||||
if err != nil { |
||||
s.log.Warn("Failed to write notification to systemd", "err", err) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue