Repository: go-kratos/kratos
Pull request #1996 (2022-05-19 10:59:54)

feat(contrib/opensergo): add opensergo reportMetadata fields
<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need mark it as a WIP(Work In Progress) PR or draft PR
4. Please use a conventional commits format title: `<type>[optional scope]: <description>`
    
    Some suggestion on <type>:

    fix: A bug fix
    feat: A new feature
    test: Adding missing tests or correcting existing tests
    refactor: A code change that neither fixes a bug nor adds a feature
    break: Changes has break change

    docs: Documentation only changes
    deps: Changes external dependencies
    style: Changes that do not affect the meaning of the code (white-space, formatting, etc)
    chore Daily work, examples, etc.
    ci: Changes to our CI configuration files and scripts
-->

#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->
add some fields for opensergo reportMeatadata

#### Other special notes for reviewer:
<!--
* Somethings that need extra attention for reviewer
* Some additional notes, TODO list, etc.
-->
I'm still not sure about some parts, any advice is welcome.
* ~~Is there any way to get the HTTP options information defined in proto file~~
* For the `description` field, I'm not sure if it should be read from proto file automatically or use a config option for users.
* For the `node` field, do we need to assume the deployment environment, such as kubernetes, to get some information automatically.


Patch:
diff --git a/contrib/opensergo/README.md b/contrib/opensergo/README.md
new file mode 100644
index 00000000..82cfecce
--- /dev/null
+++ b/contrib/opensergo/README.md
@@ -0,0 +1,32 @@
+# OpenSergo
+
+## Usage
+
+```go
+	osServer, err := opensergo.New(opensergo.WithEndpoint("localhost:9090"))
+	if err != nil {
+		panic("init opensergo error")
+	}
+
+	s := &server{}
+	grpcSrv := grpc.NewServer(
+		grpc.Address(":9000"),
+		grpc.Middleware(
+			recovery.Recovery(),
+		),
+	)
+	helloworld.RegisterGreeterServer(grpcSrv, s)
+
+	app := kratos.New(
+		kratos.Name(Name),
+		kratos.Server(
+			grpcSrv,
+		),
+	)
+
+	osServer.ReportMetadata(context.Background(), app)
+
+	if err := app.Run(); err != nil {
+		log.Fatal(err)
+	}
+```
\ No newline at end of file
diff --git a/contrib/opensergo/go.mod b/contrib/opensergo/go.mod
index e1ebf519..635de717 100644
--- a/contrib/opensergo/go.mod
+++ b/contrib/opensergo/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/go-kratos/kratos/v2 v2.2.2
 	github.com/opensergo/opensergo-go v0.0.0-20220331070310-e5b01fee4d1c
 	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
+	google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd
 	google.golang.org/grpc v1.46.2
 	google.golang.org/protobuf v1.28.0
 )
@@ -17,6 +18,5 @@ require (
 	golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
 	golang.org/x/text v0.3.7 // indirect
-	google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
 	gopkg.in/yaml.v3 v3.0.0 // indirect
 )
diff --git a/contrib/opensergo/opensergo.go b/contrib/opensergo/opensergo.go
index 536a8d94..f22e16e0 100644
--- a/contrib/opensergo/opensergo.go
+++ b/contrib/opensergo/opensergo.go
@@ -12,8 +12,10 @@ import (
 	"github.com/go-kratos/kratos/v2"
 	v1 "github.com/opensergo/opensergo-go/proto/service_contract/v1"
 	"golang.org/x/net/context"
+	"google.golang.org/genproto/googleapis/api/annotations"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/reflect/protoregistry"
 )
