Skip to content

Commit

Permalink
Merge pull request #17 from simplifi/global-logger
Browse files Browse the repository at this point in the history
Global logger
  • Loading branch information
cjonesy authored Oct 9, 2024
2 parents f38c347 + 9af8888 commit 2f83892
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 31 deletions.
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

0 comments on commit 2f83892

Please sign in to comment.