Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global logger #17

Merged
merged 3 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/goverseer/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,20 @@ type ExecutionerConfig struct {
Config map[string]interface{}
}

// LoggerConfig is the configuration for the global logger
type LoggerConfig struct {
// Level is the log level
Level string
}

// Config is the configuration for a watcher and executioner
type Config struct {
// Name is the name of the configuration, this will show up in logs
Name string

// Logger is the configuration for the logger
Logger LoggerConfig

// Watcher is the configuration for the watcher
// it is dynamic because the configuration can be different for each watcher
Watcher WatcherConfig
Expand Down
19 changes: 19 additions & 0 deletions internal/goverseer/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ executioner:
// watcher and a log executioner with no configuration provided
testConfigWatcherToLogNoConfig = `
name: WatcherToLog
watcher:
type: time
executioner:
type: log
`
// testConfigLogger is a basic test configuration for testing
// the main logger configuration
testConfigLogger = `
name: Logger
logger:
level: debug
watcher:
type: time
executioner:
Expand Down Expand Up @@ -75,4 +86,12 @@ func TestFromFile(t *testing.T) {
"An executioner with no Config should have an empty map for the value")
assert.Equal(t, map[string]interface{}(nil), config.Watcher.Config,
"A watcher with no Config should have an empty map for the value")

// Test with a config with logger configuration
_, testConfig = writeTestConfigs(t, testConfigLogger)
config, err = FromFile(testConfig)
assert.NoError(t, err,
"Parsing a config file with a valid logger config should not error")
assert.Equal(t, "debug", config.Logger.Level,
"An config file with logger configuration should parse correctly")
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"os/exec"
"strings"

"github.com/charmbracelet/log"
"github.com/simplifi/goverseer/internal/goverseer/config"
"github.com/simplifi/goverseer/internal/goverseer/logger"
)

const (
Expand Down Expand Up @@ -153,37 +153,41 @@ func (e *ShellExecutioner) enableOutputStreaming(cmd *exec.Cmd) error {
if err != nil {
return fmt.Errorf("error creating stdout pipe: %w", err)
}
go e.streamOutput(stdOut, log.InfoLevel)
go e.streamOutput(stdOut, "info")

// Stream stderr of the command to the logger
stdErr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("error creating stderr pipe: %w", err)
}
go e.streamOutput(stdErr, log.ErrorLevel)
go e.streamOutput(stdErr, "error")

return nil
}

// streamOutput streams the output of a pipe to the logger
func (e *ShellExecutioner) streamOutput(pipe io.ReadCloser, logLevel log.Level) {
func (e *ShellExecutioner) streamOutput(pipe io.ReadCloser, outputType string) {
scanner := bufio.NewScanner(pipe)
for {
select {
case <-e.ctx.Done():
log.Info("stopping output scanner")
logger.Log.Info("stopping output scanner")
return
default:
if scanner.Scan() {
log.Log(logLevel, "command", "output", scanner.Text())
if outputType == "error" {
logger.Log.Error("command", "output", scanner.Text())
} else {
logger.Log.Info("command", "output", scanner.Text())
}
} else {
if err := scanner.Err(); err != nil {
// Avoid logging errors if the context was canceled mid-scan
// This will happen when the executioner is being stopped
if e.ctx.Err() == context.Canceled {
continue
}
log.Error("error reading output", "err", err)
logger.Log.Error("error reading output", "err", err)
}
return
}
Expand All @@ -204,7 +208,7 @@ func (e *ShellExecutioner) writeTempData(data interface{}) (string, error) {
return "", fmt.Errorf("error writing data to temp file: %w", err)
}

log.Info("wrote data to work dir", "path", tempDataFile.Name())
logger.Log.Info("wrote data to work dir", "path", tempDataFile.Name())
return tempDataFile.Name(), nil
}

Expand All @@ -224,7 +228,7 @@ func (e *ShellExecutioner) Execute(data interface{}) error {
}

if e.PersistData {
log.Warn("persisting data", "path", tempDataPath)
logger.Log.Warn("persisting data", "path", tempDataPath)
} else {
defer os.Remove(tempDataPath)
}
Expand Down Expand Up @@ -270,6 +274,6 @@ func (e *ShellExecutioner) Execute(data interface{}) error {

// Stop signals the executioner to stop
func (e *ShellExecutioner) Stop() {
log.Info("shutting down executor")
logger.Log.Info("shutting down executor")
close(e.stop)
}
46 changes: 46 additions & 0 deletions internal/goverseer/logger/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package logger

import (
"os"

"github.com/charmbracelet/log"
)

// Global logger instance
var Log *log.Logger

const (
// DefaultLogLevel is the default log level for the logger
DefaultLogLevel = log.InfoLevel
)

// init initializes the global logger instance. It sets the output to stdout
// and the log level to the DefaultLogLevel.
func init() {
Log = log.New(os.Stdout)
Log.SetLevel(DefaultLogLevel)
}

// SetLevel sets the logging level for the global logger.
// It accepts a string representation of the log level,
// such as "debug", "info", "warn", "error", or "fatal".
// If an invalid level is provided, it defaults to the DefaultLogLevel
// and logs a warning message.
func SetLevel(level string) {
// Attempt to parse the provided log level string into a log.Level value.
lvl, err := log.ParseLevel(level)

// If the parsing fails, it indicates an invalid log level was provided.
if err != nil {
Log.Warn("Invalid log level provided, using default instead",
"level", level,
"default", DefaultLogLevel,
"error", err)

// Set the log level to the default level.
lvl = DefaultLogLevel
}

// Set the log level of the global logger to the determined level.
Log.SetLevel(lvl)
}
12 changes: 7 additions & 5 deletions internal/goverseer/overseer/overseer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package overseer
import (
"sync"

"github.com/charmbracelet/log"
"github.com/simplifi/goverseer/internal/goverseer/config"
"github.com/simplifi/goverseer/internal/goverseer/executioner"
"github.com/simplifi/goverseer/internal/goverseer/logger"
"github.com/simplifi/goverseer/internal/goverseer/watcher"
)

Expand All @@ -31,6 +31,8 @@ type Overseer struct {

// New creates a new Overseer
func New(cfg *config.Config) (*Overseer, error) {
logger.SetLevel(cfg.Logger.Level)

watcher, err := watcher.New(cfg)
if err != nil {
return nil, err
Expand Down Expand Up @@ -69,7 +71,7 @@ func (o *Overseer) Run() {
go func() {
defer o.waitGroup.Done()
if err := o.executioner.Execute(data); err != nil {
log.Error("error running executioner", "err", err)
logger.Log.Error("error running executioner", "err", err)
}
}()
}
Expand All @@ -78,14 +80,14 @@ func (o *Overseer) Run() {

// Stop signals the overseer to stop
func (o *Overseer) Stop() {
log.Info("shutting down overseer")
logger.Log.Info("shutting down overseer")
close(o.stop)
o.watcher.Stop()
o.executioner.Stop()

log.Info("waiting for overseer to finish")
logger.Log.Info("waiting for overseer to finish")
// Wait here so we don't close the changes channel before the executioner is done
o.waitGroup.Wait()
log.Info("done")
logger.Log.Info("done")
close(o.change)
}
40 changes: 38 additions & 2 deletions internal/goverseer/overseer/overseer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import (
"testing"
"time"

"github.com/charmbracelet/log"
"github.com/simplifi/goverseer/internal/goverseer/config"
"github.com/simplifi/goverseer/internal/goverseer/logger"
"github.com/stretchr/testify/assert"
)

// TestOverseer tests the Overseer
func TestOverseer(t *testing.T) {
// TestOverseer_Run tests the Overseer's Run function
func TestOverseer_Run(t *testing.T) {
cfg := &config.Config{
Name: "TestManager",
Watcher: config.WatcherConfig{
Expand Down Expand Up @@ -45,3 +48,36 @@ func TestOverseer(t *testing.T) {
overseer.Stop()
wg.Wait()
}

func TestOverseer_New(t *testing.T) {
cfg := &config.Config{
Name: "TestManager",
Watcher: config.WatcherConfig{
Type: "time",
Config: map[string]interface{}{
"poll_seconds": 1,
},
},
Executioner: config.ExecutionerConfig{
Type: "log",
Config: map[string]interface{}{
"tag": "test",
},
},
}

_, err := New(cfg)
assert.NoError(t, err,
"Creating a new Overseer with no logger config should not error")
assert.Equal(t, logger.DefaultLogLevel, logger.Log.GetLevel(),
"Creating a new Overseer with no logger config should set the default log level")

cfg.Logger = config.LoggerConfig{
Level: "debug",
}
_, err = New(cfg)
assert.NoError(t, err,
"Creating a new Overseer from a valid config should not error")
assert.Equal(t, log.DebugLevel, logger.Log.GetLevel(),
"Creating a new Overseer should set the configured log level")
}
10 changes: 5 additions & 5 deletions internal/goverseer/watcher/file_watcher/file_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"os"
"time"

"github.com/charmbracelet/log"
"github.com/simplifi/goverseer/internal/goverseer/config"
"github.com/simplifi/goverseer/internal/goverseer/logger"
)

const (
Expand Down Expand Up @@ -91,7 +91,7 @@ func New(cfg config.Config) (*FileWatcher, error) {
// Watch watches the file for changes and sends the path to the changes channel
// The changes channel is where the path to the file is sent when it changes
func (w *FileWatcher) Watch(changes chan interface{}) {
log.Info("starting watcher")
logger.Log.Info("starting watcher")

for {
select {
Expand All @@ -100,12 +100,12 @@ func (w *FileWatcher) Watch(changes chan interface{}) {
case <-time.After(time.Duration(w.PollSeconds) * time.Second):
info, err := os.Stat(w.Path)
if err != nil {
log.Error("error getting file info",
logger.Log.Error("error getting file info",
"path", w.Path,
"err", err)
}
if err == nil && info.ModTime().After(w.lastValue) {
log.Info("file changed",
logger.Log.Info("file changed",
"path", w.Path,
"mod_time", info.ModTime())
w.lastValue = info.ModTime()
Expand All @@ -117,6 +117,6 @@ func (w *FileWatcher) Watch(changes chan interface{}) {

// Stop signals the watcher to stop
func (w *FileWatcher) Stop() {
log.Info("shutting down watcher")
logger.Log.Info("shutting down watcher")
close(w.stop)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"net/http"
"time"

"github.com/charmbracelet/log"
"github.com/simplifi/goverseer/internal/goverseer/config"
"github.com/simplifi/goverseer/internal/goverseer/logger"
)

const (
Expand Down Expand Up @@ -219,7 +219,7 @@ func (w *GceMetadataWatcher) getMetadata() (*gceMetadataResponse, error) {
// Watch watches the GCE metadata for changes and sends value to changes channel
// The changes channel is where the value is sent when it changes
func (w *GceMetadataWatcher) Watch(change chan interface{}) {
log.Info("starting watcher")
logger.Log.Info("starting watcher")

for {
select {
Expand All @@ -234,7 +234,7 @@ func (w *GceMetadataWatcher) Watch(change chan interface{}) {
continue
}

log.Error("error getting metadata", "err", err)
logger.Log.Error("error getting metadata", "err", err)

// Usually getMetadata opens up a connection to the metadata server
// and waits for a change. If there is an error we want to wait for a
Expand All @@ -247,7 +247,7 @@ func (w *GceMetadataWatcher) Watch(change chan interface{}) {

// Only send a change if it has actually changed by comparing etags
if w.lastETag != gceMetadata.etag {
log.Info("change detected",
logger.Log.Info("change detected",
"key", w.Key,
"etag", gceMetadata.etag,
"previous_etag", w.lastETag)
Expand All @@ -262,6 +262,6 @@ func (w *GceMetadataWatcher) Watch(change chan interface{}) {

// Stop signals the watcher to stop
func (w *GceMetadataWatcher) Stop() {
log.Info("shutting down watcher")
logger.Log.Info("shutting down watcher")
w.cancel()
}
Loading