Skip to content

Commit

Permalink
Merge pull request #65 from kuskoman/moreeeee-refactor
Browse files Browse the repository at this point in the history
More refactor, 100% test coverage
  • Loading branch information
kuskoman authored Mar 24, 2023
2 parents a32ca15 + c683257 commit a69a018
Show file tree
Hide file tree
Showing 12 changed files with 66 additions and 87 deletions.
3 changes: 2 additions & 1 deletion cmd/exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package main
import (
"log"

"github.com/joho/godotenv"
"github.com/kuskoman/logstash-exporter/collectors"
"github.com/kuskoman/logstash-exporter/config"
"github.com/kuskoman/logstash-exporter/server"
"github.com/prometheus/client_golang/prometheus"
)

func main() {
warn := config.InitializeEnv()
warn := godotenv.Load()
if warn != nil {
log.Println(warn)
}
Expand Down
7 changes: 0 additions & 7 deletions config/env_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ package config

import (
"os"

"github.com/joho/godotenv"
)

// InitializeEnv loads the environment variables from the .env file
func InitializeEnv() error {
return godotenv.Load()
}

func getEnvWithDefault(key string, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
Expand Down
9 changes: 0 additions & 9 deletions config/env_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,3 @@ func TestGetEnvWithDefault(t *testing.T) {
}
})
}

func TestInitializeEnv(t *testing.T) {
t.Run("should throw an error if .env file is not found", func(t *testing.T) {
err := InitializeEnv()
if err == nil {
t.Errorf("expected error but got nil")
}
})
}
3 changes: 3 additions & 0 deletions config/logstash_config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package config

var (
// LogstashUrl is the URL of the Logstash instance to be monitored.
// Defaults to http://localhost:9600
// Can be overridden by setting the LOGSTASH_URL environment variable
LogstashUrl = getEnvWithDefault("LOGSTASH_URL", "http://localhost:9600")
)
1 change: 1 addition & 0 deletions config/prometheus_config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package config

// PrometheusNamespace is the namespace used for all metrics exported by this exporter.
const PrometheusNamespace = "logstash"
11 changes: 11 additions & 0 deletions config/server_config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
package config

var (
// Port is the port the exporter will listen on.
// Defaults to 9198
// Can be overridden by setting the PORT environment variable
Port = getEnvWithDefault("PORT", "9198")

// Host is the host the exporter will listen on.
// Defaults to an empty string, which will listen on all interfaces
// Can be overridden by setting the HOST environment variable
// For windows, use "localhost", because an empty string will not work
// with the default windows firewall configuration.
// Alternatively you can change the firewall configuration to allow
// connections to the port from all interfaces.
Host = getEnvWithDefault("HOST", "")
)
3 changes: 3 additions & 0 deletions fetcher/logstash_client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import (
"github.com/kuskoman/logstash-exporter/fetcher/responses"
)

// Client is an interface for the Logstash client able to fetch data from the Logstash API
type Client interface {
GetNodeInfo(ctx context.Context) (*responses.NodeInfoResponse, error)
GetNodeStats(ctx context.Context) (*responses.NodeStatsResponse, error)
}

// DefaultClient is the default implementation of the Client interface
type DefaultClient struct {
httpClient *http.Client
endpoint string
}

const defaultLogstashEndpoint = "http://localhost:9600"

