mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
- Add newLogReader function with optional ANSI color code stripping - Implement logReaderNoColor() and logReaderRaw() methods for different use cases - Add comprehensive documentation for logReader struct and all related methods - Add extensive test coverage with 16+ test cases covering edge cases The new functionality allows consumers to choose between raw log data (with ANSI color codes) or stripped content (without color codes), making logs more suitable for different processing pipelines and display environments.
408 lines
15 KiB
Go
408 lines
15 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// logWriter is an internal buffer to keep track of runtime log when no logging is enabled.
|
|
// This provides in-memory log storage for debugging and monitoring purposes
|
|
type logWriter struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
size int
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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()
|
|
|
|
// 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())
|
|
}
|
|
|
|
// 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()
|
|
})
|
|
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")
|
|
}
|
|
|
|
// Normal log content.
|
|
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())
|
|
}
|