Skip to content

Commit 8674662

Browse files
authored
Merge pull request #47 from PaperCutSoftware/feature/custom_logformat
Feature/custom logformat
2 parents 5a01292 + 62af1b7 commit 8674662

11 files changed

Lines changed: 239 additions & 38 deletions

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ The configuration file is the heart of Silver. Here is a comprehensive example w
8383
"LogFileMaxSizeMb": 50,
8484
"LogFileMaxBackupFiles": 5,
8585
86+
// Custom format for log timestamps (e.g., "2006-01-02 15:04:05.000").
87+
// If set, this overrides the default timestamp format.
88+
// This is in GO format, see https://pkg.go.dev/time#Layout
89+
"LogFileTimestampFormat": "2006-01-02 15:04:05.000000",
90+
8691
// File to store the current main service PID.
8792
"PidFile": "${ServiceRoot}/${ServiceName}.pid",
8893
@@ -434,4 +439,3 @@ This project is licensed under the MIT Licence. See the `LICENSE` file for detai
434439
## **About This Project**
435440

436441
Silver is an open-source project actively maintained and supported by PaperCut Software. It is battle-tested technology, used in production to manage server and desktop components for millions of laptops and servers running [PaperCut's print management software](https://www.papercut.com/) for nearly a decade. Silver is a better tool thanks to the collective effort of its community. A big thank you to everyone who has contributed their time, ideas, and code to the project.
437-

conf/examples/silver-kitchen-sink.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"LogFile": "${ServiceName}.log",
1010
"LogFileMaxSizeMb": 200,
1111
"LogFileMaxBackupFiles": 5,
12+
"LogFileTimestampFormat": "2006-01-02 15:04:05",
1213
"PidFile": "${ServiceName}.pid"
1314
},
1415
"EnvironmentVars": {

conf/examples/silver-log-stdout.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"Description": "Only does one thing, but does it well!"
55
},
66
"ServiceConfig": {
7-
"LogFile": "os.stdout"
7+
"LogFile": "os.stdout",
8+
"LogFileTimestampFormat": "2006-01-02 15:04:05"
89
},
910
"Services": [
1011
{

lib/logging/logging.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ func init() {
6464
changeOwnerOfFileFunc = changeOwnerOfFile
6565
}
6666

67+
type logWriter struct {
68+
io.Writer
69+
timeformat string
70+
}
71+
72+
// custom Write to add timestamps with custom format
73+
func (w logWriter) Write(bytes []byte) (int, error) {
74+
timestamp := time.Now().Format(w.timeformat) + " "
75+
n1, err := io.WriteString(w.Writer, timestamp)
76+
if err != nil {
77+
return n1, err
78+
}
79+
n2, err := w.Writer.Write(bytes)
80+
return n1 + n2, err
81+
}
82+
6783
func (f *flusher) run(rf *rollingFile) {
6884
tick := time.Tick(f.interval)
6985
for {
@@ -176,12 +192,12 @@ func openLogFile(name string, owner string) (f *os.File, err error) {
176192
}
177193

178194
// NewFileLogger implements a rolling logger with default maximum size (50Mb)
179-
func NewFileLogger(file string, owner string) (logger *log.Logger) {
180-
return NewFileLoggerWithMaxSize(file, owner, defaultMaxSize, defaultMaxBackupFiles)
195+
func NewFileLogger(file string, owner string, timeformat string) (logger *log.Logger) {
196+
return NewFileLoggerWithMaxSize(file, owner, defaultMaxSize, defaultMaxBackupFiles, timeformat)
181197
}
182198

183199
// NewFileLoggerWithMaxSize implements a rolling logger with a set size
184-
func NewFileLoggerWithMaxSize(file string, owner string, maxSize int64, maxBackupFiles int) (logger *log.Logger) {
200+
func NewFileLoggerWithMaxSize(file string, owner string, maxSize int64, maxBackupFiles int, timeformat string) (logger *log.Logger) {
185201
rf, err := newRollingFile(file, owner, maxSize, maxBackupFiles)
186202
// This trick ensures that the flusher goroutine does not keep
187203
// the returned wrapper object from being garbage collected. When it is
@@ -190,7 +206,13 @@ func NewFileLoggerWithMaxSize(file string, owner string, maxSize int64, maxBacku
190206
rfWrapper := &rollingFileWrapper{rf}
191207
runtime.SetFinalizer(rfWrapper, stopFlusher)
192208
if err == nil {
193-
logger = log.New(rfWrapper, "", log.Ldate|log.Ltime)
209+
var writer io.Writer = rfWrapper
210+
flags := log.Ldate | log.Ltime
211+
if timeformat != "" {
212+
writer = logWriter{Writer: rfWrapper, timeformat: timeformat}
213+
flags = 0
214+
}
215+
logger = log.New(writer, "", flags)
194216
} else {
195217
fmt.Fprintf(os.Stderr, "WARNING: Unable to set up log file: %v\n", err)
196218
logger = NewNilLogger()
@@ -213,13 +235,25 @@ func NewNilLogger() *log.Logger {
213235
}
214236

215237
// NewConsoleErrorLogger is a basic logger to Stderr
216-
func NewConsoleErrorLogger() (logger *log.Logger) {
217-
logger = log.New(os.Stderr, "", log.Ldate|log.Ltime)
238+
func NewConsoleErrorLogger(timeformat string) (logger *log.Logger) {
239+
var writer io.Writer = os.Stderr
240+
flags := log.Ldate | log.Ltime
241+
if timeformat != "" {
242+
writer = logWriter{Writer: os.Stderr, timeformat: timeformat}
243+
flags = 0
244+
}
245+
logger = log.New(writer, "", flags)
218246
return logger
219247
}
220248

221249
// NewConsoleLogger is a basic logger to Stdout
222-
func NewConsoleLogger() (logger *log.Logger) {
223-
logger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
250+
func NewConsoleLogger(timeformat string) (logger *log.Logger) {
251+
var writer io.Writer = os.Stdout
252+
flags := log.Ldate | log.Ltime
253+
if timeformat != "" {
254+
writer = logWriter{Writer: os.Stdout, timeformat: timeformat}
255+
flags = 0
256+
}
257+
logger = log.New(writer, "", flags)
224258
return logger
225259
}

lib/logging/logging_test.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package logging
33
import (
44
"fmt"
55
"os"
6+
"regexp"
67
"strings"
78
"testing"
89
"time"
@@ -11,30 +12,64 @@ import (
1112
func TestStandardLogging(t *testing.T) {
1213
lname := fmt.Sprintf("%s/test-standard-log-%d.log", os.TempDir(), time.Now().Unix())
1314

14-
logger := NewFileLogger(lname, "")
15+
logger := NewFileLogger(lname, "", "")
1516
defer func() {
1617
os.Remove(lname)
1718
}()
1819

1920
msg := "TestStandardLogging"
20-
logger.Printf(msg)
21+
logger.Print(msg)
2122
CloseAllOpenFileLoggers()
2223

2324
output, err := os.ReadFile(lname)
2425
if err != nil {
2526
t.Errorf("Unable to read file: %v", err)
2627
}
2728

28-
if !strings.Contains(string(output), msg) {
29-
t.Errorf("Expected '%s', got '%s'", msg, output)
29+
outstr := string(output)
30+
// Expect format: YYYY-MM-DD HH:MM:SS <msg>
31+
re := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
32+
if !re.MatchString(outstr) {
33+
t.Errorf("expected timestamp prefix, got: %q", outstr)
34+
}
35+
if !strings.Contains(outstr, msg) {
36+
t.Errorf("Expected '%s', got '%s'", msg, outstr)
37+
}
38+
}
39+
40+
func TestStandardTwelveHourLogging(t *testing.T) {
41+
lname := fmt.Sprintf("%s/test-standard-log-%d.log", os.TempDir(), time.Now().Unix())
42+
43+
logger := NewFileLogger(lname, "", "2006/01/02 03:04:05 PM")
44+
defer func() {
45+
os.Remove(lname)
46+
}()
47+
48+
msg := "TestStandardLogging"
49+
logger.Print(msg)
50+
CloseAllOpenFileLoggers()
51+
52+
output, err := os.ReadFile(lname)
53+
if err != nil {
54+
t.Errorf("Unable to read file: %v", err)
55+
}
56+
57+
outstr := string(output)
58+
// Expect format: YYYY/MM/DD HH:MM:SS [AM|PM] <msg>
59+
re := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} [APM]{2} `)
60+
if !re.MatchString(outstr) {
61+
t.Errorf("expected timestamp prefix, got: %q", outstr)
62+
}
63+
if !strings.Contains(outstr, msg) {
64+
t.Errorf("Expected '%s', got '%s'", msg, outstr)
3065
}
3166
}
3267

3368
func TestRollingLog(t *testing.T) {
3469
lname := fmt.Sprintf("%s/test-rolling-log-%d.log", os.TempDir(), time.Now().Unix())
3570

3671
// Create the logger with log rotation for max 5 backup files.
37-
logger := NewFileLoggerWithMaxSize(lname, "", 1024, 5)
72+
logger := NewFileLoggerWithMaxSize(lname, "", 1024, 5, "2006.01.02 03:04:05 PM")
3873
defer func() {
3974
// Clean up all the log files after the test.
4075
for i := 0; i <= 5; i++ { // Remove the main log file and the 5 backups.
@@ -59,8 +94,13 @@ func TestRollingLog(t *testing.T) {
5994
if err != nil {
6095
t.Errorf("Unable to read file: %v", err)
6196
}
62-
if !strings.Contains(string(output), msg) {
63-
t.Errorf("Expected '%s', got '%s'", msg, output)
97+
outputStr := string(output)
98+
if !strings.Contains(outputStr, msg) {
99+
t.Errorf("Expected '%s', got '%s'", msg, outputStr)
100+
}
101+
re := regexp.MustCompile(`^\d{4}.\d{2}.\d{2} \d{2}:\d{2}:\d{2} [APM]{2} `)
102+
if !re.MatchString(outputStr) {
103+
t.Errorf("expected timestamp prefix, got: %q", outputStr)
64104
}
65105
rolledFileName = fmt.Sprintf("%s.%d", lname, i+1) // lname.1, lname.2, etc.
66106
}
@@ -74,9 +114,8 @@ func TestRollingLog(t *testing.T) {
74114

75115
func TestRollingLogFlush_IsFlushed(t *testing.T) {
76116
// Arrange
77-
//lname := fmt.Sprintf("%s/test-flushed-log-%d.log", os.TempDir(), time.Now().Unix())
78117
lname := fmt.Sprintf("test-flushed-log-%d.log", time.Now().Unix())
79-
logger := NewFileLoggerWithMaxSize(lname, "", 10024, 5)
118+
logger := NewFileLoggerWithMaxSize(lname, "", 10024, 5, "")
80119
defer func() {
81120
CloseAllOpenFileLoggers()
82121
os.Remove(lname)

lib/logging/owner_unix_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestLogIsOwnedByCorrectUser(t *testing.T) {
2323

2424
lname := fmt.Sprintf("%s/test-standard-log-%d.log", os.TempDir(), time.Now().Unix())
2525

26-
logger := NewFileLogger(lname, userName)
26+
logger := NewFileLogger(lname, userName, "")
2727
defer func() {
2828
os.Remove(lname)
2929
}()

service/config/config.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ type ServiceDescription struct {
4040
}
4141

4242
type ServiceConfig struct {
43-
StopFile string
44-
ReloadFile string
45-
LogFile string
46-
LogFileMaxSizeMb int64
47-
LogFileMaxBackupFiles int
48-
PidFile string
49-
UserLevel bool
50-
UserName string
43+
StopFile string
44+
ReloadFile string
45+
LogFile string
46+
LogFileMaxSizeMb int64
47+
LogFileMaxBackupFiles int
48+
PidFile string
49+
UserLevel bool
50+
UserName string
51+
LogFileTimestampFormat string
5152
}
5253

5354
type command struct {

service/main.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
// SILVER - Service Wrapper
22
//
3-
// Copyright (c) 2014-2021 PaperCut Software http://www.papercut.com/
3+
// Copyright (c) 2014-2025 PaperCut Software http://www.papercut.com/
44
// Use of this source code is governed by an MIT or GPL Version 2 license.
55
// See the project's LICENSE file for more information.
66
package main
77

88
import (
99
"fmt"
10-
"io/ioutil"
1110
"log"
1211
"os"
1312
"path"
@@ -106,10 +105,10 @@ func osServiceControl(ctx *context) int {
106105
logFile = serviceName + ".log"
107106
}
108107
if logFile == "os.stdout" {
109-
ctx.logger = logging.NewConsoleLogger()
110-
ctx.errorLogger = logging.NewConsoleErrorLogger()
108+
ctx.logger = logging.NewConsoleLogger(ctx.conf.ServiceConfig.LogFileTimestampFormat)
109+
ctx.errorLogger = logging.NewConsoleErrorLogger(ctx.conf.ServiceConfig.LogFileTimestampFormat)
111110
} else {
112-
ctx.logger = logging.NewFileLoggerWithMaxSize(logFile, ctx.conf.ServiceConfig.UserName, maxSize, ctx.conf.ServiceConfig.LogFileMaxBackupFiles)
111+
ctx.logger = logging.NewFileLoggerWithMaxSize(logFile, ctx.conf.ServiceConfig.UserName, maxSize, ctx.conf.ServiceConfig.LogFileMaxBackupFiles, ctx.conf.ServiceConfig.LogFileTimestampFormat)
113112
ctx.errorLogger = ctx.logger // use the same output for both stdout and errors
114113
}
115114

@@ -146,7 +145,7 @@ func osServiceControl(ctx *context) int {
146145

147146
pidFile := ctx.conf.ServiceConfig.PidFile
148147
if pidFile != "" {
149-
_ = ioutil.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644)
148+
_ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644)
150149
}
151150
return 0
152151
}
@@ -197,7 +196,7 @@ func setupEnvironment(conf *config.Config) {
197196
_ = os.Setenv("SILVER_SERVICE_PID", fmt.Sprint(os.Getpid()))
198197

199198
// If we have HTTP proxy conf, load this
200-
if b, err := ioutil.ReadFile(proxyConfFile()); err == nil {
199+
if b, err := os.ReadFile(proxyConfFile()); err == nil {
201200
proxy := strings.TrimSpace(string(b))
202201
if proxy != "" {
203202
_ = os.Setenv("SILVER_HTTP_PROXY", proxy)
@@ -215,7 +214,7 @@ func writeProxyConf() error {
215214
if err != nil {
216215
proxy = ""
217216
}
218-
return ioutil.WriteFile(proxyConfFile(), []byte(proxy+"\n"), 0644)
217+
return os.WriteFile(proxyConfFile(), []byte(proxy+"\n"), 0644)
219218
}
220219

221220
func proxyConfFile() string {
@@ -256,7 +255,7 @@ func execCommand(ctx *context, args []string) int {
256255
// Append any extra commands
257256
cmdConf.Args = append(cmd.Args, args[1:]...)
258257
// FIXME: Maybe unit conversion should be in the config layer?
259-
cmdConf.ExecTimeout = (time.Second * time.Duration(cmd.TimeoutSecs))
258+
cmdConf.ExecTimeout = time.Second * time.Duration(cmd.TimeoutSecs)
260259

261260
exitCode, err := cmdutil.Execute(cmdConf)
262261
if err != nil {
@@ -335,7 +334,7 @@ func doStop(ctx *context) {
335334
// Create stop file... another method to signal services to stop.
336335
sf := stopFileName(ctx)
337336
if sf != "" {
338-
_ = ioutil.WriteFile(sf, nil, 0644)
337+
_ = os.WriteFile(sf, nil, 0644)
339338
defer os.Remove(sf)
340339
}
341340
if ctx.cronManager != nil {

0 commit comments

Comments
 (0)