Browse Source

Allow to rotate logs on demand

Log file can be rotated sending a SIGUSR1 signal on Unix based systems and
using "sftpgo service rotatelogs" on Windows

Fixes #133
Nicola Murino 5 years ago
parent
commit
0056984d4b

+ 9 - 6
.github/workflows/development.yml

@@ -16,7 +16,8 @@ jobs:
         with:
         with:
           version: v1.27
           version: v1.27
 
 
-  test-deploy-unix:
+  tests-upload-unix:
+    name: Run tests and upload build artifacts on Linux and macOS
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     strategy:
     strategy:
       matrix:
       matrix:
@@ -40,7 +41,7 @@ jobs:
       - name: Initialize data provider
       - name: Initialize data provider
         run: ./sftpgo initprovider
         run: ./sftpgo initprovider
 
 
-      - name: Run tests using SQLite provider
+      - name: Run test cases using SQLite as data provider
         run: go test -v ./... -coverprofile=coverage.txt -covermode=atomic
         run: go test -v ./... -coverprofile=coverage.txt -covermode=atomic
 
 
       - name: Upload to Codecov
       - name: Upload to Codecov
@@ -63,7 +64,7 @@ jobs:
           name: sftpgo-${{ matrix.os }}-go${{ matrix.go }}
           name: sftpgo-${{ matrix.os }}-go${{ matrix.go }}
           path: output
           path: output
 
 
-      - name: Run tests using bolt provider
+      - name: Run test cases using bolt as data provider
         if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.14' }}
         if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.14' }}
         run: |
         run: |
           rm -f sftpgo.db
           rm -f sftpgo.db
@@ -73,14 +74,15 @@ jobs:
         env:
         env:
           SFTPGO_DATA_PROVIDER__DRIVER: bolt
           SFTPGO_DATA_PROVIDER__DRIVER: bolt
 
 
-      - name: Run tests using memory provider
+      - name: Run test cases using the memory data provider
         if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.14' }}
         if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.14' }}
         run: go test -v ./... -covermode=atomic
         run: go test -v ./... -covermode=atomic
         env:
         env:
             SFTPGO_DATA_PROVIDER__DRIVER: memory
             SFTPGO_DATA_PROVIDER__DRIVER: memory
             SFTPGO_DATA_PROVIDER__NAME:
             SFTPGO_DATA_PROVIDER__NAME:
 
 
-  test-deploy-windows:
+  tests-upload-windows:
+    name: Run tests and upload build artifact on Windows
     runs-on: windows-latest
     runs-on: windows-latest
     steps:
     steps:
       - uses: actions/checkout@v2
       - uses: actions/checkout@v2
@@ -116,7 +118,8 @@ jobs:
           name: sftpgo-windows
           name: sftpgo-windows
           path: output
           path: output
 
 
-  test-postgresql:
+  tests-postgresql:
+    name: Run test cases using PostgreSQL as data provider
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     env:
     env:
       SFTPGO_DATA_PROVIDER__DRIVER: postgresql
       SFTPGO_DATA_PROVIDER__DRIVER: postgresql

+ 2 - 0
README.md

