bldr/util/logfile/attach.go
Rama master · réplica de lectura del repositorio original.
package logfile
import (
"errors"
"io"
"os"
"path/filepath"
"time"
"github.com/aperturerobotics/cli"
"github.com/mattn/go-isatty"
"github.com/sirupsen/logrus"
)
// ParseLogFileSpecs parses a list of raw spec strings into LogFileSpecs.
// Entries with value "none" are filtered out.
func ParseLogFileSpecs(raw []string, ts time.Time) ([]LogFileSpec, error) {
var specs []LogFileSpec
for _, r := range raw {
spec, err := ParseLogFileSpec(r, ts)
if errors.Is(err, ErrDisabled) {
continue
}
if err != nil {
return nil, err
}
specs = append(specs, spec)
}
return specs, nil
}
// AttachLogFiles opens log files and attaches hooks to the logger.
// Returns a cleanup function that closes all hooks and files.
func AttachLogFiles(logger *logrus.Logger, specs []LogFileSpec) (func(), error) {
var hooks []*FileHook
for _, spec := range specs {
dir := filepath.Dir(spec.Path)
if err := os.MkdirAll(dir, 0o755); err != nil {
// Close any already-opened hooks before returning.
for _, h := range hooks {
h.Close()
}
return nil, err
}
// Open with append semantics: multiple processes can resolve the
// same log-file path within the same second (same-second {ts}
// template or an explicit shared spec), and a truncate-on-open
// here would destroy the records a still-running first writer
// already emitted.
f, err := os.OpenFile(spec.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
for _, h := range hooks {
h.Close()
}
return nil, err
}
// Mark the file in use so concurrent prunes keep it while open.
markLogInUse(f)
h := NewFileHook(f, spec.Level, spec.Format)
logger.AddHook(h)
hooks = append(hooks, h)
}
cleanup := func() {
for _, h := range hooks {
h.Close()
}
}
return cleanup, nil
}
// EnsureLoggerLevel raises the logger level to the most verbose hook level.
// If the logger level is already sufficient, this is a no-op. Otherwise it
// redirects the logger's console output through a level-filtered hook so
// that --log-level controls console verbosity independently of file hooks.
func EnsureLoggerLevel(logger *logrus.Logger, specs []LogFileSpec) {
// Find the verbosity needed to deliver every file hook level.
maxLevel := logger.GetLevel()
for _, spec := range specs {
if spec.Level > maxLevel {
maxLevel = spec.Level
}
}
if maxLevel <= logger.GetLevel() {
return
}
// Preserve color output: logrus TextFormatter detects TTY via
// entry.Logger.Out, which will be io.Discard after the swap. Build
// a new formatter with ForceColors when the original output is a TTY.
formatter := logger.Formatter
if tf, ok := formatter.(*logrus.TextFormatter); ok && !tf.DisableColors && writerIsTerminal(logger.Out) {
formatter = &logrus.TextFormatter{
ForceColors: true,
ForceQuote: tf.ForceQuote,
DisableQuote: tf.DisableQuote,
EnvironmentOverrideColors: tf.EnvironmentOverrideColors,
DisableTimestamp: tf.DisableTimestamp,
FullTimestamp: tf.FullTimestamp,
TimestampFormat: tf.TimestampFormat,
DisableSorting: tf.DisableSorting,
SortingFunc: tf.SortingFunc,
DisableLevelTruncation: tf.DisableLevelTruncation,
PadLevelText: tf.PadLevelText,
QuoteEmptyFields: tf.QuoteEmptyFields,
FieldMap: tf.FieldMap,
CallerPrettyfier: tf.CallerPrettyfier,
}
}
// Route console records through their own threshold before raising logger verbosity.
consoleHook := NewConsoleHook(logger.Out, formatter, logger.GetLevel())
logger.AddHook(consoleHook)
logger.SetOutput(io.Discard)
logger.SetLevel(maxLevel)
}
// writerIsTerminal reports whether w is connected to a terminal.
func writerIsTerminal(w io.Writer) bool {
if f, ok := w.(interface{ Fd() uintptr }); ok {
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}
return false
}
// BuildLogFileFlag returns a CLI flag for --log-file / --log-files.
func BuildLogFileFlag(dest *cli.StringSlice) *cli.StringSliceFlag {
return &cli.StringSliceFlag{
Name: "log-file",
Aliases: []string{"log-files"},
Usage: "file logging spec: [level=LEVEL;][format=FORMAT;]path=PATH",
EnvVars: []string{"BLDR_LOG_FILE"},
Destination: dest,
}
}