platform_build_soong/ui/logger/logger.go
Liz Kammer f2a80c6396 Add non-zero exit + error message to soong metrics
Improves some error messages that would have given little/no information

Bug: 254650145
Test: relevant unit tests
Test: Induce errors and check metrics proto
Change-Id: Ife6116af74af6e62c2f8ae8774e53c28178fb8d0
2022-10-24 10:51:56 -04:00

343 lines
9.7 KiB
Go

// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package logger implements a logging package designed for command line
// utilities. It uses the standard 'log' package and function, but splits
// output between stderr and a rotating log file.
//
// In addition to the standard logger functions, Verbose[f|ln] calls only go to
// the log file by default, unless SetVerbose(true) has been called.
//
// The log file also includes extended date/time/source information, which are
// omitted from the stderr output for better readability.
//
// In order to better handle resource cleanup after a Fatal error, the Fatal
// functions panic instead of calling os.Exit(). To actually do the cleanup,
// and prevent the printing of the panic, call defer logger.Cleanup() at the
// beginning of your main function.
package logger
import (
"android/soong/ui/metrics"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"sync"
"syscall"
)
type Logger interface {
// Print* prints to both stderr and the file log.
// Arguments to Print are handled in the manner of fmt.Print.
Print(v ...interface{})
// Arguments to Printf are handled in the manner of fmt.Printf
Printf(format string, v ...interface{})
// Arguments to Println are handled in the manner of fmt.Println
Println(v ...interface{})
// Verbose* is equivalent to Print*, but skips stderr unless the
// logger has been configured in verbose mode.
Verbose(v ...interface{})
Verbosef(format string, v ...interface{})
Verboseln(v ...interface{})
// Fatal* is equivalent to Print* followed by a call to panic that
// can be converted to an error using Recover, or will be converted
// to a call to os.Exit(1) with a deferred call to Cleanup()
Fatal(v ...interface{})
Fatalf(format string, v ...interface{})
Fatalln(v ...interface{})
// Panic is equivalent to Print* followed by a call to panic.
Panic(v ...interface{})
Panicf(format string, v ...interface{})
Panicln(v ...interface{})
// Output writes the string to both stderr and the file log.
Output(calldepth int, str string) error
}
// fatalError is the type used when Fatal[f|ln]
type fatalError struct {
error
}
func fileRotation(from, baseName, ext string, cur, max int) error {
newName := baseName + "." + strconv.Itoa(cur) + ext
if _, err := os.Lstat(newName); err == nil {
if cur+1 <= max {
fileRotation(newName, baseName, ext, cur+1, max)
}
}
if err := os.Rename(from, newName); err != nil {
return fmt.Errorf("Failed to rotate %s to %s. %s", from, newName, err)
}
return nil
}
// CreateFileWithRotation returns a new os.File using os.Create, renaming any
// existing files to <filename>.#.<ext>, keeping up to maxCount files.
// <filename>.1.<ext> is the most recent backup, <filename>.2.<ext> is the
// second most recent backup, etc.
func CreateFileWithRotation(filename string, maxCount int) (*os.File, error) {
lockFileName := filepath.Join(filepath.Dir(filename), ".lock_"+filepath.Base(filename))
lockFile, err := os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
defer lockFile.Close()
err = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX)
if err != nil {
return nil, err
}
if _, err := os.Lstat(filename); err == nil {
ext := filepath.Ext(filename)
basename := filename[:len(filename)-len(ext)]
if err = fileRotation(filename, basename, ext, 1, maxCount); err != nil {
return nil, err
}
}
return os.Create(filename)
}
// Recover can be used with defer in a GoRoutine to convert a Fatal panics to
// an error that can be handled.
func Recover(fn func(err error)) {
p := recover()
if p == nil {
return
} else if log, ok := p.(fatalError); ok {
fn(error(log))
} else {
panic(p)
}
}
type stdLogger struct {
stderr *log.Logger
verbose bool
fileLogger *log.Logger
mutex sync.Mutex
file *os.File
metrics *metrics.Metrics
}
var _ Logger = &stdLogger{}
// New creates a new Logger. The out variable sets the destination, commonly
// os.Stderr, but it may be a buffer for tests, or a separate log file if
// the user doesn't need to see the output.
func New(out io.Writer) *stdLogger {
return NewWithMetrics(out, nil)
}
func NewWithMetrics(out io.Writer, m *metrics.Metrics) *stdLogger {
return &stdLogger{
stderr: log.New(out, "", log.Ltime),
fileLogger: log.New(ioutil.Discard, "", log.Ldate|log.Lmicroseconds|log.Llongfile),
metrics: m,
}
}
// SetVerbose controls whether Verbose[f|ln] logs to stderr as well as the
// file-backed log.
func (s *stdLogger) SetVerbose(v bool) *stdLogger {
s.verbose = v
return s
}
// SetOutput controls where the file-backed log will be saved. It will keep
// some number of backups of old log files.
func (s *stdLogger) SetOutput(path string) *stdLogger {
if f, err := CreateFileWithRotation(path, 5); err == nil {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.file != nil {
s.file.Close()
}
s.file = f
s.fileLogger.SetOutput(f)
} else {
s.Fatal(err.Error())
}
return s
}
type panicWriter struct{}
func (panicWriter) Write([]byte) (int, error) { panic("write to panicWriter") }
// Close disables logging to the file and closes the file handle.
func (s *stdLogger) Close() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.file != nil {
s.fileLogger.SetOutput(panicWriter{})
s.file.Close()
s.file = nil
}
}
// Cleanup should be used with defer in your main function. It will close the
// log file and convert any Fatal panics back to os.Exit(1)
func (s *stdLogger) Cleanup() {
fatal := false
p := recover()
if _, ok := p.(fatalError); ok {
fatal = true
p = nil
} else if p != nil {
s.Println(p)
}
s.Close()
if p != nil {
panic(p)
} else if fatal {
os.Exit(1)
}
}
type verbosityLevel int
const (
verboseLog verbosityLevel = iota
infoLog
fatalLog
panicLog
)
// Output writes string to both stderr and the file log.
func (s *stdLogger) Output(calldepth int, str string) error {
return s.output(calldepth, str, infoLog)
}
// output writes string to stderr, the file log, and if fatal or panic, to metrics.
func (s *stdLogger) output(calldepth int, str string, level verbosityLevel) error {
if level != verboseLog || s.verbose {
s.stderr.Output(calldepth+1, str)
}
if level >= fatalLog {
s.metrics.SetFatalOrPanicMessage(str)
}
return s.fileLogger.Output(calldepth+1, str)
}
// VerboseOutput is equivalent to Output, but only goes to the file log
// unless SetVerbose(true) has been called.
func (s *stdLogger) VerboseOutput(calldepth int, str string) error {
return s.output(calldepth, str, verboseLog)
}
// Print prints to both stderr and the file log.
// Arguments are handled in the manner of fmt.Print.
func (s *stdLogger) Print(v ...interface{}) {
output := fmt.Sprint(v...)
s.output(2, output, infoLog)
}
// Printf prints to both stderr and the file log.
// Arguments are handled in the manner of fmt.Printf.
func (s *stdLogger) Printf(format string, v ...interface{}) {
output := fmt.Sprintf(format, v...)
s.output(2, output, infoLog)
}
// Println prints to both stderr and the file log.
// Arguments are handled in the manner of fmt.Println.
func (s *stdLogger) Println(v ...interface{}) {
output := fmt.Sprintln(v...)
s.output(2, output, infoLog)
}
// Verbose is equivalent to Print, but only goes to the file log unless
// SetVerbose(true) has been called.
func (s *stdLogger) Verbose(v ...interface{}) {
output := fmt.Sprint(v...)
s.VerboseOutput(2, output)
}
// Verbosef is equivalent to Printf, but only goes to the file log unless
// SetVerbose(true) has been called.
func (s *stdLogger) Verbosef(format string, v ...interface{}) {
output := fmt.Sprintf(format, v...)
s.VerboseOutput(2, output)
}
// Verboseln is equivalent to Println, but only goes to the file log unless
// SetVerbose(true) has been called.
func (s *stdLogger) Verboseln(v ...interface{}) {
output := fmt.Sprintln(v...)
s.VerboseOutput(2, output)
}
// Fatal is equivalent to Print() followed by a call to panic() that
// Cleanup will convert to a os.Exit(1).
func (s *stdLogger) Fatal(v ...interface{}) {
output := fmt.Sprint(v...)
s.output(2, output, fatalLog)
panic(fatalError{errors.New(output)})
}
// Fatalf is equivalent to Printf() followed by a call to panic() that
// Cleanup will convert to a os.Exit(1).
func (s *stdLogger) Fatalf(format string, v ...interface{}) {
output := fmt.Sprintf(format, v...)
s.output(2, output, fatalLog)
panic(fatalError{errors.New(output)})
}
// Fatalln is equivalent to Println() followed by a call to panic() that
// Cleanup will convert to a os.Exit(1).
func (s *stdLogger) Fatalln(v ...interface{}) {
output := fmt.Sprintln(v...)
s.output(2, output, fatalLog)
panic(fatalError{errors.New(output)})
}
// Panic is equivalent to Print() followed by a call to panic().
func (s *stdLogger) Panic(v ...interface{}) {
output := fmt.Sprint(v...)
s.output(2, output, panicLog)
panic(output)
}
// Panicf is equivalent to Printf() followed by a call to panic().
func (s *stdLogger) Panicf(format string, v ...interface{}) {
output := fmt.Sprintf(format, v...)
s.output(2, output, panicLog)
panic(output)
}
// Panicln is equivalent to Println() followed by a call to panic().
func (s *stdLogger) Panicln(v ...interface{}) {
output := fmt.Sprintln(v...)
s.output(2, output, panicLog)
panic(output)
}