Skip to content

Commit

Permalink
Support reload using signal
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Pasquier <spasquie@redhat.com>
  • Loading branch information
simonpasquier committed Jun 16, 2023
1 parent d430269 commit 8dc99a1
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 15 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ require (
)

require (
github.com/mitchellh/go-ps v1.0.0
github.com/onsi/gomega v1.27.6
go.opentelemetry.io/contrib/propagators/autoprop v0.38.0
go4.org/intern v0.0.0-20220617035311-6925f38cc365
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,8 @@ github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
Expand Down
116 changes: 101 additions & 15 deletions pkg/reloader/reloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import (
"bytes"
"compress/gzip"
"context"
"fmt"
"hash"
"io"
"net/http"
Expand All @@ -66,12 +67,14 @@ import (
"regexp"
"strings"
"sync"
"syscall"
"time"

"github.com/fsnotify/fsnotify"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/minio/sha256-simd"
ps "github.com/mitchellh/go-ps"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
Expand All @@ -84,15 +87,15 @@ import (
// Referenced environment variables must be of the form `$(var)` (not `$var` or `${var}`).
type Reloader struct {
logger log.Logger
reloadURL *url.URL
httpClient http.Client
cfgFile string
cfgOutputFile string
watchInterval time.Duration
retryInterval time.Duration
watchedDirs []string
watcher *watcher

tr TriggerReloader

lastCfgHash []byte
lastWatchedDirsHash []byte
forceReload bool
Expand All @@ -105,10 +108,21 @@ type Reloader struct {
configApply prometheus.Counter
}

// TriggerReloader reloads the configuration of the process.
type TriggerReloader interface {
TriggerReload(ctx context.Context) error
}

// Options bundles options for the Reloader.
type Options struct {
// ReloadURL is a prometheus URL to trigger reloads.
// ReloadURL is the Prometheus URL to trigger reloads.
ReloadURL *url.URL
// HTTP client used to connect to the web server.
HTTPClient http.Client
// ProcessName is the process executable name to trigger reloads. If not
// empty, the reloader sends a SIGHUP signal to the matching process ID
// instead of using the HTTP reload endpoint.
ProcessName string
// CfgFile is a path to the prometheus config file to watch.
CfgFile string
// CfgOutputFile is a path for the output config file.
Expand All @@ -124,7 +138,7 @@ type Options struct {
// WatchInterval controls how often reloader re-reads config and directories.
WatchInterval time.Duration
// RetryInterval controls how often the reloader retries a reloading of the
// configuration in case the endpoint returned an error.
// configuration in case the reload operation returned an error.
RetryInterval time.Duration
}

Expand All @@ -138,7 +152,6 @@ func New(logger log.Logger, reg prometheus.Registerer, o *Options) *Reloader {
}
r := &Reloader{
logger: logger,
reloadURL: o.ReloadURL,
cfgFile: o.CfgFile,
cfgOutputFile: o.CfgOutputFile,
watcher: newWatcher(logger, reg, o.DelayInterval),
Expand Down Expand Up @@ -183,6 +196,13 @@ func New(logger log.Logger, reg prometheus.Registerer, o *Options) *Reloader {
},
),
}

if o.ProcessName != "" {
r.tr = NewPIDReloader(o.ProcessName)
} else {
r.tr = NewHTTPReloader(r.logger, o.ReloadURL, o.HTTPClient)
}

return r
}

Expand All @@ -201,6 +221,12 @@ func (r *Reloader) Watch(ctx context.Context) error {
return nil
}

if _, ok := r.tr.(*PIDReloader); ok {
level.Info(r.logger).Log("msg", "reloading via process signal")
} else {
level.Info(r.logger).Log("msg", "reloading via HTTP")
}

defer runutil.CloseWithLogOnErr(r.logger, r.watcher, "config watcher close")

if r.cfgFile != "" {
Expand Down Expand Up @@ -361,8 +387,9 @@ func (r *Reloader) apply(ctx context.Context) error {
if r.watchInterval == 0 {
return nil
}

r.reloads.Inc()
if err := r.triggerReload(ctx); err != nil {
if err := r.tr.TriggerReload(ctx); err != nil {
r.reloadErrors.Inc()
r.lastReloadSuccess.Set(0)
return errors.Wrap(err, "trigger reload")
Expand Down Expand Up @@ -410,28 +437,87 @@ func hashFile(h hash.Hash, fn string) error {
return nil
}

func (r *Reloader) triggerReload(ctx context.Context) error {
req, err := http.NewRequest("POST", r.reloadURL.String(), nil)
type PIDReloader struct {
pname string
}

func NewPIDReloader(pname string) *PIDReloader {
return &PIDReloader{
pname: pname,
}
}

func (pr *PIDReloader) TriggerReload(ctx context.Context) error {
procs, err := ps.Processes()
if err != nil {
return fmt.Errorf("list processes: %w", err)
}

var proc ps.Process
for i := range procs {
if pr.pname == procs[i].Executable() {
proc = procs[i]
break
}
}

if proc == nil {
return fmt.Errorf("failed to find process matching %q", pr.pname)
}

p, err := os.FindProcess(proc.Pid())
if err != nil {
return fmt.Errorf("find process err: %w", err)
}

if proc == nil {
return fmt.Errorf("failed to find process with pid %d", proc.Pid())
}

if err := p.Signal(syscall.SIGHUP); err != nil {
return fmt.Errorf("failed to send SIGHUP to pid %d: %w", p.Pid, err)
}

return nil
}

var _ = TriggerReloader(&PIDReloader{})

type HTTPReloader struct {
logger log.Logger

u *url.URL
c http.Client
}

var _ = TriggerReloader(&HTTPReloader{})

func NewHTTPReloader(logger log.Logger, u *url.URL, c http.Client) *HTTPReloader {
return &HTTPReloader{
logger: logger,
u: u,
c: c,
}
}

func (hr *HTTPReloader) TriggerReload(ctx context.Context) error {
req, err := http.NewRequest("POST", hr.u.String(), nil)
if err != nil {
return errors.Wrap(err, "create request")
}
req = req.WithContext(ctx)

resp, err := r.httpClient.Do(req)
resp, err := hr.c.Do(req)
if err != nil {
return errors.Wrap(err, "reload request failed")
}
defer runutil.ExhaustCloseWithLogOnErr(r.logger, resp.Body, "trigger reload resp body")
defer runutil.ExhaustCloseWithLogOnErr(hr.logger, resp.Body, "trigger reload resp body")

if resp.StatusCode != 200 {
return errors.Errorf("received non-200 response: %s; have you set `--web.enable-lifecycle` Prometheus flag?", resp.Status)
}
return nil
}

// SetHttpClient sets Http client for reloader.
func (r *Reloader) SetHttpClient(client http.Client) {
r.httpClient = client
return nil
}

// ReloadURLFromBase returns the standard Prometheus reload URL from its base URL.
Expand Down

0 comments on commit 8dc99a1

Please sign in to comment.