Files
meowlib/lokiwriter_test.go
ycc c784f6f315
Some checks failed
continuous-integration/drone/push Build is failing
loki disable if non valid url
2026-02-09 19:06:47 +01:00

192 lines
5.5 KiB
Go

package meowlib
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewLokiWriterDisabledOnEmptyURL(t *testing.T) {
w := NewLokiWriter("", map[string]string{"app": "test"})
assert.True(t, w.disabled)
}
func TestNewLokiWriterDisabledOnInvalidURL(t *testing.T) {
cases := []string{
"not-a-url",
"ftp://example.com/loki",
"://missing-scheme",
"http://",
"justtext",
}
for _, u := range cases {
w := NewLokiWriter(u, map[string]string{"app": "test"})
assert.True(t, w.disabled, "expected disabled for URL: %s", u)
}
}
func TestNewLokiWriterEnabledOnValidURL(t *testing.T) {
cases := []string{
"http://localhost:3100/loki/api/v1/push",
"https://log.redroom.link/loki/api/v1/push",
}
for _, u := range cases {
w := NewLokiWriter(u, map[string]string{"app": "test"})
assert.False(t, w.disabled, "expected enabled for URL: %s", u)
}
}
func TestWriteDisabledReturnsLength(t *testing.T) {
w := NewLokiWriter("", map[string]string{"app": "test"})
msg := []byte(`{"level":"info","message":"should be discarded"}`)
n, err := w.Write(msg)
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
}
func TestWriteToMockLoki(t *testing.T) {
var received LokiPayload
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
err := json.NewDecoder(r.Body).Decode(&received)
assert.NoError(t, err)
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
labels := map[string]string{"app": "meowlib", "env": "test"}
w := NewLokiWriter(server.URL, labels)
msg := []byte(`{"level":"info","message":"hello from test"}`)
n, err := w.Write(msg)
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
// Verify payload structure
assert.Len(t, received.Streams, 1)
assert.Equal(t, "meowlib", received.Streams[0].Stream["app"])
assert.Equal(t, "test", received.Streams[0].Stream["env"])
assert.Equal(t, "info", received.Streams[0].Stream["level"])
assert.Len(t, received.Streams[0].Values, 1)
assert.Equal(t, "hello from test", received.Streams[0].Values[0][1])
}
func TestWriteInvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("server should not be called for invalid JSON input")
}))
defer server.Close()
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
msg := []byte(`not json at all`)
n, err := w.Write(msg)
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
}
func TestCircuitBreakerOpensAfterFailures(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
msg := []byte(`{"level":"error","message":"fail test"}`)
// Send maxFailures requests to trip the circuit breaker
for i := 0; i < maxFailures; i++ {
n, err := w.Write(msg)
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
}
assert.True(t, w.circuitOpen)
// Next write should be silently discarded (no server call)
prevCount := callCount
n, err := w.Write(msg)
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
assert.Equal(t, prevCount, callCount, "no HTTP call should be made while circuit is open")
}
func TestCircuitBreakerResetsAfterSuccess(t *testing.T) {
failFirst := true
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if failFirst {
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusNoContent)
}
}))
defer server.Close()
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
msg := []byte(`{"level":"info","message":"recovery test"}`)
// Cause some failures (but not enough to open circuit)
w.Write(msg)
w.Write(msg)
assert.False(t, w.circuitOpen)
assert.Equal(t, 2, w.failureCount)
// Now succeed
failFirst = false
w.Write(msg)
assert.Equal(t, 0, w.failureCount)
}
func TestCircuitBreakerRetriesAfterTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
// Manually open the circuit with an old failure time
w.mu.Lock()
w.circuitOpen = true
w.failureCount = maxFailures
w.lastFailureTime = time.Now().Add(-circuitOpenTime - time.Second)
w.mu.Unlock()
msg := []byte(`{"level":"info","message":"retry test"}`)
n, err := w.Write(msg)
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
// Circuit should now be closed after successful retry
assert.False(t, w.circuitOpen)
assert.Equal(t, 0, w.failureCount)
}
func TestWriteToRealLoki(t *testing.T) {
lokiURL := "https://log.redroom.link/loki/api/v1/push"
// Quick connectivity check
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://log.redroom.link/ready")
if err != nil || resp.StatusCode != http.StatusOK {
t.Skip("Loki not reachable, skipping live test")
}
resp.Body.Close()
labels := map[string]string{"app": "meowlib", "env": "test"}
w := NewLokiWriter(lokiURL, labels)
assert.False(t, w.disabled)
msg := fmt.Sprintf(`{"level":"info","message":"lokiwriter_test at %s"}`, time.Now().Format(time.RFC3339))
n, err := w.Write([]byte(msg))
assert.NoError(t, err)
assert.Equal(t, len(msg), n)
assert.Equal(t, 0, w.failureCount)
}