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

Implement hot reload #360

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
febad68
Add startup manager
kuskoman Feb 26, 2024
fbb060f
Refactor startup manager
kuskoman Feb 26, 2024
4c795d5
Try to get reload working
satk0 Jul 24, 2024
c53e108
Fix prometheus reloading
satk0 Jul 24, 2024
a5818ac
Remove comments etc
satk0 Jul 25, 2024
9422419
Make hot reload functional
satk0 Jul 25, 2024
4933eec
Fix for vim
satk0 Jul 25, 2024
2c642fa
Make it optable via hot-reload flag
satk0 Aug 1, 2024
8e60748
Change to small size letters
satk0 Sep 4, 2024
c29a9bc
Make file watcher react to modification
satk0 Sep 4, 2024
ce98b21
Use default nil value for collector
satk0 Sep 4, 2024
c4c9d22
Make changes according to review
satk0 Sep 4, 2024
a12867a
Small fix
satk0 Sep 10, 2024
ac7e54b
Check if configs are equal on reloading
satk0 Sep 17, 2024
c1f6f72
Refactor according to the request
satk0 Sep 27, 2024
cae3839
Change flag name to -watch
satk0 Sep 27, 2024
1913c8e
Fix linting
satk0 Oct 3, 2024
a1dc29f
Ran go mod tidy
satk0 Oct 3, 2024
0e8f0e1
Exclude startup manager from codecov
satk0 Oct 3, 2024
48ffa6c
Patch codecov plz shut up
satk0 Oct 3, 2024
e0c6d88
Remove ignore section from codecov definition
kuskoman Oct 7, 2024
3e0b054
Tidy gomod
kuskoman Oct 7, 2024
33c2934
Start refactoring hot reload functionality
kuskoman Oct 7, 2024
bc42e63
Tidy gomod
kuskoman Oct 7, 2024
63e36a9
More debug, still not working
kuskoman Oct 7, 2024
37ec870
Add a few more logs
kuskoman Oct 7, 2024
ec0ab03
Handle slog settings
kuskoman Oct 7, 2024
13682cf
Change logs to be lowercased
kuskoman Oct 7, 2024
d9212fe
Make file watcher actually watch files
kuskoman Oct 7, 2024
8155e23
Handle unregistering prometheus
kuskoman Oct 7, 2024
dd928be
Start extracting file watcher
kuskoman Oct 7, 2024
c39f359
Fix file watcher test
kuskoman Oct 8, 2024
3d40ac1
Split code into even more packages
kuskoman Oct 8, 2024
25e43d5
Fix linter errors
kuskoman Oct 8, 2024
93c95a2
Start recreating startup manager
kuskoman Oct 9, 2024
ca4082d
Upgrade dependencies
kuskoman Oct 16, 2024
837e1c3
Add multiple reloads to startup manager
kuskoman Oct 16, 2024
e73e00e
Add error handling to reloader
kuskoman Oct 16, 2024
cf89b68
Try to trace why application is exitting after config change
kuskoman Oct 16, 2024
fea5b02
Fix error handling in startup_manager
kuskoman Oct 16, 2024
e12c2b6
Refactor getting slog logger
kuskoman Oct 16, 2024
3ffc4e1
Fix linter errors
kuskoman Oct 16, 2024
81df5f3
Fix lint
satk0 Oct 17, 2024
335faa1
Potential fix to tests
satk0 Oct 19, 2024
101401c
Fix tests
satk0 Oct 20, 2024
6c5a768
Fix linting
satk0 Oct 20, 2024
a2201a4
Fix patchcov maybe
satk0 Oct 20, 2024
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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ out/main-%:
run:
go run cmd/exporter/main.go

#: Runs the Go Exporter application with watching the configuration file
run-and-watch-config:
go run cmd/exporter/main.go -watch

#: Builds a binary executable for Linux
build-linux: out/main-linux
#: Builds a binary executable for Darwin
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ See more in the [Migration](#migration) section.

- `make all`: Builds binary executables for all OS (Win, Darwin, Linux).
- `make run`: Runs the Go Exporter application.
- `make run-and-watch-config`: Runs the Go Exporter application with watching the configuration file.
- `make build-linux`: Builds a binary executable for Linux.
- `make build-darwin`: Builds a binary executable for Darwin.
- `make build-windows`: Builds a binary executable for Windows.
Expand Down
73 changes: 13 additions & 60 deletions cmd/exporter/main.go
Original file line number Diff line number Diff line change
@@ -1,81 +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,
)
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)
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ module github.com/kuskoman/logstash-exporter
go 1.23

require (
github.com/joho/godotenv v1.5.1
github.com/fsnotify/fsnotify v1.7.0
github.com/prometheus/client_golang v1.20.5
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/gkampitakis/ciinfo v0.3.0 // indirect
github.com/gkampitakis/go-diff v1.3.2 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/maruel/natural v1.1.1 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8=
github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
Expand All @@ -13,10 +15,8 @@ github.com/gkampitakis/go-snaps v0.5.7 h1:uVGjHR4t4pPHU944udMx7VKHpwepZXmvDMF+yD
github.com/gkampitakis/go-snaps v0.5.7/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand Down
90 changes: 90 additions & 0 deletions internal/file_utils/file_test_utils.go
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)
}
}
131 changes: 131 additions & 0 deletions internal/file_utils/file_test_utils_test.go
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")
}
})
}
Loading