forked from kuskoman/logstash-exporter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement startup manager with hot reload (kuskoman#360)
* Add startup manager * Refactor startup manager * Try to get reload working * Fix prometheus reloading * Remove comments etc * Make hot reload functional * Fix for vim * Make it optable via hot-reload flag * Change to small size letters * Make file watcher react to modification * Use default nil value for collector * Make changes according to review * Small fix * Check if configs are equal on reloading * Refactor according to the request * Change flag name to -watch * Fix linting * Ran go mod tidy * Exclude startup manager from codecov * Patch codecov plz shut up * Remove ignore section from codecov definition * Tidy gomod * Start refactoring hot reload functionality * Tidy gomod * More debug, still not working * Add a few more logs * Handle slog settings * Change logs to be lowercased * Make file watcher actually watch files * Handle unregistering prometheus * Start extracting file watcher * Fix file watcher test * Split code into even more packages * Fix linter errors * Start recreating startup manager * Upgrade dependencies * Add multiple reloads to startup manager * Add error handling to reloader * Try to trace why application is exitting after config change * Fix error handling in startup_manager * Refactor getting slog logger * Fix linter errors * Fix lint * Potential fix to tests * Fix tests * Fix linting * Fix patchcov maybe * Upgrade deps --------- Co-authored-by: Jakub Surdej <kubasurdej@gmail.com>
- Loading branch information
Showing
24 changed files
with
1,530 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,82 +1,34 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"log" | ||
"context" | ||
"log/slog" | ||
"os" | ||
"strconv" | ||
|
||
"github.com/joho/godotenv" | ||
"github.com/prometheus/client_golang/prometheus" | ||
|
||
"github.com/kuskoman/logstash-exporter/internal/server" | ||
"github.com/kuskoman/logstash-exporter/pkg/config" | ||
"github.com/kuskoman/logstash-exporter/pkg/manager" | ||
"github.com/kuskoman/logstash-exporter/internal/flags" | ||
"github.com/kuskoman/logstash-exporter/internal/startup_manager" | ||
) | ||
|
||
func main() { | ||
versionFlag := flag.Bool("version", false, "prints the version and exits") | ||
helpFlag := flag.Bool("help", false, "prints the help message and exits") | ||
configLocationFlag := flag.String("config", config.ExporterConfigLocation, "location of the exporter config file") | ||
|
||
flag.Parse() | ||
|
||
if *helpFlag { | ||
fmt.Printf("Usage of %s:\n", os.Args[0]) | ||
fmt.Println() | ||
fmt.Println("Flags:") | ||
flag.PrintDefaults() | ||
flagsConfig, err := flags.ParseFlags(os.Args[1:]) | ||
if err != nil { | ||
slog.Error("failed to parse flags", "err", err) | ||
return | ||
} | ||
|
||
if *versionFlag { | ||
fmt.Printf("%s\n", config.SemanticVersion) | ||
return | ||
if shouldExit := flags.HandleFlags(flagsConfig); shouldExit { | ||
os.Exit(0) | ||
} | ||
|
||
warn := godotenv.Load() | ||
|
||
exporterConfig, err := config.GetConfig(*configLocationFlag) | ||
startupManager, err := startup_manager.NewStartupManager(flagsConfig.ConfigLocation, flagsConfig) | ||
if err != nil { | ||
log.Fatalf("failed to get exporter config: %s", err) | ||
slog.Error("failed to create startup manager", "err", err) | ||
os.Exit(1) | ||
} | ||
|
||
logger, err := config.SetupSlog(exporterConfig.Logging.Level, exporterConfig.Logging.Format) | ||
if err != nil { | ||
log.Printf("failed to load .env file: %s", err) | ||
log.Fatalf("failed to setup slog: %s", err) | ||
} | ||
|
||
slog.SetDefault(logger) | ||
|
||
if warn != nil { | ||
slog.Warn("failed to load .env file", "error", warn) | ||
} | ||
|
||
host := exporterConfig.Server.Host | ||
port := strconv.Itoa(exporterConfig.Server.Port) | ||
|
||
slog.Debug("application starting... ") | ||
versionInfo := config.GetVersionInfo() | ||
slog.Info(versionInfo.String()) | ||
|
||
slog.Debug("http timeout", "timeout", exporterConfig.Logstash.HttpTimeout) | ||
|
||
collectorManager := manager.NewCollectorManager( | ||
exporterConfig.Logstash.Servers, | ||
exporterConfig.Logstash.HttpTimeout, | ||
exporterConfig.Logstash.HttpInsecure, | ||
) | ||
prometheus.MustRegister(collectorManager) | ||
|
||
appServer := server.NewAppServer(host, port, exporterConfig, exporterConfig.Logstash.HttpTimeout) | ||
|
||
slog.Info("starting server on", "host", host, "port", port) | ||
if err := appServer.ListenAndServe(); err != nil { | ||
slog.Error("failed to listen and serve", "err", err) | ||
ctx := context.TODO() | ||
if err := startupManager.Initialize(ctx); err != nil { | ||
slog.Error("critical error", "error", err) | ||
os.Exit(1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package file_utils | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func CreateTempFileInDir(t *testing.T, content, dir string) string { | ||
t.Helper() | ||
|
||
tempFile, err := os.CreateTemp(dir, "testfile") | ||
if err != nil { | ||
t.Fatalf("failed to create temp file: %v", err) | ||
} | ||
|
||
if _, err := tempFile.WriteString(content); err != nil { | ||
tempFile.Close() | ||
t.Fatalf("failed to write to temp file: %v", err) | ||
} | ||
|
||
if err := tempFile.Close(); err != nil { | ||
t.Fatalf("failed to close temp file: %v", err) | ||
} | ||
|
||
return tempFile.Name() | ||
} | ||
|
||
func AppendToFilex3(t *testing.T, file, content string) { | ||
t.Helper() | ||
// ************ Add a content three times to make sure its written *********** | ||
f, err := os.OpenFile(file, os.O_APPEND | os.O_WRONLY, 0644) | ||
if err != nil { | ||
t.Fatalf("failed to open a file: %v", err) | ||
} | ||
|
||
defer f.Close() | ||
|
||
if err := f.Sync(); err != nil { | ||
t.Fatalf("failed to sync file: %v", err) | ||
} | ||
|
||
if _, err := f.Write([]byte(content)); err != nil { | ||
f.Close() // ignore error; Write error takes precedence | ||
t.Fatalf("failed to write to file: %v", err) | ||
} | ||
if err := f.Sync(); err != nil { | ||
t.Fatalf("failed to sync file: %v", err) | ||
} | ||
|
||
time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete | ||
if _, err := f.Write([]byte(content)); err != nil { | ||
f.Close() // ignore error; Write error takes precedence | ||
t.Fatalf("failed to write to file: %v", err) | ||
} | ||
|
||
if err := f.Sync(); err != nil { | ||
t.Fatalf("failed to sync file: %v", err) | ||
} | ||
time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete | ||
if _, err := f.Write([]byte(content)); err != nil { | ||
f.Close() // ignore error; Write error takes precedence | ||
t.Fatalf("failed to write to file: %v", err) | ||
} | ||
if err := f.Sync(); err != nil { | ||
t.Fatalf("failed to sync file: %v", err) | ||
} | ||
} | ||
|
||
// CreateTempFile creates a temporary file with the given content and returns the path to it. | ||
func CreateTempFile(t *testing.T, content string) string { | ||
return CreateTempFileInDir(t, content, "") | ||
} | ||
|
||
func RemoveDir(t *testing.T, path string) { | ||
t.Helper() | ||
|
||
if err := os.RemoveAll(path); err != nil { | ||
t.Errorf("failed to remove temp file: %v", err) | ||
} | ||
} | ||
|
||
// RemoveFile removes the file at the given path. | ||
func RemoveFile(t *testing.T, path string) { | ||
t.Helper() | ||
|
||
if err := os.Remove(path); err != nil { | ||
t.Errorf("failed to remove temp file: %v", err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package file_utils | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
) | ||
|
||
func TestCreateTempFile(t *testing.T) { | ||
t.Run("creates temporary file with given content", func(t *testing.T) { | ||
content := "hello world" | ||
path := CreateTempFile(t, content) | ||
defer RemoveFile(t, path) | ||
|
||
// Check if the file exists | ||
if _, err := os.Stat(path); os.IsNotExist(err) { | ||
t.Fatalf("expected file to exist, but it does not: %v", err) | ||
} | ||
|
||
// Read the file content and verify it matches | ||
readContent, err := os.ReadFile(path) | ||
if err != nil { | ||
t.Fatalf("failed to read temp file: %v", err) | ||
} | ||
|
||
if string(readContent) != content { | ||
t.Errorf("expected file content to be '%s', got '%s'", content, string(readContent)) | ||
} | ||
}) | ||
} | ||
|
||
func TestCreateTempFileInDir(t *testing.T) { | ||
t.Run("creates temporary file with given content in directory", func(t *testing.T) { | ||
content := "hello world" | ||
dname, err := os.MkdirTemp("", "sampledir") | ||
if err != nil { | ||
t.Fatalf("failed to create dir: %v", err) | ||
} | ||
|
||
path := CreateTempFileInDir(t, content, dname) | ||
defer RemoveDir(t, dname) | ||
|
||
// Check if the file exists | ||
if _, err := os.Stat(path); os.IsNotExist(err) { | ||
t.Fatalf("expected file to exist, but it does not: %v", err) | ||
} | ||
|
||
// Read the file content and verify it matches | ||
readContent, err := os.ReadFile(path) | ||
if err != nil { | ||
t.Fatalf("failed to read temp file: %v", err) | ||
} | ||
|
||
if string(readContent) != content { | ||
t.Errorf("expected file content to be '%s', got '%s'", content, string(readContent)) | ||
} | ||
}) | ||
} | ||
|
||
func TestAppendToFilex3(t *testing.T) { | ||
t.Run("removes the dir at the given path", func(t *testing.T) { | ||
content := "hello world" | ||
new_content := "!" | ||
expected := "hello world!!!" | ||
path := CreateTempFile(t, content) | ||
defer RemoveFile(t, path) | ||
|
||
// Read the file content before modification and verify it matches | ||
readContent, err := os.ReadFile(path) | ||
if err != nil { | ||
t.Fatalf("failed to read temp file: %v", err) | ||
} | ||
|
||
if string(readContent) != content { | ||
t.Errorf("expected file content to be '%s', got '%s'", content, string(readContent)) | ||
} | ||
|
||
AppendToFilex3(t, path, new_content) | ||
|
||
// Read the file content after modification and verify it matches | ||
readContent, err = os.ReadFile(path) | ||
if err != nil { | ||
t.Fatalf("failed to read temp file: %v", err) | ||
} | ||
|
||
if string(readContent) != expected { | ||
t.Errorf("expected file content to be '%s', got '%s'", content, string(readContent)) | ||
} | ||
|
||
}) | ||
} | ||
|
||
func TestRemoveDir(t *testing.T) { | ||
t.Run("removes the dir at the given path", func(t *testing.T) { | ||
dname, err := os.MkdirTemp("", "sampledir") | ||
if err != nil { | ||
t.Fatalf("failed to create dir: %v", err) | ||
} | ||
|
||
// Ensure the dir exists before removing | ||
if _, err := os.Stat(dname); os.IsNotExist(err) { | ||
t.Fatalf("expected file to exist, but it does not: %v", err) | ||
} | ||
|
||
RemoveDir(t, dname) | ||
|
||
// Ensure the dir does not exist after removing | ||
if _, err := os.Stat(dname); !os.IsNotExist(err) { | ||
t.Errorf("expected file to be removed, but it still exists") | ||
} | ||
|
||
}) | ||
} | ||
|
||
func TestRemoveFile(t *testing.T) { | ||
t.Run("removes the file at the given path", func(t *testing.T) { | ||
content := "file to be deleted" | ||
path := CreateTempFile(t, content) | ||
|
||
// Ensure the file exists before removing | ||
if _, err := os.Stat(path); os.IsNotExist(err) { | ||
t.Fatalf("expected file to exist, but it does not: %v", err) | ||
} | ||
|
||
RemoveFile(t, path) | ||
|
||
// Ensure the file does not exist after removing | ||
if _, err := os.Stat(path); !os.IsNotExist(err) { | ||
t.Errorf("expected file to be removed, but it still exists") | ||
} | ||
}) | ||
} |
Oops, something went wrong.