snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
This commit is contained in:
405
logger/log.go
Normal file
405
logger/log.go
Normal file
@@ -0,0 +1,405 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package logger provides logging facilities for the NATS server
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default file permissions for log files.
|
||||
const defaultLogPerms = os.FileMode(0640)
|
||||
|
||||
// Logger is the server logger
|
||||
type Logger struct {
|
||||
sync.Mutex
|
||||
logger *log.Logger
|
||||
debug bool
|
||||
trace bool
|
||||
infoLabel string
|
||||
warnLabel string
|
||||
errorLabel string
|
||||
fatalLabel string
|
||||
debugLabel string
|
||||
traceLabel string
|
||||
fl *fileLogger
|
||||
}
|
||||
|
||||
type LogOption interface {
|
||||
isLoggerOption()
|
||||
}
|
||||
|
||||
// LogUTC controls whether timestamps in the log output should be UTC or local time.
|
||||
type LogUTC bool
|
||||
|
||||
func (l LogUTC) isLoggerOption() {}
|
||||
|
||||
func logFlags(time bool, opts ...LogOption) int {
|
||||
flags := 0
|
||||
if time {
|
||||
flags = log.LstdFlags | log.Lmicroseconds
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
switch v := opt.(type) {
|
||||
case LogUTC:
|
||||
if time && bool(v) {
|
||||
flags |= log.LUTC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
// NewStdLogger creates a logger with output directed to Stderr
|
||||
func NewStdLogger(time, debug, trace, colors, pid bool, opts ...LogOption) *Logger {
|
||||
flags := logFlags(time, opts...)
|
||||
|
||||
pre := ""
|
||||
if pid {
|
||||
pre = pidPrefix()
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
logger: log.New(os.Stderr, pre, flags),
|
||||
debug: debug,
|
||||
trace: trace,
|
||||
}
|
||||
|
||||
if colors {
|
||||
setColoredLabelFormats(l)
|
||||
} else {
|
||||
setPlainLabelFormats(l)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// NewFileLogger creates a logger with output directed to a file
|
||||
func NewFileLogger(filename string, time, debug, trace, pid bool, opts ...LogOption) *Logger {
|
||||
flags := logFlags(time, opts...)
|
||||
|
||||
pre := ""
|
||||
if pid {
|
||||
pre = pidPrefix()
|
||||
}
|
||||
|
||||
fl, err := newFileLogger(filename, pre, time)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
logger: log.New(fl, pre, flags),
|
||||
debug: debug,
|
||||
trace: trace,
|
||||
fl: fl,
|
||||
}
|
||||
fl.Lock()
|
||||
fl.l = l
|
||||
fl.Unlock()
|
||||
|
||||
setPlainLabelFormats(l)
|
||||
return l
|
||||
}
|
||||
|
||||
type writerAndCloser interface {
|
||||
Write(b []byte) (int, error)
|
||||
Close() error
|
||||
Name() string
|
||||
}
|
||||
|
||||
type fileLogger struct {
|
||||
out int64
|
||||
canRotate int32
|
||||
sync.Mutex
|
||||
l *Logger
|
||||
f writerAndCloser
|
||||
limit int64
|
||||
olimit int64
|
||||
pid string
|
||||
time bool
|
||||
closed bool
|
||||
maxNumFiles int
|
||||
}
|
||||
|
||||
func newFileLogger(filename, pidPrefix string, time bool) (*fileLogger, error) {
|
||||
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||
f, err := os.OpenFile(filename, fileflags, defaultLogPerms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
fl := &fileLogger{
|
||||
canRotate: 0,
|
||||
f: f,
|
||||
out: stats.Size(),
|
||||
pid: pidPrefix,
|
||||
time: time,
|
||||
}
|
||||
return fl, nil
|
||||
}
|
||||
|
||||
func (l *fileLogger) setLimit(limit int64) {
|
||||
l.Lock()
|
||||
l.olimit, l.limit = limit, limit
|
||||
atomic.StoreInt32(&l.canRotate, 1)
|
||||
rotateNow := l.out > l.limit
|
||||
l.Unlock()
|
||||
if rotateNow {
|
||||
l.l.Noticef("Rotating logfile...")
|
||||
}
|
||||
}
|
||||
|
||||
func (l *fileLogger) setMaxNumFiles(max int) {
|
||||
l.Lock()
|
||||
l.maxNumFiles = max
|
||||
l.Unlock()
|
||||
}
|
||||
|
||||
func (l *fileLogger) logDirect(label, format string, v ...any) int {
|
||||
var entrya = [256]byte{}
|
||||
var entry = entrya[:0]
|
||||
if l.pid != "" {
|
||||
entry = append(entry, l.pid...)
|
||||
}
|
||||
if l.time {
|
||||
now := time.Now()
|
||||
year, month, day := now.Date()
|
||||
hour, min, sec := now.Clock()
|
||||
microsec := now.Nanosecond() / 1000
|
||||
entry = append(entry, fmt.Sprintf("%04d/%02d/%02d %02d:%02d:%02d.%06d ",
|
||||
year, month, day, hour, min, sec, microsec)...)
|
||||
}
|
||||
entry = append(entry, label...)
|
||||
entry = append(entry, fmt.Sprintf(format, v...)...)
|
||||
entry = append(entry, '\r', '\n')
|
||||
l.f.Write(entry)
|
||||
return len(entry)
|
||||
}
|
||||
|
||||
func (l *fileLogger) logPurge(fname string) {
|
||||
var backups []string
|
||||
lDir := filepath.Dir(fname)
|
||||
lBase := filepath.Base(fname)
|
||||
entries, err := os.ReadDir(lDir)
|
||||
if err != nil {
|
||||
l.logDirect(l.l.errorLabel, "Unable to read directory %q for log purge (%v), will attempt next rotation", lDir, err)
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || entry.Name() == lBase || !strings.HasPrefix(entry.Name(), lBase) {
|
||||
continue
|
||||
}
|
||||
if stamp, found := strings.CutPrefix(entry.Name(), fmt.Sprintf("%s%s", lBase, ".")); found {
|
||||
_, err := time.Parse("2006:01:02:15:04:05.999999999", strings.Replace(stamp, ".", ":", 5))
|
||||
if err == nil {
|
||||
backups = append(backups, entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
currBackups := len(backups)
|
||||
maxBackups := l.maxNumFiles - 1
|
||||
if currBackups > maxBackups {
|
||||
// backups sorted oldest to latest based on timestamped lexical filename (ReadDir)
|
||||
for i := 0; i < currBackups-maxBackups; i++ {
|
||||
if err := os.Remove(filepath.Join(lDir, string(os.PathSeparator), backups[i])); err != nil {
|
||||
l.logDirect(l.l.errorLabel, "Unable to remove backup log file %q (%v), will attempt next rotation", backups[i], err)
|
||||
// Bail fast, we'll try again next rotation
|
||||
return
|
||||
}
|
||||
l.logDirect(l.l.infoLabel, "Purged log file %q", backups[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *fileLogger) Write(b []byte) (int, error) {
|
||||
if atomic.LoadInt32(&l.canRotate) == 0 {
|
||||
n, err := l.f.Write(b)
|
||||
if err == nil {
|
||||
atomic.AddInt64(&l.out, int64(n))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
l.Lock()
|
||||
n, err := l.f.Write(b)
|
||||
if err == nil {
|
||||
l.out += int64(n)
|
||||
if l.out > l.limit {
|
||||
if err := l.f.Close(); err != nil {
|
||||
l.limit *= 2
|
||||
l.logDirect(l.l.errorLabel, "Unable to close logfile for rotation (%v), will attempt next rotation at size %v", err, l.limit)
|
||||
l.Unlock()
|
||||
return n, err
|
||||
}
|
||||
fname := l.f.Name()
|
||||
now := time.Now()
|
||||
bak := fmt.Sprintf("%s.%04d.%02d.%02d.%02d.%02d.%02d.%09d", fname,
|
||||
now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(),
|
||||
now.Second(), now.Nanosecond())
|
||||
os.Rename(fname, bak)
|
||||
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||
f, err := os.OpenFile(fname, fileflags, defaultLogPerms)
|
||||
if err != nil {
|
||||
l.Unlock()
|
||||
panic(fmt.Sprintf("Unable to re-open the logfile %q after rotation: %v", fname, err))
|
||||
}
|
||||
l.f = f
|
||||
n := l.logDirect(l.l.infoLabel, "Rotated log, backup saved as %q", bak)
|
||||
l.out = int64(n)
|
||||
l.limit = l.olimit
|
||||
if l.maxNumFiles > 0 {
|
||||
l.logPurge(fname)
|
||||
}
|
||||
}
|
||||
}
|
||||
l.Unlock()
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (l *fileLogger) close() error {
|
||||
l.Lock()
|
||||
if l.closed {
|
||||
l.Unlock()
|
||||
return nil
|
||||
}
|
||||
l.closed = true
|
||||
l.Unlock()
|
||||
return l.f.Close()
|
||||
}
|
||||
|
||||
// SetSizeLimit sets the size of a logfile after which a backup
|
||||
// is created with the file name + "year.month.day.hour.min.sec.nanosec"
|
||||
// and the current log is truncated.
|
||||
func (l *Logger) SetSizeLimit(limit int64) error {
|
||||
l.Lock()
|
||||
if l.fl == nil {
|
||||
l.Unlock()
|
||||
return fmt.Errorf("can set log size limit only for file logger")
|
||||
}
|
||||
fl := l.fl
|
||||
l.Unlock()
|
||||
fl.setLimit(limit)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMaxNumFiles sets the number of archived log files that will be retained
|
||||
func (l *Logger) SetMaxNumFiles(max int) error {
|
||||
l.Lock()
|
||||
if l.fl == nil {
|
||||
l.Unlock()
|
||||
return fmt.Errorf("can set log max number of files only for file logger")
|
||||
}
|
||||
fl := l.fl
|
||||
l.Unlock()
|
||||
fl.setMaxNumFiles(max)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTestLogger creates a logger with output directed to Stderr with a prefix.
|
||||
// Useful for tracing in tests when multiple servers are in the same pid
|
||||
func NewTestLogger(prefix string, time bool) *Logger {
|
||||
flags := 0
|
||||
if time {
|
||||
flags = log.LstdFlags | log.Lmicroseconds
|
||||
}
|
||||
l := &Logger{
|
||||
logger: log.New(os.Stderr, prefix, flags),
|
||||
debug: true,
|
||||
trace: true,
|
||||
}
|
||||
setColoredLabelFormats(l)
|
||||
return l
|
||||
}
|
||||
|
||||
// Close implements the io.Closer interface to clean up
|
||||
// resources in the server's logger implementation.
|
||||
// Caller must ensure threadsafety.
|
||||
func (l *Logger) Close() error {
|
||||
if l.fl != nil {
|
||||
return l.fl.close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate the pid prefix string
|
||||
func pidPrefix() string {
|
||||
return fmt.Sprintf("[%d] ", os.Getpid())
|
||||
}
|
||||
|
||||
func setPlainLabelFormats(l *Logger) {
|
||||
l.infoLabel = "[INF] "
|
||||
l.debugLabel = "[DBG] "
|
||||
l.warnLabel = "[WRN] "
|
||||
l.errorLabel = "[ERR] "
|
||||
l.fatalLabel = "[FTL] "
|
||||
l.traceLabel = "[TRC] "
|
||||
}
|
||||
|
||||
func setColoredLabelFormats(l *Logger) {
|
||||
colorFormat := "[\x1b[%sm%s\x1b[0m] "
|
||||
l.infoLabel = fmt.Sprintf(colorFormat, "32", "INF")
|
||||
l.debugLabel = fmt.Sprintf(colorFormat, "36", "DBG")
|
||||
l.warnLabel = fmt.Sprintf(colorFormat, "0;93", "WRN")
|
||||
l.errorLabel = fmt.Sprintf(colorFormat, "31", "ERR")
|
||||
l.fatalLabel = fmt.Sprintf(colorFormat, "31", "FTL")
|
||||
l.traceLabel = fmt.Sprintf(colorFormat, "33", "TRC")
|
||||
}
|
||||
|
||||
// Noticef logs a notice statement
|
||||
func (l *Logger) Noticef(format string, v ...any) {
|
||||
l.logger.Printf(l.infoLabel+format, v...)
|
||||
}
|
||||
|
||||
// Warnf logs a notice statement
|
||||
func (l *Logger) Warnf(format string, v ...any) {
|
||||
l.logger.Printf(l.warnLabel+format, v...)
|
||||
}
|
||||
|
||||
// Errorf logs an error statement
|
||||
func (l *Logger) Errorf(format string, v ...any) {
|
||||
l.logger.Printf(l.errorLabel+format, v...)
|
||||
}
|
||||
|
||||
// Fatalf logs a fatal error
|
||||
func (l *Logger) Fatalf(format string, v ...any) {
|
||||
l.logger.Fatalf(l.fatalLabel+format, v...)
|
||||
}
|
||||
|
||||
// Debugf logs a debug statement
|
||||
func (l *Logger) Debugf(format string, v ...any) {
|
||||
if l.debug {
|
||||
l.logger.Printf(l.debugLabel+format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// Tracef logs a trace statement
|
||||
func (l *Logger) Tracef(format string, v ...any) {
|
||||
if l.trace {
|
||||
l.logger.Printf(l.traceLabel+format, v...)
|
||||
}
|
||||
}
|
||||
327
logger/log_test.go
Normal file
327
logger/log_test.go
Normal file
@@ -0,0 +1,327 @@
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStdLogger(t *testing.T) {
|
||||
logger := NewStdLogger(false, false, false, false, false)
|
||||
|
||||
flags := logger.logger.Flags()
|
||||
if flags != 0 {
|
||||
t.Fatalf("Expected %v, received %v\n", 0, flags)
|
||||
}
|
||||
|
||||
if logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
|
||||
}
|
||||
|
||||
if logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdLoggerWithDebugTraceAndTime(t *testing.T) {
|
||||
logger := NewStdLogger(true, true, true, false, false)
|
||||
|
||||
flags := logger.logger.Flags()
|
||||
if flags != log.LstdFlags|log.Lmicroseconds {
|
||||
t.Fatalf("Expected %d, received %d\n", log.LstdFlags, flags)
|
||||
}
|
||||
|
||||
if !logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||
}
|
||||
|
||||
if !logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdLoggerNotice(t *testing.T) {
|
||||
expectOutput(t, func() {
|
||||
logger := NewStdLogger(false, false, false, false, false)
|
||||
logger.Noticef("foo")
|
||||
}, "[INF] foo\n")
|
||||
}
|
||||
|
||||
func TestStdLoggerNoticeWithColor(t *testing.T) {
|
||||
expectOutput(t, func() {
|
||||
logger := NewStdLogger(false, false, false, true, false)
|
||||
logger.Noticef("foo")
|
||||
}, "[\x1b[32mINF\x1b[0m] foo\n")
|
||||
}
|
||||
|
||||
func TestStdLoggerDebug(t *testing.T) {
|
||||
expectOutput(t, func() {
|
||||
logger := NewStdLogger(false, true, false, false, false)
|
||||
logger.Debugf("foo %s", "bar")
|
||||
}, "[DBG] foo bar\n")
|
||||
}
|
||||
|
||||
func TestStdLoggerDebugWithOutDebug(t *testing.T) {
|
||||
expectOutput(t, func() {
|
||||
logger := NewStdLogger(false, false, false, false, false)
|
||||
logger.Debugf("foo")
|
||||
}, "")
|
||||
}
|
||||
|
||||
func TestStdLoggerTrace(t *testing.T) {
|
||||
expectOutput(t, func() {
|
||||
logger := NewStdLogger(false, false, true, false, false)
|
||||
logger.Tracef("foo")
|
||||
}, "[TRC] foo\n")
|
||||
}
|
||||
|
||||
func TestStdLoggerTraceWithOutDebug(t *testing.T) {
|
||||
expectOutput(t, func() {
|
||||
logger := NewStdLogger(false, false, false, false, false)
|
||||
logger.Tracef("foo")
|
||||
}, "")
|
||||
}
|
||||
|
||||
func TestFileLogger(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
file := createFileAtDir(t, tmpDir, "nats-server:log_")
|
||||
file.Close()
|
||||
|
||||
logger := NewFileLogger(file.Name(), false, false, false, false)
|
||||
defer logger.Close()
|
||||
logger.Noticef("foo")
|
||||
|
||||
buf, err := os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read logfile: %v", err)
|
||||
}
|
||||
if len(buf) <= 0 {
|
||||
t.Fatal("Expected a non-zero length logfile")
|
||||
}
|
||||
|
||||
if string(buf) != "[INF] foo\n" {
|
||||
t.Fatalf("Expected '%s', received '%s'\n", "[INFO] foo", string(buf))
|
||||
}
|
||||
|
||||
file = createFileAtDir(t, tmpDir, "nats-server:log_")
|
||||
file.Close()
|
||||
|
||||
logger = NewFileLogger(file.Name(), true, false, true, true)
|
||||
defer logger.Close()
|
||||
logger.Errorf("foo")
|
||||
|
||||
buf, err = os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read logfile: %v", err)
|
||||
}
|
||||
if len(buf) <= 0 {
|
||||
t.Fatal("Expected a non-zero length logfile")
|
||||
}
|
||||
str := string(buf)
|
||||
errMsg := fmt.Sprintf("Expected '%s', received '%s'\n", "[pid] <date> [ERR] foo", str)
|
||||
pidEnd := strings.Index(str, " ")
|
||||
infoStart := strings.LastIndex(str, "[ERR]")
|
||||
if pidEnd == -1 || infoStart == -1 {
|
||||
t.Fatalf("%v", errMsg)
|
||||
}
|
||||
pid := str[0:pidEnd]
|
||||
if pid[0] != '[' || pid[len(pid)-1] != ']' {
|
||||
t.Fatalf("%v", errMsg)
|
||||
}
|
||||
|
||||
date := str[pidEnd:infoStart]
|
||||
dateRegExp := "[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}"
|
||||
reg, err := regexp.Compile(dateRegExp)
|
||||
if err != nil {
|
||||
t.Fatalf("Compile date regexp error: %v", err)
|
||||
}
|
||||
if matched := reg.Match([]byte(date)); !matched {
|
||||
t.Fatalf("Date string '%s' does not match '%s'", date, dateRegExp)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(str, "[ERR] foo\n") {
|
||||
t.Fatalf("%v", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLoggerSizeLimit(t *testing.T) {
|
||||
// Create std logger
|
||||
logger := NewStdLogger(true, false, false, false, true)
|
||||
if err := logger.SetSizeLimit(1000); err == nil ||
|
||||
!strings.Contains(err.Error(), "only for file logger") {
|
||||
t.Fatalf("Expected error about being able to use only for file logger, got %v", err)
|
||||
}
|
||||
logger.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file := createFileAtDir(t, tmpDir, "log_")
|
||||
file.Close()
|
||||
|
||||
logger = NewFileLogger(file.Name(), true, false, false, true)
|
||||
defer logger.Close()
|
||||
logger.SetSizeLimit(1000)
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
logger.Noticef("This is a line in the log file")
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading logs dir: %v", err)
|
||||
}
|
||||
if len(files) == 1 {
|
||||
t.Fatalf("Expected file to have been rotated")
|
||||
}
|
||||
lastBackup := files[len(files)-1]
|
||||
if err := logger.Close(); err != nil {
|
||||
t.Fatalf("Error closing log: %v", err)
|
||||
}
|
||||
content, err := os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading latest log: %v", err)
|
||||
}
|
||||
if !bytes.Contains(content, []byte("Rotated log")) ||
|
||||
!bytes.Contains(content, []byte(lastBackup.Name())) {
|
||||
t.Fatalf("Should be statement about rotated log and backup name, got %s", content)
|
||||
}
|
||||
|
||||
tmpDir = t.TempDir()
|
||||
|
||||
// Recreate logger and don't set a limit
|
||||
file = createFileAtDir(t, tmpDir, "log_")
|
||||
file.Close()
|
||||
logger = NewFileLogger(file.Name(), true, false, false, true)
|
||||
defer logger.Close()
|
||||
for i := 0; i < 50; i++ {
|
||||
logger.Noticef("This is line %d in the log file", i+1)
|
||||
}
|
||||
files, err = os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading logs dir: %v", err)
|
||||
}
|
||||
if len(files) != 1 {
|
||||
t.Fatalf("Expected file to not be rotated")
|
||||
}
|
||||
|
||||
// Now set a limit that is below current size
|
||||
logger.SetSizeLimit(1000)
|
||||
// Should have triggered rotation
|
||||
files, err = os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading logs dir: %v", err)
|
||||
}
|
||||
if len(files) <= 1 {
|
||||
t.Fatalf("Expected file to have been rotated")
|
||||
}
|
||||
if err := logger.Close(); err != nil {
|
||||
t.Fatalf("Error closing log: %v", err)
|
||||
}
|
||||
lastBackup = files[len(files)-1]
|
||||
content, err = os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading latest log: %v", err)
|
||||
}
|
||||
if !bytes.Contains(content, []byte("Rotated log")) ||
|
||||
!bytes.Contains(content, []byte(lastBackup.Name())) {
|
||||
t.Fatalf("Should be statement about rotated log and backup name, got %s", content)
|
||||
}
|
||||
|
||||
logger = NewFileLogger(file.Name(), true, false, false, true)
|
||||
defer logger.Close()
|
||||
logger.SetSizeLimit(1000)
|
||||
|
||||
// Check error on rotate.
|
||||
logger.Lock()
|
||||
logger.fl.Lock()
|
||||
failClose := &fileLogFailClose{logger.fl.f, true}
|
||||
logger.fl.f = failClose
|
||||
logger.fl.Unlock()
|
||||
logger.Unlock()
|
||||
// Write a big line that will force rotation.
|
||||
// Since we fail to close the log file, we should have bumped the limit to 2000
|
||||
logger.Noticef("This is a big line: %v", make([]byte, 1000))
|
||||
|
||||
// Remove the failure
|
||||
failClose.fail = false
|
||||
// Write a big line that makes rotation happen
|
||||
logger.Noticef("This is a big line: %v", make([]byte, 2000))
|
||||
// Close
|
||||
logger.Close()
|
||||
|
||||
files, err = os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading logs dir: %v", err)
|
||||
}
|
||||
lastBackup = files[len(files)-1]
|
||||
content, err = os.ReadFile(filepath.Join(tmpDir, lastBackup.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading backup file: %v", err)
|
||||
}
|
||||
if !bytes.Contains(content, []byte("on purpose")) || !bytes.Contains(content, []byte("size 2000")) {
|
||||
t.Fatalf("Expected error that file could not rotated and max size bumped to 2000, got %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
type fileLogFailClose struct {
|
||||
writerAndCloser
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (l *fileLogFailClose) Close() error {
|
||||
if l.fail {
|
||||
return fmt.Errorf("on purpose")
|
||||
}
|
||||
return l.writerAndCloser.Close()
|
||||
}
|
||||
|
||||
func expectOutput(t *testing.T, f func(), expected string) {
|
||||
old := os.Stderr // keep backup of the real stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
f()
|
||||
|
||||
outC := make(chan string)
|
||||
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
os.Stderr.Close()
|
||||
os.Stderr = old // restoring the real stdout
|
||||
out := <-outC
|
||||
if out != expected {
|
||||
t.Fatalf("Expected '%s', received '%s'\n", expected, out)
|
||||
}
|
||||
}
|
||||
|
||||
func createFileAtDir(t *testing.T, dir, prefix string) *os.File {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp(dir, prefix)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
132
logger/syslog.go
Normal file
132
logger/syslog.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/syslog"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SysLogger provides a system logger facility
|
||||
type SysLogger struct {
|
||||
writer *syslog.Writer
|
||||
debug bool
|
||||
trace bool
|
||||
}
|
||||
|
||||
// SetSyslogName sets the name to use for the syslog.
|
||||
// Currently used only on Windows.
|
||||
func SetSyslogName(name string) {}
|
||||
|
||||
// GetSysLoggerTag generates the tag name for use in syslog statements. If
|
||||
// the executable is linked, the name of the link will be used as the tag,
|
||||
// otherwise, the name of the executable is used. "nats-server" is the default
|
||||
// for the NATS server.
|
||||
func GetSysLoggerTag() string {
|
||||
procName := os.Args[0]
|
||||
if strings.ContainsRune(procName, os.PathSeparator) {
|
||||
parts := strings.FieldsFunc(procName, func(c rune) bool {
|
||||
return c == os.PathSeparator
|
||||
})
|
||||
procName = parts[len(parts)-1]
|
||||
}
|
||||
return procName
|
||||
}
|
||||
|
||||
// NewSysLogger creates a new system logger
|
||||
func NewSysLogger(debug, trace bool) *SysLogger {
|
||||
w, err := syslog.New(syslog.LOG_DAEMON|syslog.LOG_NOTICE, GetSysLoggerTag())
|
||||
if err != nil {
|
||||
log.Fatalf("error connecting to syslog: %q", err.Error())
|
||||
}
|
||||
|
||||
return &SysLogger{
|
||||
writer: w,
|
||||
debug: debug,
|
||||
trace: trace,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRemoteSysLogger creates a new remote system logger
|
||||
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
|
||||
network, addr := getNetworkAndAddr(fqn)
|
||||
w, err := syslog.Dial(network, addr, syslog.LOG_DEBUG, GetSysLoggerTag())
|
||||
if err != nil {
|
||||
log.Fatalf("error connecting to syslog: %q", err.Error())
|
||||
}
|
||||
|
||||
return &SysLogger{
|
||||
writer: w,
|
||||
debug: debug,
|
||||
trace: trace,
|
||||
}
|
||||
}
|
||||
|
||||
func getNetworkAndAddr(fqn string) (network, addr string) {
|
||||
u, err := url.Parse(fqn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
network = u.Scheme
|
||||
if network == "udp" || network == "tcp" {
|
||||
addr = u.Host
|
||||
} else if network == "unix" {
|
||||
addr = u.Path
|
||||
} else {
|
||||
log.Fatalf("error invalid network type: %q", u.Scheme)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Noticef logs a notice statement
|
||||
func (l *SysLogger) Noticef(format string, v ...any) {
|
||||
l.writer.Notice(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Warnf logs a warning statement
|
||||
func (l *SysLogger) Warnf(format string, v ...any) {
|
||||
l.writer.Warning(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Fatalf logs a fatal error
|
||||
func (l *SysLogger) Fatalf(format string, v ...any) {
|
||||
l.writer.Crit(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Errorf logs an error statement
|
||||
func (l *SysLogger) Errorf(format string, v ...any) {
|
||||
l.writer.Err(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Debugf logs a debug statement
|
||||
func (l *SysLogger) Debugf(format string, v ...any) {
|
||||
if l.debug {
|
||||
l.writer.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Tracef logs a trace statement
|
||||
func (l *SysLogger) Tracef(format string, v ...any) {
|
||||
if l.trace {
|
||||
l.writer.Notice(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
237
logger/syslog_test.go
Normal file
237
logger/syslog_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var serverFQN string
|
||||
|
||||
func TestSysLogger(t *testing.T) {
|
||||
logger := NewSysLogger(false, false)
|
||||
|
||||
if logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
|
||||
}
|
||||
|
||||
if logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysLoggerWithDebugAndTrace(t *testing.T) {
|
||||
logger := NewSysLogger(true, true)
|
||||
|
||||
if !logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||
}
|
||||
|
||||
if !logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||
}
|
||||
}
|
||||
|
||||
func testTag(t *testing.T, exePath, expected string) {
|
||||
os.Args[0] = exePath
|
||||
if result := GetSysLoggerTag(); result != expected {
|
||||
t.Fatalf("Expected %s, received %s", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func restoreArg(orig string) {
|
||||
os.Args[0] = orig
|
||||
}
|
||||
|
||||
func TestSysLoggerTagGen(t *testing.T) {
|
||||
origArg := os.Args[0]
|
||||
defer restoreArg(origArg)
|
||||
|
||||
testTag(t, "nats-server", "nats-server")
|
||||
testTag(t, filepath.Join(".", "nats-server"), "nats-server")
|
||||
testTag(t, filepath.Join("home", "bin", "nats-server"), "nats-server")
|
||||
testTag(t, filepath.Join("..", "..", "nats-server"), "nats-server")
|
||||
testTag(t, "nats-server.service1", "nats-server.service1")
|
||||
testTag(t, "nats-server_service1", "nats-server_service1")
|
||||
testTag(t, "nats-server-service1", "nats-server-service1")
|
||||
testTag(t, "nats-server service1", "nats-server service1")
|
||||
}
|
||||
|
||||
func TestSysLoggerTag(t *testing.T) {
|
||||
origArg := os.Args[0]
|
||||
defer restoreArg(origArg)
|
||||
|
||||
os.Args[0] = "ServerLoggerTag"
|
||||
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||
logger.Noticef("foo")
|
||||
|
||||
line := <-done
|
||||
data := strings.Split(line, "[")
|
||||
if len(data) != 2 {
|
||||
t.Fatalf("Unexpected syslog line %s\n", line)
|
||||
}
|
||||
|
||||
if !strings.Contains(data[0], os.Args[0]) {
|
||||
t.Fatalf("Expected '%s', received '%s'\n", os.Args[0], data[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteSysLogger(t *testing.T) {
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||
|
||||
if !logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||
}
|
||||
|
||||
if !logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||
}
|
||||
logger.Noticef("foo")
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestRemoteSysLoggerNotice(t *testing.T) {
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||
|
||||
logger.Noticef("foo %s", "bar")
|
||||
expectSyslogOutput(t, <-done, "foo bar\n")
|
||||
}
|
||||
|
||||
func TestRemoteSysLoggerDebug(t *testing.T) {
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||
|
||||
logger.Debugf("foo %s", "qux")
|
||||
expectSyslogOutput(t, <-done, "foo qux\n")
|
||||
}
|
||||
|
||||
func TestRemoteSysLoggerDebugDisabled(t *testing.T) {
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, false, false)
|
||||
|
||||
logger.Debugf("foo %s", "qux")
|
||||
rcvd := <-done
|
||||
if rcvd != "" {
|
||||
t.Fatalf("Unexpected syslog response %s\n", rcvd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteSysLoggerTrace(t *testing.T) {
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, true, true)
|
||||
|
||||
logger.Tracef("foo %s", "qux")
|
||||
expectSyslogOutput(t, <-done, "foo qux\n")
|
||||
}
|
||||
|
||||
func TestRemoteSysLoggerTraceDisabled(t *testing.T) {
|
||||
done := make(chan string)
|
||||
startServer(done)
|
||||
logger := NewRemoteSysLogger(serverFQN, true, false)
|
||||
|
||||
logger.Tracef("foo %s", "qux")
|
||||
rcvd := <-done
|
||||
if rcvd != "" {
|
||||
t.Fatalf("Unexpected syslog response %s\n", rcvd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworkAndAddrUDP(t *testing.T) {
|
||||
n, a := getNetworkAndAddr("udp://foo.com:1000")
|
||||
|
||||
if n != "udp" {
|
||||
t.Fatalf("Unexpected network %s\n", n)
|
||||
}
|
||||
|
||||
if a != "foo.com:1000" {
|
||||
t.Fatalf("Unexpected addr %s\n", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworkAndAddrTCP(t *testing.T) {
|
||||
n, a := getNetworkAndAddr("tcp://foo.com:1000")
|
||||
|
||||
if n != "tcp" {
|
||||
t.Fatalf("Unexpected network %s\n", n)
|
||||
}
|
||||
|
||||
if a != "foo.com:1000" {
|
||||
t.Fatalf("Unexpected addr %s\n", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworkAndAddrUnix(t *testing.T) {
|
||||
n, a := getNetworkAndAddr("unix:///foo.sock")
|
||||
|
||||
if n != "unix" {
|
||||
t.Fatalf("Unexpected network %s\n", n)
|
||||
}
|
||||
|
||||
if a != "/foo.sock" {
|
||||
t.Fatalf("Unexpected addr %s\n", a)
|
||||
}
|
||||
}
|
||||
func expectSyslogOutput(t *testing.T, line string, expected string) {
|
||||
data := strings.Split(line, "]: ")
|
||||
if len(data) != 2 {
|
||||
t.Fatalf("Unexpected syslog line %s\n", line)
|
||||
}
|
||||
|
||||
if data[1] != expected {
|
||||
t.Fatalf("Expected '%s', received '%s'\n", expected, data[1])
|
||||
}
|
||||
}
|
||||
|
||||
func runSyslog(c net.PacketConn, done chan<- string) {
|
||||
var buf [4096]byte
|
||||
var rcvd string
|
||||
for {
|
||||
n, _, err := c.ReadFrom(buf[:])
|
||||
if err != nil || n == 0 {
|
||||
break
|
||||
}
|
||||
rcvd += string(buf[:n])
|
||||
}
|
||||
done <- rcvd
|
||||
}
|
||||
|
||||
func startServer(done chan<- string) {
|
||||
c, e := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if e != nil {
|
||||
log.Fatalf("net.ListenPacket failed udp :0 %v", e)
|
||||
}
|
||||
|
||||
serverFQN = fmt.Sprintf("udp://%s", c.LocalAddr().String())
|
||||
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
go runSyslog(c, done)
|
||||
}
|
||||
112
logger/syslog_windows.go
Normal file
112
logger/syslog_windows.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package logger logs to the windows event log
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
var natsEventSource = "NATS-Server"
|
||||
|
||||
// SetSyslogName sets the name to use for the system log event source
|
||||
func SetSyslogName(name string) {
|
||||
natsEventSource = name
|
||||
}
|
||||
|
||||
// SysLogger logs to the windows event logger
|
||||
type SysLogger struct {
|
||||
writer *eventlog.Log
|
||||
debug bool
|
||||
trace bool
|
||||
}
|
||||
|
||||
// NewSysLogger creates a log using the windows event logger
|
||||
func NewSysLogger(debug, trace bool) *SysLogger {
|
||||
if err := eventlog.InstallAsEventCreate(natsEventSource, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
|
||||
if !strings.Contains(err.Error(), "registry key already exists") {
|
||||
panic(fmt.Sprintf("could not access event log: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
w, err := eventlog.Open(natsEventSource)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not open event log: %v", err))
|
||||
}
|
||||
|
||||
return &SysLogger{
|
||||
writer: w,
|
||||
debug: debug,
|
||||
trace: trace,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRemoteSysLogger creates a remote event logger
|
||||
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
|
||||
w, err := eventlog.OpenRemote(fqn, natsEventSource)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not open event log: %v", err))
|
||||
}
|
||||
|
||||
return &SysLogger{
|
||||
writer: w,
|
||||
debug: debug,
|
||||
trace: trace,
|
||||
}
|
||||
}
|
||||
|
||||
func formatMsg(tag, format string, v ...any) string {
|
||||
orig := fmt.Sprintf(format, v...)
|
||||
return fmt.Sprintf("pid[%d][%s]: %s", os.Getpid(), tag, orig)
|
||||
}
|
||||
|
||||
// Noticef logs a notice statement
|
||||
func (l *SysLogger) Noticef(format string, v ...any) {
|
||||
l.writer.Info(1, formatMsg("NOTICE", format, v...))
|
||||
}
|
||||
|
||||
// Warnf logs a warning statement
|
||||
func (l *SysLogger) Warnf(format string, v ...any) {
|
||||
l.writer.Info(1, formatMsg("WARN", format, v...))
|
||||
}
|
||||
|
||||
// Fatalf logs a fatal error
|
||||
func (l *SysLogger) Fatalf(format string, v ...any) {
|
||||
msg := formatMsg("FATAL", format, v...)
|
||||
l.writer.Error(5, msg)
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
// Errorf logs an error statement
|
||||
func (l *SysLogger) Errorf(format string, v ...any) {
|
||||
l.writer.Error(2, formatMsg("ERROR", format, v...))
|
||||
}
|
||||
|
||||
// Debugf logs a debug statement
|
||||
func (l *SysLogger) Debugf(format string, v ...any) {
|
||||
if l.debug {
|
||||
l.writer.Info(3, formatMsg("DEBUG", format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Tracef logs a trace statement
|
||||
func (l *SysLogger) Tracef(format string, v ...any) {
|
||||
if l.trace {
|
||||
l.writer.Info(4, formatMsg("TRACE", format, v...))
|
||||
}
|
||||
}
|
||||
140
logger/syslog_windows_test.go
Normal file
140
logger/syslog_windows_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
// Skips testing if we do not have privledges to run this test.
|
||||
// This lets us skip the tests for general (non admin/system) users.
|
||||
func checkPrivledges(t *testing.T) {
|
||||
src := "NATS-eventlog-testsource"
|
||||
defer eventlog.Remove(src)
|
||||
if err := eventlog.InstallAsEventCreate(src, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
|
||||
if strings.Contains(err.Error(), "Access is denied") {
|
||||
// Skip this test because elevated privileges are required.
|
||||
t.SkipNow()
|
||||
}
|
||||
// let the tests report other types of errors
|
||||
}
|
||||
}
|
||||
|
||||
// lastLogEntryContains reads the last entry (/c:1 /rd:true) written
|
||||
// to the event log by the NATS-Server source, returning true if the
|
||||
// passed text was found, false otherwise.
|
||||
func lastLogEntryContains(t *testing.T, text string) bool {
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
cmd := exec.Command("wevtutil.exe", "qe", "Application", "/q:*[System[Provider[@Name='NATS-Server']]]",
|
||||
"/rd:true", "/c:1")
|
||||
if output, err = cmd.Output(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %v", err)
|
||||
}
|
||||
return strings.Contains(string(output), text)
|
||||
}
|
||||
|
||||
// TestSysLogger tests event logging on windows
|
||||
func TestSysLogger(t *testing.T) {
|
||||
checkPrivledges(t)
|
||||
logger := NewSysLogger(false, false)
|
||||
if logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
|
||||
}
|
||||
|
||||
if logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
|
||||
}
|
||||
logger.Noticef("%s", "Noticef")
|
||||
if !lastLogEntryContains(t, "[NOTICE]: Noticef") {
|
||||
t.Fatalf("missing log entry")
|
||||
}
|
||||
|
||||
logger.Errorf("%s", "Errorf")
|
||||
if !lastLogEntryContains(t, "[ERROR]: Errorf") {
|
||||
t.Fatalf("missing log entry")
|
||||
}
|
||||
|
||||
logger.Tracef("%s", "Tracef")
|
||||
if lastLogEntryContains(t, "Tracef") {
|
||||
t.Fatalf("should not contain log entry")
|
||||
}
|
||||
|
||||
logger.Debugf("%s", "Debugf")
|
||||
if lastLogEntryContains(t, "Debugf") {
|
||||
t.Fatalf("should not contain log entry")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSysLoggerWithDebugAndTrace tests event logging
|
||||
func TestSysLoggerWithDebugAndTrace(t *testing.T) {
|
||||
checkPrivledges(t)
|
||||
logger := NewSysLogger(true, true)
|
||||
if !logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||
}
|
||||
|
||||
if !logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||
}
|
||||
|
||||
logger.Tracef("%s", "Tracef")
|
||||
if !lastLogEntryContains(t, "[TRACE]: Tracef") {
|
||||
t.Fatalf("missing log entry")
|
||||
}
|
||||
|
||||
logger.Debugf("%s", "Debugf")
|
||||
if !lastLogEntryContains(t, "[DEBUG]: Debugf") {
|
||||
t.Fatalf("missing log entry")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoteSysLoggerWithDebugAndTrace tests remote event logging
|
||||
func TestRemoteSysLoggerWithDebugAndTrace(t *testing.T) {
|
||||
checkPrivledges(t)
|
||||
logger := NewRemoteSysLogger("", true, true)
|
||||
if !logger.debug {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
|
||||
}
|
||||
|
||||
if !logger.trace {
|
||||
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
|
||||
}
|
||||
logger.Tracef("NATS %s", "[TRACE]: Remote Noticef")
|
||||
if !lastLogEntryContains(t, "Remote Noticef") {
|
||||
t.Fatalf("missing log entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysLoggerFatalf(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if !lastLogEntryContains(t, "[FATAL]: Fatalf") {
|
||||
t.Fatalf("missing log entry")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
checkPrivledges(t)
|
||||
logger := NewSysLogger(true, true)
|
||||
logger.Fatalf("%s", "Fatalf")
|
||||
t.Fatalf("did not panic when expected to")
|
||||
}
|
||||
Reference in New Issue
Block a user