@@ -62,6 +62,8 @@ Some Linux distro packages are available:
   - [sftpgo-bin](https://aur.archlinux.org/packages/sftpgo-bin/). This package follows stable releases downloading the prebuilt linux binary from GitHub. It does not require `git`, `gcc` and `go` to build.
   - [sftpgo-bin](https://aur.archlinux.org/packages/sftpgo-bin/). This package follows stable releases downloading the prebuilt linux binary from GitHub. It does not require `git`, `gcc` and `go` to build.
   - [sftpgo-git](https://aur.archlinux.org/packages/sftpgo-git/). This package builds and installs the latest git master. It requires `git`, `gcc` and `go` to build.
   - [sftpgo-git](https://aur.archlinux.org/packages/sftpgo-git/). This package builds and installs the latest git master. It requires `git`, `gcc` and `go` to build.
 
 
+You can easily test new features selecting a commit from the [Actions](./actions) page and downloading the matching build artifacts for Linux, macOS or Windows. GitHub stores artifacts for 90 days.
+
 Alternately, you can [build from source](./docs/build-from-source.md).
 Alternately, you can [build from source](./docs/build-from-source.md).
 
 
 ## Configuration
 ## Configuration

+ 2 - 2
cmd/reload_windows.go

@@ -21,10 +21,10 @@ var (
 			}
 			}
 			err := s.Reload()
 			err := s.Reload()
 			if err != nil {
 			if err != nil {
-				fmt.Printf("Error reloading service: %v\r\n", err)
+				fmt.Printf("Error sending reload signal: %v\r\n", err)
 				os.Exit(1)
 				os.Exit(1)
 			} else {
 			} else {
-				fmt.Printf("Service reloaded!\r\n")
+				fmt.Printf("Reload signal sent!\r\n")
 			}
 			}
 		},
 		},
 	}
 	}

+ 35 - 0
cmd/rotatelogs_windows.go

@@ -0,0 +1,35 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/drakkan/sftpgo/service"
+)
+
+var (
+	rotateLogCmd = &cobra.Command{
+		Use:   "rotatelogs",
+		Short: "Signal to the running service to close the existing log file and immediately create a new one",
+		Run: func(cmd *cobra.Command, args []string) {
+			s := service.WindowsService{
+				Service: service.Service{
+					Shutdown: make(chan bool),
+				},
+			}
+			err := s.RotateLogFile()
+			if err != nil {
+				fmt.Printf("Error sending rotate log file signal to the service: %v\r\n", err)
+				os.Exit(1)
+			} else {
+				fmt.Printf("Rotate log file signal sent!\r\n")
+			}
+		},
+	}
+)
+
+func init() {
+	serviceCmd.AddCommand(rotateLogCmd)
+}

+ 1 - 1
docs/account.md

@@ -13,7 +13,7 @@ For each account, the following properties can be configured:
 - `max_sessions` maximum concurrent sessions. 0 means unlimited.
 - `max_sessions` maximum concurrent sessions. 0 means unlimited.
 - `quota_size` maximum size allowed as bytes. 0 means unlimited.
 - `quota_size` maximum size allowed as bytes. 0 means unlimited.
 - `quota_files` maximum number of files allowed. 0 means unlimited.
 - `quota_files` maximum number of files allowed. 0 means unlimited.
-- `permissions` the following per directory permissions are supported:
+- `permissions` for SFTP paths. The following per directory permissions are supported:
   - `*` all permissions are granted
   - `*` all permissions are granted
   - `list` list items is allowed
   - `list` list items is allowed
   - `download` download files is allowed
   - `download` download files is allowed

+ 2 - 0
docs/full-configuration.md

@@ -32,6 +32,8 @@ The `serve` command supports the following flags:
 - `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable. It is unused if `log-file-path` is empty.
 - `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable. It is unused if `log-file-path` is empty.
 - `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`).
 - `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`).
 
 
+Log file can be rotated on demand sending a `SIGUSR1` signal on Unix based systems and using `sftpgo service rotatelogs` on Windows.
+
 If you don't configure any private host key, the daemon will use `id_rsa` and `id_ecdsa` in the configuration directory. If these files don't exist, the daemon will attempt to autogenerate them (if the user that executes SFTPGo has write access to the `config-dir`). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L33).
 If you don't configure any private host key, the daemon will use `id_rsa` and `id_ecdsa` in the configuration directory. If these files don't exist, the daemon will attempt to autogenerate them (if the user that executes SFTPGo has write access to the `config-dir`). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L33).
 
 
 ## Configuration file
 ## Configuration file

+ 2 - 1
docs/service.md

@@ -86,7 +86,8 @@ Usage:
 
 
 Available Commands:
 Available Commands:
   install     Install SFTPGo as Windows Service
   install     Install SFTPGo as Windows Service
