// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bufio"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path"
"reflect"
"runtime"
"testing"
"time"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
)
const (
testSocket = "/tmp/haproxyexportertest.sock"
testInfo = "Release_date: test date\nVersion: test version\n"
)
type haproxy struct {
*httptest.Server
response []byte
}
func newHaproxy(response []byte) *haproxy {
h := &haproxy{response: response}
h.Server = httptest.NewServer(handler(h))
return h
}
func handler(h *haproxy) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Write(h.response)
}
}
func handlerStale(exit chan bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
<-exit
}
}
func expectMetrics(t *testing.T, c prometheus.Collector, fixture string) {
exp, err := os.Open(path.Join("test", fixture))
if err != nil {
t.Fatalf("Error opening fixture file %q: %v", fixture, err)
}
if err := testutil.CollectAndCompare(c, exp); err != nil {
t.Fatal("Unexpected metrics returned:", err)
}
}
func TestInvalidConfig(t *testing.T) {
h := newHaproxy([]byte("not,enough,fields"))
defer h.Close()
e, _ := NewExporter(h.URL, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
expectMetrics(t, e, "invalid_config.metrics")
}
func TestServerWithoutChecks(t *testing.T) {
h := newHaproxy([]byte("test,127.0.0.1:8080,0,0,0,0,0,0,0,0,,0,,0,0,0,0,no check,1,1,0,0,,,0,,1,1,1,,0,,2,0,,0,,,,0,0,0,0,0,0,0,,,,0,0,,,,,,,,,,,"))
defer h.Close()
e, _ := NewExporter(h.URL, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
expectMetrics(t, e, "server_without_checks.metrics")
}
// TestServerBrokenCSV ensures bugs in CSV format are handled gracefully. List of known bugs:
//
// - http://permalink.gmane.org/gmane.comp.web.haproxy/26561
func TestServerBrokenCSV(t *testing.T) {
const data = `foo,FRONTEND,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,5007,0,,1,8,1,,0,,2,0,,0,L4OK,,0,,,,,,,0,,,,0,0,,,,,,,,,,,
foo,bug-missing-comma,0,0,0,0,,0,0,0,,0,,0,0,0,0,DRAIN (agent)1,1,0,0,0,5007,0,,1,8,1,,0,,2,0,,0,L4OK,,0,,,,,,,0,,,,0,0,,,,,,,,,,,
foo,foo-instance-0,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,5007,0,,1,8,1,,0,,2,0,,0,L4OK,,0,,,,,,,0,,,,0,0,,,,,,,,,,,
foo,BACKEND,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,5007,0,,1,8,1,,0,,2,0,,0,L4OK,,0,,,,,,,0,,,,0,0,,,,,,,,,,,
`
h := newHaproxy([]byte(data))
defer h.Close()
e, _ := NewExporter(h.URL, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
expectMetrics(t, e, "server_broken_csv.metrics")
}
func TestOlderHaproxyVersions(t *testing.T) {
const data = `foo,FRONTEND,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,5007,0,,1,8,1,,0,,2,
foo,foo-instance-0,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,5007,0,,1,8,1,,0,,2,
foo,BACKEND,0,0,0,0,,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,5007,0,,1,8,1,,0,,2,
`
h := newHaproxy([]byte(data))
defer h.Close()
e, _ := NewExporter(h.URL, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
expectMetrics(t, e, "older_haproxy_versions.metrics")
}
func TestConfigChangeDetection(t *testing.T) {
h := newHaproxy([]byte(""))
defer h.Close()
e, _ := NewExporter(h.URL, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
e.Collect(ch)
}()
// TODO: Add a proper test here. Vet the possibilities of the new
// client_golang to do this easily. If better test support is needed,
// add it to client_golang first. (See also readCounter() above.)
// Suck up the remaining metrics.
for range ch {
}
}
func TestDeadline(t *testing.T) {
exit := make(chan bool)
s := httptest.NewServer(handlerStale(exit))
defer func() {
// s.Close() will block until the handler
// returns, so we need to make it exit.
exit <- true
s.Close()
}()
e, err := NewExporter(s.URL, true, false, serverMetrics, excludedServerStates, 1*time.Second, log.NewNopLogger())
if err != nil {
t.Fatal(err)
}
expectMetrics(t, e, "deadline.metrics")
}
func TestNotFound(t *testing.T) {
s := httptest.NewServer(http.NotFoundHandler())
defer s.Close()
e, err := NewExporter(s.URL, true, false, serverMetrics, excludedServerStates, 1*time.Second, log.NewNopLogger())
if err != nil {
t.Fatal(err)
}
expectMetrics(t, e, "not_found.metrics")
}
func newHaproxyUnix(file, statsPayload string, infoPayload string) (io.Closer, error) {
if err := os.Remove(file); err != nil && !os.IsNotExist(err) {
return nil, err
}
l, err := net.Listen("unix", file)
if err != nil {
return nil, err
}
go func() {
for {
c, err := l.Accept()
if err != nil {
return
}
go func(c net.Conn) {
defer c.Close()
r := bufio.NewReader(c)
for {
l, err := r.ReadString('\n')
if err != nil {
return
}
switch l {
case "show info\n":
c.Write([]byte(infoPayload))
return
case "show stat\n":
c.Write([]byte(statsPayload))
return
default:
// invalid command
return
}
}
}(c)
}
}()
return l, nil
}
func TestUnixDomain(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("not on windows")
return
}
srv, err := newHaproxyUnix(testSocket, "test,127.0.0.1:8080,0,0,0,0,0,0,0,0,,0,,0,0,0,0,no check,1,1,0,0,,,0,,1,1,1,,0,,2,0,,0,,,,0,0,0,0,0,0,0,,,,0,0,,,,,,,,,,,\n", testInfo)
if err != nil {
t.Fatalf("can't start test server: %v", err)
}
defer srv.Close()
e, err := NewExporter("unix:"+testSocket, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
if err != nil {
t.Fatal(err)
}
expectMetrics(t, e, "unix_domain.metrics")
}
func TestUnixDomainNotFound(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("not on windows")
return
}
if err := os.Remove(testSocket); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
e, _ := NewExporter("unix:"+testSocket, true, false, serverMetrics, excludedServerStates, 1*time.Second, log.NewNopLogger())
expectMetrics(t, e, "unix_domain_not_found.metrics")
}
func TestUnixDomainDeadline(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("not on windows")
return
}
exit := make(chan struct{})
defer close(exit)
if err := os.Remove(testSocket); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
l, err := net.Listen("unix", testSocket)
if err != nil {
t.Fatal(err)
}
defer l.Close()
go func() {
for {
if _, err := l.Accept(); err != nil {
return
}
go func() {
// block
<-exit
}()
}
}()
e, _ := NewExporter("unix:"+testSocket, true, false, serverMetrics, excludedServerStates, 1*time.Second, log.NewNopLogger())
expectMetrics(t, e, "unix_domain_deadline.metrics")
}
func TestInvalidScheme(t *testing.T) {
e, err := NewExporter("gopher://gopher.quux.org", true, false, serverMetrics, excludedServerStates, 1*time.Second, log.NewNopLogger())
if expect, got := (*Exporter)(nil), e; expect != got {
t.Errorf("expected %v, got %v", expect, got)
}
if err == nil {
t.Fatalf("expected non-nil error")
}
if expect, got := err.Error(), `unsupported scheme: "gopher"`; expect != got {
t.Errorf("expected %q, got %q", expect, got)
}
}
func TestParseStatusField(t *testing.T) {
tests := []struct {
input string
want int64
}{
{"UP", 1},
{"UP 1/3", 1},
{"UP 2/3", 1},
{"OPEN", 1},
{"no check", 1},
{"DOWN", 0},
{"DOWN 1/2", 0},
{"NOLB", 0},
{"MAINT", 0}, // prometheus/haproxy_exporter#35
{"unknown", 0},
}
for _, tt := range tests {
if have := parseStatusField(tt.input); tt.want != have {
t.Errorf("want status value %d for input %s, have %d",
tt.want,
tt.input,
have,
)
}
}
}
func TestFilterServerMetrics(t *testing.T) {
tests := []struct {
input string
want map[int]metricInfo
}{
{input: "", want: map[int]metricInfo{}},
{input: "8", want: map[int]metricInfo{8: metricInfo{Desc: serverMetrics[8].Desc, Type: prometheus.CounterValue}}},
{input: serverMetrics.String(), want: serverMetrics},
}
for _, tt := range tests {
have, err := filterServerMetrics(tt.input)
if err != nil {
t.Errorf("unexpected error for input %s: %s", tt.input, err)
continue
}
if !reflect.DeepEqual(tt.want, have) {
t.Errorf("want filtered metrics %+v for input %q, have %+v",
tt.want,
tt.input,
have,
)
}
}
}
func BenchmarkExtract(b *testing.B) {
config, err := os.ReadFile("test/haproxy.csv")
if err != nil {
b.Fatalf("could not read config file: %v", err.Error())
}
h := newHaproxy(config)
defer h.Close()
e, _ := NewExporter(h.URL, true, false, serverMetrics, excludedServerStates, 5*time.Second, log.NewNopLogger())
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ch := make(chan prometheus.Metric)
go func(ch chan prometheus.Metric) {
for range ch {
}
}(ch)
e.Collect(ch)
close(ch)
}
runtime.GC()
runtime.ReadMemStats(&after)
b.Logf("%d bytes used after %d runs", after.Alloc-before.Alloc, b.N)
}