Adds support for showing error details (#98)
To better support printing of google.protobuf.Any messages (error details), this also makes a few other changes: 1. Allows printing of unresolvable Any messages using an "@value" field in JSON output that has the base64-encoded embedded message data. 2. Improves support for "-format text" to show expanded Any messages if possible. (Due to limitations in underlying proto package, this will usually *not* be that helpful. But this should greatly improve with v2 of the go protobuf API.) 3. Addresses a TODO in existing AnyResolver code to lazily fetch descriptors as needed instead of having to download all files eagerly.
This commit is contained in:
parent
f0723c6273
commit
5d6316f470
|
|
@ -518,7 +518,7 @@ func main() {
|
||||||
fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.NumResponses, respSuffix)
|
fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.NumResponses, respSuffix)
|
||||||
}
|
}
|
||||||
if h.Status.Code() != codes.OK {
|
if h.Status.Code() != codes.OK {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.Status.Code().String(), h.Status.Message())
|
grpcurl.PrintStatus(os.Stderr, h.Status, formatter)
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
196
format.go
196
format.go
|
|
@ -3,14 +3,19 @@ package grpcurl
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/golang/protobuf/jsonpb"
|
"github.com/golang/protobuf/jsonpb"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/jhump/protoreflect/desc"
|
"github.com/jhump/protoreflect/desc"
|
||||||
"github.com/jhump/protoreflect/dynamic"
|
"github.com/jhump/protoreflect/dynamic"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
@ -142,6 +147,8 @@ type textFormatter struct {
|
||||||
numFormatted int
|
numFormatted int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var protoTextMarshaler = proto.TextMarshaler{ExpandAny: true}
|
||||||
|
|
||||||
func (tf *textFormatter) format(m proto.Message) (string, error) {
|
func (tf *textFormatter) format(m proto.Message) (string, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if tf.useSeparator && tf.numFormatted > 0 {
|
if tf.useSeparator && tf.numFormatted > 0 {
|
||||||
|
|
@ -166,7 +173,7 @@ func (tf *textFormatter) format(m proto.Message) (string, error) {
|
||||||
if _, err := buf.Write(b); err != nil {
|
if _, err := buf.Write(b); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
} else if err := proto.MarshalText(&buf, m); err != nil {
|
} else if err := protoTextMarshaler.Marshal(&buf, m); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,24 +195,137 @@ const (
|
||||||
FormatText = Format("text")
|
FormatText = Format("text")
|
||||||
)
|
)
|
||||||
|
|
||||||
func anyResolver(source DescriptorSource) (jsonpb.AnyResolver, error) {
|
type anyResolver struct {
|
||||||
// TODO: instead of pro-actively downloading file descriptors to
|
source DescriptorSource
|
||||||
// build a dynamic resolver, it would be better if the resolver
|
|
||||||
// impl was lazy, and simply downloaded the descriptors as needed
|
|
||||||
// when asked to resolve a particular type URL
|
|
||||||
|
|
||||||
// best effort: build resolver with whatever files we can
|
er dynamic.ExtensionRegistry
|
||||||
// load, ignoring any errors
|
|
||||||
files, _ := GetAllFiles(source)
|
|
||||||
|
|
||||||
var er dynamic.ExtensionRegistry
|
mu sync.RWMutex
|
||||||
for _, fd := range files {
|
mf *dynamic.MessageFactory
|
||||||
er.AddExtensionsFromFile(fd)
|
resolved map[string]func() proto.Message
|
||||||
}
|
|
||||||
mf := dynamic.NewMessageFactoryWithExtensionRegistry(&er)
|
|
||||||
return dynamic.AnyResolver(mf, files...), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *anyResolver) Resolve(typeUrl string) (proto.Message, error) {
|
||||||
|
mname := typeUrl
|
||||||
|
if slash := strings.LastIndex(mname, "/"); slash >= 0 {
|
||||||
|
mname = mname[slash+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.RLock()
|
||||||
|
factory := r.resolved[mname]
|
||||||
|
r.mu.RUnlock()
|
||||||
|
|
||||||
|
// already resolved?
|
||||||
|
if factory != nil {
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
// double-check, in case we were racing with another goroutine
|
||||||
|
// that resolved this one
|
||||||
|
factory = r.resolved[mname]
|
||||||
|
if factory != nil {
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use descriptor source to resolve message type
|
||||||
|
d, err := r.source.FindSymbol(mname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
md, ok := d.(*desc.MessageDescriptor)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown message: %s", typeUrl)
|
||||||
|
}
|
||||||
|
// populate any extensions for this message, too
|
||||||
|
if exts, err := r.source.AllExtensionsForType(mname); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err := r.er.AddExtension(exts...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.mf == nil {
|
||||||
|
r.mf = dynamic.NewMessageFactoryWithExtensionRegistry(&r.er)
|
||||||
|
}
|
||||||
|
|
||||||
|
factory = func() proto.Message {
|
||||||
|
return r.mf.NewMessage(md)
|
||||||
|
}
|
||||||
|
if r.resolved == nil {
|
||||||
|
r.resolved = map[string]func() proto.Message{}
|
||||||
|
}
|
||||||
|
r.resolved[mname] = factory
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// anyResolverWithFallback can provide a fallback value for unknown
|
||||||
|
// messages that will format itself to JSON using an "@value" field
|
||||||
|
// that has the base64-encoded data for the unknown message value.
|
||||||
|
type anyResolverWithFallback struct {
|
||||||
|
jsonpb.AnyResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r anyResolverWithFallback) Resolve(typeUrl string) (proto.Message, error) {
|
||||||
|
msg, err := r.AnyResolver.Resolve(typeUrl)
|
||||||
|
if err == nil {
|
||||||
|
return msg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try "default" resolution logic. This mirrors the default behavior
|
||||||
|
// of jsonpb, which checks to see if the given message name is registered
|
||||||
|
// in the proto package.
|
||||||
|
mname := typeUrl
|
||||||
|
if slash := strings.LastIndex(mname, "/"); slash >= 0 {
|
||||||
|
mname = mname[slash+1:]
|
||||||
|
}
|
||||||
|
mt := proto.MessageType(mname)
|
||||||
|
if mt != nil {
|
||||||
|
return reflect.New(mt.Elem()).Interface().(proto.Message), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, fallback to a special placeholder that can marshal itself
|
||||||
|
// to JSON using a special "@value" property to show base64-encoded
|
||||||
|
// data for the embedded message
|
||||||
|
return &unknownAny{TypeUrl: typeUrl, Error: fmt.Sprintf("%s is not recognized; see @value for raw binary message data", mname)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type unknownAny struct {
|
||||||
|
TypeUrl string `json:"@type"`
|
||||||
|
Error string `json:"@error"`
|
||||||
|
Value string `json:"@value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) MarshalJSONPB(jsm *jsonpb.Marshaler) ([]byte, error) {
|
||||||
|
if jsm.Indent != "" {
|
||||||
|
return json.MarshalIndent(a, "", jsm.Indent)
|
||||||
|
}
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) Unmarshal(b []byte) error {
|
||||||
|
a.Value = base64.StdEncoding.EncodeToString(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) Reset() {
|
||||||
|
a.Value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) String() string {
|
||||||
|
b, err := a.MarshalJSONPB(&jsonpb.Marshaler{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("ERROR: %v", err.Error())
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) ProtoMessage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ proto.Message = (*unknownAny)(nil)
|
||||||
|
|
||||||
// RequestParserAndFormatterFor returns a request parser and formatter for the
|
// RequestParserAndFormatterFor returns a request parser and formatter for the
|
||||||
// given format. The given descriptor source may be used for parsing message
|
// given format. The given descriptor source may be used for parsing message
|
||||||
// data (if needed by the format). The flags emitJSONDefaultFields and
|
// data (if needed by the format). The flags emitJSONDefaultFields and
|
||||||
|
|
@ -214,11 +334,8 @@ func anyResolver(source DescriptorSource) (jsonpb.AnyResolver, error) {
|
||||||
func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) {
|
func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) {
|
||||||
switch format {
|
switch format {
|
||||||
case FormatJSON:
|
case FormatJSON:
|
||||||
resolver, err := anyResolver(descSource)
|
resolver := anyResolver{source: descSource}
|
||||||
if err != nil {
|
return NewJSONRequestParser(in, &resolver), NewJSONFormatter(emitJSONDefaultFields, anyResolverWithFallback{AnyResolver: &resolver}), nil
|
||||||
return nil, nil, fmt.Errorf("error creating message resolver: %v", err)
|
|
||||||
}
|
|
||||||
return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, resolver), nil
|
|
||||||
case FormatText:
|
case FormatText:
|
||||||
return NewTextRequestParser(in), NewTextFormatter(includeTextSeparator), nil
|
return NewTextRequestParser(in), NewTextFormatter(includeTextSeparator), nil
|
||||||
default:
|
default:
|
||||||
|
|
@ -295,3 +412,42 @@ func (h *DefaultEventHandler) OnReceiveTrailers(stat *status.Status, md metadata
|
||||||
fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", MetadataToString(md))
|
fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", MetadataToString(md))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintStatus prints details about the given status to the given writer. The given
|
||||||
|
// formatter is used to print any detail messages that may be included in the status.
|
||||||
|
// If the given status has a code of OK, "OK" is printed and that is all. Otherwise,
|
||||||
|
// "ERROR:" is printed along with a line showing the code, one showing the message
|
||||||
|
// string, and each detail message if any are present. The detail messages will be
|
||||||
|
// printed as proto text format or JSON, depending on the given formatter.
|
||||||
|
func PrintStatus(w io.Writer, stat *status.Status, formatter Formatter) {
|
||||||
|
if stat.Code() == codes.OK {
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "ERROR:\n Code: %s\n Message: %s\n", stat.Code().String(), stat.Message())
|
||||||
|
|
||||||
|
statpb := stat.Proto()
|
||||||
|
if len(statpb.Details) > 0 {
|
||||||
|
fmt.Fprintf(w, " Details:\n")
|
||||||
|
for i, det := range statpb.Details {
|
||||||
|
prefix := fmt.Sprintf(" %d)", i+1)
|
||||||
|
fmt.Fprintf(w, "%s\t", prefix)
|
||||||
|
prefix = strings.Repeat(" ", len(prefix)) + "\t"
|
||||||
|
|
||||||
|
output, err := formatter(det)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "Error parsing detail message: %v\n", err)
|
||||||
|
} else {
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if i == 0 {
|
||||||
|
// first line is already indented
|
||||||
|
fmt.Fprintf(w, "%s\n", line)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "%s%s\n", prefix, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue