These changes introduce a new field, `Raw`, to `diag.Message`. This field indicates that the contents of the message are not a format string and should not be rendered via `Sprintf` during stringification. The plugin std{out,err} readers have been updated to use raw messages, and the event reader in `pulumi` has been fixed s.t. it does not format event payloads before display. Fixes #551.
316 lines
8.6 KiB
Go
316 lines
8.6 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package diag
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"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.
|
|
}
|
|
|
|
// DefaultSink returns a default sink that simply logs output to stderr/stdout.
|
|
func DefaultSink(stdout io.Writer, stderr io.Writer, opts FormatOptions) Sink {
|
|
contract.Require(stdout != nil, "stdout")
|
|
contract.Require(stderr != nil, "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.Fprint(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.Fprint(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.Fprint(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.Fprint(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.Fprint(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)
|
|
}
|
|
|
|
if diag.Raw {
|
|
buffer.WriteString(diag.Message)
|
|
} else {
|
|
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
|
|
}
|