fix: configurable log directory for windows

9;133u

0;133u
This commit is contained in:
jamesread 2026-06-16 11:24:19 +01:00
commit 9d64b64bf9
13 changed files with 179 additions and 11 deletions

View File

@ -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 | - | `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] | `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] | `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] | `Prometheus` | Prometheus configuration. | `-` | Requires Restart | xref:advanced_configuration/prometheus.adoc[Prometheus]
|=== |===

View File

@ -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`] 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" 2. Unzip and run "OliveTin.exe"
include::partial$install/windows_service_logs.adoc[]
include::partial$install/post_generic.adoc[] include::partial$install/post_generic.adoc[]

View File

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

View File

@ -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. * **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. * **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. * **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-binary]
== Manual or binary run == Manual or binary run

View File

@ -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).
====

View File

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

View File

@ -2,6 +2,7 @@ package config
import ( import (
"fmt" "fmt"
"runtime"
"strings" "strings"
"text/template" "text/template"
@ -18,6 +19,7 @@ func (cfg *Config) Sanitize() {
cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLogHistoryPageSize()
cfg.sanitizeLocalUsers() cfg.sanitizeLocalUsers()
cfg.sanitizeSecurityHeaders() cfg.sanitizeSecurityHeaders()
cfg.sanitizeServiceLogs()
// log.Infof("cfg %p", cfg) // 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) { func (action *Action) sanitize(cfg *Config) {
if action.Timeout < 3 { if action.Timeout < 3 {
action.Timeout = 3 action.Timeout = 3

View File

@ -1,8 +1,11 @@
package config package config
import ( import (
"bytes"
"runtime"
"testing" "testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -160,3 +163,22 @@ func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
}) })
require.NoError(t, err) 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")
}

View File

@ -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
}

View File

@ -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", ""))
}

View File

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

View File

@ -50,9 +50,20 @@ func (m *otWindowsService) Execute(args []string, r <-chan svc.ChangeRequest, st
} }
} }
func setupLogging() { func setupLogging(serviceLogDirectory string) {
logsDir := path.Join(GetConfigFilePath(), "logs") 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 { if err := os.MkdirAll(logsDir, 0755); err != nil {
log.Errorf("Failed to create logs directory %v: %v", logsDir, err) log.Errorf("Failed to create logs directory %v: %v", logsDir, err)
return return
@ -68,12 +79,13 @@ func setupLogging() {
if err != nil { if err != nil {
log.Errorf("Failed to open log file: %v", err) log.Errorf("Failed to open log file: %v", err)
} else { return
}
log.Infof("Switching to log file: %v", f.Name()) log.Infof("Switching to log file: %v", f.Name())
log.SetOutput(f) log.SetOutput(f)
log.Infof("Opened log file: %v", f.Name()) log.Infof("Opened log file: %v", f.Name())
} }
}
func GetConfigFilePath() string { func GetConfigFilePath() string {
programDataDir := path.Join(os.Getenv("ProgramData"), "OliveTin") programDataDir := path.Join(os.Getenv("ProgramData"), "OliveTin")
@ -135,8 +147,8 @@ func startServiceHandler(mode string) {
} }
func Start(mode string) { func Start(mode string, serviceLogDirectory string) {
setupLogging() setupLogging(serviceLogDirectory)
go startServiceHandler(mode) go startServiceHandler(mode)
} }

View File

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