// NewClient returns a new instance of the DefaultClient configured with the given endpoint
func NewClient(endpoint string) Client {
if endpoint == "" {
endpoint = defaultLogstashEndpoint
Expand Down
2 changes: 2 additions & 0 deletions fetcher/logstash_client/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"github.com/kuskoman/logstash-exporter/fetcher/responses"
)

// GetNodeInfo fetches the node info from the "/" endpoint of the Logstash API
func (client *DefaultClient) GetNodeInfo(ctx context.Context) (*responses.NodeInfoResponse, error) {
fullPath := client.endpoint
return getMetrics[responses.NodeInfoResponse](ctx, client.httpClient, fullPath)
}

// GetNodeStats fetches the node stats from the "/_node/stats" endpoint of the Logstash API
func (client *DefaultClient) GetNodeStats(ctx context.Context) (*responses.NodeStatsResponse, error) {
fullPath := fmt.Sprintf("%s/_node/stats", client.endpoint)
return getMetrics[responses.NodeStatsResponse](ctx, client.httpClient, fullPath)
Expand Down
1 change: 1 addition & 0 deletions fetcher/responses/nodeinfo_response.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package responses

// NodeInfoResponse is the response from the "/" endpoint of the Logstash API
type NodeInfoResponse struct {
Host string `json:"host"`
Version string `json:"version"`
Expand Down
1 change: 1 addition & 0 deletions fetcher/responses/nodestats_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ type QueueResponse struct {
EventsCount int `json:"events_count"`
}

// NodeStatsResponse is the response from the _node/stats API.
type NodeStatsResponse struct {
Host string `json:"host"`
Version string `json:"version"`
Expand Down
56 changes: 8 additions & 48 deletions prometheus_helper/prometheus_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,12 @@ import (
dto "github.com/prometheus/client_model/go"
)

// SimpleDescHelper is a helper struct that can be used to create prometheus.Desc objects
type SimpleDescHelper struct {
Namespace string
Subsystem string
}

// NewDesc creates a new prometheus.Desc with the namespace and subsystem. The help text is set to the name.
func (h *SimpleDescHelper) NewDesc(name string) *prometheus.Desc {
help := name
return prometheus.NewDesc(prometheus.BuildFQName(h.Namespace, h.Subsystem, name), help, nil, nil)
}

// NewDescWithHelp creates a new prometheus.Desc with the namespace and subsystem.
func (h *SimpleDescHelper) NewDescWithHelp(name string, help string) *prometheus.Desc {
return prometheus.NewDesc(prometheus.BuildFQName(h.Namespace, h.Subsystem, name), help, nil, nil)
Expand All @@ -42,56 +37,21 @@ func ExtractFqName(metric string) (string, error) {
return matches[1], nil
}

// CustomCollector is a custom prometheus.Collector that collects only the given metric.
type CustomCollector struct {
metric prometheus.Metric
}

// Describe implements the prometheus.Collector interface.
func (c *CustomCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.metric.Desc()
}

// Collect implements the prometheus.Collector interface.
func (c *CustomCollector) Collect(ch chan<- prometheus.Metric) {
ch <- c.metric
}

// ExtractValueFromMetric extracts the value from a prometheus.Metric object.
// It creates a custom collector and registry, registers the given metric, and then collects
// the metric value using the registry.
// Returns the extracted float64 value from the metric's Gauge.
func ExtractValueFromMetric(metric prometheus.Metric) (float64, error) {
// Custom collector that collects only the given metric.
collector := &CustomCollector{
metric: metric,
}

// Create a custom registry and register the collector.
registry := prometheus.NewRegistry()
err := registry.Register(collector)
var dtoMetric dto.Metric
err := metric.Write(&dtoMetric)
if err != nil {
return 0, err
return 0, fmt.Errorf("error writing metric: %v", err)
}

var metricValue float64
metricChannel := make(chan prometheus.Metric)
go func() {
registry.Collect(metricChannel)
close(metricChannel)
}()

for collectedMetric := range metricChannel {
if collectedMetric.Desc().String() == metric.Desc().String() {
var dtoMetric dto.Metric
err = collectedMetric.Write(&dtoMetric)
if err != nil {
return 0, fmt.Errorf("error writing metric: %v", err)
}
metricValue = dtoMetric.GetGauge().GetValue()
break
}
gauge := dtoMetric.GetGauge()
if gauge == nil {
return 0, errors.New("the metric is not a Gauge")
}

return metricValue, nil
return gauge.GetValue(), nil
}
56 changes: 34 additions & 22 deletions prometheus_helper/prometheus_helper_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package prometheus_helper

import (
"errors"
"fmt"
"testing"

"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)

func TestSimpleDescHelper(t *testing.T) {
Expand All @@ -13,17 +15,17 @@ func TestSimpleDescHelper(t *testing.T) {
Subsystem: "test",
}

t.Run("NewDesc", func(t *testing.T) {
desc := helper.NewDesc("metric")
expectedDesc := "Desc{fqName: \"logstash_exporter_test_metric\", help: \"metric\", constLabels: {}, variableLabels: []}"
t.Run("NewDescWithHelp", func(t *testing.T) {
desc := helper.NewDescWithHelp("metric", "help")
expectedDesc := "Desc{fqName: \"logstash_exporter_test_metric\", help: \"help\", constLabels: {}, variableLabels: []}"
if desc.String() != expectedDesc {
t.Errorf("incorrect metric description, expected %s but got %s", expectedDesc, desc.String())
}
})

t.Run("NewDescWithHelp", func(t *testing.T) {
desc := helper.NewDescWithHelp("metric", "help")
expectedDesc := "Desc{fqName: \"logstash_exporter_test_metric\", help: \"help\", constLabels: {}, variableLabels: []}"
t.Run("NewDescWithHelpAndLabel", func(t *testing.T) {
desc := helper.NewDescWithHelpAndLabel("metric", "help", "label")
expectedDesc := "Desc{fqName: \"logstash_exporter_test_metric\", help: \"help\", constLabels: {}, variableLabels: [label]}"
if desc.String() != expectedDesc {
t.Errorf("incorrect metric description, expected %s but got %s", expectedDesc, desc.String())
}
Expand All @@ -40,7 +42,6 @@ func TestExtractFqdnName(t *testing.T) {
metricSubname := "fqdn_metric"

descriptors := []*prometheus.Desc{
helper.NewDesc(metricSubname),
helper.NewDescWithHelp(metricSubname, "help"),
helper.NewDescWithHelpAndLabel(metricSubname, "help", "label"),
}
Expand All @@ -65,6 +66,16 @@ func TestExtractFqdnName(t *testing.T) {
})
}

type badMetricStub struct{}

func (m *badMetricStub) Desc() *prometheus.Desc {
return nil
}

func (m *badMetricStub) Write(*dto.Metric) error {
return errors.New("writing metric failed")
}

func TestExtractValueFromMetric(t *testing.T) {
t.Run("should extract value from a metric", func(t *testing.T) {
metricDesc := prometheus.NewDesc("test_metric", "test metric help", nil, nil)
Expand All @@ -81,30 +92,31 @@ func TestExtractValueFromMetric(t *testing.T) {
}
})

t.Run("should return an error when unable to write metric", func(t *testing.T) {
metricDesc := prometheus.NewDesc("test_metric", "test metric help", nil, nil)
exampleErr := fmt.Errorf("example error")
invalidMetric := prometheus.NewInvalidMetric(metricDesc, exampleErr)
t.Run("should return error if writing metric fails", func(t *testing.T) {
badMetric := &badMetricStub{}
val, err := ExtractValueFromMetric(badMetric)

customCollector := &CustomCollector{
metric: invalidMetric,
if err == nil {
t.Errorf("Expected error, but got nil")
}

registry := prometheus.NewRegistry()
err := registry.Register(customCollector)

if err != nil {
t.Errorf("Unexpected error: %v", err)
if val != 0 {
t.Errorf("Expected value to be 0, got %f", val)
}
})

extractedValue, err := ExtractValueFromMetric(invalidMetric)
t.Run("should return error if the metric is not a Gauge", func(t *testing.T) {
metricDesc := prometheus.NewDesc("test_counter_metric", "test counter metric help", nil, nil)
metricValue := 42.0
metric := prometheus.MustNewConstMetric(metricDesc, prometheus.CounterValue, metricValue)

val, err := ExtractValueFromMetric(metric)
if err == nil {
t.Errorf("Expected error but got nil")
t.Errorf("Expected error, but got nil")
}

if extractedValue != 0 {
t.Errorf("Expected extracted value to be 0, got %f", extractedValue)
if val != 0 {
t.Errorf("Expected value to be 0, got %f", val)
}
})
}

0 comments on commit a69a018

Please sign in to comment.