mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-27 12:52:27 +02:00
8abeeea4c3
Add file-backed persistence to the internal logWriter so runtime logs survive service restarts. When internal logging is enabled (CD mode, no explicit log_path), writes are teed to both the existing in-memory ring buffer and a rotated file on disk (ctrld.log in the home directory). File rotation: 5MB max with 1 backup (ctrld.log.1), so max ~10MB on disk. Log view/send now reads from the persisted files (including backup) to provide complete history across restarts. Live tail continues to use the in-memory subscriber mechanism unchanged. Activation: same conditions as existing internal logging — CD mode only, no log_path configured. No new config options or dependencies.
669 lines
21 KiB
Go
669 lines
21 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
// Log writer constants for buffer management and log formatting
|
|
const (
|
|
// logWriterSize is the default buffer size for log writers
|
|
// This provides sufficient space for runtime logs without excessive memory usage
|
|
logWriterSize = 1024 * 1024 * 5 // 5 MB
|
|
|
|
// logWriterSmallSize is used for memory-constrained environments
|
|
// This reduces memory footprint while still maintaining log functionality
|
|
logWriterSmallSize = 1024 * 1024 * 1 // 1 MB
|
|
|
|
// logWriterInitialSize is the initial buffer allocation
|
|
// This provides immediate space for early log entries
|
|
logWriterInitialSize = 32 * 1024 // 32 KB
|
|
|
|
// logWriterSentInterval controls how often logs are sent to external systems
|
|
// This balances real-time logging with system performance
|
|
logWriterSentInterval = time.Minute
|
|
|
|
// logWriterInitEndMarker marks the end of initialization logs
|
|
// This helps separate startup logs from runtime logs
|
|
logWriterInitEndMarker = "\n\n=== INIT_END ===\n\n"
|
|
|
|
// logWriterLogEndMarker marks the end of log sections
|
|
// This provides clear boundaries for log parsing and analysis
|
|
logWriterLogEndMarker = "\n\n=== LOG_END ===\n\n"
|
|
|
|
logFileName = "ctrld.log"
|
|
logFileMaxSize = 1024 * 1024 * 5 // 5 MB
|
|
)
|
|
|
|
// Custom level encoders that handle NOTICE level
|
|
// Since NOTICE and WARN share the same numeric value (1), we handle them specially
|
|
// in the encoder to display NOTICE messages with the "NOTICE" prefix.
|
|
// Note: WARN messages will also display as "NOTICE" because they share the same level value.
|
|
// This is the intended behavior for visual distinction.
|
|
|
|
// noticeLevelEncoder provides custom level encoding for NOTICE level
|
|
// This ensures NOTICE messages are clearly distinguished from other log levels
|
|
func noticeLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
|
switch l {
|
|
case ctrld.NoticeLevel:
|
|
enc.AppendString("NOTICE")
|
|
default:
|
|
zapcore.CapitalLevelEncoder(l, enc)
|
|
}
|
|
}
|
|
|
|
// noticeColorLevelEncoder provides colored level encoding for NOTICE level
|
|
// This uses cyan color to make NOTICE messages visually distinct in terminal output
|
|
func noticeColorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
|
switch l {
|
|
case ctrld.NoticeLevel:
|
|
enc.AppendString("\x1b[36mNOTICE\x1b[0m") // Cyan color for NOTICE
|
|
default:
|
|
zapcore.CapitalColorLevelEncoder(l, enc)
|
|
}
|
|
}
|
|
|
|
// logViewResponse represents the response structure for log viewing requests
|
|
// This provides a consistent JSON format for log data retrieval
|
|
type logViewResponse struct {
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
// logSentResponse represents the response structure for log sending operations
|
|
// This includes size information and error details for debugging
|
|
type logSentResponse struct {
|
|
Size int64 `json:"size"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// logReader provides read access to log data with size information.
|
|
//
|
|
// This struct encapsulates log reading functionality for external consumers,
|
|
// providing both the log content and metadata about the log size. It supports
|
|
// reading from both internal log buffers (when no external logging is configured)
|
|
// and external log files (when logging to file is enabled).
|
|
//
|
|
// Fields:
|
|
// - r: An io.ReadCloser that provides access to the log content
|
|
// - size: The total size of the log data in bytes
|
|
//
|
|
// The logReader is used by the control server to serve log content to clients
|
|
// and by various CLI commands that need to display or process log data.
|
|
type logReader struct {
|
|
r io.ReadCloser
|
|
size int64
|
|
}
|
|
|
|
// logSubscriber represents a subscriber to live log output.
|
|
type logSubscriber struct {
|
|
ch chan []byte
|
|
}
|
|
|
|
// logWriter is an internal buffer to keep track of runtime log when no logging is enabled.
|
|
// When a file path is configured via setLogFile, writes are also persisted to
|
|
// a rotated file on disk (max logFileMaxSize, 1 backup) so logs survive restarts.
|
|
type logWriter struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
size int
|
|
subscribers []*logSubscriber
|
|
|
|
// File persistence fields.
|
|
logFile *os.File
|
|
logFilePath string
|
|
logFileSize int64
|
|
}
|
|
|
|
// newLogWriter creates an internal log writer.
|
|
// This provides the default log writer with standard buffer size
|
|
func newLogWriter() *logWriter {
|
|
return newLogWriterWithSize(logWriterSize)
|
|
}
|
|
|
|
// newSmallLogWriter creates an internal log writer with small buffer size.
|
|
// This is used in memory-constrained environments or for temporary logging
|
|
func newSmallLogWriter() *logWriter {
|
|
return newLogWriterWithSize(logWriterSmallSize)
|
|
}
|
|
|
|
// newLogWriterWithSize creates an internal log writer with a given buffer size.
|
|
// This allows customization of log buffer size based on specific requirements
|
|
func newLogWriterWithSize(size int) *logWriter {
|
|
lw := &logWriter{size: size}
|
|
return lw
|
|
}
|
|
|
|
// setLogFile configures file-backed persistence for the log writer.
|
|
// The directory is created if it does not exist. An existing file is
|
|
// opened in append mode and its current size is tracked for rotation.
|
|
func (lw *logWriter) setLogFile(path string) error {
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
|
return fmt.Errorf("creating log directory: %w", err)
|
|
}
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("opening log file: %w", err)
|
|
}
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("stat log file: %w", err)
|
|
}
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
lw.logFile = f
|
|
lw.logFilePath = path
|
|
lw.logFileSize = st.Size()
|
|
return nil
|
|
}
|
|
|
|
// rotateLogFile rotates the current log file to a .1 backup.
|
|
// It returns true if lw.logFile is usable after the call, false otherwise.
|
|
// Must be called with lw.mu held.
|
|
func (lw *logWriter) rotateLogFile() bool {
|
|
if lw.logFile == nil {
|
|
return false
|
|
}
|
|
lw.logFile.Close()
|
|
backupPath := lw.logFilePath + ".1"
|
|
// Best effort: rename current to backup (overwrites old backup).
|
|
os.Rename(lw.logFilePath, backupPath)
|
|
f, err := os.OpenFile(lw.logFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
// If we can't reopen, disable file logging.
|
|
lw.logFile = nil
|
|
lw.logFileSize = 0
|
|
return false
|
|
}
|
|
lw.logFile = f
|
|
lw.logFileSize = 0
|
|
return true
|
|
}
|
|
|
|
// closeLogFile closes the backing file if open.
|
|
func (lw *logWriter) closeLogFile() {
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
if lw.logFile != nil {
|
|
lw.logFile.Close()
|
|
lw.logFile = nil
|
|
}
|
|
}
|
|
|
|
// logFilePaths returns the paths to the current log file and its backup
|
|
// (if they exist) for inclusion in log send payloads.
|
|
func (lw *logWriter) logFilePaths() (current, backup string) {
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
if lw.logFilePath == "" {
|
|
return "", ""
|
|
}
|
|
current = lw.logFilePath
|
|
bp := lw.logFilePath + ".1"
|
|
if _, err := os.Stat(bp); err == nil {
|
|
backup = bp
|
|
}
|
|
return current, backup
|
|
}
|
|
|
|
// Subscribe returns a channel that receives new log data as it's written,
|
|
// and an unsubscribe function to clean up when done.
|
|
func (lw *logWriter) Subscribe() (<-chan []byte, func()) {
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
sub := &logSubscriber{ch: make(chan []byte, 256)}
|
|
lw.subscribers = append(lw.subscribers, sub)
|
|
unsub := func() {
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
for i, s := range lw.subscribers {
|
|
if s == sub {
|
|
lw.subscribers = append(lw.subscribers[:i], lw.subscribers[i+1:]...)
|
|
close(sub.ch)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return sub.ch, unsub
|
|
}
|
|
|
|
// tailLastLines returns the last n lines from the current buffer.
|
|
func (lw *logWriter) tailLastLines(n int) []byte {
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
data := lw.buf.Bytes()
|
|
if n <= 0 || len(data) == 0 {
|
|
return nil
|
|
}
|
|
// Find the last n newlines from the end.
|
|
count := 0
|
|
pos := len(data)
|
|
for pos > 0 {
|
|
pos--
|
|
if data[pos] == '\n' {
|
|
count++
|
|
if count == n+1 {
|
|
pos++ // move past this newline
|
|
break
|
|
}
|
|
}
|
|
}
|
|
result := make([]byte, len(data)-pos)
|
|
copy(result, data[pos:])
|
|
return result
|
|
}
|
|
|
|
// Write implements io.Writer interface for logWriter
|
|
// This manages buffer overflow by discarding old data while preserving important markers
|
|
func (lw *logWriter) Write(p []byte) (int, error) {
|
|
lw.mu.Lock()
|
|
defer lw.mu.Unlock()
|
|
|
|
// Fan-out to subscribers (non-blocking).
|
|
if len(lw.subscribers) > 0 {
|
|
cp := make([]byte, len(p))
|
|
copy(cp, p)
|
|
for _, sub := range lw.subscribers {
|
|
select {
|
|
case sub.ch <- cp:
|
|
default:
|
|
// Drop if subscriber is slow to avoid blocking the logger.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write to backing file if configured.
|
|
if lw.logFile != nil {
|
|
needsRotation := lw.logFileSize+int64(len(p)) > logFileMaxSize
|
|
if !needsRotation || lw.rotateLogFile() {
|
|
if n, err := lw.logFile.Write(p); err == nil {
|
|
lw.logFileSize += int64(n)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If writing p causes overflows, discard old data.
|
|
// This prevents unbounded memory growth while maintaining recent logs
|
|
if lw.buf.Len()+len(p) > lw.size {
|
|
buf := lw.buf.Bytes()
|
|
haveEndMarker := false
|
|
// If there's init end marker already, preserve the data til the marker.
|
|
// This ensures initialization logs are always available for debugging
|
|
if idx := bytes.LastIndex(buf, []byte(logWriterInitEndMarker)); idx >= 0 {
|
|
buf = buf[:idx+len(logWriterInitEndMarker)]
|
|
haveEndMarker = true
|
|
} else {
|
|
// Otherwise, preserve the initial size data.
|
|
buf = buf[:logWriterInitialSize]
|
|
if idx := bytes.LastIndex(buf, []byte("\n")); idx != -1 {
|
|
buf = buf[:idx]
|
|
}
|
|
}
|
|
lw.buf.Reset()
|
|
lw.buf.Write(buf)
|
|
if !haveEndMarker {
|
|
lw.buf.WriteString(logWriterInitEndMarker) // 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)
|
|
}
|
|
|
|
// initLogging initializes global logging setup.
|
|
func (p *prog) initLogging(backup bool) {
|
|
logCores := initLoggingWithBackup(backup)
|
|
|
|
// Initializing internal logging after global logging.
|
|
p.initInternalLogging(logCores)
|
|
p.logger.Store(mainLog.Load())
|
|
}
|
|
|
|
// internalLogFilePath returns the path for persisted internal logs.
|
|
// The file lives in the ctrld home directory alongside other runtime state.
|
|
func internalLogFilePath() string {
|
|
return absHomeDir(logFileName)
|
|
}
|
|
|
|
// initInternalLogging performs internal logging if there's no log enabled.
|
|
func (p *prog) initInternalLogging(externalCores []zapcore.Core) {
|
|
if !p.needInternalLogging() {
|
|
return
|
|
}
|
|
p.initInternalLogWriterOnce.Do(func() {
|
|
p.Notice().Msg("Internal logging enabled")
|
|
p.internalLogWriter = newLogWriter()
|
|
p.internalLogSent = time.Now().Add(-logWriterSentInterval)
|
|
p.internalWarnLogWriter = newSmallLogWriter()
|
|
// Persist internal logs to disk so they survive restarts.
|
|
if path := internalLogFilePath(); path != "" {
|
|
if err := p.internalLogWriter.setLogFile(path); err != nil {
|
|
mainLog.Load().Warn().Err(err).Msg("could not enable persistent internal logging")
|
|
} else {
|
|
mainLog.Load().Notice().Msgf("internal log file: %s", path)
|
|
}
|
|
}
|
|
})
|
|
p.mu.Lock()
|
|
lw := p.internalLogWriter
|
|
wlw := p.internalWarnLogWriter
|
|
p.mu.Unlock()
|
|
|
|
// Create zap cores for different writers
|
|
var cores []zapcore.Core
|
|
cores = append(cores, externalCores...)
|
|
|
|
// Add core for internal log writer.
|
|
// Run the internal logging at debug level, so we could
|
|
// have enough information for troubleshooting.
|
|
internalCore := newHumanReadableZapCore(lw, zapcore.DebugLevel)
|
|
cores = append(cores, internalCore)
|
|
|
|
// Add core for internal warn log writer
|
|
warnCore := newHumanReadableZapCore(wlw, zapcore.WarnLevel)
|
|
cores = append(cores, warnCore)
|
|
|
|
// Create a multi-core logger
|
|
multiCore := zapcore.NewTee(cores...)
|
|
logger := zap.New(multiCore)
|
|
|
|
mainLog.Store(&ctrld.Logger{Logger: logger})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// logReaderNoColor returns a logReader with ANSI color codes stripped from the log content.
|
|
//
|
|
// This method is useful when log content needs to be processed by tools that don't
|
|
// handle ANSI escape sequences properly, or when storing logs in plain text format.
|
|
// It internally calls logReader(true) to strip color codes.
|
|
//
|
|
// Returns:
|
|
// - *logReader: A logReader instance with color codes removed, or nil if no logs available
|
|
// - error: Any error encountered during log reading (e.g., empty logs, file access issues)
|
|
//
|
|
// Use cases:
|
|
// - Log processing pipelines that require plain text
|
|
// - Storing logs in databases or text files
|
|
// - Displaying logs in environments that don't support color
|
|
func (p *prog) logReaderNoColor() (*logReader, error) {
|
|
return p.logReader(true)
|
|
}
|
|
|
|
// logReaderRaw returns a logReader with ANSI color codes preserved in the log content.
|
|
//
|
|
// This method maintains the original formatting of log entries including color codes,
|
|
// which is useful for displaying logs in terminals that support ANSI colors or when
|
|
// the original visual formatting needs to be preserved. It internally calls logReader(false).
|
|
//
|
|
// Returns:
|
|
// - *logReader: A logReader instance with color codes preserved, or nil if no logs available
|
|
// - error: Any error encountered during log reading (e.g., empty logs, file access issues)
|
|
//
|
|
// Use cases:
|
|
// - Terminal-based log viewers that support color
|
|
// - Interactive debugging sessions
|
|
// - Preserving original log formatting for display
|
|
func (p *prog) logReaderRaw() (*logReader, error) {
|
|
return p.logReader(false)
|
|
}
|
|
|
|
// logReader creates a logReader instance for accessing log content with optional color stripping.
|
|
//
|
|
// This is the core method that handles log reading from different sources based on the
|
|
// current logging configuration. It supports both internal logging (when no external
|
|
// logging is configured) and external file logging (when logging to file is enabled).
|
|
//
|
|
// Behavior:
|
|
// - Internal logging: Reads from internal log buffers (normal logs + warning logs)
|
|
// and combines them with appropriate markers for separation
|
|
// - External logging: Reads directly from the configured log file
|
|
// - Empty logs: Returns appropriate error messages when no log content is available
|
|
//
|
|
// Parameters:
|
|
// - stripColor: If true, removes ANSI color codes from log content; if false, preserves them
|
|
//
|
|
// Returns:
|
|
// - *logReader: A logReader instance providing access to log content and size metadata
|
|
// - error: Any error encountered during log reading, including:
|
|
// - "nil internal log writer" - Internal logging not properly initialized
|
|
// - "nil internal warn log writer" - Warning log writer not properly initialized
|
|
// - "internal log is empty" - No content in internal log buffers
|
|
// - "log file is empty" - External log file exists but contains no data
|
|
// - File system errors when accessing external log files
|
|
//
|
|
// The method handles thread-safe access to internal log buffers and provides
|
|
// comprehensive error handling for various edge cases.
|
|
func (p *prog) logReader(stripColor bool) (*logReader, error) {
|
|
if p.needInternalLogging() {
|
|
p.mu.Lock()
|
|
lw := p.internalLogWriter
|
|
wlw := p.internalWarnLogWriter
|
|
p.mu.Unlock()
|
|
if lw == nil {
|
|
return nil, errors.New("nil internal log writer")
|
|
}
|
|
if wlw == nil {
|
|
return nil, errors.New("nil internal warn log writer")
|
|
}
|
|
|
|
// If we have a persisted log file, read from disk (includes data
|
|
// from previous runs that the in-memory buffer wouldn't have).
|
|
current, backup := lw.logFilePaths()
|
|
if current != "" {
|
|
return p.logReaderFromFiles(current, backup, wlw)
|
|
}
|
|
|
|
// Fall back to in-memory buffer.
|
|
lw.mu.Lock()
|
|
lwReader := newLogReader(&lw.buf, stripColor)
|
|
lwSize := lw.buf.Len()
|
|
lw.mu.Unlock()
|
|
// Warn log content.
|
|
wlw.mu.Lock()
|
|
wlwReader := newLogReader(&wlw.buf, stripColor)
|
|
wlwSize := wlw.buf.Len()
|
|
wlw.mu.Unlock()
|
|
reader := io.MultiReader(lwReader, bytes.NewReader([]byte(logWriterLogEndMarker)), wlwReader)
|
|
lr := &logReader{r: io.NopCloser(reader)}
|
|
lr.size = int64(lwSize + wlwSize)
|
|
if lr.size == 0 {
|
|
return nil, errors.New("internal log is empty")
|
|
}
|
|
return lr, nil
|
|
}
|
|
if p.cfg.Service.LogPath == "" {
|
|
return &logReader{r: io.NopCloser(strings.NewReader(""))}, nil
|
|
}
|
|
f, err := os.Open(normalizeLogFilePath(p.cfg.Service.LogPath))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lr := &logReader{r: f}
|
|
if st, err := f.Stat(); err == nil {
|
|
lr.size = st.Size()
|
|
} else {
|
|
return nil, fmt.Errorf("f.Stat: %w", err)
|
|
}
|
|
if lr.size == 0 {
|
|
return nil, errors.New("log file is empty")
|
|
}
|
|
return lr, nil
|
|
}
|
|
|
|
// newHumanReadableZapCore creates a zap core optimized for human-readable log output.
|
|
//
|
|
// Features:
|
|
// - Uses development encoder configuration for enhanced readability
|
|
// - Console encoding with colored log levels for easy visual scanning
|
|
// - Millisecond precision timestamps in human-friendly format
|
|
// - Structured field output with clear key-value pairs
|
|
// - Ideal for development, debugging, and interactive terminal sessions
|
|
//
|
|
// Parameters:
|
|
// - w: The output writer (e.g., os.Stdout, file, buffer)
|
|
// - level: Minimum log level to capture (e.g., Debug, Info, Warn, Error)
|
|
//
|
|
// Returns a zapcore.Core configured for human consumption.
|
|
func newHumanReadableZapCore(w io.Writer, level zapcore.Level) zapcore.Core {
|
|
encoderConfig := zap.NewDevelopmentEncoderConfig()
|
|
encoderConfig.TimeKey = "time"
|
|
encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli)
|
|
encoderConfig.EncodeLevel = noticeColorLevelEncoder
|
|
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
|
return zapcore.NewCore(encoder, zapcore.AddSync(w), level)
|
|
}
|
|
|
|
// newMachineFriendlyZapCore creates a zap core optimized for machine processing and log aggregation.
|
|
//
|
|
// Features:
|
|
// - Uses production encoder configuration for consistent, parseable output
|
|
// - Console encoding with non-colored log levels for log parsing tools
|
|
// - Millisecond precision timestamps in ISO-like format
|
|
// - Structured field output optimized for log aggregation systems
|
|
// - Ideal for production environments, log shipping, and automated analysis
|
|
//
|
|
// Parameters:
|
|
// - w: The output writer (e.g., os.Stdout, file, buffer)
|
|
// - level: Minimum log level to capture (e.g., Debug, Info, Warn, Error)
|
|
//
|
|
// Returns a zapcore.Core configured for machine consumption and log aggregation.
|
|
func newMachineFriendlyZapCore(w io.Writer, level zapcore.Level) zapcore.Core {
|
|
encoderConfig := zap.NewProductionEncoderConfig()
|
|
encoderConfig.TimeKey = "time"
|
|
encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli)
|
|
encoderConfig.EncodeLevel = noticeLevelEncoder
|
|
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
|
return zapcore.NewCore(encoder, zapcore.AddSync(w), level)
|
|
}
|
|
|
|
// ansiRegex is a regular expression to match ANSI color codes.
|
|
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
|
|
|
// newLogReader creates a reader for log buffer content with optional ANSI color stripping.
|
|
//
|
|
// This function provides flexible log content access by allowing consumers to choose
|
|
// between raw log data (with ANSI color codes) or stripped content (without color codes).
|
|
// The color stripping is useful when logs need to be processed by tools that don't
|
|
// handle ANSI escape sequences properly, or when storing logs in plain text format.
|
|
//
|
|
// Parameters:
|
|
// - buf: The log buffer containing the log data to read
|
|
// - stripColor: If true, strips ANSI color codes from the log content;
|
|
// if false, returns raw log content with color codes preserved
|
|
//
|
|
// Returns an io.Reader that provides access to the processed log content.
|
|
func newLogReader(buf *bytes.Buffer, stripColor bool) io.Reader {
|
|
if stripColor {
|
|
return strings.NewReader(ansiRegex.ReplaceAllString(buf.String(), ""))
|
|
}
|
|
return strings.NewReader(buf.String())
|
|
}
|
|
|
|
// logReaderFromFiles builds a logReader that concatenates the backup file
|
|
// (if it exists), the current log file, and the in-memory warn log buffer.
|
|
func (p *prog) logReaderFromFiles(current, backup string, wlw *logWriter) (*logReader, error) {
|
|
var rcs []io.ReadCloser
|
|
var totalSize int64
|
|
|
|
closeAll := func() {
|
|
for _, rc := range rcs {
|
|
rc.Close()
|
|
}
|
|
}
|
|
|
|
// Read backup file first (older entries).
|
|
if backup != "" {
|
|
if bf, err := os.Open(backup); err == nil {
|
|
if st, err := bf.Stat(); err == nil {
|
|
totalSize += st.Size()
|
|
}
|
|
rcs = append(rcs, bf)
|
|
}
|
|
}
|
|
|
|
// Read current file.
|
|
cf, err := os.Open(current)
|
|
if err != nil {
|
|
closeAll()
|
|
return nil, fmt.Errorf("opening current log file: %w", err)
|
|
}
|
|
if st, err := cf.Stat(); err == nil {
|
|
totalSize += st.Size()
|
|
}
|
|
rcs = append(rcs, cf)
|
|
|
|
// Append warn log content from memory.
|
|
wlw.mu.Lock()
|
|
warnData := make([]byte, wlw.buf.Len())
|
|
copy(warnData, wlw.buf.Bytes())
|
|
wlw.mu.Unlock()
|
|
|
|
if len(warnData) > 0 {
|
|
rcs = append(rcs, io.NopCloser(bytes.NewReader([]byte(logWriterLogEndMarker))))
|
|
rcs = append(rcs, io.NopCloser(bytes.NewReader(warnData)))
|
|
totalSize += int64(len(logWriterLogEndMarker) + len(warnData))
|
|
}
|
|
|
|
if totalSize == 0 {
|
|
closeAll()
|
|
return nil, errors.New("internal log is empty")
|
|
}
|
|
|
|
readers := make([]io.Reader, len(rcs))
|
|
closers := make([]io.Closer, len(rcs))
|
|
for i, rc := range rcs {
|
|
readers[i] = rc
|
|
closers[i] = rc
|
|
}
|
|
combined := io.MultiReader(readers...)
|
|
lr := &logReader{
|
|
r: &multiCloser{Reader: combined, closers: closers},
|
|
size: totalSize,
|
|
}
|
|
return lr, nil
|
|
}
|
|
|
|
// multiCloser wraps an io.Reader and closes multiple underlying closers.
|
|
type multiCloser struct {
|
|
io.Reader
|
|
closers []io.Closer
|
|
}
|
|
|
|
func (mc *multiCloser) Close() error {
|
|
var firstErr error
|
|
for _, c := range mc.closers {
|
|
if err := c.Close(); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|