pulumi/pkg/diag/sink.go
joeduffy 141a112950 Improve output formatting
This change improves our output formatting by generally adding
fewer prefixes.  As shown in pulumi/pulumi#359, we were being
excessively verbose in many places, including prefixing every
console.out with "langhost[nodejs].stdout: ", displaying full
stack traces for simple errors like missing configuration, etc.

Overall, this change includes the following:

* Don't prefix stdout and stderr output from the program, other
  than the standard "info:" prefix.  I experimented with various
  schemes here, but they all felt gratuitous.  Simply emitting
  the output seems fine, especially as it's closer to what would
  happen if you just ran the program under node.

* Do NOT make writes to stderr fail the plan/deploy.  Previously
  we assumed that any console.errors, for instance, meant that
  the overall program should fail.  This simply isn't how stderr
  is treated generally and meant you couldn't use certain
  logging techniques and libraries, among other things.

* Do make sure that stderr writes in the program end up going to
  stderr in the Pulumi CLI output, however, so that redirection
  works as it should.  This required a new Infoerr log level.

* Make a small fix to the planning logic so we don't attempt to
  print the summary if an error occurs.

* Finally, add a new error type, RunError, that when thrown and
  uncaught does not result in a full stack trace being printed.
  Anyone can use this, however, we currently use it for config
  errors so that we can terminate with a pretty error message,
  rather than the monstrosity shown in pulumi/pulumi#359.
2017-09-23 05:20:11 -07:00