-  reload      Reload the SFTPGo Windows Service sending a `paramchange` request
+  reload      Reload the SFTPGo Windows Service sending a "paramchange" request
+  rotatelogs  Signal to the running service to close the existing log file and immediately create a new one
   start       Start SFTPGo Windows Service
   start       Start SFTPGo Windows Service
   status      Retrieve the status for the SFTPGo Windows Service
   status      Retrieve the status for the SFTPGo Windows Service
   stop        Stop SFTPGo Windows Service
   stop        Stop SFTPGo Windows Service

+ 1 - 1
init/sftpgo.service

@@ -1,5 +1,5 @@
 [Unit]
 [Unit]
-Description=SFTPGo sftp server
+Description=SFTPGo SFTP Server
 After=network.target
 After=network.target
 
 
 [Service]
 [Service]

+ 14 - 2
logger/logger.go

@@ -9,6 +9,7 @@
 package logger
 package logger
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
@@ -37,6 +38,7 @@ const (
 var (
 var (
 	logger        zerolog.Logger
 	logger        zerolog.Logger
 	consoleLogger zerolog.Logger
 	consoleLogger zerolog.Logger
+	rollingLogger *lumberjack.Logger
 )
 )
 
 
 // GetLogger get the configured logger instance
 // GetLogger get the configured logger instance
@@ -48,13 +50,14 @@ func GetLogger() *zerolog.Logger {
 func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge int, logCompress bool, level zerolog.Level) {
 func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge int, logCompress bool, level zerolog.Level) {
 	zerolog.TimeFieldFormat = dateFormat
 	zerolog.TimeFieldFormat = dateFormat
 	if isLogFilePathValid(logFilePath) {
 	if isLogFilePathValid(logFilePath) {
-		logger = zerolog.New(&lumberjack.Logger{
+		rollingLogger = &lumberjack.Logger{
 			Filename:   logFilePath,
 			Filename:   logFilePath,
 			MaxSize:    logMaxSize,
 			MaxSize:    logMaxSize,
 			MaxBackups: logMaxBackups,
 			MaxBackups: logMaxBackups,
 			MaxAge:     logMaxAge,
 			MaxAge:     logMaxAge,
 			Compress:   logCompress,
 			Compress:   logCompress,
-		})
+		}
+		logger = zerolog.New(rollingLogger)
 		EnableConsoleLogger(level)
 		EnableConsoleLogger(level)
 	} else {
 	} else {
 		logger = zerolog.New(logSyncWrapper{
 		logger = zerolog.New(logSyncWrapper{
@@ -69,6 +72,7 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge
 // ConsoleLogger will not be affected
 // ConsoleLogger will not be affected
 func DisableLogger() {
 func DisableLogger() {
 	logger = zerolog.Nop()
 	logger = zerolog.Nop()
+	rollingLogger = nil
 }
 }
 
 
 // EnableConsoleLogger enables the console logger
 // EnableConsoleLogger enables the console logger
@@ -81,6 +85,14 @@ func EnableConsoleLogger(level zerolog.Level) {
 	consoleLogger = zerolog.New(consoleOutput).With().Timestamp().Logger().Level(level)
 	consoleLogger = zerolog.New(consoleOutput).With().Timestamp().Logger().Level(level)
 }
 }
 
 
+// RotateLogFile closes the existing log file and immediately create a new one
+func RotateLogFile() error {
+	if rollingLogger != nil {
+		return rollingLogger.Rotate()
+	}
+	return errors.New("logging to file is disabled")
+}
+
 // Log logs at the specified level for the specified sender
 // Log logs at the specified level for the specified sender
 func Log(level LogLevel, sender string, connectionID string, format string, v ...interface{}) {
 func Log(level LogLevel, sender string, connectionID string, format string, v ...interface{}) {
 	switch level {
 	switch level {

+ 1 - 0
service/service.go

@@ -124,6 +124,7 @@ func (s *Service) Start() error {
 func (s *Service) Wait() {
 func (s *Service) Wait() {
 	if s.PortableMode != 1 {
 	if s.PortableMode != 1 {
 		registerSigHup()
 		registerSigHup()
+		registerSigUSR1()
 	}
 	}
 	<-s.Shutdown
 	<-s.Shutdown
 }
 }

+ 29 - 3
service/service_windows.go

@@ -17,8 +17,10 @@ import (
 )
 )
 
 
 const (
 const (
-	serviceName = "SFTPGo"
-	serviceDesc = "Full featured and highly configurable SFTP server"
+	serviceName     = "SFTPGo"
+	serviceDesc     = "Full featured and highly configurable SFTP server"
+	rotateLogCmd    = svc.Cmd(128)
+	acceptRotateLog = svc.Accepted(rotateLogCmd)
 )
 )
 
 
 // Status defines service status
 // Status defines service status
@@ -63,7 +65,7 @@ func (s Status) String() string {
 }
 }
 
 
 func (s *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
 func (s *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
-	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange
+	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange | acceptRotateLog
 	changes <- svc.Status{State: svc.StartPending}
 	changes <- svc.Status{State: svc.StartPending}
 	if err := s.Service.Start(); err != nil {
 	if err := s.Service.Start(); err != nil {
 		return true, 1
 		return true, 1
@@ -91,6 +93,12 @@ loop:
 			if err != nil {
 			if err != nil {
 				logger.Warn(logSender, "", "error reloading TLS certificate: %v", err)
 				logger.Warn(logSender, "", "error reloading TLS certificate: %v", err)
 			}
 			}
+		case rotateLogCmd:
+			logger.Debug(logSender, "", "Received log file rotation request")
+			err := logger.RotateLogFile()
+			if err != nil {
+				logger.Warn(logSender, "", "error rotating log file: %v", err)
+			}
 		default:
 		default:
 			continue loop
 			continue loop
 		}
 		}
@@ -157,6 +165,24 @@ func (s *WindowsService) Reload() error {
 	return nil
 	return nil
 }
 }
 
 
+func (s *WindowsService) RotateLogFile() error {
+	m, err := mgr.Connect()
+	if err != nil {
+		return err
+	}
+	defer m.Disconnect()
+	service, err := m.OpenService(serviceName)
+	if err != nil {
+		return fmt.Errorf("could not access service: %v", err)
+	}
+	defer service.Close()
+	_, err = service.Control(rotateLogCmd)
+	if err != nil {
+		return fmt.Errorf("could not send control=%d: %v", rotateLogCmd, err)
+	}
+	return nil
+}
+
 func (s *WindowsService) Install(args ...string) error {
 func (s *WindowsService) Install(args ...string) error {
 	exePath, err := s.getExePath()
 	exePath, err := s.getExePath()
 	if err != nil {
 	if err != nil {

+ 1 - 2
service/sighup_windows.go

@@ -1,4 +1,3 @@
 package service
 package service
 
 
-func registerSigHup() {
-}
+func registerSigHup() {}

+ 25 - 0
service/sigusr1_unix.go

@@ -0,0 +1,25 @@
+// +build !windows
+
+package service
+
+import (
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/drakkan/sftpgo/logger"
+)
+
+func registerSigUSR1() {
+	sig := make(chan os.Signal, 1)
+	signal.Notify(sig, syscall.SIGUSR1)
+	go func() {
+		for range sig {
+			logger.Debug(logSender, "", "Received log file rotation request")
+			err := logger.RotateLogFile()
+			if err != nil {
+				logger.Warn(logSender, "", "error rotating log file: %v", err)
+			}
+		}
+	}()
+}

+ 3 - 0
service/sigusr1_windows.go

@@ -0,0 +1,3 @@
+package service
+
+func registerSigUSR1() {}