v0.22.1 cli flag and behavior, and test output improvement
This commit is contained in:
1 parent
71d1b82f83
commit
4eba6fc834
89 files changed
+1531
-661
No files matched your search
@@ -244,6 +244,7 @@ type NullSinkOptions struct{}
|
||||
type ConsoleSinkOptions struct {
|
||||
Target string `toml:"target"` // "stdout", "stderr"
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
Escape string `toml:"escape"` // control characters as <hex>: "auto" (on a terminal), "always", "never"
|
||||
}
|
||||
|
||||
// FileSinkOptions defines settings for a file-based sink
|
||||
|
||||
+23
-49
@@ -3,11 +3,12 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"logwisp/internal/core"
|
||||
"github.com/lixenwraith/logwisp/internal/core"
|
||||
|
||||
lconfig "github.com/lixenwraith/config"
|
||||
)
|
||||
@@ -44,6 +45,11 @@ func Load(args []string) (*Manager, error) {
|
||||
}
|
||||
m := &Manager{path: configPath, explicit: isExplicit, specs: specs}
|
||||
initial := defaults()
|
||||
if _, err := os.Stat(configPath); errors.Is(err, fs.ErrNotExist) {
|
||||
// Without a file lw is a command-line tool: warnings and errors only
|
||||
initial.Logging.Level = "warn"
|
||||
initial.StatusReporter = false
|
||||
}
|
||||
cfg, err := lconfig.NewBuilder().
|
||||
WithTarget(initial).
|
||||
WithEnvPrefix("LOGWISP_").
|
||||
@@ -134,20 +140,13 @@ func (m *Manager) Watch() <-chan string {
|
||||
|
||||
func (m *Manager) Close() { m.config.StopAutoUpdate() }
|
||||
|
||||
// defaults provides the default configuration values for the application
|
||||
// defaults provides the default configuration values for the application.
|
||||
// Data goes to stdout, so lw's own log goes to stderr.
|
||||
func defaults() *Config {
|
||||
return &Config{
|
||||
// Top-level flag defaults
|
||||
ShowVersion: false,
|
||||
Quiet: false,
|
||||
|
||||
// Runtime behavior defaults
|
||||
StatusReporter: true,
|
||||
ConfigAutoReload: false,
|
||||
|
||||
// Existing defaults
|
||||
StatusReporter: true,
|
||||
Logging: &LogConfig{
|
||||
Output: "stdout",
|
||||
Output: "stderr",
|
||||
Level: "info",
|
||||
Format: "txt",
|
||||
File: &LogFileConfig{
|
||||
@@ -158,45 +157,20 @@ func defaults() *Config {
|
||||
RetentionHours: 168, // 7 days
|
||||
},
|
||||
Console: &LogConsoleConfig{
|
||||
Target: "stdout",
|
||||
},
|
||||
},
|
||||
Pipelines: []PipelineConfig{
|
||||
{
|
||||
Name: "default_pipeline",
|
||||
Flow: &FlowConfig{
|
||||
RateLimit: &RateLimitConfig{
|
||||
Rate: 5,
|
||||
Burst: 10,
|
||||
Policy: "drop",
|
||||
MaxEntrySizeBytes: 65536,
|
||||
},
|
||||
Format: &FormatConfig{
|
||||
Type: "json",
|
||||
SanitizerPolicy: "json",
|
||||
},
|
||||
},
|
||||
PluginSources: []PluginSourceConfig{
|
||||
{
|
||||
ID: "default_source",
|
||||
Type: "random",
|
||||
Config: map[string]any{
|
||||
"special": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginSinks: []PluginSinkConfig{
|
||||
{
|
||||
ID: "default_sink",
|
||||
Type: "console",
|
||||
Config: map[string]any{
|
||||
"target": "stdout",
|
||||
"buffer_size": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
Target: "stderr",
|
||||
},
|
||||
},
|
||||
Pipelines: []PipelineConfig{pipeDefault("pipe")},
|
||||
}
|
||||
}
|
||||
|
||||
// pipeDefault is a filter in the Unix sense: stdin to stdout, line for line
|
||||
func pipeDefault(name string) PipelineConfig {
|
||||
return PipelineConfig{
|
||||
Name: name,
|
||||
Flow: &FlowConfig{Format: &FormatConfig{Type: "raw"}},
|
||||
PluginSources: []PluginSourceConfig{{ID: "stdin", Type: "console", Config: map[string]any{}}},
|
||||
PluginSinks: []PluginSinkConfig{{ID: "stdout", Type: "console", Config: map[string]any{}}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"logwisp/internal/testutil"
|
||||
"github.com/lixenwraith/logwisp/internal/testutil"
|
||||
)
|
||||
|
||||
func TestResolveConfigArguments(t *testing.T) {
|
||||
@@ -56,9 +56,9 @@ func TestLoadPrecedenceAndDetachedSnapshots(t *testing.T) {
|
||||
t.Fatalf("snapshot: %+v %v", first, err)
|
||||
}
|
||||
first.Logging.Level = "mutated"
|
||||
first.Pipelines[0].PluginSources[0].Config["special"] = false
|
||||
first.Pipelines[0].PluginSources[0].Config["buffer_size"] = 1
|
||||
second, err := m.Snapshot()
|
||||
if err != nil || second.Logging.Level != "warn" || second.Pipelines[0].PluginSources[0].Config["special"] != true {
|
||||
if _, aliased := second.Pipelines[0].PluginSources[0].Config["buffer_size"]; err != nil || second.Logging.Level != "warn" || aliased {
|
||||
t.Fatalf("snapshot aliases another result: %+v %v", second, err)
|
||||
}
|
||||
env, err := Load([]string{"-c=selected.toml"})
|
||||
@@ -268,3 +268,36 @@ port = 9000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A file makes lw a service, logging at info with the status reporter; without
|
||||
// one it is a command-line tool that reports warnings and errors only.
|
||||
func TestNoConfigFileMeansQuietLogging(t *testing.T) {
|
||||
isolateConfig(t)
|
||||
for _, c := range []struct {
|
||||
file, level string
|
||||
reporter bool
|
||||
}{{"", "warn", false}, {"[logging]\noutput = \"stderr\"\n", "info", true}} {
|
||||
if c.file != "" {
|
||||
testutil.WriteFile(t, "logwisp.toml", c.file)
|
||||
}
|
||||
m, err := Load(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := m.Snapshot()
|
||||
m.Close()
|
||||
if err != nil || cfg.Logging.Level != c.level || cfg.StatusReporter != c.reporter || cfg.Logging.Output != "stderr" {
|
||||
t.Fatalf("file %q: %+v %+v %v", c.file, cfg, cfg.Logging, err)
|
||||
}
|
||||
}
|
||||
t.Setenv("LOGWISP_LOGGING_LEVEL", "debug")
|
||||
os.Remove("logwisp.toml")
|
||||
m, err := Load(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer m.Close()
|
||||
if cfg, err := m.Snapshot(); err != nil || cfg.Logging.Level != "debug" {
|
||||
t.Fatalf("environment over the file-less default: %+v %v", cfg, err)
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -131,7 +131,8 @@ func envPipelineSpecs() []pipelineSpec {
|
||||
}
|
||||
|
||||
// buildPipelines turns specs, in order, into pipelines; specs before the first
|
||||
// --pipeline go to one named "cli". Every call returns fresh maps.
|
||||
// --pipeline go to one named "cli". A pipeline without a source reads stdin,
|
||||
// one without a sink writes stdout. Every call returns fresh maps.
|
||||
func buildPipelines(specs []pipelineSpec) ([]PipelineConfig, error) {
|
||||
var pipelines []PipelineConfig
|
||||
var seen map[*specKind]bool
|
||||
@@ -160,6 +161,15 @@ func buildPipelines(specs []pipelineSpec) ([]PipelineConfig, error) {
|
||||
return nil, fmt.Errorf("%s %s: %w", s.name, s.value, err)
|
||||
}
|
||||
}
|
||||
for i := range pipelines {
|
||||
pipe := pipeDefault(pipelines[i].Name)
|
||||
if len(pipelines[i].PluginSources) == 0 {
|
||||
pipelines[i].PluginSources = pipe.PluginSources
|
||||
}
|
||||
if len(pipelines[i].PluginSinks) == 0 {
|
||||
pipelines[i].PluginSinks = pipe.PluginSinks
|
||||
}
|
||||
}
|
||||
return pipelines, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"logwisp/internal/testutil"
|
||||
"github.com/lixenwraith/logwisp/internal/testutil"
|
||||
)
|
||||
|
||||
func loadPipelines(t *testing.T, args ...string) []PipelineConfig {
|
||||
@@ -160,6 +161,21 @@ func TestSpecPipelinesReplaceFilePipelines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A spec pipeline without a source reads stdin and one without a sink writes
|
||||
// stdout, as a filter does; two pipelines cannot both read stdin.
|
||||
func TestSpecPipelinesDefaultToStdio(t *testing.T) {
|
||||
isolateConfig(t)
|
||||
got := loadPipelines(t, "--filter", "include,patterns=ERROR")
|
||||
if p := got[0]; len(p.PluginSources) != 1 || p.PluginSources[0].Type != "console" || p.PluginSources[0].ID != "stdin" ||
|
||||
len(p.PluginSinks) != 1 || p.PluginSinks[0].Type != "console" || p.PluginSinks[0].ID != "stdout" {
|
||||
t.Fatalf("pipeline: %+v", p)
|
||||
}
|
||||
_, err := Load([]string{"--pipeline", "a", "--sink", "null", "--pipeline", "b", "--sink", "null"})
|
||||
if err == nil || !strings.Contains(err.Error(), `console source already reads stdin in pipeline "a"`) {
|
||||
t.Fatalf("second stdin reader: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Without a file a reload still rebuilds; once the discovered default
|
||||
// appears it is read, and its pipelines still yield to the specs. Its later
|
||||
// removal fails the reload instead of keeping the removed file's values.
|
||||
@@ -171,13 +187,13 @@ func TestReloadKeepsSpecPipelines(t *testing.T) {
|
||||
}
|
||||
defer m.Close()
|
||||
first, err := m.Reload()
|
||||
if err != nil || !first.StatusReporter {
|
||||
if err != nil || first.StatusReporter {
|
||||
t.Fatalf("reload without a file: %+v %v", first, err)
|
||||
}
|
||||
first.Pipelines[0].PluginSources[0].Config["special"] = "mutated"
|
||||
testutil.WriteFile(t, "logwisp.toml", "status_reporter = false\n[[pipelines]]\nname = \"file\"\n")
|
||||
testutil.WriteFile(t, "logwisp.toml", "status_reporter = true\n[[pipelines]]\nname = \"file\"\n")
|
||||
next, err := m.Reload()
|
||||
if err != nil || next.StatusReporter || len(next.Pipelines) != 1 || next.Pipelines[0].Name != "cli" ||
|
||||
if err != nil || !next.StatusReporter || len(next.Pipelines) != 1 || next.Pipelines[0].Name != "cli" ||
|
||||
next.Pipelines[0].PluginSources[0].Config["special"] != "true" {
|
||||
t.Fatalf("reload: %+v %v", next, err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package config
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"logwisp/internal/testutil"
|
||||
"github.com/lixenwraith/logwisp/internal/testutil"
|
||||
)
|
||||
|
||||
func isolateConfig(t *testing.T) {
|
||||
|
||||
@@ -38,6 +38,7 @@ func ValidateConfig(cfg *Config) error {
|
||||
return fmt.Errorf("logging: %w", err)
|
||||
}
|
||||
|
||||
stdinReader := ""
|
||||
for i, p := range cfg.Pipelines {
|
||||
if err := lconfig.NonEmpty(p.Name); err != nil {
|
||||
return fmt.Errorf("pipeline[%d].name: %w", i, err)
|
||||
@@ -48,6 +49,15 @@ func ValidateConfig(cfg *Config) error {
|
||||
if len(p.PluginSinks) == 0 {
|
||||
return fmt.Errorf("pipeline[%d]: no sinks defined", i)
|
||||
}
|
||||
for _, src := range p.PluginSources {
|
||||
if src.Type != "console" {
|
||||
continue
|
||||
}
|
||||
if stdinReader != "" {
|
||||
return fmt.Errorf("pipeline %q: a console source already reads stdin in pipeline %q", p.Name, stdinReader)
|
||||
}
|
||||
stdinReader = p.Name
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in new issue
Block a user