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
This commit is contained in:
Nicola Murino 2020-06-22 19:11:53 +02:00
parent 44fb276464
commit 0056984d4b
14 changed files with 127 additions and 18 deletions

View file

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

View file

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

View file

@ -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
cmd/rotatelogs_windows.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,8 +17,10 @@ import (
) )
const ( const (
serviceName = "SFTPGo" serviceName = "SFTPGo"
serviceDesc = "Full featured and highly configurable SFTP server" 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 {

View file

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

25
service/sigusr1_unix.go Normal file
View file

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

View file

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