Send every log line to a syslog server as well (closes #28)
check / check (push) Successful in 3m3s
check / check (push) Successful in 3m3s
With SWWAF_LOG_REMOTE_URL set (syslog+udp, syslog+tcp or syslog+tls), every line on stdout is also sent as the message of an RFC 5424 record, octet-counted over TCP and TLS, from a bounded buffer that drops its oldest line when full, so a slow or unreachable server holds up nothing. Failed connections are retried with backoff; lines sent, dropped and waiting are metrics. At a stop the lines still waiting get at most two seconds. SWWAF_LOG_REMOTE_APP_NAME defaults to SWWAF_INSTANCE_NAME; while sending, an app name RFC 5424 does not allow stops the start. Standard library only: log/syslog writes only the older format. Model: opus-5-5
This commit was merged in pull request #84.
This commit is contained in:
@@ -10,6 +10,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -385,6 +387,112 @@ func TestRulesDirThatDoesNotExistStopsTheStart(t *testing.T) {
|
||||
": no such file or directory")
|
||||
}
|
||||
|
||||
func TestEveryLineIsAlsoSentToTheRemoteLogEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
endpoint, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = endpoint.Close()
|
||||
}()
|
||||
|
||||
env := map[string]string{
|
||||
listenAddr: localhost + ":0",
|
||||
upstreamURL: startApp(t),
|
||||
stateDir: t.TempDir(),
|
||||
rulesDir: t.TempDir(),
|
||||
"SWWAF_LOG_REMOTE_URL": "syslog+tcp://" + endpoint.Addr().String(),
|
||||
}
|
||||
|
||||
out := runUntilStopped(t, env, func(url string) {
|
||||
wantGreeting(t, url)
|
||||
})
|
||||
out.line(t, "type", "request")
|
||||
|
||||
// smallwebwaf connected as it started, and closes the connection once
|
||||
// it has sent the lines written as it stopped.
|
||||
conn, err := endpoint.Accept()
|
||||
if err != nil {
|
||||
t.Fatalf("accept: %v", err)
|
||||
}
|
||||
|
||||
received, err := io.ReadAll(conn)
|
||||
_ = conn.Close()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
|
||||
// Lines written at once by several goroutines may reach stdout and
|
||||
// the endpoint in different orders.
|
||||
sent := messages(t, string(received))
|
||||
written := slices.Collect(strings.Lines(out.text()))
|
||||
|
||||
slices.Sort(sent)
|
||||
slices.Sort(written)
|
||||
|
||||
if !slices.Equal(sent, written) {
|
||||
t.Errorf("sent\n%v\nwrote\n%v", sent, written)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStalledRemoteLogEndpointHoldsUpNoRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const token = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
// The endpoint takes connections and never answers, so the TLS
|
||||
// handshake of each waits on it, and no line is ever sent.
|
||||
endpoint, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = endpoint.Close()
|
||||
}()
|
||||
|
||||
env := map[string]string{
|
||||
listenAddr: localhost + ":0",
|
||||
upstreamURL: startApp(t),
|
||||
stateDir: t.TempDir(),
|
||||
rulesDir: t.TempDir(),
|
||||
"SWWAF_LOG_REMOTE_URL": "syslog+tls://" + endpoint.Addr().String(),
|
||||
"SWWAF_LOG_REMOTE_BUFFER": "1",
|
||||
"SWWAF_METRICS_TOKEN": token,
|
||||
}
|
||||
|
||||
out := runUntilStopped(t, env, func(url string) {
|
||||
wantGreeting(t, url)
|
||||
|
||||
// More than one line has been written, and the buffer holds the
|
||||
// last.
|
||||
metrics := metricsText(t, url+"_smallwebwaf/metrics", token)
|
||||
for _, series := range []string{
|
||||
"smallwebwaf_remote_log_lines_sent_total 0",
|
||||
"smallwebwaf_remote_log_buffer_depth 1",
|
||||
} {
|
||||
if !strings.Contains(metrics, "\n"+series+"\n") {
|
||||
t.Errorf("no %q in the metrics:\n%s", series, metrics)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(metrics, "\nsmallwebwaf_remote_log_lines_dropped_total 0\n") ||
|
||||
!strings.Contains(metrics, "\nsmallwebwaf_remote_log_lines_dropped_total ") {
|
||||
t.Errorf("no line dropped in the metrics:\n%s", metrics)
|
||||
}
|
||||
|
||||
// Closed, the endpoint refuses the connection made to send the
|
||||
// lines still waiting at the stop, which then does not wait.
|
||||
_ = endpoint.Close()
|
||||
})
|
||||
|
||||
out.line(t, "type", "request")
|
||||
}
|
||||
|
||||
func TestStateFileThatDoesNotParseStopsTheStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -581,6 +689,69 @@ func wantGreeting(t *testing.T, url string) {
|
||||
}
|
||||
}
|
||||
|
||||
// messages returns the message of each record in received, octet-counted
|
||||
// frames of RFC 5424 records with the default facility and app name, each
|
||||
// with the newline that ends a line on stdout.
|
||||
func messages(t *testing.T, received string) []string {
|
||||
t.Helper()
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
header := " " + hostname + " " + hostname + " - - - "
|
||||
|
||||
var found []string
|
||||
|
||||
for received != "" {
|
||||
count, rest, _ := strings.Cut(received, " ")
|
||||
|
||||
length, err := strconv.Atoi(count)
|
||||
if err != nil || length > len(rest) {
|
||||
t.Fatalf("no frame at %q", received)
|
||||
}
|
||||
|
||||
record := rest[:length]
|
||||
received = rest[length:]
|
||||
|
||||
_, message, ok := strings.Cut(record, header)
|
||||
if !ok || !strings.HasPrefix(record, "<134>1 ") {
|
||||
t.Fatalf("record %q, want priority <134> and header %q", record, header)
|
||||
}
|
||||
|
||||
found = append(found, message+"\n")
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// metricsText asks for the metrics at url with token, and returns them.
|
||||
func metricsText(t *testing.T, url, token string) string {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, url,
|
||||
http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
transport := &http.Transport{}
|
||||
defer transport.CloseIdleConnections()
|
||||
|
||||
res, err := (&http.Client{Transport: transport}).Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
_ = res.Body.Close()
|
||||
|
||||
if err != nil || res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("metrics answered %d (%v)", res.StatusCode, err)
|
||||
}
|
||||
|
||||
return string(body)
|
||||
}
|
||||
|
||||
// wantRefused checks that a request to url is refused with 403, the
|
||||
// default SWWAF_BAN_RESPONSE.
|
||||
func wantRefused(t *testing.T, url string) {
|
||||
|
||||
Reference in New Issue
Block a user