@@ -73,15 +75,18 @@ func New(opts ...Option) (*OpenSergo, error) {
 }
 
 func (s *OpenSergo) ReportMetadata(ctx context.Context, app kratos.AppInfo) error {
-	services, err := s.listServiceDescriptors()
+	services, types, err := listDescriptors()
 	if err != nil {
 		return err
 	}
+
 	serviceMetadata := &v1.ServiceMetadata{
 		ServiceContract: &v1.ServiceContract{
 			Services: services,
+			Types:    types,
 		},
 	}
+
 	for _, endpoint := range app.Endpoint() {
 		u, err := url.Parse(endpoint) //nolint
 		if err != nil {
@@ -104,11 +109,12 @@ func (s *OpenSergo) ReportMetadata(ctx context.Context, app kratos.AppInfo) erro
 	_, err = s.mdClient.ReportMetadata(ctx, &v1.ReportMetadataRequest{
 		AppName:         app.Name(),
 		ServiceMetadata: []*v1.ServiceMetadata{serviceMetadata},
+		// TODO: Node: *v1.Node,
 	})
 	return err
 }
 
-func (s *OpenSergo) listServiceDescriptors() (services []*v1.ServiceDescriptor, err error) {
+func listDescriptors() (services []*v1.ServiceDescriptor, types []*v1.TypeDescriptor, err error) {
 	protoregistry.GlobalFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool {
 		for i := 0; i < fd.Services().Len(); i++ {
 			var (
@@ -120,19 +126,78 @@ func (s *OpenSergo) listServiceDescriptors() (services []*v1.ServiceDescriptor,
 				mName := string(md.Name())
 				inputType := string(md.Input().FullName())
 				outputType := string(md.Output().FullName())
+				isClientStreaming := md.IsStreamingClient()
+				isServerStreaming := md.IsStreamingServer()
+				pattern := proto.GetExtension(md.Options(), annotations.E_Http).(*annotations.HttpRule).GetPattern()
+				var httpPath, httpMethod string
+				if pattern != nil {
+					httpPath, httpMethod = HTTPPatternInfo(pattern)
+				}
 				methodDesc := v1.MethodDescriptor{
-					Name:        mName,
-					InputTypes:  []string{inputType},
-					OutputTypes: []string{outputType},
+					Name:            mName,
+					InputTypes:      []string{inputType},
+					OutputTypes:     []string{outputType},
+					ClientStreaming: &isClientStreaming,
+					ServerStreaming: &isServerStreaming,
+					HttpPaths:       []string{httpPath},
+					HttpMethods:     []string{httpMethod},
+					// TODO: Description: *string,
 				}
 				methods = append(methods, &methodDesc)
 			}
 			services = append(services, &v1.ServiceDescriptor{
 				Name:    string(sd.Name()),
 				Methods: methods,
+				// TODO: Description: *string,
+			})
+		}
+
+		for i := 0; i < fd.Messages().Len(); i++ {
+			var (
+				fields []*v1.FieldDescriptor
+				md     = fd.Messages().Get(i)
+			)
+
+			for j := 0; j < md.Fields().Len(); j++ {
+				fd := md.Fields().Get(j)
+				kind := fd.Kind()
+				typeName := kind.String()
+
+				fields = append(fields, &v1.FieldDescriptor{
+					Name:     string(fd.Name()),
+					Number:   int32(fd.Number()),
+					Type:     v1.FieldDescriptor_Type(kind),
+					TypeName: &typeName,
+					// TODO: Description: *string,
+				})
+			}
+
+			types = append(types, &v1.TypeDescriptor{
+				Name:   string(md.Name()),
+				Fields: fields,
 			})
 		}
+
 		return true
 	})
 	return
 }
+
+func HTTPPatternInfo(pattern interface{}) (method string, path string) {
+	switch p := pattern.(type) {
+	case *annotations.HttpRule_Get:
+		return "GET", p.Get
+	case *annotations.HttpRule_Post:
+		return "POST", p.Post
+	case *annotations.HttpRule_Delete:
+		return "DELETE", p.Delete
+	case *annotations.HttpRule_Patch:
+		return "PATCH", p.Patch
+	case *annotations.HttpRule_Put:
+		return "PUT", p.Put
+	case *annotations.HttpRule_Custom:
+		return p.Custom.Kind, p.Custom.Path
+	default:
+		return "", ""
+	}
+}
diff --git a/log/global.go b/log/global.go
index 0e98bae5..72f871f7 100644
--- a/log/global.go
+++ b/log/global.go
@@ -38,7 +38,7 @@ func SetLogger(logger Logger) {
 
 // GetLogger returns global logger appliance as logger in current process.
 func GetLogger() Logger {
-	return global
+	return global.GetLogger()
 }
 
 // Log Print log by level and keyvals.
diff --git a/log/log.go b/log/log.go
index 2a290b70..264dd8c4 100644
--- a/log/log.go
+++ b/log/log.go
@@ -14,7 +14,7 @@ type Logger interface {
 }
 
 type logger struct {
-	logs      []Logger
+	logger    Logger
 	prefix    []interface{}
 	hasValuer bool
 	ctx       context.Context
@@ -27,10 +27,8 @@ func (c *logger) Log(level Level, keyvals ...interface{}) error {
 		bindValues(c.ctx, kvs)
 	}
 	kvs = append(kvs, keyvals...)
-	for _, l := range c.logs {
-		if err := l.Log(level, kvs...); err != nil {
-			return err
-		}
+	if err := c.logger.Log(level, kvs...); err != nil {
+		return err
 	}
 	return nil
 }
@@ -39,13 +37,13 @@ func (c *logger) Log(level Level, keyvals ...interface{}) error {
 func With(l Logger, kv ...interface{}) Logger {
 	c, ok := l.(*logger)
 	if !ok {
-		return &logger{logs: []Logger{l}, prefix: kv, hasValuer: containsValuer(kv)}
+		return &logger{logger: l, prefix: kv, hasValuer: containsValuer(kv), ctx: context.Background()}
 	}
 	kvs := make([]interface{}, 0, len(c.prefix)+len(kv))
 	kvs = append(kvs, kv...)
 	kvs = append(kvs, c.prefix...)
 	return &logger{
-		logs:      c.logs,
+		logger:    l,
 		prefix:    kvs,
 		hasValuer: containsValuer(kvs),
 		ctx:       c.ctx,
@@ -57,17 +55,12 @@ func With(l Logger, kv ...interface{}) Logger {
 func WithContext(ctx context.Context, l Logger) Logger {
 	c, ok := l.(*logger)
 	if !ok {
-		return &logger{logs: []Logger{l}, ctx: ctx}
+		return &logger{logger: l, ctx: ctx}
 	}
 	return &logger{
-		logs:      c.logs,
+		logger:    l,
 		prefix:    c.prefix,
 		hasValuer: c.hasValuer,
 		ctx:       ctx,
 	}
 }
-
-// MultiLogger wraps multi logger.
-func MultiLogger(logs ...Logger) Logger {
-	return &logger{logs: logs}
-}
diff --git a/log/value.go b/log/value.go
index 86e91559..8154ae93 100644
--- a/log/value.go
+++ b/log/value.go
@@ -9,9 +9,8 @@ import (
 )
 
 var (
-	defaultDepth = 3
 	// DefaultCaller is a Valuer that returns the file and line.
-	DefaultCaller = Caller(defaultDepth)
+	DefaultCaller = Caller(3)
 
 	// DefaultTimestamp is a Valuer that returns the current wallclock time.
 	DefaultTimestamp = Timestamp(time.RFC3339)


Test patch:
diff --git a/log/helper_test.go b/log/helper_test.go
index dc888a39..0cb4ba87 100644
--- a/log/helper_test.go
+++ b/log/helper_test.go
@@ -73,7 +73,10 @@ func TestContext(t *testing.T) {
 
 func Trace() Valuer {
 	return func(ctx context.Context) interface{} {
-		s := ctx.Value(traceKey{}).(string)
+		s, ok := ctx.Value(traceKey{}).(string)
+		if !ok {
+			return nil
+		}
 		return s
 	}
 }
diff --git a/log/log_test.go b/log/log_test.go
index dea00f4d..542bb79d 100644
--- a/log/log_test.go
+++ b/log/log_test.go
@@ -2,7 +2,6 @@ package log
 
 import (
 	"context"
-	"os"
 	"testing"
 )
 
@@ -12,14 +11,6 @@ func TestInfo(t *testing.T) {
 	_ = logger.Log(LevelInfo, "key1", "value1")
 }
 
-func TestWrapper(t *testing.T) {
-	out := NewStdLogger(os.Stdout)
-	err := NewStdLogger(os.Stderr)
-
-	l := With(MultiLogger(out, err), "ts", DefaultTimestamp, "caller", DefaultCaller)
-	_ = l.Log(LevelInfo, "msg", "test")
-}
-
 func TestWithContext(t *testing.T) {
 	WithContext(context.Background(), nil)
 }