324 lines
9 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package diag
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"sync"
"github.com/golang/glog"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// Sink facilitates pluggable diagnostics messages.
type Sink interface {
// Count fetches the total number of diagnostics issued (errors plus warnings).
Count() int
// Infos fetches the number of debug messages issued.
Debugs() int
// Infos fetches the number of stdout informational messages issued.
Infos() int
// Infos fetches the number of stderr informational messages issued.
Infoerrs() int
// Errors fetches the number of errors issued.
Errors() int
// Warnings fetches the number of warnings issued.
Warnings() int
// Success returns true if this sink is currently error-free.
Success() bool
// Logf issues a log message.
Logf(sev Severity, diag *Diag, args ...interface{})
// Debugf issues a debugging message.
Debugf(diag *Diag, args ...interface{})
// Infof issues an informational message (to stdout).
Infof(diag *Diag, args ...interface{})
// Infoerrf issues an informational message (to stderr).
Infoerrf(diag *Diag, args ...interface{})
// Errorf issues a new error diagnostic.
Errorf(diag *Diag, args ...interface{})
// Warningf issues a new warning diagnostic.
Warningf(diag *Diag, args ...interface{})
// Stringify stringifies a diagnostic in the usual way (e.g., "error: MU123: Lumi.yaml:7:39: error goes here\n").
Stringify(sev Severity, diag *Diag, args ...interface{}) string
// StringifyLocation stringifies a source document location.
StringifyLocation(sev Severity, doc *Document, loc *Location) string
}
// Severity dictates the kind of diagnostic.
type Severity string
const (
Debug Severity = "debug"
Info Severity = "info"
Infoerr Severity = "info#err"
Warning Severity = "warning"
Error Severity = "error"
)
// FormatOptions controls the output style and content.
type FormatOptions struct {
Pwd string // the working directory.
Colors bool // if true, output will be colorized.
Debug bool // if true, debugging will be output to stdout.
Stdout io.Writer // if non nil, use this writer instead of os.Stdout for output
Stderr io.Writer // if non nil, use this writer instead of os.Stderr for errors
}
// DefaultSink returns a default sink that simply logs output to stderr/stdout.
func DefaultSink(opts FormatOptions) Sink {
// Default info to stdout if there's no override in the options.
stdout := io.Writer(os.Stdout)
if opts.Stdout != nil {
stdout = opts.Stdout
}
// Default info(err), error, warning to stderr if there's no override in the options.
stderr := io.Writer(os.Stderr)
if opts.Stderr != nil {
stderr = opts.Stderr
}
// Discard debug output by default unless requested.
debug := ioutil.Discard
if opts.Debug {
debug = stdout
}
return newDefaultSink(opts, map[Severity]io.Writer{
Debug: debug,
Info: stdout,
Infoerr: stderr,
Error: stderr,
Warning: stderr,
})
}
func newDefaultSink(opts FormatOptions, writers map[Severity]io.Writer) *defaultSink {
contract.Assert(writers[Debug] != nil)
contract.Assert(writers[Info] != nil)
contract.Assert(writers[Infoerr] != nil)
contract.Assert(writers[Error] != nil)
contract.Assert(writers[Warning] != nil)
return &defaultSink{
opts: opts,
counts: make(map[Severity]int),
writers: writers,
}
}
const DefaultSinkIDPrefix = "PU"
// defaultSink is the default sink which logs output to stderr/stdout.
type defaultSink struct {
opts FormatOptions // a set of options that control output style and content.
writers map[Severity]io.Writer // the writers to use for each kind of diagnostic severity.
counts map[Severity]int // the number of messages that have been issued per severity.
mutex sync.RWMutex // a mutex for guarding updates to the counts map
}
func (d *defaultSink) Count() int { return d.Debugs() + d.Infos() + d.Errors() + d.Warnings() }
func (d *defaultSink) Debugs() int { return d.getCount(Debug) }
func (d *defaultSink) Infos() int { return d.getCount(Info) }
func (d *defaultSink) Infoerrs() int { return d.getCount(Infoerr) }
func (d *defaultSink) Errors() int { return d.getCount(Error) }
func (d *defaultSink) Warnings() int { return d.getCount(Warning) }
func (d *defaultSink) Success() bool { return d.Errors() == 0 }
func (d *defaultSink) Logf(sev Severity, diag *Diag, args ...interface{}) {
switch sev {
case Debug:
d.Debugf(diag, args...)
case Info:
d.Infof(diag, args...)
case Infoerr:
d.Infoerrf(diag, args...)
case Warning:
d.Warningf(diag, args...)
case Error:
d.Errorf(diag, args...)
default:
contract.Failf("Unrecognized severity: %v", sev)
}
}
func (d *defaultSink) Debugf(diag *Diag, args ...interface{}) {
// For debug messages, write both to the glogger and a stream, if there is one.
glog.V(3).Infof(diag.Message, args...)
msg := d.Stringify(Debug, diag, args...)
if glog.V(9) {
glog.V(9).Infof("defaultSink::Debug(%v)", msg[:len(msg)-1])
}
fmt.Fprintf(d.writers[Debug], msg)
d.incrementCount(Debug)
}
func (d *defaultSink) Infof(diag *Diag, args ...interface{}) {
msg := d.Stringify(Info, diag, args...)
if glog.V(5) {
glog.V(5).Infof("defaultSink::Info(%v)", msg[:len(msg)-1])
}
fmt.Fprintf(d.writers[Info], msg)
d.incrementCount(Info)
}
func (d *defaultSink) Infoerrf(diag *Diag, args ...interface{}) {
msg := d.Stringify(Info /* not Infoerr, just "info: "*/, diag, args...)
if glog.V(5) {
glog.V(5).Infof("defaultSink::Infoerr(%v)", msg[:len(msg)-1])
}
fmt.Fprintf(d.writers[Infoerr], msg)
d.incrementCount(Infoerr)
}
func (d *defaultSink) Errorf(diag *Diag, args ...interface{}) {
msg := d.Stringify(Error, diag, args...)
if glog.V(5) {
glog.V(5).Infof("defaultSink::Error(%v)", msg[:len(msg)-1])
}
fmt.Fprintf(d.writers[Error], msg)
d.incrementCount(Error)
}
func (d *defaultSink) Warningf(diag *Diag, args ...interface{}) {
msg := d.Stringify(Warning, diag, args...)
if glog.V(5) {
glog.V(5).Infof("defaultSink::Warning(%v)", msg[:len(msg)-1])
}
fmt.Fprintf(d.writers[Warning], msg)
d.incrementCount(Warning)
}
func (d *defaultSink) incrementCount(sev Severity) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.counts[sev]++
}
func (d *defaultSink) getCount(sev Severity) int {
d.mutex.RLock()
defer d.mutex.RUnlock()
return d.counts[sev]
}
func (d *defaultSink) useColor(sev Severity) bool {
// we will use color so long as we're not spewing to debug (which is colorless).
return d.opts.Colors
}
func (d *defaultSink) Stringify(sev Severity, diag *Diag, args ...interface{}) string {
var buffer bytes.Buffer
// First print the location if there is one.
if diag.Doc != nil || diag.Loc != nil {
buffer.WriteString(d.StringifyLocation(sev, diag.Doc, diag.Loc))
buffer.WriteString(": ")
}
// Now print the message category's prefix (error/warning).
if d.useColor(sev) {
switch sev {
case Debug:
buffer.WriteString(colors.SpecDebug)
case Info, Infoerr:
buffer.WriteString(colors.SpecInfo)
case Error:
buffer.WriteString(colors.SpecError)
case Warning:
buffer.WriteString(colors.SpecWarning)
default:
contract.Failf("Unrecognized diagnostic severity: %v", sev)
}
}
buffer.WriteString(string(sev))
if diag.ID > 0 {
buffer.WriteString(" ")
buffer.WriteString(DefaultSinkIDPrefix)
buffer.WriteString(strconv.Itoa(int(diag.ID)))
}
buffer.WriteString(": ")
if d.useColor(sev) {
buffer.WriteString(colors.Reset)
}
// Finally, actually print the message itself.
if d.useColor(sev) {
buffer.WriteString(colors.SpecNote)
}
buffer.WriteString(fmt.Sprintf(diag.Message, args...))
if d.useColor(sev) {
buffer.WriteString(colors.Reset)
}
buffer.WriteRune('\n')
// TODO[pulumi/pulumi#15]: support Clang-style expressive diagnostics. This would entail, for example, using
// the buffer within the target document, to demonstrate the offending line/column range of code.
s := buffer.String()
// If colorization was requested, compile and execute the directives now.
if d.useColor(sev) {
s = colors.ColorizeText(s)
}
return s
}
func (d *defaultSink) StringifyLocation(sev Severity, doc *Document, loc *Location) string {
var buffer bytes.Buffer
if doc != nil {
if d.useColor(sev) {
buffer.WriteString(colors.SpecLocation)
}
file := doc.File
if d.opts.Pwd != "" {
// If a PWD is available, try to create a relative path.
rel, err := filepath.Rel(d.opts.Pwd, file)
if err == nil {
file = rel
}
}
buffer.WriteString(file)
}
if loc != nil && !loc.IsEmpty() {
buffer.WriteRune('(')
buffer.WriteString(strconv.Itoa(loc.Start.Line))
buffer.WriteRune(',')
buffer.WriteString(strconv.Itoa(loc.Start.Column))
buffer.WriteRune(')')
}
var s string
if doc != nil || loc != nil {
if d.useColor(sev) {
buffer.WriteString(colors.Reset)
}
s = buffer.String()
// If colorization was requested, compile and execute the directives now.
if d.useColor(sev) {
s = colors.ColorizeText(s)
}
}
return s
}