mirror of https://github.com/pulumi/pulumi.git
388 lines
9.7 KiB
Go
388 lines
9.7 KiB
Go
// Copyright 2016-2022, Pulumi Corporation.
|
|
//
|
|
// 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 rpcdebug
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
)
|
|
|
|
type DebugInterceptor struct {
|
|
logFile string
|
|
mutex *sync.Mutex
|
|
}
|
|
|
|
type DebugInterceptorOptions struct {
|
|
LogFile string
|
|
Mutex *sync.Mutex
|
|
}
|
|
|
|
type LogOptions struct {
|
|
Metadata interface{}
|
|
}
|
|
|
|
// Each LogFile should have a unique instance of DebugInterceptor for proper locking.
|
|
func NewDebugInterceptor(opts DebugInterceptorOptions) (*DebugInterceptor, error) {
|
|
if opts.LogFile == "" {
|
|
return nil, errors.New("logFile cannot be empty")
|
|
}
|
|
i := &DebugInterceptor{logFile: opts.LogFile}
|
|
|
|
if opts.Mutex != nil {
|
|
i.mutex = opts.Mutex
|
|
} else {
|
|
i.mutex = &sync.Mutex{}
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func (i *DebugInterceptor) ServerOptions(opts LogOptions) []grpc.ServerOption {
|
|
return []grpc.ServerOption{
|
|
grpc.ChainUnaryInterceptor(i.DebugServerInterceptor(opts)),
|
|
grpc.ChainStreamInterceptor(i.DebugStreamServerInterceptor(opts)),
|
|
}
|
|
}
|
|
|
|
func (i *DebugInterceptor) DialOptions(opts LogOptions) []grpc.DialOption {
|
|
return []grpc.DialOption{
|
|
grpc.WithChainUnaryInterceptor(i.DebugClientInterceptor(opts)),
|
|
grpc.WithChainStreamInterceptor(i.DebugStreamClientInterceptor(opts)),
|
|
}
|
|
}
|
|
|
|
// Logs all gRPC converations in JSON format.
|
|
//
|
|
// To enable, call InitDebugInterceptors first in your process main to
|
|
// configure the location of the Go file.
|
|
func (i *DebugInterceptor) DebugServerInterceptor(opts LogOptions) grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, req interface{},
|
|
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
|
|
) (interface{}, error) {
|
|
log := debugInterceptorLogEntry{
|
|
Method: info.FullMethod,
|
|
Metadata: opts.Metadata,
|
|
}
|
|
i.trackRequest(&log, req)
|
|
resp, err := handler(ctx, req)
|
|
i.trackResponse(&log, resp)
|
|
if e := i.record(log); e != nil {
|
|
return resp, e
|
|
}
|
|
return resp, err
|
|
}
|
|
}
|
|
|
|
// Like debugServerInterceptor but for streaming calls.
|
|
func (i *DebugInterceptor) DebugStreamServerInterceptor(opts LogOptions) grpc.StreamServerInterceptor {
|
|
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
ssWrapped := &debugServerStream{
|
|
interceptor: i,
|
|
method: info.FullMethod,
|
|
innerServerStream: ss,
|
|
metadata: opts.Metadata,
|
|
}
|
|
err := handler(srv, ssWrapped)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Like debugServerInterceptor but for GRPC client connections.
|
|
func (i *DebugInterceptor) DebugClientInterceptor(opts LogOptions) grpc.UnaryClientInterceptor {
|
|
return func(ctx context.Context, method string, req, reply interface{},
|
|
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, gopts ...grpc.CallOption,
|
|
) error {
|
|
// Ignoring weird entries with empty method and nil req and reply.
|
|
if method == "" {
|
|
return invoker(ctx, method, req, reply, cc, gopts...)
|
|
}
|
|
|
|
log := debugInterceptorLogEntry{
|
|
Method: method,
|
|
Metadata: opts.Metadata,
|
|
}
|
|
i.trackRequest(&log, req)
|
|
err := invoker(ctx, method, req, reply, cc, gopts...)
|
|
if err != nil {
|
|
i.track(&log, err)
|
|
} else {
|
|
i.trackResponse(&log, reply)
|
|
}
|
|
if e := i.record(log); e != nil {
|
|
return e
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Like debugClientInterceptor but for streaming calls.
|
|
func (i *DebugInterceptor) DebugStreamClientInterceptor(opts LogOptions) grpc.StreamClientInterceptor {
|
|
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
|
|
streamer grpc.Streamer, gopts ...grpc.CallOption,
|
|
) (grpc.ClientStream, error) {
|
|
stream, err := streamer(ctx, desc, cc, method, gopts...)
|
|
|
|
wrappedStream := &debugClientStream{
|
|
innerClientStream: stream,
|
|
interceptor: i,
|
|
method: method,
|
|
metadata: opts.Metadata,
|
|
}
|
|
|
|
return wrappedStream, err
|
|
}
|
|
}
|
|
|
|
func (i *DebugInterceptor) record(log debugInterceptorLogEntry) error {
|
|
i.mutex.Lock()
|
|
defer i.mutex.Unlock()
|
|
|
|
f, err := os.OpenFile(i.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to append GRPC debug logs to file %s: %w", i.logFile, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if err := json.NewEncoder(f).Encode(log); err != nil {
|
|
return fmt.Errorf("Failed to encode GRPC debug logs: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (*DebugInterceptor) track(log *debugInterceptorLogEntry, err error) {
|
|
log.Errors = append(log.Errors, err.Error())
|
|
}
|
|
|
|
func (i *DebugInterceptor) trackRequest(log *debugInterceptorLogEntry, req interface{}) {
|
|
j, err := i.transcode(req)
|
|
if err != nil {
|
|
i.track(log, err)
|
|
} else {
|
|
log.Request = j
|
|
}
|
|
}
|
|
|
|
func (i *DebugInterceptor) trackResponse(log *debugInterceptorLogEntry, resp interface{}) {
|
|
j, err := i.transcode(resp)
|
|
if err != nil {
|
|
i.track(log, err)
|
|
} else {
|
|
log.Response = j
|
|
}
|
|
}
|
|
|
|
func (*DebugInterceptor) transcode(obj interface{}) (json.RawMessage, error) {
|
|
if obj == nil {
|
|
return json.RawMessage("null"), nil
|
|
}
|
|
|
|
m, ok := obj.(protoreflect.ProtoMessage)
|
|
if !ok {
|
|
return json.RawMessage("null"),
|
|
fmt.Errorf("failed to decode, expecting protoreflect.ProtoMessage, got %v",
|
|
reflect.TypeOf(obj))
|
|
}
|
|
|
|
buf, err := protojson.Marshal(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
// Wraps grpc.ServerStream with interceptor hooks for SendMsg, RecvMsg.
|
|
type debugServerStream struct {
|
|
innerServerStream grpc.ServerStream
|
|
interceptor *DebugInterceptor
|
|
method string
|
|
metadata interface{}
|
|
}
|
|
|
|
func (dss *debugServerStream) errorEntry(err error) debugInterceptorLogEntry {
|
|
return debugInterceptorLogEntry{
|
|
Metadata: dss.metadata,
|
|
Method: dss.method,
|
|
Errors: []string{err.Error()},
|
|
}
|
|
}
|
|
|
|
func (dss *debugServerStream) SetHeader(md metadata.MD) error {
|
|
return dss.innerServerStream.SetHeader(md)
|
|
}
|
|
|
|
func (dss *debugServerStream) SendHeader(md metadata.MD) error {
|
|
return dss.innerServerStream.SendHeader(md)
|
|
}
|
|
|
|
func (dss *debugServerStream) SetTrailer(md metadata.MD) {
|
|
dss.innerServerStream.SetTrailer(md)
|
|
}
|
|
|
|
func (dss *debugServerStream) Context() context.Context {
|
|
return dss.innerServerStream.Context()
|
|
}
|
|
|
|
func (dss *debugServerStream) SendMsg(m interface{}) error {
|
|
err := dss.innerServerStream.SendMsg(m)
|
|
if err != nil {
|
|
if e := dss.interceptor.record(dss.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
req, err := dss.interceptor.transcode(m)
|
|
if err != nil {
|
|
if e := dss.interceptor.record(dss.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
if e := dss.interceptor.record(debugInterceptorLogEntry{
|
|
Metadata: dss.metadata,
|
|
Method: dss.method,
|
|
Request: req,
|
|
}); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (dss *debugServerStream) RecvMsg(m interface{}) error {
|
|
err := dss.innerServerStream.RecvMsg(m)
|
|
if err == io.EOF {
|
|
return err
|
|
} else if err != nil {
|
|
if e := dss.interceptor.record(dss.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
resp, err := dss.interceptor.transcode(m)
|
|
if err != nil {
|
|
if e := dss.interceptor.record(dss.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
if e := dss.interceptor.record(debugInterceptorLogEntry{
|
|
Method: dss.method,
|
|
Metadata: dss.metadata,
|
|
Response: resp,
|
|
}); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
var _ grpc.ServerStream = &debugServerStream{}
|
|
|
|
// Wraps grpc.ClientStream with interceptor hooks for SendMsg, RecvMsg.
|
|
type debugClientStream struct {
|
|
innerClientStream grpc.ClientStream
|
|
interceptor *DebugInterceptor
|
|
method string
|
|
metadata interface{}
|
|
}
|
|
|
|
func (d *debugClientStream) errorEntry(err error) debugInterceptorLogEntry {
|
|
return debugInterceptorLogEntry{
|
|
Method: d.method,
|
|
Metadata: d.metadata,
|
|
Errors: []string{err.Error()},
|
|
}
|
|
}
|
|
|
|
func (d *debugClientStream) Header() (metadata.MD, error) {
|
|
return d.innerClientStream.Header()
|
|
}
|
|
|
|
func (d *debugClientStream) Trailer() metadata.MD {
|
|
return d.innerClientStream.Trailer()
|
|
}
|
|
|
|
func (d *debugClientStream) CloseSend() error {
|
|
return d.innerClientStream.CloseSend()
|
|
}
|
|
|
|
func (d *debugClientStream) Context() context.Context {
|
|
return d.innerClientStream.Context()
|
|
}
|
|
|
|
func (d *debugClientStream) SendMsg(m interface{}) error {
|
|
err := d.innerClientStream.SendMsg(m)
|
|
if err != nil {
|
|
if e := d.interceptor.record(d.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
req, err := d.interceptor.transcode(m)
|
|
if err != nil {
|
|
if e := d.interceptor.record(d.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
if e := d.interceptor.record(debugInterceptorLogEntry{
|
|
Method: d.method,
|
|
Metadata: d.metadata,
|
|
Request: req,
|
|
}); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (d *debugClientStream) RecvMsg(m interface{}) error {
|
|
err := d.innerClientStream.RecvMsg(m)
|
|
if err == io.EOF {
|
|
return err
|
|
} else if err != nil {
|
|
if e := d.interceptor.record(d.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
resp, err := d.interceptor.transcode(m)
|
|
if err != nil {
|
|
if e := d.interceptor.record(d.errorEntry(err)); e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
if e := d.interceptor.record(debugInterceptorLogEntry{
|
|
Method: d.method,
|
|
Metadata: d.metadata,
|
|
Response: resp,
|
|
}); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
var _ grpc.ClientStream = &debugClientStream{}
|