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
5 changes: 4 additions & 1 deletion cmd/exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"log/slog"
"net/http"
"os"
"pg-bash-exporter/internal/cache"
"pg-bash-exporter/internal/collector"
"pg-bash-exporter/internal/config"
"pg-bash-exporter/internal/executor"
Expand Down Expand Up @@ -50,9 +51,11 @@ func main() {

slog.Info("Configuration loaded and logger initialized successfully")

cache := cache.New()

exec := &executor.BashExecutor{}

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

registry := prometheus.NewRegistry()
registry.MustRegister(collector)
Expand Down
56 changes: 56 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cache

import (
"sync"
"time"
)

type Cache struct {
mu sync.Mutex
items map[string]Item
}

type Item struct {
Value string
Err error
Expiration time.Time
}

func New() *Cache {
return &Cache{
items: make(map[string]Item),
}
}

func (c *Cache) Set(key, value string, err error, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()

var expiration time.Time
if ttl > 0 {
expiration = time.Now().Add(ttl)
}

c.items[key] = Item{
Value: value,
Err: err,
Expiration: expiration,
}
}

func (c *Cache) Get(key string) (string, error, bool) {
c.mu.Lock()
defer c.mu.Unlock()

item, found := c.items[key]
if !found {
return "", nil, false
}

if !item.Expiration.IsZero() && time.Now().After(item.Expiration) {
delete(c.items, key)
return "", nil, false
}

return item.Value, item.Err, true
}
104 changes: 104 additions & 0 deletions internal/cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cache

import (
"errors"
"testing"
"time"
)

func TestCache(t *testing.T) {
testCases := []struct {
name string
key string
value string
err error
ttl time.Duration
expectFound bool
expectValue string
expectErr error
}{
{
name: "set get",
key: "key1",
value: "value1",
err: nil,
ttl: 1 * time.Minute,
expectFound: true,
expectValue: "value1",
expectErr: nil,
},
{
name: "get no existing value",
key: "nonexistent",
expectFound: false,
},
{
name: "errors caching",
key: "error key",
value: "",
err: errors.New("some error"),
ttl: 1 * time.Minute,
expectFound: true,
expectValue: "",
expectErr: errors.New("some error"),
},
{
name: "expired value",
key: "expired key",
value: "some value",
ttl: 1 * time.Millisecond,
expectFound: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cache := New()

if tc.value != "" || tc.err != nil {
cache.Set(tc.key, tc.value, tc.err, tc.ttl)
}

if tc.name == "expired value" {
time.Sleep(10 * time.Millisecond)
}

val, err, found := cache.Get(tc.key)

if found != tc.expectFound {
t.Errorf("expected found to be %v, but got %v", tc.expectFound, found)
}

if val != tc.expectValue {
t.Errorf("expected value to be '%s', but got '%s'", tc.expectValue, val)
}

if (err != nil && tc.expectErr == nil) || (err == nil && tc.expectErr != nil) || (err != nil && tc.expectErr != nil && err.Error() != tc.expectErr.Error()) {
t.Errorf("expected error to be '%v', but got '%v'", tc.expectErr, err)
}
})
}
}

func TestCache_UniqueKeys(t *testing.T) {
cache := New()

key1 := "metric1::echo hello"
value1 := "output1"

key2 := "metric2::echo hello"
value2 := "output2"

cache.Set(key1, value1, nil, 1*time.Minute)
cache.Set(key2, value2, nil, 1*time.Minute)

val, _, found := cache.Get(key1)
if !found || val != value1 {
t.Errorf("expected to find key '%s' with value '%s', but got '%s'", key1, value1, val)
}

val, _, found = cache.Get(key2)
if !found || val != value2 {
t.Errorf("expected to find key '%s' with value '%s', but got '%s'", key2, value2, val)
}
}
5 changes: 4 additions & 1 deletion internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"github.com/prometheus/client_golang/prometheus"
"log/slog"
"pg-bash-exporter/internal/cache"
"pg-bash-exporter/internal/config"
"time"
)
Expand All @@ -16,13 +17,15 @@ type Collector struct {
config *config.Config
logger *slog.Logger
executor Executor
cache *cache.Cache
}

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

Expand Down
3 changes: 2 additions & 1 deletion 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"
"pg-bash-exporter/internal/cache"
"pg-bash-exporter/internal/config"
"strings"
"testing"
Expand Down Expand Up @@ -328,7 +329,7 @@ filter_metric_filtered_sub 100
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)
collector := NewCollector(tc.config, logger, tc.executor, cache.New())
reg := prometheus.NewRegistry()
reg.MustRegister(collector)

Expand Down
5 changes: 5 additions & 0 deletions internal/collector/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ func (c *Collector) matchPattern(line, match string) (bool, error) {

return matched, nil
}

// generateCacheKey creates a unique key for caching.
func generateCacheKey(metricName, command string) string {
return fmt.Sprintf("%s::%s", metricName, command)
}
22 changes: 20 additions & 2 deletions internal/collector/processing.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,29 @@ import (
// returns command output split into lines.
// returns error if command fails to execute.
func (c *Collector) getCommandOutput(metricConfig config.Metric) ([]string, error) {
cacheKey := generateCacheKey(metricConfig.Name, metricConfig.Command)
val, err, ok := c.cache.Get(cacheKey)

ttl := c.config.Global.CacheTTL
if metricConfig.CacheTTL > config.DefaultCacheTTL {
ttl = metricConfig.CacheTTL
}

if ok {
c.logger.Debug("cache taken", "command", metricConfig.Command)
return strings.Split(strings.TrimSpace(val), "\n"), err
}

timeout := c.config.Global.Timeout

if metricConfig.Timeout > 0 {
timeout = metricConfig.Timeout
}

out, err := c.executor.ExecuteCommand(context.Background(), metricConfig.Command, timeout)

c.cache.Set(cacheKey, out, err, ttl)

if err != nil {
return nil, err
}
Expand Down Expand Up @@ -89,8 +105,10 @@ func (c *Collector) collectComplicatedMetric(ch chan<- prometheus.Metric, metric
}

for _, subMetric := range metricConfig.SubMetrics {
if matched, err := c.matchPattern(line, subMetric.Match); !matched {
c.logger.Error("invalid regex patterin in sub-metric", "sub-metric", subMetric.Name, "pattern", subMetric.Match, "error", err)
if matched, err := c.matchPattern(line, subMetric.Match); !matched || err != nil {
if err != nil {
c.logger.Error("invalid regex patterin in sub-metric", "sub-metric", subMetric.Name, "pattern", subMetric.Match, "error", err)
}
continue
}

Expand Down
12 changes: 9 additions & 3 deletions internal/config/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
"gopkg.in/yaml.v3"
)

const (
DefaultTimeout = 30 * time.Second
DefaultCacheTTL = 3 * time.Second
DefaultMaxConcurrent = 10
)

var configPathFlag string

// init sets flags with package initialization
Expand All @@ -31,13 +37,13 @@ func GetPath() string {

func (c *Config) applyDefaults() {
if c.Global.Timeout == 0 {
c.Global.Timeout = 30 * time.Second
c.Global.Timeout = DefaultTimeout
}
if c.Global.CacheTTL == 0 {
c.Global.CacheTTL = 3 * time.Second
c.Global.CacheTTL = DefaultCacheTTL
}
if c.Global.MaxConcurrent == 0 {
c.Global.MaxConcurrent = 10
c.Global.MaxConcurrent = DefaultMaxConcurrent
}
}

Expand Down