fix(config): ensure log file path is set
Fix a bug introduced in 6952516 which led to the app being unable to start if logging was enabled but no explicit path was set. In this case, the expected behaviour is to fallback to a log file in the XDG file hierarchy, but this was lost due to broken config file defaults handling. This commit separates the behaviour when setting defaults when reading an existing configuration, from those set when creating a brand new configuration.
This commit is contained in:
parent
cd2c339c10
commit
266a9307d2
1
go.mod
1
go.mod
@ -6,6 +6,7 @@ require (
|
|||||||
github.com/docker/docker v28.0.1+incompatible
|
github.com/docker/docker v28.0.1+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/gdamore/tcell/v2 v2.8.1
|
github.com/gdamore/tcell/v2 v2.8.1
|
||||||
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57
|
github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import "cmp"
|
||||||
|
|
||||||
// Destination holds the configuration for a destination.
|
// Destination holds the configuration for a destination.
|
||||||
type Destination struct {
|
type Destination struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
@ -10,6 +12,14 @@ type Destination struct {
|
|||||||
type LogFile struct {
|
type LogFile struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
Path string `yaml:"path,omitempty"`
|
Path string `yaml:"path,omitempty"`
|
||||||
|
|
||||||
|
defaultPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPath returns the path to the log file. If the path is not set, it
|
||||||
|
// returns the default log path.
|
||||||
|
func (l LogFile) GetPath() string {
|
||||||
|
return cmp.Or(l.Path, l.defaultPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RTMPSource holds the configuration for the RTMP source.
|
// RTMPSource holds the configuration for the RTMP source.
|
||||||
|
@ -47,6 +47,7 @@ func NewService(configDirFunc ConfigDirFunc, chanSize int) (*Service, error) {
|
|||||||
return nil, fmt.Errorf("app config dir: %w", err)
|
return nil, fmt.Errorf("app config dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: inject StateDirFunc
|
||||||
appStateDir, err := createAppStateDir()
|
appStateDir, err := createAppStateDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("app state dir: %w", err)
|
return nil, fmt.Errorf("app state dir: %w", err)
|
||||||
@ -58,7 +59,7 @@ func NewService(configDirFunc ConfigDirFunc, chanSize int) (*Service, error) {
|
|||||||
configC: make(chan Config, chanSize),
|
configC: make(chan Config, chanSize),
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.setDefaults(&svc.current)
|
svc.populateConfigOnBuild(&svc.current)
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
@ -127,6 +128,7 @@ func (s *Service) readConfig() (cfg Config, _ error) {
|
|||||||
return cfg, fmt.Errorf("unmarshal: %w", err)
|
return cfg, fmt.Errorf("unmarshal: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.populateConfigOnRead(&cfg)
|
||||||
if err = validate(cfg); err != nil {
|
if err = validate(cfg); err != nil {
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
@ -138,7 +140,7 @@ func (s *Service) readConfig() (cfg Config, _ error) {
|
|||||||
|
|
||||||
func (s *Service) writeDefaultConfig() (Config, error) {
|
func (s *Service) writeDefaultConfig() (Config, error) {
|
||||||
var cfg Config
|
var cfg Config
|
||||||
s.setDefaults(&cfg)
|
s.populateConfigOnBuild(&cfg)
|
||||||
|
|
||||||
cfgBytes, err := marshalConfig(cfg)
|
cfgBytes, err := marshalConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -175,10 +177,24 @@ func (s *Service) writeConfig(cfgBytes []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setDefaults is called to set default values for a new, empty configuration.
|
// populateConfigOnBuild is called to set default values for a new, empty
|
||||||
func (s *Service) setDefaults(cfg *Config) {
|
// configuration.
|
||||||
|
//
|
||||||
|
// This function may set exported fields to arbitrary values.
|
||||||
|
func (s *Service) populateConfigOnBuild(cfg *Config) {
|
||||||
cfg.Sources.RTMP.Enabled = true
|
cfg.Sources.RTMP.Enabled = true
|
||||||
cfg.Sources.RTMP.StreamKey = "live"
|
cfg.Sources.RTMP.StreamKey = "live"
|
||||||
|
|
||||||
|
s.populateConfigOnRead(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateConfigOnRead is called to set default values for a configuration
|
||||||
|
// read from an existing file.
|
||||||
|
//
|
||||||
|
// This function should not update any exported values, which would be a
|
||||||
|
// confusing experience for the user.
|
||||||
|
func (s *Service) populateConfigOnRead(cfg *Config) {
|
||||||
|
cfg.LogFile.defaultPath = filepath.Join(s.appStateDir, "octoplex.log")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: validate URL format
|
// TODO: validate URL format
|
||||||
|
@ -4,10 +4,13 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.netflux.io/rob/octoplex/internal/config"
|
"git.netflux.io/rob/octoplex/internal/config"
|
||||||
"git.netflux.io/rob/octoplex/internal/shortid"
|
"git.netflux.io/rob/octoplex/internal/shortid"
|
||||||
|
gocmp "github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -19,6 +22,9 @@ var configComplete []byte
|
|||||||
//go:embed testdata/logfile.yml
|
//go:embed testdata/logfile.yml
|
||||||
var configLogfile []byte
|
var configLogfile []byte
|
||||||
|
|
||||||
|
//go:embed testdata/no-logfile.yml
|
||||||
|
var configNoLogfile []byte
|
||||||
|
|
||||||
//go:embed testdata/invalid-destination-url.yml
|
//go:embed testdata/invalid-destination-url.yml
|
||||||
var configInvalidDestinationURL []byte
|
var configInvalidDestinationURL []byte
|
||||||
|
|
||||||
@ -49,7 +55,8 @@ func TestConfigServiceCreateConfig(t *testing.T) {
|
|||||||
|
|
||||||
cfg, err := service.ReadOrCreateConfig()
|
cfg, err := service.ReadOrCreateConfig()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, cfg.LogFile, "expected no log file")
|
require.False(t, cfg.LogFile.Enabled, "expected logging to be disabled")
|
||||||
|
require.Empty(t, cfg.LogFile.Path, "expected no log file")
|
||||||
|
|
||||||
p := filepath.Join(systemConfigDir, "octoplex", "config.yaml")
|
p := filepath.Join(systemConfigDir, "octoplex", "config.yaml")
|
||||||
cfgBytes, err := os.ReadFile(p)
|
cfgBytes, err := os.ReadFile(p)
|
||||||
@ -71,8 +78,9 @@ func TestConfigServiceReadConfig(t *testing.T) {
|
|||||||
name: "complete",
|
name: "complete",
|
||||||
configBytes: configComplete,
|
configBytes: configComplete,
|
||||||
want: func(t *testing.T, cfg config.Config) {
|
want: func(t *testing.T, cfg config.Config) {
|
||||||
require.Equal(
|
require.Empty(
|
||||||
t,
|
t,
|
||||||
|
gocmp.Diff(
|
||||||
config.Config{
|
config.Config{
|
||||||
LogFile: config.LogFile{
|
LogFile: config.LogFile{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@ -90,7 +98,11 @@ func TestConfigServiceReadConfig(t *testing.T) {
|
|||||||
URL: "rtmp://rtmp.example.com:1935/live",
|
URL: "rtmp://rtmp.example.com:1935/live",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, cfg)
|
},
|
||||||
|
cfg,
|
||||||
|
cmpopts.IgnoreUnexported(config.LogFile{}),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -100,6 +112,13 @@ func TestConfigServiceReadConfig(t *testing.T) {
|
|||||||
assert.Equal(t, "/tmp/octoplex.log", cfg.LogFile.Path)
|
assert.Equal(t, "/tmp/octoplex.log", cfg.LogFile.Path)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "logging enabled, no logfile",
|
||||||
|
configBytes: configNoLogfile,
|
||||||
|
want: func(t *testing.T, cfg config.Config) {
|
||||||
|
assert.True(t, strings.HasSuffix(cfg.LogFile.GetPath(), "/octoplex/octoplex.log"), "expected %q to end with /tmp/octoplex.log", cfg.LogFile.GetPath())
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid destination URL",
|
name: "invalid destination URL",
|
||||||
configBytes: configInvalidDestinationURL,
|
configBytes: configInvalidDestinationURL,
|
||||||
|
5
internal/config/testdata/no-logfile.yml
vendored
Normal file
5
internal/config/testdata/no-logfile.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
logfile:
|
||||||
|
enabled: true
|
||||||
|
destinations:
|
||||||
|
- url: rtmp://rtmp.example.com:1935/live
|
2
main.go
2
main.go
@ -162,7 +162,7 @@ func buildLogger(cfg config.LogFile) (*slog.Logger, error) {
|
|||||||
return slog.New(slog.DiscardHandler), nil
|
return slog.New(slog.DiscardHandler), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fptr, err := os.OpenFile(cfg.Path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
fptr, err := os.OpenFile(cfg.GetPath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error opening log file: %w", err)
|
return nil, fmt.Errorf("error opening log file: %w", err)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user