Files
ctrld/cmd/cli/log_writer.go
Cuong Manh Le 8a96b8bec4 cmd/cli: adopt FilteredLevelWriter when doing internal logging
Without verbose log, we use internal log writer with log level set to
debug. However, this will affect other writers, like console log, since
they are default to notice level.

By adopting FilteredLevelWriter, we can make internal log writer run in
debug level, but all others will run in default level instead.
2024-12-19 21:50:00 +07:00

133 lines
3.0 KiB
Go

package cli
import (
"bytes"
"errors"
"io"
"os"
"sync"
"time"
"github.com/rs/zerolog"
"github.com/Control-D-Inc/ctrld"
)
const (
logWriterSize = 1024 * 1024 * 5 // 5 MB
logWriterInitialSize = 32 // 32 B
logSentInterval = time.Minute
logTruncatedMarker = "...\n"
)
type logViewResponse struct {
Data string `json:"data"`
}
// logWriter is an internal buffer to keep track of runtime log when no logging is enabled.
type logWriter struct {
mu sync.Mutex
buf bytes.Buffer
size int
}
// newLogWriter creates an internal log writer with a fixed buffer size.
func newLogWriter() *logWriter {
lw := &logWriter{size: logWriterSize}
return lw
}
func (lw *logWriter) Write(p []byte) (int, error) {
lw.mu.Lock()
defer lw.mu.Unlock()
// If writing p causes overflows, discard old data.
if lw.buf.Len()+len(p) > lw.size {
buf := lw.buf.Bytes()
buf = buf[:logWriterInitialSize]
if idx := bytes.LastIndex(buf, []byte("\n")); idx != -1 {
buf = buf[:idx]
}
lw.buf.Reset()
lw.buf.Write(buf)
lw.buf.WriteString(logTruncatedMarker) // indicate that the log was truncated.
}
// If p is bigger than buffer size, truncate p by half until its size is smaller.
for len(p)+lw.buf.Len() > lw.size {
p = p[len(p)/2:]
}
return lw.buf.Write(p)
}
// initInternalLogging performs internal logging if there's no log enabled.
func (p *prog) initInternalLogging(writers []io.Writer) {
if !p.needInternalLogging() {
return
}
p.initInternalLogWriterOnce.Do(func() {
mainLog.Load().Notice().Msg("internal logging enabled")
lw := newLogWriter()
p.internalLogWriter = lw
p.internalLogSent = time.Now().Add(-logSentInterval)
})
p.mu.Lock()
lw := p.internalLogWriter
p.mu.Unlock()
// If ctrld was run without explicit verbose level,
// run the internal logging at debug level, so we could
// have enough information for troubleshooting.
if verbose == 0 {
for i := range writers {
w := &zerolog.FilteredLevelWriter{
Writer: zerolog.LevelWriterAdapter{Writer: writers[i]},
Level: zerolog.NoticeLevel,
}
writers[i] = w
}
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
writers = append(writers, lw)
multi := zerolog.MultiLevelWriter(writers...)
l := mainLog.Load().Output(multi).With().Logger()
mainLog.Store(&l)
ctrld.ProxyLogger.Store(&l)
}
// needInternalLogging reports whether prog needs to run internal logging.
func (p *prog) needInternalLogging() bool {
// Do not run in non-cd mode.
if cdUID == "" {
return false
}
// Do not run if there's already log file.
if p.cfg.Service.LogPath != "" {
return false
}
return true
}
func (p *prog) logContent() ([]byte, error) {
var data []byte
if p.needInternalLogging() {
p.mu.Lock()
lw := p.internalLogWriter
p.mu.Unlock()
if lw == nil {
return nil, errors.New("nil internal log writer")
}
lw.mu.Lock()
data = lw.buf.Bytes()
lw.mu.Unlock()
} else {
if p.cfg.Service.LogPath == "" {
return nil, nil
}
buf, err := os.ReadFile(p.cfg.Service.LogPath)
if err != nil {
return nil, err
}
data = buf
}
return data, nil
}