Преглед изворни кода

fix: configurable log directory for windows
9;133u

0;133u

jamesread пре 2 недеља
родитељ
комит
9d64b64bf9

+ 1 - 0
docs/modules/ROOT/pages/config.adoc

@@ -117,6 +117,7 @@ All configuration options are covered in the solution sections
 | `WebUIDir` | The directory to serve the web UI from. | Calculated at runtime. | Requires Restart | -
 | `CronSupportForSeconds` | Whether or not to support seconds in cron expressions. | `false` | Requires Restart | xref:action_execution/oncron.adoc[Cron]
 | `SaveLogs` | Whether or not to save logs to disk. | `[]` | Requires Restart | xref:action_customization/savelogs.adoc[Save Logs]
+| `ServiceLogs` | Windows process log directory (`serviceLogs.directory`). | `%ProgramData%\OliveTin\logs\` on Windows | Requires Restart | xref:install/windows_service.adoc#windows-service-logs[Windows service logs]
 | `Prometheus` | Prometheus configuration. | `-` | Requires Restart | xref:advanced_configuration/prometheus.adoc[Prometheus]
 |===
 

+ 2 - 0
docs/modules/ROOT/pages/install/windows.adoc

@@ -9,4 +9,6 @@ You can instal install OliveTin as a Windows service, follow the instructions to
 1. You can download the latest version of OliveTin here: link:https://github.com/OliveTin/OliveTin/releases/latest/download/OliveTin-windows-amd64.zip[`OliveTin-windows-amd64.zip`]
 2. Unzip and run "OliveTin.exe"
 
+include::partial$install/windows_service_logs.adoc[]
+
 include::partial$install/post_generic.adoc[]

+ 3 - 1
docs/modules/ROOT/pages/install/windows_service.adoc

@@ -16,6 +16,8 @@ You can download the latest version of OliveTin here: link:https://github.com/Ol
 * Create c:/ProgramData/OliveTin/
 ** Copy the **config.yaml** file into this directory.
 
+include::partial$install/windows_service_logs.adoc[]
+
 == Test OliveTin startup
 
 Open a command prompt and make suer you are in the c:/Program Files/OliveTin/ directory, then run:
@@ -37,7 +39,7 @@ serviceHostMode: "winsvc-standard"
 
 logLevel: info
 
-actions: 
+actions:
   ...
 ----
 

+ 21 - 0
docs/modules/ROOT/pages/troubleshooting/service-logs.adoc

@@ -16,6 +16,27 @@ How you read logs depends on how OliveTin is installed:
 * **Container (Docker / Podman):** logs are what the container runtime captured from OliveTin’s standard output.
 * **systemd service:** logs are stored by the system journal for the `OliveTin` unit.
 * **Manual / binary** (for example `./OliveTin` in a terminal): messages print to that terminal—copy them from there, or redirect output to a file when testing.
+* **Windows (binary or service):** OliveTin writes process logs to a file under `%ProgramData%\OliveTin\logs\` by default. You can override this with xref:install/windows_service.adoc#windows-service-logs[`serviceLogs.directory`] in `config.yaml`.
+
+[#windows]
+== Windows (binary or service)
+
+By default, OliveTin on Windows writes process logs to:
+
+[source]
+----
+%ProgramData%\OliveTin\logs\OliveTin-service-<timestamp>.log
+----
+
+To use a custom directory (for portable installs), set in `config.yaml`:
+
+[source,yaml]
+----
+serviceLogs:
+  directory: C:\Path\To\Logs\
+----
+
+See xref:install/windows_service.adoc#windows-service-logs[Windows service logs directory] for details. This setting is ignored on non-Windows platforms.
 
 [#manual-binary]
 == Manual or binary run

+ 27 - 0
docs/modules/ROOT/partials/install/windows_service_logs.adoc

@@ -0,0 +1,27 @@
+[#windows-service-logs]
+== Process log directory
+
+On Windows, OliveTin writes its **process logs** (startup messages, configuration load, errors, and internal diagnostics) to a log file on disk. By default these files are stored under `%ProgramData%\OliveTin\logs\` as `OliveTin-service-<timestamp>.log`.
+
+This is separate from xref:action_customization/savelogs.adoc[action execution logs] (`saveLogs`), which persist command output from individual actions.
+
+Portable or self-contained installs often keep OliveTin, its configuration, and its logs together in one folder. Without a custom path, process logs always go to `%ProgramData%`, even when you run OliveTin from another location.
+
+Add a `serviceLogs` block to your `config.yaml`:
+
+[source,yaml]
+----
+serviceLogs:
+  directory: ./logs/service/
+----
+
+OliveTin creates the directory if it does not exist and writes a new timestamped log file there on each startup.
+
+If `serviceLogs.directory` is omitted, OliveTin uses the default location: `%ProgramData%\OliveTin\logs\`.
+
+Relative paths (for example `./logs/service/`) are resolved from the directory containing `OliveTin.exe`. This keeps portable installs self-contained when you colocate logs with the application.
+
+[NOTE]
+====
+`serviceLogs.directory` is **Windows only**. If you set it on Linux, macOS, or in a container, OliveTin logs an error at startup and ignores the setting. On those platforms, use xref:troubleshooting/service-logs.adoc[service logs troubleshooting] for how to read process output (for example `journalctl` or container logs).
+====

+ 5 - 0
service/internal/config/config.go

@@ -176,6 +176,7 @@ type Config struct {
 	Prometheus                      PrometheusConfig           `koanf:"prometheus"`
 	Security                        SecurityConfig             `koanf:"security"`
 	SaveLogs                        SaveLogsConfig             `koanf:"saveLogs"`
+	ServiceLogs                     ServiceLogsConfig          `koanf:"serviceLogs"`
 	DefaultIconForActions           string                     `koanf:"defaultIconForActions"`
 	DefaultIconForDirectories       string                     `koanf:"defaultIconForDirectories"`
 	DefaultIconForBack              string                     `koanf:"defaultIconForBack"`
@@ -230,6 +231,10 @@ type SaveLogsConfig struct {
 	OutputDirectory  string `koanf:"outputDirectory"`
 }
 
+type ServiceLogsConfig struct {
+	Directory string `koanf:"directory"`
+}
+
 type LogDebugOptions struct {
 	SingleFrontendRequests       bool `koanf:"singleFrontendRequests"`
 	SingleFrontendRequestHeaders bool `koanf:"singleFrontendRequestHeaders"`

+ 12 - 0
service/internal/config/sanitize.go

@@ -2,6 +2,7 @@ package config
 
 import (
 	"fmt"
+	"runtime"
 	"strings"
 	"text/template"
 
@@ -18,6 +19,7 @@ func (cfg *Config) Sanitize() {
 	cfg.sanitizeLogHistoryPageSize()
 	cfg.sanitizeLocalUsers()
 	cfg.sanitizeSecurityHeaders()
+	cfg.sanitizeServiceLogs()
 
 	// log.Infof("cfg %p", cfg)
 
@@ -168,6 +170,16 @@ func (cfg *Config) sanitizeLogLevel() {
 	}
 }
 
+func (cfg *Config) sanitizeServiceLogs() {
+	if cfg.ServiceLogs.Directory == "" {
+		return
+	}
+
+	if runtime.GOOS != "windows" {
+		log.Errorf("serviceLogs.directory is configured but this option is only supported on Windows")
+	}
+}
+
 func (action *Action) sanitize(cfg *Config) {
 	if action.Timeout < 3 {
 		action.Timeout = 3

+ 22 - 0
service/internal/config/sanitize_test.go

@@ -1,8 +1,11 @@
 package config
 
 import (
+	"bytes"
+	"runtime"
 	"testing"
 
+	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -160,3 +163,22 @@ func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
 	})
 	require.NoError(t, err)
 }
+
+func TestSanitizeServiceLogsUnsupportedPlatform(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("serviceLogs.directory platform check only applies on non-Windows")
+	}
+
+	var logBuffer bytes.Buffer
+	previousOutput := logrus.StandardLogger().Out
+	logrus.SetOutput(&logBuffer)
+	t.Cleanup(func() {
+		logrus.SetOutput(previousOutput)
+	})
+
+	cfg := DefaultConfig()
+	cfg.ServiceLogs.Directory = "/var/log/OliveTin"
+	cfg.Sanitize()
+
+	assert.Contains(t, logBuffer.String(), "serviceLogs.directory is configured but this option is only supported on Windows")
+}

+ 44 - 0
service/internal/servicehost/log_directory.go

@@ -0,0 +1,44 @@
+package servicehost
+
+import (
+	"os"
+	"path/filepath"
+)
+
+func resolveLogDirectory(dir string, baseDir string) string {
+	if dir == "" {
+		return ""
+	}
+
+	if filepath.IsAbs(dir) {
+		return dir
+	}
+
+	if baseDir == "" {
+		return dir
+	}
+
+	return filepath.Join(baseDir, dir)
+}
+
+func executableDirectory() (string, error) {
+	ex, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Dir(ex), nil
+}
+
+func configuredServiceLogDirectory(dir string) (string, error) {
+	if dir == "" {
+		return "", nil
+	}
+
+	exeDir, err := executableDirectory()
+	if err != nil {
+		return "", err
+	}
+
+	return resolveLogDirectory(dir, exeDir), nil
+}

+ 20 - 0
service/internal/servicehost/log_directory_test.go

@@ -0,0 +1,20 @@
+package servicehost
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestResolveLogDirectory(t *testing.T) {
+	t.Parallel()
+
+	baseDir := filepath.Join(t.TempDir(), "OliveTin")
+	absoluteDir := t.TempDir()
+
+	assert.Equal(t, "", resolveLogDirectory("", baseDir))
+	assert.Equal(t, absoluteDir, resolveLogDirectory(absoluteDir, baseDir))
+	assert.Equal(t, filepath.Join(baseDir, "logs", "service"), resolveLogDirectory("./logs/service", baseDir))
+	assert.Equal(t, "logs/service", resolveLogDirectory("logs/service", ""))
+}

+ 1 - 1
service/internal/servicehost/servicehost_nonwin.go

@@ -7,7 +7,7 @@ import (
 	log "github.com/sirupsen/logrus"
 )
 
-func Start(mode string) {
+func Start(_ string, _ string) {
 	log.Debugf("servicehost nonwin")
 }
 

+ 20 - 8
service/internal/servicehost/servicehost_windows.go

@@ -50,9 +50,20 @@ func (m *otWindowsService) Execute(args []string, r <-chan svc.ChangeRequest, st
 	}
 }
 
-func setupLogging() {
-	logsDir := path.Join(GetConfigFilePath(), "logs")
+func setupLogging(serviceLogDirectory string) {
+	logsDir, err := configuredServiceLogDirectory(serviceLogDirectory)
+	if err != nil {
+		log.Warnf("Failed to resolve serviceLogs.directory relative to executable: %v", err)
+	}
+
+	if logsDir == "" {
+		logsDir = path.Join(GetConfigFilePath(), "logs")
+	}
 
+	openServiceLogFile(logsDir)
+}
+
+func openServiceLogFile(logsDir string) {
 	if err := os.MkdirAll(logsDir, 0755); err != nil {
 		log.Errorf("Failed to create logs directory %v: %v", logsDir, err)
 		return
@@ -68,11 +79,12 @@ func setupLogging() {
 
 	if err != nil {
 		log.Errorf("Failed to open log file: %v", err)
-	} else {
-		log.Infof("Switching to log file: %v", f.Name())
-		log.SetOutput(f)
-		log.Infof("Opened log file: %v", f.Name())
+		return
 	}
+
+	log.Infof("Switching to log file: %v", f.Name())
+	log.SetOutput(f)
+	log.Infof("Opened log file: %v", f.Name())
 }
 
 func GetConfigFilePath() string {
@@ -135,8 +147,8 @@ func startServiceHandler(mode string) {
 
 }
 
-func Start(mode string) {
-	setupLogging()
+func Start(mode string, serviceLogDirectory string) {
+	setupLogging(serviceLogDirectory)
 
 	go startServiceHandler(mode)
 }

+ 1 - 1
service/main.go

@@ -245,7 +245,7 @@ func warnIfPuidGuid() {
 }
 
 func main() {
-	servicehost.Start(cfg.ServiceHostMode)
+	servicehost.Start(cfg.ServiceHostMode, cfg.ServiceLogs.Directory)
 
 	log.WithFields(log.Fields{
 		"configDir": cfg.GetDir(),