v0.27.2 minor update: long lines, switches, tls
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
This commit is contained in:
1 parent
4513a9e62b
commit
a1648ce17b
16 files changed
+159
-34
No files matched your search
@@ -4,11 +4,7 @@ package config
|
||||
|
||||
// Config is the top-level configuration structure for the LogWisp application
|
||||
type Config struct {
|
||||
// Top-level flags for application control
|
||||
ShowVersion bool `toml:"version"`
|
||||
Check bool `toml:"check"` // build every plugin, report, exit: lw --check
|
||||
Dump bool `toml:"dump"` // print the effective configuration, exit: lw --dump
|
||||
Quiet bool `toml:"quiet"`
|
||||
Quiet bool `toml:"quiet"`
|
||||
|
||||
// Console sinks without their own color: "auto" (a terminal, NO_COLOR
|
||||
// unset, TERM not dumb), "always", "never"
|
||||
|
||||
@@ -229,7 +229,8 @@ port = 9000
|
||||
}
|
||||
|
||||
// A misspelled table path above a plugin's config drops that whole table, so
|
||||
// the file itself is checked too; only config_file is tolerated, as documented.
|
||||
// the file itself is checked too; only config_file, as documented, and the
|
||||
// switches earlier lw --dump output wrote are tolerated.
|
||||
func TestConfigFileRejectsUnknownKeys(t *testing.T) {
|
||||
isolateConfig(t)
|
||||
for key, body := range map[string]string{
|
||||
@@ -237,6 +238,9 @@ func TestConfigFileRejectsUnknownKeys(t *testing.T) {
|
||||
"logging.levle": "[logging]\nlevle = \"debug\"",
|
||||
} {
|
||||
testutil.WriteFile(t, "typo.toml", `config_file = "ignored.toml"
|
||||
check = false
|
||||
dump = false
|
||||
version = false
|
||||
[[pipelines]]
|
||||
name = "typo"
|
||||
[[pipelines.plugin_sources]]
|
||||
|
||||
@@ -174,5 +174,8 @@ func checkFileKeys(path string) error {
|
||||
return err
|
||||
}
|
||||
delete(root, "config_file") // runtime metadata, documented as ignored in the file
|
||||
for _, key := range []string{"version", "check", "dump"} {
|
||||
delete(root, key) // command-line switches, which earlier lw --dump output wrote
|
||||
}
|
||||
return checkKeys(root, reflect.TypeOf(Config{}), "")
|
||||
}
|
||||
@@ -40,6 +40,12 @@ type FileSource struct {
|
||||
// Configuration
|
||||
config *config.FileSourceOptions
|
||||
|
||||
// lw's stdout and stderr when they are files: a console sink writes there,
|
||||
// so following them would feed lw its own output. ownSkipped is per path
|
||||
// already reported, touched only by monitorLoop.
|
||||
own []os.FileInfo
|
||||
ownSkipped map[string]bool
|
||||
|
||||
// Application
|
||||
subscribers []chan core.LogEntry
|
||||
watchers map[string]*fileWatcher
|
||||
@@ -105,8 +111,14 @@ func NewFileSourcePlugin(
|
||||
config: opts,
|
||||
subscribers: make([]chan core.LogEntry, 0),
|
||||
watchers: make(map[string]*fileWatcher),
|
||||
ownSkipped: make(map[string]bool),
|
||||
logger: logger,
|
||||
}
|
||||
for _, f := range []*os.File{os.Stdout, os.Stderr} {
|
||||
if info, err := f.Stat(); err == nil && info.Mode().IsRegular() {
|
||||
fs.own = append(fs.own, info)
|
||||
}
|
||||
}
|
||||
fs.lastEntryTime.Store(time.Time{})
|
||||
|
||||
fs.session = proxy.CreateSession(
|
||||
@@ -388,15 +400,39 @@ func (fs *FileSource) scanFile() ([]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if re.MatchString(name) {
|
||||
files = append(files, filepath.Join(fs.config.Directory, name))
|
||||
path := filepath.Join(fs.config.Directory, entry.Name())
|
||||
if re.MatchString(entry.Name()) && !fs.isOwn(path) {
|
||||
files = append(files, path)
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// isOwn reports whether path is lw's own stdout or stderr, warning once per path
|
||||
func (fs *FileSource) isOwn(path string) bool {
|
||||
if len(fs.own) == 0 {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, own := range fs.own {
|
||||
if os.SameFile(info, own) {
|
||||
if !fs.ownSkipped[path] {
|
||||
fs.ownSkipped[path] = true
|
||||
fs.logger.Warn("msg", "Not following lw's own output",
|
||||
"component", "file_source",
|
||||
"instance_id", fs.id,
|
||||
"path", path)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// globToRegex converts a simple glob pattern to a regular expression.
|
||||
func globToRegex(glob string) string {
|
||||
regex := regexp.QuoteMeta(glob)
|
||||
|
||||
@@ -4,14 +4,41 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lixenwraith/logwisp/internal/config"
|
||||
"github.com/lixenwraith/logwisp/internal/core"
|
||||
"github.com/lixenwraith/logwisp/internal/testutil"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// A file source never follows the file lw's stdout or stderr is: `lw > out.log`
|
||||
// in the directory it follows would read its own output back, endlessly
|
||||
func TestFileSourceSkipsItsOwnOutput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
testutil.WriteFile(t, filepath.Join(dir, "app.log"), "")
|
||||
testutil.WriteFile(t, filepath.Join(dir, "out.log"), "")
|
||||
own, err := os.Stat(filepath.Join(dir, "out.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := &FileSource{
|
||||
config: &config.FileSourceOptions{Directory: dir, Pattern: "*.log"},
|
||||
own: []os.FileInfo{own},
|
||||
ownSkipped: map[string]bool{},
|
||||
logger: log.NewLogger(),
|
||||
}
|
||||
files, err := source.scanFile()
|
||||
if err != nil || len(files) != 1 || filepath.Base(files[0]) != "app.log" {
|
||||
t.Fatalf("scanFile = %v, %v; want app.log alone", files, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoppedWatcherReturnsNormally(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "session.jsonl")
|
||||
testutil.WriteFile(t, path, "")
|
||||
@@ -45,6 +72,25 @@ func TestRemoveWatcherPreservesReplacement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A line longer than core.MaxLogEntryBytes continues in the next entry, and the
|
||||
// lines around it are read once: the watcher used to stop at it for good
|
||||
func TestLongLineContinuesInTheNextEntry(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "app.log")
|
||||
long := strings.Repeat("x", core.MaxLogEntryBytes+10)
|
||||
testutil.WriteFile(t, path, "first\n"+long+"\nafter\n")
|
||||
var got []string
|
||||
w := newFileWatcher(path, true, true, func(e core.LogEntry) { got = append(got, e.Message) }, log.NewLogger())
|
||||
for range 2 {
|
||||
if err := w.checkFile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
want := []string{"first", long[:core.MaxLogEntryBytes], long[core.MaxLogEntryBytes:], "after"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("read %d entries, want %d: first, the long line in two, after", len(got), len(want))
|
||||
}
|
||||
}
|
||||
|
||||
// A rotated file reappears under its archive name with the same inode. Its
|
||||
// replacement watcher resumes where the original stopped, so a `from = "start"`
|
||||
// source does not re-emit every record the file already delivered.
|
||||
|
||||
@@ -251,6 +251,7 @@ func (w *fileWatcher) checkFile() error {
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), core.MaxLogEntryBytes)
|
||||
scanner.Split(scanLinesCapped)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
@@ -311,6 +312,17 @@ func (w *fileWatcher) checkFile() error {
|
||||
|
||||
// initPosition records the file's metadata and, unless the watcher was created
|
||||
// to read from the start, sets the initial read position to the end
|
||||
// scanLinesCapped is bufio.ScanLines, except that a line longer than
|
||||
// core.MaxLogEntryBytes continues in the next one, as on the console source:
|
||||
// the scanner would otherwise stop at it and every poll re-read the lines before.
|
||||
func scanLinesCapped(data []byte, atEOF bool) (int, []byte, error) {
|
||||
advance, token, err := bufio.ScanLines(data, atEOF)
|
||||
if advance == 0 && token == nil && err == nil && len(data) >= core.MaxLogEntryBytes {
|
||||
return core.MaxLogEntryBytes, data[:core.MaxLogEntryBytes], nil
|
||||
}
|
||||
return advance, token, err
|
||||
}
|
||||
|
||||
func (w *fileWatcher) initPosition() error {
|
||||
file, err := os.Open(w.directory)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,8 +36,8 @@ func Server(o *config.TLSOptions, host string) (*tls.Config, error) {
|
||||
}
|
||||
files, issuer := o.CertFile != "" || o.KeyFile != "", o.IssuerCertFile != "" || o.IssuerKeyFile != ""
|
||||
switch {
|
||||
case o.PinSHA256 != "":
|
||||
return nil, fmt.Errorf("tls: pin_sha256 applies to dialers")
|
||||
case o.CAFile != "" || o.ServerName != "" || o.InsecureSkipVerify || o.PinSHA256 != "":
|
||||
return nil, fmt.Errorf("tls: ca_file, server_name, insecure_skip_verify and pin_sha256 apply to dialers")
|
||||
case files && (issuer || o.SelfSigned) || issuer && o.SelfSigned:
|
||||
return nil, fmt.Errorf("tls: set one of cert_file and key_file, self_signed, or issuer_cert_file and issuer_key_file")
|
||||
case len(o.Hosts) > 0 && !issuer && !o.SelfSigned:
|
||||
@@ -91,8 +91,8 @@ func Client(o *config.TLSOptions, host string) (*tls.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
switch {
|
||||
case o.SelfSigned || o.IssuerCertFile != "" || o.IssuerKeyFile != "" || len(o.Hosts) > 0:
|
||||
return nil, fmt.Errorf("tls: self_signed, issuer_cert_file, issuer_key_file and hosts apply to listeners")
|
||||
case o.SelfSigned || o.IssuerCertFile != "" || o.IssuerKeyFile != "" || len(o.Hosts) > 0 || o.ClientAuth || o.ClientCAFile != "":
|
||||
return nil, fmt.Errorf("tls: self_signed, issuer_cert_file, issuer_key_file, hosts, client_auth and client_ca_file apply to listeners")
|
||||
case o.PinSHA256 != "" && (o.CAFile != "" || o.InsecureSkipVerify):
|
||||
return nil, fmt.Errorf("tls: pin_sha256 replaces ca_file and insecure_skip_verify: set one")
|
||||
}
|
||||
|
||||
@@ -179,9 +179,11 @@ func TestTLSOptionConflicts(t *testing.T) {
|
||||
{config.TLSOptions{IssuerCertFile: "c"}, true, "must be set together"},
|
||||
{config.TLSOptions{CertFile: "c"}, true, "must be set together"},
|
||||
{config.TLSOptions{Hosts: []string{"a"}, CertFile: "c", KeyFile: "k"}, true, "hosts applies to"},
|
||||
{config.TLSOptions{SelfSigned: true, PinSHA256: pin}, true, "applies to dialers"},
|
||||
{config.TLSOptions{SelfSigned: true, PinSHA256: pin}, true, "apply to dialers"},
|
||||
{config.TLSOptions{SelfSigned: true, CAFile: "ca"}, true, "apply to dialers"},
|
||||
{config.TLSOptions{}, true, "listeners need"},
|
||||
{config.TLSOptions{SelfSigned: true}, false, "apply to listeners"},
|
||||
{config.TLSOptions{ClientAuth: true, ClientCAFile: "ca"}, false, "apply to listeners"},
|
||||
{config.TLSOptions{PinSHA256: pin, CAFile: "ca"}, false, "set one"},
|
||||
{config.TLSOptions{PinSHA256: pin, InsecureSkipVerify: true}, false, "set one"},
|
||||
{config.TLSOptions{PinSHA256: "sha256//short"}, false, "want sha256//BASE64"},
|
||||
|
||||
Reference in new issue
Block a user