fleet/server/logging/filesystem.go
Victor Lyuboslavsky 77eb458658
Migrated logging and google calendar files to use slog (#40541)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40540 

# Checklist for submitter
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
  - Changes present in previous PR

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Switched the application logging to Go's standard slog with
context-aware logging, improving structured logs and observability
across services (status, audit, result, integrations).
* Replaced legacy logging implementations and updated runtime wiring to
propagate contextual loggers for more consistent, searchable log output.

* **Tests**
  * Updated test suites to use the new slog discard/logger setup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 12:48:54 -06:00

152 lines
3.8 KiB
Go

package logging
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"sync"
"syscall"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
type filesystemLogWriter struct {
writer io.WriteCloser
}
// NewFilesystemLogWriter creates a logger that writes to a file.
//
// The logFile can be rotated by sending a `SIGHUP` signal to Fleet if
// enableRotation is true
//
// The enableCompression argument is only used when enableRotation is true.
func NewFilesystemLogWriter(ctx context.Context, path string, appLogger *slog.Logger, enableRotation, enableCompression bool, maxSize, maxAge, maxBackups int) (*filesystemLogWriter, error) {
// Fail early if the process does not have the necessary
// permissions to open the file at path.
file, err := openFile(path)
if err != nil {
return nil, fmt.Errorf("perm check: %w", err)
}
if !enableRotation {
// no log rotation, use "raw" bufio implementation
return &filesystemLogWriter{
writer: newRawLogWriter(file),
}, nil
}
// Use lumberjack logger that supports rotation
file.Close()
fsLogger := &lumberjack.Logger{
Filename: path,
MaxSize: maxSize, // megabytes
MaxBackups: maxBackups,
MaxAge: maxAge, // days
Compress: enableCompression,
}
appLogger = appLogger.With("component", "filesystem-logger")
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP)
go func() {
for {
<-sig // block on signal
if err := fsLogger.Rotate(); err != nil {
appLogger.ErrorContext(ctx, "log rotation error", "err", err)
}
}
}()
return &filesystemLogWriter{fsLogger}, nil
}
// If writer is based on bufio we want to flush after a batch of
// writes so log entry gets completely written to the logfile.
type flusher interface {
Flush() error
}
// Write writes the provided logs to the filesystem
func (l *filesystemLogWriter) Write(ctx context.Context, logs []json.RawMessage) error {
for _, log := range logs {
// Add newline to separate logs in output file
log = append(log, '\n')
if _, err := l.writer.Write(log); err != nil {
return ctxerr.Wrap(ctx, err, "writing log")
}
}
if flusher, ok := l.writer.(flusher); ok {
if err := flusher.Flush(); err != nil {
return ctxerr.Wrap(ctx, err, "flushing log")
}
}
return nil
}
// rawLogWriter implements writing to logs directly through bufio
type rawLogWriter struct {
file *os.File
buff *bufio.Writer
mtx sync.Mutex
}
func newRawLogWriter(file *os.File) *rawLogWriter {
buff := bufio.NewWriter(file)
return &rawLogWriter{file: file, buff: buff}
}
// Write bytes to file
func (l *rawLogWriter) Write(b []byte) (int, error) {
l.mtx.Lock()
defer l.mtx.Unlock()
if l.buff == nil || l.file == nil {
return 0, errors.New("filesystemLogWriter: can't write to closed file")
}
if _, statErr := os.Stat(l.file.Name()); errors.Is(statErr, os.ErrNotExist) {
f, err := secure.OpenFile(l.file.Name(), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return 0, fmt.Errorf("create file for filesystemLogWriter %s: %w", l.file.Name(), err)
}
l.file = f
l.buff = bufio.NewWriter(f)
}
return l.buff.Write(b)
}
// Flush writes all buffered bytes to log file
func (l *rawLogWriter) Flush() error {
l.mtx.Lock()
defer l.mtx.Unlock()
if l.buff == nil {
return errors.New("can't write to a closed file")
}
return l.buff.Flush()
}
// Close log file
func (l *rawLogWriter) Close() error {
l.mtx.Lock()
defer l.mtx.Unlock()
if l.buff != nil {
if err := l.buff.Flush(); err != nil {
return err
}
l.buff = nil
}
if l.file != nil {
if err := l.file.Close(); err != nil {
return err
}
l.file = nil
}
return nil
}
func openFile(path string) (*os.File, error) {
return os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644) // nolint:gosec // G302
}