Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cmd/exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func main() {

exec := &executor.BashExecutor{}

metricsCollector := collector.NewCollector(&cfg, slog.Default(), exec, cache)
metricsCollector := collector.NewCollector(&cfg, slog.Default(), exec, cache, configPath)

registry := prometheus.NewRegistry()
registry.MustRegister(metricsCollector)
Expand Down Expand Up @@ -90,6 +90,19 @@ func main() {
}
}()

hReload := make(chan os.Signal, 1)
signal.Notify(hReload, syscall.SIGHUP)

go func() {
for {
<-hReload
slog.Info("received SIGHUP, attempting to reload config")
if err := metricsCollector.ReloadConfig(); err != nil {
slog.Error("config reload failed", "error", err)
}
}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

Expand Down
45 changes: 36 additions & 9 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,29 @@ type Executor interface {
}

type Collector struct {
config *config.Config
logger *slog.Logger
executor Executor
cache *cache.Cache
config *config.Config
logger *slog.Logger
executor Executor
cache *cache.Cache
configPath string

mu sync.RWMutex
}

func NewCollector(cfg *config.Config, logger *slog.Logger, exec Executor, cache *cache.Cache) *Collector {
func NewCollector(cfg *config.Config, logger *slog.Logger, exec Executor, cache *cache.Cache, configPath string) *Collector {
return &Collector{
config: cfg,
logger: logger,
executor: exec,
cache: cache,
config: cfg,
logger: logger,
executor: exec,
cache: cache,
configPath: configPath,
}
}

func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
c.mu.RLock()
defer c.mu.RUnlock()

for _, metricConfig := range c.config.Metrics {
if len(metricConfig.SubMetrics) == 0 {
dynLblNames := getLabelNames(metricConfig.DynamicLabels)
Expand Down Expand Up @@ -65,7 +72,27 @@ func (c *Collector) Describe(ch chan<- *prometheus.Desc) {

}

func (c *Collector) ReloadConfig() error {
var newCfg config.Config

if err := config.Load(c.configPath, &newCfg); err != nil {
c.logger.Error("failed to reload config", "error", err)
return err
}

c.mu.Lock()
defer c.mu.Unlock()

c.config = &newCfg

c.logger.Info("config reloaded successfully")
return nil
}

func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.mu.RLock()
defer c.mu.RUnlock()

start := time.Now()

defer func() {
Expand Down
75 changes: 72 additions & 3 deletions internal/collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil"
"io"
"log/slog"
"os"
"pg-bash-exporter/internal/cache"
"pg-bash-exporter/internal/config"
"strings"
Expand Down Expand Up @@ -238,7 +239,7 @@ matched_metric_mem{label_name="label2"} 200
Name: "connections",
Help: "number of connetions.",
Type: "gauge",
Command: "echo -e 'tcp 150\nudp 25",
Command: "echo -e 'tcp 150\nudp 25'",
Field: 1,
DynamicLabels: []config.DynamicLabel{
{Name: "type", Field: 0},
Expand Down Expand Up @@ -396,7 +397,7 @@ not_blacklisted_metric 1
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
collector := NewCollector(tc.config, logger, tc.executor, cache.New())
collector := NewCollector(tc.config, logger, tc.executor, cache.New(), "")
reg := prometheus.NewRegistry()
reg.MustRegister(collector)

Expand Down Expand Up @@ -610,7 +611,7 @@ func TestInternalMetrics(t *testing.T) {
err: errors.New("error"),
}

collector := NewCollector(cfg, logger, executor, cache)
collector := NewCollector(cfg, logger, executor, cache, "")

ch := make(chan prometheus.Metric, 10)
go func() {
Expand Down Expand Up @@ -649,3 +650,71 @@ func TestInternalMetrics(t *testing.T) {
t.Errorf("CacheMisses: wnted 2, got %v", val)
}
}

func TestReloadConfig(t *testing.T) {
logger := slog.New(slog.NewJSONHandler(io.Discard, nil))

configV1 := `
server:
listen_address: ":8080"
metrics_path: "/metrics"
logging:
level: "info"
metrics:
- name: "metric_v1"
help: "help v1"
type: "gauge"
command: "echo 1"
`
configV2 := `
server:
listen_address: ":8080"
metrics_path: "/metrics"
logging:
level: "info"
metrics:
- name: "metric_v2"
help: "help v2"
type: "counter"
command: "echo 2"
`

tmpfile, err := os.CreateTemp(t.TempDir(), "test-config-*.yaml")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tmpfile.Name())

if _, err := tmpfile.Write([]byte(configV1)); err != nil {
t.Fatalf("failed to write v1 config: %v", err)
}

var cfg config.Config
if err := config.Load(tmpfile.Name(), &cfg); err != nil {
t.Fatalf("failed to load v1 config: %v", err)
}

collector := NewCollector(&cfg, logger, &mockExecutor{}, cache.New(), tmpfile.Name())

if collector.config.Metrics[0].Name != "metric_v1" {
t.Fatalf("expected initial metric to be metric_v1, got %s", collector.config.Metrics[0].Name)
}

if err := os.WriteFile(tmpfile.Name(), []byte(configV2), 0644); err != nil {
t.Fatalf("failed to write v2 config: %v", err)
}

if err := collector.ReloadConfig(); err != nil {
t.Fatalf("reload failed: %v", err)
}

if len(collector.config.Metrics) != 1 {
t.Fatalf("expected 1 metric after reload, got %d", len(collector.config.Metrics))
}
if collector.config.Metrics[0].Name != "metric_v2" {
t.Errorf("expected metric_v2 after reload, got %s", collector.config.Metrics[0].Name)
}
if collector.config.Metrics[0].Type != "counter" {
t.Errorf("expected counter type after reload, got %s", collector.config.Metrics[0].Type)
}
}