Compare commits

..

42 Commits

Author SHA1 Message Date
7ef5c5d804 backport from v3
Some checks failed
/ autoupdate (push) Failing after 1m3s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-06 01:17:19 +03:00
1cbab38d24 tracer and logger improvements
Some checks failed
/ autoupdate (push) Failing after 1m2s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-05 01:27:22 +03:00
c766477aaa backport from v3
Some checks failed
/ autoupdate (push) Failing after 1m2s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-05 00:03:37 +03:00
f28f8e13b3 logger/slog: add stacktrace support
Some checks failed
/ autoupdate (push) Failing after 1m22s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 23:42:15 +03:00
1cbc353479 logger: fixes
Some checks failed
/ autoupdate (push) Failing after 1m16s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 22:22:05 +03:00
7050313bc1 Merge pull request 'issue_294' (#301) from devstigneev/micro:issue_294 into master
Some checks failed
/ autoupdate (push) Failing after 1m3s
Reviewed-on: #301
2024-02-29 23:37:51 +03:00
3654961fde Merge pull request 'Обновить server/options.go' (#305) from vtolstov-patch-1 into master
Some checks failed
/ autoupdate (push) Failing after 2m27s
Reviewed-on: #305
2024-02-29 23:27:56 +03:00
fa2b7c924e Обновить server/options.go
Some checks failed
pr / test (pull_request) Failing after 3m15s
lint / lint (pull_request) Successful in 11m33s
Signed-off-by: Василий Толстов <v.tolstov@unistack.org>
2024-02-29 23:27:18 +03:00
de83f42149 Merge pull request 'add gracefultimeout in server' (#303) from devstigneev/micro:issue_293 into master
Some checks failed
/ autoupdate (push) Failing after 1m5s
Reviewed-on: #303
2024-02-29 22:31:49 +03:00
17ace07b1d skip test service register memory
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-29 18:43:25 +03:00
e212e3dc50 move test
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-29 18:02:12 +03:00
bf0c3016cb add gracefultimeout in server
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-29 13:28:27 +03:00
4124b49481 Merge branch 'master' into issue_294
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-29 13:07:26 +03:00
a0348a2664 implements register noop
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-29 13:06:21 +03:00
f06e0b21e8 add variadic (#302)
Some checks failed
/ autoupdate (push) Failing after 1m0s
Co-authored-by: Vasiliy Tolstov <v.tolstov@unistack.org>
Reviewed-on: #302
Co-authored-by: Evstigneev Denis <danteevstigneev@yandex.ru>
Co-committed-by: Evstigneev Denis <danteevstigneev@yandex.ru>
2024-02-29 01:27:33 +03:00
3be0566550 Merge pull request 'issue_296' (#300) from devstigneev/micro:issue_296 into master
Some checks failed
/ autoupdate (push) Failing after 1m21s
Reviewed-on: #300
2024-02-28 23:52:30 +03:00
249a64db74 add TODO
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-28 23:51:25 +03:00
a428061bf3 fixed changex, have cycle imports 2024-02-28 23:51:05 +03:00
e6feca2fb1 fixup
Some checks failed
pr / test (pull_request) Failing after 2m44s
lint / lint (pull_request) Successful in 11m1s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-28 23:50:38 +03:00
37fa3d6696 Merge remote-tracking branch 'origin/issue_296' into issue_296
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
# Conflicts:
#	options/options.go
2024-02-28 17:02:17 +03:00
47940a5ca2 add option metadata 2024-02-28 17:01:59 +03:00
d667bbee0c add option metadata 2024-02-28 15:06:29 +03:00
96dd5d869a Обновить .gitea/workflows/pr.yml
Some checks failed
/ autoupdate (push) Failing after 1m3s
2024-02-28 00:24:02 +03:00
ea90948315 Обновить .gitea/workflows/autoupdate.yml
Some checks failed
/ autoupdate (push) Failing after 1m9s
2024-02-28 00:17:09 +03:00
a382ea7d45 Merge pull request '#97 add As for Broker.' (#298) from kgorbunov/micro:#97 into master
Some checks failed
/ autoupdate (push) Failing after 1m0s
Reviewed-on: #298
2024-02-27 22:49:03 +03:00
Gorbunov Kirill Andreevich
cdfeaa7e20 #97 add As for all interface.
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-27 22:36:18 +03:00
Gorbunov Kirill Andreevich
c09674ae92 #97 add As for all interface.
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-27 22:11:39 +03:00
Gorbunov Kirill Andreevich
0d497ca0df #97 add As for Broker.
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-02-27 21:05:42 +03:00
bb0c415a77 Merge pull request 'config: add conditions' (#287) from cond-config into master
Some checks failed
/ autoupdate (push) Failing after 1m4s
Reviewed-on: #287
2024-01-15 00:51:30 +03:00
8182cb008a config: add conditions
Some checks failed
lint / lint (pull_request) Successful in 1m27s
pr / test (pull_request) Failing after 1m13s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-01-15 00:49:27 +03:00
0771fa0647 fixup multiple client handling (#281)
Some checks failed
/ autoupdate (push) Failing after 1m28s
Reviewed-on: #281
2023-11-13 08:23:22 +03:00
b1fd82adf8 Merge pull request 'database: add FormatDSN' (#277) from database-new into master
Some checks failed
/ autoupdate (push) Failing after 1m26s
Reviewed-on: #277
2023-11-02 01:35:12 +03:00
4bc0e25017 database: add FormatDSN
Some checks failed
lint / lint (pull_request) Failing after 1m28s
pr / test (pull_request) Failing after 2m37s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-11-02 01:31:26 +03:00
aec552aa0b Merge pull request 'database: initial import for dsn parsing' (#275) from database into master
Some checks failed
/ autoupdate (push) Failing after 1m28s
Reviewed-on: #275
2023-11-01 23:44:01 +03:00
cc81bed81d cleanup
Some checks failed
lint / lint (pull_request) Failing after 1m29s
pr / test (pull_request) Failing after 2m37s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-11-01 23:42:07 +03:00
7fde39fba5 database: initial import for dsn parsing
Some checks failed
lint / lint (pull_request) Failing after 1m29s
pr / test (pull_request) Failing after 2m40s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-11-01 23:40:39 +03:00
79438f11e0 logger: slog break import cycle
All checks were successful
/ autoupdate (push) Successful in 1m13s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-17 01:48:50 +03:00
8d19abfebd small fixes
Some checks are pending
/ autoupdate (push) Has started running
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-17 01:11:35 +03:00
77f3731329 change dep
Some checks are pending
/ autoupdate (push) Waiting to run
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-17 01:10:52 +03:00
dbcf6bb74a logger: use key names from opts (#271)
All checks were successful
/ autoupdate (push) Successful in 1m14s
Reviewed-on: #271
2023-10-17 01:05:55 +03:00
17698440ed Merge pull request 'logger: improvements' (#270) from logtrace into master
All checks were successful
/ autoupdate (push) Successful in 1m13s
Reviewed-on: #270
2023-10-17 01:00:20 +03:00
eb8851ab58 logger: improvements
Some checks failed
lint / lint (pull_request) Successful in 1m8s
pr / test (pull_request) Failing after 1m5s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-17 01:00:00 +03:00
29 changed files with 1408 additions and 237 deletions

View File

@@ -5,8 +5,8 @@ on:
- 'master'
- 'v3'
schedule:
- cron: '* * * * *'
#- cron: '@hourly'
#- cron: '* * * * *'
- cron: '@hourly'
jobs:
autoupdate:

View File

@@ -20,4 +20,4 @@ jobs:
- name: test
env:
INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./...
run: go test -v -mod readonly -race -coverprofile=coverage.txt -covermode=atomic ./...

View File

@@ -7,8 +7,8 @@ import (
"strings"
"time"
"github.com/google/uuid"
"dario.cat/mergo"
"github.com/google/uuid"
"go.unistack.org/micro/v4/options"
mid "go.unistack.org/micro/v4/util/id"
rutil "go.unistack.org/micro/v4/util/reflect"
@@ -40,6 +40,10 @@ func (c *defaultConfig) Init(opts ...options.Option) error {
}
func (c *defaultConfig) Load(ctx context.Context, opts ...options.Option) error {
if c.opts.SkipLoad != nil && c.opts.SkipLoad(ctx, c) {
return nil
}
if err := DefaultBeforeLoad(ctx, c); err != nil && !c.opts.AllowFail {
return err
}
@@ -292,7 +296,11 @@ func fillValues(valueOf reflect.Value, tname string) error {
return nil
}
func (c *defaultConfig) Save(ctx context.Context, opts ...options.Option) error {
func (c *defaultConfig) Save(ctx context.Context, _ ...options.Option) error {
if c.opts.SkipSave != nil && c.opts.SkipSave(ctx, c) {
return nil
}
if err := DefaultBeforeSave(ctx, c); err != nil {
return err
}

View File

@@ -43,6 +43,10 @@ type Options struct {
AfterInit []func(context.Context, Config) error
// AllowFail flag to allow fail in config source
AllowFail bool
// SkipLoad runs only if condition returns true
SkipLoad func(context.Context, Config) bool
// SkipSave runs only if condition returns true
SkipSave func(context.Context, Config) bool
}
// NewOptions new options struct with filed values

157
database/dsn.go Normal file
View File

@@ -0,0 +1,157 @@
package database
import (
"crypto/tls"
"errors"
"fmt"
"net/url"
"strings"
)
var (
ErrInvalidDSNAddr = errors.New("invalid dsn addr")
ErrInvalidDSNUnescaped = errors.New("dsn must be escaped")
ErrInvalidDSNNoSlash = errors.New("dsn must contains slash")
)
type Config struct {
TLSConfig *tls.Config
Username string
Password string
Scheme string
Host string
Port string
Database string
Params []string
}
func (cfg *Config) FormatDSN() string {
var s strings.Builder
if len(cfg.Scheme) > 0 {
s.WriteString(cfg.Scheme + "://")
}
// [username[:password]@]
if len(cfg.Username) > 0 {
s.WriteString(cfg.Username)
if len(cfg.Password) > 0 {
s.WriteByte(':')
s.WriteString(url.PathEscape(cfg.Password))
}
s.WriteByte('@')
}
// [host:port]
if len(cfg.Host) > 0 {
s.WriteString(cfg.Host)
if len(cfg.Port) > 0 {
s.WriteByte(':')
s.WriteString(cfg.Port)
}
}
// /dbname
s.WriteByte('/')
s.WriteString(url.PathEscape(cfg.Database))
for i := 0; i < len(cfg.Params); i += 2 {
if i == 0 {
s.WriteString("?")
} else {
s.WriteString("&")
}
s.WriteString(cfg.Params[i])
s.WriteString("=")
s.WriteString(cfg.Params[i+1])
}
return s.String()
}
func ParseDSN(dsn string) (*Config, error) {
cfg := &Config{}
// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
// Find last '/' that goes before dbname
foundSlash := false
for i := len(dsn) - 1; i >= 0; i-- {
if dsn[i] == '/' {
foundSlash = true
var j, k int
// left part is empty if i <= 0
if i > 0 {
// Find the first ':' in dsn
for j = i; j >= 0; j-- {
if dsn[j] == ':' {
cfg.Scheme = dsn[0:j]
}
}
// [username[:password]@][host]
// Find the last '@' in dsn[:i]
for j = i; j >= 0; j-- {
if dsn[j] == '@' {
// username[:password]
// Find the second ':' in dsn[:j]
for k = 0; k < j; k++ {
if dsn[k] == ':' {
if cfg.Scheme == dsn[:k] {
continue
}
var err error
cfg.Password, err = url.PathUnescape(dsn[k+1 : j])
if err != nil {
return nil, err
}
break
}
}
cfg.Username = dsn[len(cfg.Scheme)+3 : k]
break
}
}
for k = j + 1; k < i; k++ {
if dsn[k] == ':' {
cfg.Host = dsn[j+1 : k]
cfg.Port = dsn[k+1 : i]
break
}
}
}
// dbname[?param1=value1&...&paramN=valueN]
// Find the first '?' in dsn[i+1:]
for j = i + 1; j < len(dsn); j++ {
if dsn[j] == '?' {
parts := strings.Split(dsn[j+1:], "&")
cfg.Params = make([]string, 0, len(parts)*2)
for _, p := range parts {
k, v, found := strings.Cut(p, "=")
if !found {
continue
}
cfg.Params = append(cfg.Params, k, v)
}
break
}
}
var err error
dbname := dsn[i+1 : j]
if cfg.Database, err = url.PathUnescape(dbname); err != nil {
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
}
break
}
}
if !foundSlash && len(dsn) > 0 {
return nil, ErrInvalidDSNNoSlash
}
return cfg, nil
}

31
database/dsn_test.go Normal file
View File

@@ -0,0 +1,31 @@
package database
import (
"net/url"
"testing"
)
func TestParseDSN(t *testing.T) {
cfg, err := ParseDSN("postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2")
if err != nil {
t.Fatal(err)
}
if cfg.Password != "p@ssword#" {
t.Fatalf("parsing error")
}
}
func TestFormatDSN(t *testing.T) {
src := "postgres://username:p@ssword#@host:12345/dbname?key1=val2&key2=val2"
cfg, err := ParseDSN(src)
if err != nil {
t.Fatal(err)
}
dst, err := url.PathUnescape(cfg.FormatDSN())
if err != nil {
t.Fatal(err)
}
if src != dst {
t.Fatalf("\n%s\n%s", src, dst)
}
}

View File

@@ -17,7 +17,7 @@ func TestFSMStart(t *testing.T) {
wrapper := func(next StateFunc) StateFunc {
return func(sctx context.Context, s State, opts ...StateOption) (State, error) {
sctx = logger.NewContext(sctx, logger.Fields("state", s.Name()))
sctx = logger.NewContext(sctx, logger.Attrs("state", s.Name()))
return next(sctx, s, opts...)
}
}

View File

@@ -1,26 +1,25 @@
// Package logger provides a log interface
package logger // import "go.unistack.org/micro/v4/logger"
package logger
import (
"context"
"os"
"go.unistack.org/micro/v4/options"
)
var (
// DefaultLogger variable
DefaultLogger = NewLogger(WithLevel(ParseLevel(os.Getenv("MICRO_LOG_LEVEL"))))
// DefaultLevel used by logger
DefaultLevel = InfoLevel
// DefaultCallerSkipCount used by logger
DefaultCallerSkipCount = 2
)
type ContextAttrFunc func(ctx context.Context) []interface{}
var DefaultContextAttrFuncs []ContextAttrFunc
var (
// DefaultLogger variable
DefaultLogger = NewLogger()
// DefaultLevel used by logger
DefaultLevel = InfoLevel
// DefaultCallerSkipCount used by logger
DefaultCallerSkipCount = 2
)
// Logger is a generic logging interface
type Logger interface {
// Init initialises options
@@ -49,8 +48,10 @@ type Logger interface {
Fatal(ctx context.Context, msg string, attrs ...interface{})
// Log logs message with needed level
Log(ctx context.Context, level Level, msg string, attrs ...interface{})
// String returns the name of logger
// String returns the type name of logger
String() string
// String returns the name of logger
Name() string
}
// Info writes formatted msg to default logger on info level

75
logger/noop.go Normal file
View File

@@ -0,0 +1,75 @@
package logger
import (
"context"
"go.unistack.org/micro/v4/options"
)
type noopLogger struct {
opts Options
}
func NewLogger(opts ...options.Option) Logger {
options := NewOptions(opts...)
return &noopLogger{opts: options}
}
func (l *noopLogger) V(lvl Level) bool {
return false
}
func (l *noopLogger) Level(lvl Level) {
}
func (l *noopLogger) Init(opts ...options.Option) error {
for _, o := range opts {
o(&l.opts)
}
return nil
}
func (l *noopLogger) Clone(opts ...options.Option) Logger {
nl := &noopLogger{opts: l.opts}
for _, o := range opts {
o(&nl.opts)
}
return nl
}
func (l *noopLogger) Attrs(attrs ...interface{}) Logger {
return l
}
func (l *noopLogger) Options() Options {
return l.opts
}
func (l *noopLogger) Name() string {
return l.opts.Name
}
func (l *noopLogger) String() string {
return "noop"
}
func (l *noopLogger) Log(ctx context.Context, lvl Level, msg string, attrs ...interface{}) {
}
func (l *noopLogger) Info(ctx context.Context, msg string, attrs ...interface{}) {
}
func (l *noopLogger) Debug(ctx context.Context, msg string, attrs ...interface{}) {
}
func (l *noopLogger) Error(ctx context.Context, msg string, attrs ...interface{}) {
}
func (l *noopLogger) Trace(ctx context.Context, msg string, attrs ...interface{}) {
}
func (l *noopLogger) Warn(ctx context.Context, msg string, attrs ...interface{}) {
}
func (l *noopLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) {
}

View File

@@ -3,6 +3,7 @@ package logger
import (
"context"
"io"
"log/slog"
"os"
"reflect"
@@ -26,6 +27,22 @@ type Options struct {
CallerSkipCount int
// ContextAttrFuncs contains funcs that executed before log func on context
ContextAttrFuncs []ContextAttrFunc
// TimeKey is the key used for the time of the log call
TimeKey string
// LevelKey is the key used for the level of the log call
LevelKey string
// MessageKey is the key used for the message of the log call
MessageKey string
// ErrorKey is the key used for the error info
ErrorKey string
// SourceKey is the key used for the source file and line of the log call
SourceKey string
// StacktraceKey is the key used for the stacktrace
StacktraceKey string
// AddStacktrace controls writing of stacktaces on error
AddStacktrace bool
// AddSource enabled writing source file and position in log
AddSource bool
}
// NewOptions creates new options struct
@@ -37,24 +54,28 @@ func NewOptions(opts ...options.Option) Options {
CallerSkipCount: DefaultCallerSkipCount,
Context: context.Background(),
ContextAttrFuncs: DefaultContextAttrFuncs,
AddSource: true,
}
_ = WithMicroKeys()(&options)
for _, o := range opts {
o(&options)
_ = o(&options)
}
return options
}
// WithContextAttrFuncs appends default funcs for the context arrts filler
func WithContextAttrFuncs(attrs ...interface{}) options.Option {
func WithContextAttrFuncs(fncs ...ContextAttrFunc) options.Option {
return func(src interface{}) error {
v, err := options.Get(src, ".ContextAttrFuncs")
if err != nil {
return err
} else if rutil.IsZero(v) {
v = reflect.MakeSlice(reflect.TypeOf(v), 0, len(attrs)).Interface()
v = reflect.MakeSlice(reflect.TypeOf(v), 0, len(fncs)).Interface()
}
cv := reflect.ValueOf(v)
for _, l := range attrs {
for _, l := range fncs {
cv = reflect.Append(cv, reflect.ValueOf(l))
}
return options.Set(src, cv.Interface(), ".ContextAttrFuncs")
@@ -88,3 +109,117 @@ func WithCallerSkipCount(c int) options.Option {
return options.Set(src, c, ".CallerSkipCount")
}
}
func WithZapKeys() options.Option {
return func(src interface{}) error {
var err error
if err = options.Set(src, "@timestamp", ".TimeKey"); err != nil {
return err
}
if err = options.Set(src, "level", ".LevelKey"); err != nil {
return err
}
if err = options.Set(src, "msg", ".MessageKey"); err != nil {
return err
}
if err = options.Set(src, "caller", ".SourceKey"); err != nil {
return err
}
if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil {
return err
}
if err = options.Set(src, "error", ".ErrorKey"); err != nil {
return err
}
return nil
}
}
func WithZerologKeys() options.Option {
return func(src interface{}) error {
var err error
if err = options.Set(src, "time", ".TimeKey"); err != nil {
return err
}
if err = options.Set(src, "level", ".LevelKey"); err != nil {
return err
}
if err = options.Set(src, "message", ".MessageKey"); err != nil {
return err
}
if err = options.Set(src, "caller", ".SourceKey"); err != nil {
return err
}
if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil {
return err
}
if err = options.Set(src, "error", ".ErrorKey"); err != nil {
return err
}
return nil
}
}
func WithSlogKeys() options.Option {
return func(src interface{}) error {
var err error
if err = options.Set(src, slog.TimeKey, ".TimeKey"); err != nil {
return err
}
if err = options.Set(src, slog.LevelKey, ".LevelKey"); err != nil {
return err
}
if err = options.Set(src, slog.MessageKey, ".MessageKey"); err != nil {
return err
}
if err = options.Set(src, slog.SourceKey, ".SourceKey"); err != nil {
return err
}
if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil {
return err
}
if err = options.Set(src, "error", ".ErrorKey"); err != nil {
return err
}
return nil
}
}
func WithMicroKeys() options.Option {
return func(src interface{}) error {
var err error
if err = options.Set(src, "timestamp", ".TimeKey"); err != nil {
return err
}
if err = options.Set(src, "level", ".LevelKey"); err != nil {
return err
}
if err = options.Set(src, "msg", ".MessageKey"); err != nil {
return err
}
if err = options.Set(src, "caller", ".SourceKey"); err != nil {
return err
}
if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil {
return err
}
if err = options.Set(src, "error", ".ErrorKey"); err != nil {
return err
}
return nil
}
}
// WithAddStacktrace controls writing stacktrace on error
func WithAddStacktrace(v bool) options.Option {
return func(src interface{}) error {
return options.Set(src, v, ".AddStacktrace")
}
}
// WitAddSource controls writing source file and pos in log
func WithAddSource(v bool) options.Option {
return func(src interface{}) error {
return options.Set(src, v, ".AddSource")
}
}

View File

@@ -1,16 +1,22 @@
package logger
package slog
import (
"context"
"log/slog"
"os"
"regexp"
"runtime"
"strconv"
"sync"
"time"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/options"
"go.unistack.org/micro/v4/tracer"
)
var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`)
var (
traceValue = slog.StringValue("trace")
debugValue = slog.StringValue("debug")
@@ -20,29 +26,32 @@ var (
fatalValue = slog.StringValue("fatal")
)
var renameAttr = func(_ []string, a slog.Attr) slog.Attr {
func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.SourceKey:
source := a.Value.Any().(*slog.Source)
a.Value = slog.StringValue(source.File + ":" + strconv.Itoa(source.Line))
a.Key = "caller"
a.Key = s.opts.SourceKey
case slog.TimeKey:
a.Key = "timestamp"
a.Key = s.opts.TimeKey
case slog.MessageKey:
a.Key = s.opts.MessageKey
case slog.LevelKey:
level := a.Value.Any().(slog.Level)
lvl := slogToLoggerLevel(level)
a.Key = s.opts.LevelKey
switch {
case lvl < DebugLevel:
case lvl < logger.DebugLevel:
a.Value = traceValue
case lvl < InfoLevel:
case lvl < logger.InfoLevel:
a.Value = debugValue
case lvl < WarnLevel:
case lvl < logger.WarnLevel:
a.Value = infoValue
case lvl < ErrorLevel:
case lvl < logger.ErrorLevel:
a.Value = warnValue
case lvl < FatalLevel:
case lvl < logger.FatalLevel:
a.Value = errorValue
case lvl >= FatalLevel:
case lvl >= logger.FatalLevel:
a.Value = fatalValue
default:
a.Value = infoValue
@@ -55,93 +64,97 @@ var renameAttr = func(_ []string, a slog.Attr) slog.Attr {
type slogLogger struct {
slog *slog.Logger
leveler *slog.LevelVar
opts Options
opts logger.Options
mu sync.RWMutex
}
func (s *slogLogger) Clone(opts ...options.Option) Logger {
func (s *slogLogger) Clone(opts ...options.Option) logger.Logger {
s.mu.RLock()
options := s.opts
for _, o := range opts {
o(&options)
_ = o(&options)
}
l := &slogLogger{
opts: options,
}
if slog, ok := s.opts.Context.Value(loggerKey{}).(*slog.Logger); ok {
l.slog = slog
return nil
}
l.leveler = new(slog.LevelVar)
handleOpt := &slog.HandlerOptions{
ReplaceAttr: renameAttr,
ReplaceAttr: l.renameAttr,
Level: l.leveler,
AddSource: true,
AddSource: l.opts.AddSource,
}
l.leveler.Set(loggerToSlogLevel(l.opts.Level))
handler := slog.NewJSONHandler(options.Out, handleOpt)
l.slog = slog.New(handler).With(options.Attrs...)
s.mu.RUnlock()
return l
}
func (s *slogLogger) V(level Level) bool {
func (s *slogLogger) V(level logger.Level) bool {
return s.opts.Level.Enabled(level)
}
func (s *slogLogger) Level(level Level) {
func (s *slogLogger) Level(level logger.Level) {
s.leveler.Set(loggerToSlogLevel(level))
}
func (s *slogLogger) Options() Options {
func (s *slogLogger) Options() logger.Options {
return s.opts
}
func (s *slogLogger) Attrs(attrs ...interface{}) Logger {
nl := &slogLogger{opts: s.opts}
nl.leveler = new(slog.LevelVar)
nl.leveler.Set(s.leveler.Level())
func (s *slogLogger) Attrs(attrs ...interface{}) logger.Logger {
s.mu.RLock()
l := &slogLogger{opts: s.opts}
l.leveler = new(slog.LevelVar)
l.leveler.Set(s.leveler.Level())
handleOpt := &slog.HandlerOptions{
ReplaceAttr: renameAttr,
Level: s.leveler,
AddSource: true,
ReplaceAttr: l.renameAttr,
Level: l.leveler,
AddSource: l.opts.AddSource,
}
handler := slog.NewJSONHandler(s.opts.Out, handleOpt)
nl.slog = slog.New(handler).With(attrs...)
l.slog = slog.New(handler).With(attrs...)
return nl
s.mu.RUnlock()
return l
}
func (s *slogLogger) Init(opts ...options.Option) error {
s.mu.Lock()
if len(s.opts.ContextAttrFuncs) == 0 {
s.opts.ContextAttrFuncs = logger.DefaultContextAttrFuncs
}
for _, o := range opts {
if err := o(&s.opts); err != nil {
return err
}
}
if slog, ok := s.opts.Context.Value(loggerKey{}).(*slog.Logger); ok {
s.slog = slog
return nil
}
s.leveler = new(slog.LevelVar)
handleOpt := &slog.HandlerOptions{
ReplaceAttr: renameAttr,
ReplaceAttr: s.renameAttr,
Level: s.leveler,
AddSource: true,
AddSource: s.opts.AddSource,
}
s.leveler.Set(loggerToSlogLevel(s.opts.Level))
handler := slog.NewJSONHandler(s.opts.Out, handleOpt)
s.slog = slog.New(handler).With(s.opts.Attrs...)
s.mu.Unlock()
return nil
}
func (s *slogLogger) Log(ctx context.Context, lvl Level, msg string, attrs ...interface{}) {
func (s *slogLogger) Log(ctx context.Context, lvl logger.Level, msg string, attrs ...interface{}) {
if !s.V(lvl) {
return
}
@@ -149,77 +162,125 @@ func (s *slogLogger) Log(ctx context.Context, lvl Level, msg string, attrs ...in
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), loggerToSlogLevel(lvl), msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
for _, a := range attrs {
if ve, ok := a.(error); ok && ve != nil {
attrs = append(attrs, slog.String(s.opts.ErrorKey, ve.Error()))
break
}
}
if s.opts.AddStacktrace && lvl == logger.ErrorLevel {
stackInfo := make([]byte, 1024*1024)
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
traceLines := reTrace.Split(string(stackInfo[:stackSize]), -1)
if len(traceLines) != 0 {
attrs = append(attrs, slog.String(s.opts.StacktraceKey, traceLines[len(traceLines)-1]))
}
}
}
r.Add(attrs...)
r.Attrs(func(a slog.Attr) bool {
if a.Key == s.opts.ErrorKey {
if span, ok := tracer.SpanFromContext(ctx); ok {
span.SetStatus(tracer.SpanStatusError, a.Value.String())
return false
}
}
return true
})
_ = s.slog.Handler().Handle(ctx, r)
}
func (s *slogLogger) Info(ctx context.Context, msg string, attrs ...interface{}) {
if !s.V(InfoLevel) {
if !s.V(logger.InfoLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelInfo, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
r.Add(attrs...)
_ = s.slog.Handler().Handle(ctx, r)
}
func (s *slogLogger) Debug(ctx context.Context, msg string, attrs ...interface{}) {
if !s.V(InfoLevel) {
if !s.V(logger.DebugLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelDebug, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
r.Add(attrs...)
_ = s.slog.Handler().Handle(ctx, r)
}
func (s *slogLogger) Trace(ctx context.Context, msg string, attrs ...interface{}) {
if !s.V(InfoLevel) {
if !s.V(logger.TraceLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelDebug-1, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
r.Add(attrs...)
_ = s.slog.Handler().Handle(ctx, r)
}
func (s *slogLogger) Error(ctx context.Context, msg string, attrs ...interface{}) {
if !s.V(InfoLevel) {
if !s.V(logger.ErrorLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelError, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
for _, a := range attrs {
if ve, ok := a.(error); ok && ve != nil {
attrs = append(attrs, slog.String(s.opts.ErrorKey, ve.Error()))
break
}
}
if s.opts.AddStacktrace {
stackInfo := make([]byte, 1024*1024)
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
traceLines := reTrace.Split(string(stackInfo[:stackSize]), -1)
if len(traceLines) != 0 {
attrs = append(attrs, slog.String(s.opts.StacktraceKey, traceLines[len(traceLines)-1]))
}
}
}
r.Add(attrs...)
r.Attrs(func(a slog.Attr) bool {
if a.Key == s.opts.ErrorKey {
if span, ok := tracer.SpanFromContext(ctx); ok {
span.SetStatus(tracer.SpanStatusError, a.Value.String())
return false
}
}
return true
})
_ = s.slog.Handler().Handle(ctx, r)
}
func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}) {
if !s.V(InfoLevel) {
if !s.V(logger.FatalLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelError+1, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
r.Add(attrs...)
_ = s.slog.Handler().Handle(ctx, r)
@@ -227,60 +288,64 @@ func (s *slogLogger) Fatal(ctx context.Context, msg string, attrs ...interface{}
}
func (s *slogLogger) Warn(ctx context.Context, msg string, attrs ...interface{}) {
if !s.V(InfoLevel) {
if !s.V(logger.WarnLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelWarn, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx))
attrs = append(attrs, fn(ctx)...)
}
r.Add(attrs...)
_ = s.slog.Handler().Handle(ctx, r)
}
func (s *slogLogger) Name() string {
return s.opts.Name
}
func (s *slogLogger) String() string {
return "slog"
}
func NewLogger(opts ...options.Option) Logger {
func NewLogger(opts ...options.Option) logger.Logger {
l := &slogLogger{
opts: NewOptions(opts...),
opts: logger.NewOptions(opts...),
}
return l
}
func loggerToSlogLevel(level Level) slog.Level {
func loggerToSlogLevel(level logger.Level) slog.Level {
switch level {
case DebugLevel:
case logger.DebugLevel:
return slog.LevelDebug
case WarnLevel:
case logger.WarnLevel:
return slog.LevelWarn
case ErrorLevel:
case logger.ErrorLevel:
return slog.LevelError
case TraceLevel:
case logger.TraceLevel:
return slog.LevelDebug - 1
case FatalLevel:
case logger.FatalLevel:
return slog.LevelError + 1
default:
return slog.LevelInfo
}
}
func slogToLoggerLevel(level slog.Level) Level {
func slogToLoggerLevel(level slog.Level) logger.Level {
switch level {
case slog.LevelDebug:
return DebugLevel
return logger.DebugLevel
case slog.LevelWarn:
return WarnLevel
return logger.WarnLevel
case slog.LevelError:
return ErrorLevel
return logger.ErrorLevel
case slog.LevelDebug - 1:
return TraceLevel
return logger.TraceLevel
case slog.LevelError + 1:
return FatalLevel
return logger.FatalLevel
default:
return InfoLevel
return logger.InfoLevel
}
}

View File

@@ -1,21 +1,37 @@
package logger
package slog
import (
"bytes"
"context"
"fmt"
"log"
"testing"
"go.unistack.org/micro/v4/logger"
)
func TestError(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf), logger.WithAddStacktrace(true))
if err := l.Init(); err != nil {
t.Fatal(err)
}
l.Error(ctx, "msg", fmt.Errorf("message"))
if !bytes.Contains(buf.Bytes(), []byte(`"stacktrace":"`)) {
t.Fatalf("logger stacktrace not works, buf contains: %s", buf.Bytes())
}
}
func TestContext(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
nl, ok := FromContext(NewContext(ctx, l.Attrs("key", "val")))
nl, ok := logger.FromContext(logger.NewContext(ctx, l.Attrs("key", "val")))
if !ok {
t.Fatal("context without logger")
}
@@ -28,7 +44,7 @@ func TestContext(t *testing.T) {
func TestAttrs(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
@@ -45,15 +61,15 @@ func TestFromContextWithFields(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
var ok bool
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
nl := l.Attrs("key", "val")
ctx = NewContext(ctx, nl)
ctx = logger.NewContext(ctx, nl)
l, ok = FromContext(ctx)
l, ok = logger.FromContext(ctx)
if !ok {
t.Fatalf("context does not have logger")
}
@@ -67,11 +83,11 @@ func TestFromContextWithFields(t *testing.T) {
func TestClone(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
nl := l.Clone(WithLevel(ErrorLevel))
nl := l.Clone(logger.WithLevel(logger.ErrorLevel))
if err := nl.Init(); err != nil {
t.Fatal(err)
}
@@ -87,11 +103,11 @@ func TestClone(t *testing.T) {
func TestRedirectStdLogger(t *testing.T) {
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(ErrorLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
fn := RedirectStdLogger(l, ErrorLevel)
fn := logger.RedirectStdLogger(l, logger.ErrorLevel)
defer fn()
log.Print("test")
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"error"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test"`))) {
@@ -101,11 +117,11 @@ func TestRedirectStdLogger(t *testing.T) {
func TestStdLogger(t *testing.T) {
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
lg := NewStdLogger(l, ErrorLevel)
lg := logger.NewStdLogger(l, logger.ErrorLevel)
lg.Print("test")
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"error"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test"`))) {
t.Fatalf("logger error, buf %s", buf.Bytes())
@@ -115,7 +131,7 @@ func TestStdLogger(t *testing.T) {
func TestLogger(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}

94
micro.go Normal file
View File

@@ -0,0 +1,94 @@
package micro
import (
"reflect"
"go.unistack.org/micro/v4/broker"
"go.unistack.org/micro/v4/client"
"go.unistack.org/micro/v4/codec"
"go.unistack.org/micro/v4/flow"
"go.unistack.org/micro/v4/fsm"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/meter"
"go.unistack.org/micro/v4/register"
"go.unistack.org/micro/v4/resolver"
"go.unistack.org/micro/v4/router"
"go.unistack.org/micro/v4/selector"
"go.unistack.org/micro/v4/server"
"go.unistack.org/micro/v4/store"
"go.unistack.org/micro/v4/sync"
"go.unistack.org/micro/v4/tracer"
)
func As(b any, target any) bool {
if b == nil {
return false
}
if target == nil {
return false
}
val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Ptr || val.IsNil() {
return false
}
targetType := typ.Elem()
if targetType.Kind() != reflect.Interface {
switch {
case targetType.Implements(brokerType):
break
case targetType.Implements(loggerType):
break
case targetType.Implements(clientType):
break
case targetType.Implements(serverType):
break
case targetType.Implements(codecType):
break
case targetType.Implements(flowType):
break
case targetType.Implements(fsmType):
break
case targetType.Implements(meterType):
break
case targetType.Implements(registerType):
break
case targetType.Implements(resolverType):
break
case targetType.Implements(selectorType):
break
case targetType.Implements(storeType):
break
case targetType.Implements(syncType):
break
case targetType.Implements(serviceType):
break
case targetType.Implements(routerType):
break
default:
return false
}
}
if reflect.TypeOf(b).AssignableTo(targetType) {
val.Elem().Set(reflect.ValueOf(b))
return true
}
return false
}
var brokerType = reflect.TypeOf((*broker.Broker)(nil)).Elem()
var loggerType = reflect.TypeOf((*logger.Logger)(nil)).Elem()
var clientType = reflect.TypeOf((*client.Client)(nil)).Elem()
var serverType = reflect.TypeOf((*server.Server)(nil)).Elem()
var codecType = reflect.TypeOf((*codec.Codec)(nil)).Elem()
var flowType = reflect.TypeOf((*flow.Flow)(nil)).Elem()
var fsmType = reflect.TypeOf((*fsm.FSM)(nil)).Elem()
var meterType = reflect.TypeOf((*meter.Meter)(nil)).Elem()
var registerType = reflect.TypeOf((*register.Register)(nil)).Elem()
var resolverType = reflect.TypeOf((*resolver.Resolver)(nil)).Elem()
var routerType = reflect.TypeOf((*router.Router)(nil)).Elem()
var selectorType = reflect.TypeOf((*selector.Selector)(nil)).Elem()
var storeType = reflect.TypeOf((*store.Store)(nil)).Elem()
var syncType = reflect.TypeOf((*sync.Sync)(nil)).Elem()
var tracerType = reflect.TypeOf((*tracer.Tracer)(nil)).Elem()
var serviceType = reflect.TypeOf((*Service)(nil)).Elem()

103
micro_test.go Normal file
View File

@@ -0,0 +1,103 @@
package micro
import (
"context"
"fmt"
"reflect"
"testing"
"go.unistack.org/micro/v4/broker"
"go.unistack.org/micro/v4/fsm"
"go.unistack.org/micro/v4/options"
)
func TestAs(t *testing.T) {
var b *bro
broTarget := &bro{name: "kafka"}
fsmTarget := &fsmT{name: "fsm"}
testCases := []struct {
b any
target any
match bool
want any
}{
{
broTarget,
&b,
true,
broTarget,
},
{
nil,
&b,
false,
nil,
},
{
fsmTarget,
&b,
false,
nil,
},
}
for i, tc := range testCases {
name := fmt.Sprintf("%d:As(Errorf(..., %v), %v)", i, tc.b, tc.target)
// Clear the target pointer, in case it was set in a previous test.
rtarget := reflect.ValueOf(tc.target)
rtarget.Elem().Set(reflect.Zero(reflect.TypeOf(tc.target).Elem()))
t.Run(name, func(t *testing.T) {
match := As(tc.b, tc.target)
if match != tc.match {
t.Fatalf("match: got %v; want %v", match, tc.match)
}
if !match {
return
}
if got := rtarget.Elem().Interface(); got != tc.want {
t.Fatalf("got %#v, want %#v", got, tc.want)
}
})
}
}
type bro struct {
name string
}
func (p *bro) Name() string { return p.name }
func (p *bro) Init(opts ...options.Option) error { return nil }
// Options returns broker options
func (p *bro) Options() broker.Options { return broker.Options{} }
// Address return configured address
func (p *bro) Address() string { return "" }
// Connect connects to broker
func (p *bro) Connect(ctx context.Context) error { return nil }
// Disconnect disconnect from broker
func (p *bro) Disconnect(ctx context.Context) error { return nil }
// Publish message, msg can be single broker.Message or []broker.Message
func (p *bro) Publish(ctx context.Context, msg interface{}, opts ...options.Option) error { return nil }
// Subscribe subscribes to topic message via handler
func (p *bro) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...options.Option) (broker.Subscriber, error) {
return nil, nil
}
// String type of broker
func (p *bro) String() string { return p.name }
type fsmT struct {
name string
}
func (f *fsmT) Start(ctx context.Context, a interface{}, o ...Option) (interface{}, error) {
return nil, nil
}
func (f *fsmT) Current() string { return f.name }
func (f *fsmT) Reset() {}
func (f *fsmT) State(s string, sf fsm.StateFunc) {}

View File

@@ -151,9 +151,30 @@ func ContentType(ct string) Option {
}
// Metadata pass additional metadata
func Metadata(md metadata.Metadata) Option {
func Metadata(md ...any) Option {
var result metadata.Metadata
if len(md) == 1 {
switch vt := md[0].(type) {
case metadata.Metadata:
result = metadata.Copy(vt)
case map[string]string:
result = metadata.Copy(vt)
default:
result = metadata.New(0)
}
} else {
result = metadata.New(len(md) / 2)
for idx := 0; idx < len(md)/2; idx += 2 {
k, kok := md[idx].(string)
v, vok := md[idx+1].(string)
if kok && vok {
result.Set(k, v)
}
}
}
return func(src interface{}) error {
return Set(src, metadata.Copy(md), ".Metadata")
return Set(src, result, ".Metadata")
}
}

View File

@@ -1,10 +1,13 @@
package options_test
import (
"fmt"
"testing"
"go.unistack.org/micro/v4/codec"
"go.unistack.org/micro/v4/metadata"
"go.unistack.org/micro/v4/options"
"go.unistack.org/micro/v4/util/reflect"
)
func TestAddress(t *testing.T) {
@@ -84,3 +87,75 @@ func TestLabels(t *testing.T) {
t.Fatal("failed to set labels")
}
}
func TestMetadataAny(t *testing.T) {
type s struct {
Metadata metadata.Metadata
}
testCases := []struct {
Name string
Data any
Expected metadata.Metadata
}{
{
"strings_even",
[]any{"key1", "val1", "key2", "val2"},
metadata.Metadata{
"Key1": "val1",
"Key2": "val2",
},
},
{
"strings_odd",
[]any{"key1", "val1", "key2"},
metadata.Metadata{
"Key1": "val1",
},
},
{
Name: "map",
Data: map[string]string{
"key1": "val1",
"key2": "val2",
},
Expected: metadata.Metadata{
"Key1": "val1",
"Key2": "val2",
},
},
{
"metadata.Metadata",
metadata.Metadata{
"key1": "val1",
"key2": "val2",
},
metadata.Metadata{
"Key1": "val1",
"Key2": "val2",
},
},
}
for _, tt := range testCases {
t.Run(tt.Name, func(t *testing.T) {
src := &s{}
var opts []options.Option
switch valData := tt.Data.(type) {
case []any:
opts = append(opts, options.Metadata(valData...))
case map[string]string, metadata.Metadata:
opts = append(opts, options.Metadata(valData))
}
for _, o := range opts {
if err := o(src); err != nil {
t.Fatal(err)
}
if !reflect.Equal(tt.Expected, src.Metadata) {
t.Fatal(fmt.Sprintf("expected: %v, actual: %v", tt.Expected, src.Metadata))
}
}
})
}
}

View File

@@ -1,7 +1,8 @@
package register
package memory
import (
"context"
"go.unistack.org/micro/v4/register"
"sync"
"time"
@@ -16,7 +17,7 @@ var (
type node struct {
LastSeen time.Time
*Node
*register.Node
TTL time.Duration
}
@@ -25,23 +26,23 @@ type record struct {
Version string
Metadata map[string]string
Nodes map[string]*node
Endpoints []*Endpoint
Endpoints []*register.Endpoint
}
type memory struct {
sync.RWMutex
records map[string]services
watchers map[string]*watcher
opts Options
opts register.Options
}
// services is a KV map with service name as the key and a map of records as the value
type services map[string]map[string]*record
// NewRegister returns an initialized in-memory register
func NewRegister(opts ...Option) Register {
func NewRegister(opts ...register.Option) register.Register {
r := &memory{
opts: NewOptions(opts...),
opts: register.NewOptions(opts...),
records: make(map[string]services),
watchers: make(map[string]*watcher),
}
@@ -75,7 +76,7 @@ func (m *memory) ttlPrune() {
}
}
func (m *memory) sendEvent(r *Result) {
func (m *memory) sendEvent(r *register.Result) {
m.RLock()
watchers := make([]*watcher, 0, len(m.watchers))
for _, w := range m.watchers {
@@ -106,7 +107,7 @@ func (m *memory) Disconnect(ctx context.Context) error {
return nil
}
func (m *memory) Init(opts ...Option) error {
func (m *memory) Init(opts ...register.Option) error {
for _, o := range opts {
o(&m.opts)
}
@@ -118,15 +119,15 @@ func (m *memory) Init(opts ...Option) error {
return nil
}
func (m *memory) Options() Options {
func (m *memory) Options() register.Options {
return m.opts
}
func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOption) error {
func (m *memory) Register(ctx context.Context, s *register.Service, opts ...register.RegisterOption) error {
m.Lock()
defer m.Unlock()
options := NewRegisterOptions(opts...)
options := register.NewRegisterOptions(opts...)
// get the services for this domain from the register
srvs, ok := m.records[options.Domain]
@@ -153,7 +154,7 @@ func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOptio
m.opts.Logger.Debug(m.opts.Context, "register added new service: "+s.Name+", version "+s.Version)
}
m.records[options.Domain] = srvs
go m.sendEvent(&Result{Action: "create", Service: s})
go m.sendEvent(&register.Result{Action: "create", Service: s})
}
var addedNodes bool
@@ -176,7 +177,7 @@ func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOptio
// add the node
srvs[s.Name][s.Version].Nodes[n.ID] = &node{
Node: &Node{
Node: &register.Node{
ID: n.ID,
Address: n.Address,
Metadata: metadata,
@@ -192,7 +193,7 @@ func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOptio
if m.opts.Logger.V(logger.DebugLevel) {
m.opts.Logger.Debug(m.opts.Context, "register added new node to service: "+s.Name+", version "+s.Version)
}
go m.sendEvent(&Result{Action: "update", Service: s})
go m.sendEvent(&register.Result{Action: "update", Service: s})
} else {
// refresh TTL and timestamp
for _, n := range s.Nodes {
@@ -208,11 +209,11 @@ func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOptio
return nil
}
func (m *memory) Deregister(ctx context.Context, s *Service, opts ...DeregisterOption) error {
func (m *memory) Deregister(ctx context.Context, s *register.Service, opts ...register.DeregisterOption) error {
m.Lock()
defer m.Unlock()
options := NewDeregisterOptions(opts...)
options := register.NewDeregisterOptions(opts...)
// domain is set in metadata so it can be passed to watchers
if s.Metadata == nil {
@@ -252,7 +253,7 @@ func (m *memory) Deregister(ctx context.Context, s *Service, opts ...DeregisterO
// is cleanup
if len(version.Nodes) > 0 {
m.records[options.Domain][s.Name][s.Version] = version
go m.sendEvent(&Result{Action: "update", Service: s})
go m.sendEvent(&register.Result{Action: "update", Service: s})
return nil
}
@@ -260,7 +261,7 @@ func (m *memory) Deregister(ctx context.Context, s *Service, opts ...DeregisterO
// register and exit
if len(versions) == 1 {
delete(m.records[options.Domain], s.Name)
go m.sendEvent(&Result{Action: "delete", Service: s})
go m.sendEvent(&register.Result{Action: "delete", Service: s})
if m.opts.Logger.V(logger.DebugLevel) {
m.opts.Logger.Debug(m.opts.Context, "register removed service: "+s.Name)
@@ -270,7 +271,7 @@ func (m *memory) Deregister(ctx context.Context, s *Service, opts ...DeregisterO
// there are other versions of the service running, so only remove this version of it
delete(m.records[options.Domain][s.Name], s.Version)
go m.sendEvent(&Result{Action: "delete", Service: s})
go m.sendEvent(&register.Result{Action: "delete", Service: s})
if m.opts.Logger.V(logger.DebugLevel) {
m.opts.Logger.Debug(m.opts.Context, "register removed service: "+s.Name+", version "+s.Version)
}
@@ -278,20 +279,20 @@ func (m *memory) Deregister(ctx context.Context, s *Service, opts ...DeregisterO
return nil
}
func (m *memory) LookupService(ctx context.Context, name string, opts ...LookupOption) ([]*Service, error) {
options := NewLookupOptions(opts...)
func (m *memory) LookupService(ctx context.Context, name string, opts ...register.LookupOption) ([]*register.Service, error) {
options := register.NewLookupOptions(opts...)
// if it's a wildcard domain, return from all domains
if options.Domain == WildcardDomain {
if options.Domain == register.WildcardDomain {
m.RLock()
recs := m.records
m.RUnlock()
var services []*Service
var services []*register.Service
for domain := range recs {
srvs, err := m.LookupService(ctx, name, append(opts, LookupDomain(domain))...)
if err == ErrNotFound {
srvs, err := m.LookupService(ctx, name, append(opts, register.LookupDomain(domain))...)
if err == register.ErrNotFound {
continue
} else if err != nil {
return nil, err
@@ -300,7 +301,7 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...LookupO
}
if len(services) == 0 {
return nil, ErrNotFound
return nil, register.ErrNotFound
}
return services, nil
}
@@ -311,17 +312,17 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...LookupO
// check the domain exists
services, ok := m.records[options.Domain]
if !ok {
return nil, ErrNotFound
return nil, register.ErrNotFound
}
// check the service exists
versions, ok := services[name]
if !ok || len(versions) == 0 {
return nil, ErrNotFound
return nil, register.ErrNotFound
}
// serialize the response
result := make([]*Service, len(versions))
result := make([]*register.Service, len(versions))
var i int
@@ -333,19 +334,19 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...LookupO
return result, nil
}
func (m *memory) ListServices(ctx context.Context, opts ...ListOption) ([]*Service, error) {
options := NewListOptions(opts...)
func (m *memory) ListServices(ctx context.Context, opts ...register.ListOption) ([]*register.Service, error) {
options := register.NewListOptions(opts...)
// if it's a wildcard domain, list from all domains
if options.Domain == WildcardDomain {
if options.Domain == register.WildcardDomain {
m.RLock()
recs := m.records
m.RUnlock()
var services []*Service
var services []*register.Service
for domain := range recs {
srvs, err := m.ListServices(ctx, append(opts, ListDomain(domain))...)
srvs, err := m.ListServices(ctx, append(opts, register.ListDomain(domain))...)
if err != nil {
return nil, err
}
@@ -361,11 +362,11 @@ func (m *memory) ListServices(ctx context.Context, opts ...ListOption) ([]*Servi
// ensure the domain exists
services, ok := m.records[options.Domain]
if !ok {
return make([]*Service, 0), nil
return make([]*register.Service, 0), nil
}
// serialize the result, each version counts as an individual service
var result []*Service
var result []*register.Service
for _, service := range services {
for _, version := range service {
@@ -376,16 +377,16 @@ func (m *memory) ListServices(ctx context.Context, opts ...ListOption) ([]*Servi
return result, nil
}
func (m *memory) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
func (m *memory) Watch(ctx context.Context, opts ...register.WatchOption) (register.Watcher, error) {
id, err := id.New()
if err != nil {
return nil, err
}
wo := NewWatchOptions(opts...)
wo := register.NewWatchOptions(opts...)
// construct the watcher
w := &watcher{
exit: make(chan bool),
res: make(chan *Result),
res: make(chan *register.Result),
id: id,
wo: wo,
}
@@ -406,13 +407,13 @@ func (m *memory) String() string {
}
type watcher struct {
res chan *Result
res chan *register.Result
exit chan bool
wo WatchOptions
wo register.WatchOptions
id string
}
func (m *watcher) Next() (*Result, error) {
func (m *watcher) Next() (*register.Result, error) {
for {
select {
case r := <-m.res:
@@ -429,15 +430,15 @@ func (m *watcher) Next() (*Result, error) {
if r.Service.Metadata != nil && len(r.Service.Metadata["domain"]) > 0 {
domain = r.Service.Metadata["domain"]
} else {
domain = DefaultDomain
domain = register.DefaultDomain
}
// only send the event if watching the wildcard or this specific domain
if m.wo.Domain == WildcardDomain || m.wo.Domain == domain {
if m.wo.Domain == register.WildcardDomain || m.wo.Domain == domain {
return r, nil
}
case <-m.exit:
return nil, ErrWatcherStopped
return nil, register.ErrWatcherStopped
}
}
}
@@ -451,7 +452,7 @@ func (m *watcher) Stop() {
}
}
func serviceToRecord(s *Service, ttl time.Duration) *record {
func serviceToRecord(s *register.Service, ttl time.Duration) *record {
metadata := make(map[string]string, len(s.Metadata))
for k, v := range s.Metadata {
metadata[k] = v
@@ -466,7 +467,7 @@ func serviceToRecord(s *Service, ttl time.Duration) *record {
}
}
endpoints := make([]*Endpoint, len(s.Endpoints))
endpoints := make([]*register.Endpoint, len(s.Endpoints))
for i, e := range s.Endpoints { // TODO: vtolstov use copy
endpoints[i] = e
}
@@ -480,7 +481,7 @@ func serviceToRecord(s *Service, ttl time.Duration) *record {
}
}
func recordToService(r *record, domain string) *Service {
func recordToService(r *record, domain string) *register.Service {
metadata := make(map[string]string, len(r.Metadata))
for k, v := range r.Metadata {
metadata[k] = v
@@ -489,14 +490,14 @@ func recordToService(r *record, domain string) *Service {
// set the domain in metadata so it can be determined when a wildcard query is performed
metadata["domain"] = domain
endpoints := make([]*Endpoint, len(r.Endpoints))
endpoints := make([]*register.Endpoint, len(r.Endpoints))
for i, e := range r.Endpoints {
md := make(map[string]string, len(e.Metadata))
for k, v := range e.Metadata {
md[k] = v
}
endpoints[i] = &Endpoint{
endpoints[i] = &register.Endpoint{
Name: e.Name,
Request: e.Request,
Response: e.Response,
@@ -504,7 +505,7 @@ func recordToService(r *record, domain string) *Service {
}
}
nodes := make([]*Node, len(r.Nodes))
nodes := make([]*register.Node, len(r.Nodes))
i := 0
for _, n := range r.Nodes {
md := make(map[string]string, len(n.Metadata))
@@ -512,7 +513,7 @@ func recordToService(r *record, domain string) *Service {
md[k] = v
}
nodes[i] = &Node{
nodes[i] = &register.Node{
ID: n.ID,
Address: n.Address,
Metadata: md,
@@ -520,7 +521,7 @@ func recordToService(r *record, domain string) *Service {
i++
}
return &Service{
return &register.Service{
Name: r.Name,
Version: r.Version,
Metadata: metadata,

View File

@@ -1,19 +1,22 @@
package register
package memory
import (
"context"
"fmt"
"go.unistack.org/micro/v4"
"go.unistack.org/micro/v4/register"
"reflect"
"sync"
"testing"
"time"
)
var testData = map[string][]*Service{
var testData = map[string][]*register.Service{
"foo": {
{
Name: "foo",
Version: "1.0.0",
Nodes: []*Node{
Nodes: []*register.Node{
{
ID: "foo-1.0.0-123",
Address: "localhost:9999",
@@ -27,7 +30,7 @@ var testData = map[string][]*Service{
{
Name: "foo",
Version: "1.0.1",
Nodes: []*Node{
Nodes: []*register.Node{
{
ID: "foo-1.0.1-321",
Address: "localhost:6666",
@@ -37,7 +40,7 @@ var testData = map[string][]*Service{
{
Name: "foo",
Version: "1.0.3",
Nodes: []*Node{
Nodes: []*register.Node{
{
ID: "foo-1.0.3-345",
Address: "localhost:8888",
@@ -49,7 +52,7 @@ var testData = map[string][]*Service{
{
Name: "bar",
Version: "default",
Nodes: []*Node{
Nodes: []*register.Node{
{
ID: "bar-1.0.0-123",
Address: "localhost:9999",
@@ -63,7 +66,7 @@ var testData = map[string][]*Service{
{
Name: "bar",
Version: "latest",
Nodes: []*Node{
Nodes: []*register.Node{
{
ID: "bar-1.0.1-321",
Address: "localhost:6666",
@@ -78,7 +81,7 @@ func TestMemoryRegistry(t *testing.T) {
ctx := context.TODO()
m := NewRegister()
fn := func(k string, v []*Service) {
fn := func(k string, v []*register.Service) {
services, err := m.LookupService(ctx, k)
if err != nil {
t.Errorf("Unexpected error getting service %s: %v", k, err)
@@ -155,8 +158,8 @@ func TestMemoryRegistry(t *testing.T) {
for _, v := range testData {
for _, service := range v {
services, err := m.LookupService(ctx, service.Name)
if err != ErrNotFound {
t.Errorf("Expected error: %v, got: %v", ErrNotFound, err)
if err != register.ErrNotFound {
t.Errorf("Expected error: %v, got: %v", register.ErrNotFound, err)
}
if len(services) != 0 {
t.Errorf("Expected %d services for %s, got %d", 0, service.Name, len(services))
@@ -171,7 +174,7 @@ func TestMemoryRegistryTTL(t *testing.T) {
for _, v := range testData {
for _, service := range v {
if err := m.Register(ctx, service, RegisterTTL(time.Millisecond)); err != nil {
if err := m.Register(ctx, service, register.RegisterTTL(time.Millisecond)); err != nil {
t.Fatal(err)
}
}
@@ -200,7 +203,7 @@ func TestMemoryRegistryTTLConcurrent(t *testing.T) {
ctx := context.TODO()
for _, v := range testData {
for _, service := range v {
if err := m.Register(ctx, service, RegisterTTL(waitTime/2)); err != nil {
if err := m.Register(ctx, service, register.RegisterTTL(waitTime/2)); err != nil {
t.Fatal(err)
}
}
@@ -249,34 +252,34 @@ func TestMemoryWildcard(t *testing.T) {
m := NewRegister()
ctx := context.TODO()
testSrv := &Service{Name: "foo", Version: "1.0.0"}
testSrv := &register.Service{Name: "foo", Version: "1.0.0"}
if err := m.Register(ctx, testSrv, RegisterDomain("one")); err != nil {
if err := m.Register(ctx, testSrv, register.RegisterDomain("one")); err != nil {
t.Fatalf("Register err: %v", err)
}
if err := m.Register(ctx, testSrv, RegisterDomain("two")); err != nil {
if err := m.Register(ctx, testSrv, register.RegisterDomain("two")); err != nil {
t.Fatalf("Register err: %v", err)
}
if recs, err := m.ListServices(ctx, ListDomain("one")); err != nil {
if recs, err := m.ListServices(ctx, register.ListDomain("one")); err != nil {
t.Errorf("List err: %v", err)
} else if len(recs) != 1 {
t.Errorf("Expected 1 record, got %v", len(recs))
}
if recs, err := m.ListServices(ctx, ListDomain("*")); err != nil {
if recs, err := m.ListServices(ctx, register.ListDomain("*")); err != nil {
t.Errorf("List err: %v", err)
} else if len(recs) != 2 {
t.Errorf("Expected 2 records, got %v", len(recs))
}
if recs, err := m.LookupService(ctx, testSrv.Name, LookupDomain("one")); err != nil {
if recs, err := m.LookupService(ctx, testSrv.Name, register.LookupDomain("one")); err != nil {
t.Errorf("Lookup err: %v", err)
} else if len(recs) != 1 {
t.Errorf("Expected 1 record, got %v", len(recs))
}
if recs, err := m.LookupService(ctx, testSrv.Name, LookupDomain("*")); err != nil {
if recs, err := m.LookupService(ctx, testSrv.Name, register.LookupDomain("*")); err != nil {
t.Errorf("Lookup err: %v", err)
} else if len(recs) != 2 {
t.Errorf("Expected 2 records, got %v", len(recs))
@@ -284,7 +287,7 @@ func TestMemoryWildcard(t *testing.T) {
}
func TestWatcher(t *testing.T) {
testSrv := &Service{Name: "foo", Version: "1.0.0"}
testSrv := &register.Service{Name: "foo", Version: "1.0.0"}
ctx := context.TODO()
m := NewRegister()
@@ -320,3 +323,37 @@ func TestWatcher(t *testing.T) {
t.Fatal("expected error on Next()")
}
}
func Test_service_Register(t *testing.T) {
t.Skip()
r := NewRegister()
type args struct {
names []string
}
tests := []struct {
name string
opts []micro.Option
args args
want register.Register
}{
{
name: "service.Register",
opts: []micro.Option{micro.Register(r)},
args: args{
names: []string{"memory"},
},
want: r,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := micro.NewService(tt.opts...)
if got := s.Register(tt.args.names...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("service.Register() = %v, want %v", got, tt.want)
}
})
}
}

72
register/noop.go Normal file
View File

@@ -0,0 +1,72 @@
package register
import "context"
type noop struct {
opts Options
}
func NewRegister(opts ...Option) Register {
return &noop{
opts: NewOptions(opts...),
}
}
func (n *noop) Name() string {
return n.opts.Name
}
func (n *noop) Init(opts ...Option) error {
for _, o := range opts {
o(&n.opts)
}
return nil
}
func (n *noop) Options() Options {
return n.opts
}
func (n *noop) Connect(ctx context.Context) error {
return nil
}
func (n *noop) Disconnect(ctx context.Context) error {
return nil
}
func (n *noop) Register(ctx context.Context, service *Service, option ...RegisterOption) error {
return nil
}
func (n *noop) Deregister(ctx context.Context, service *Service, option ...DeregisterOption) error {
return nil
}
func (n *noop) LookupService(ctx context.Context, s string, option ...LookupOption) ([]*Service, error) {
return nil, nil
}
func (n *noop) ListServices(ctx context.Context, option ...ListOption) ([]*Service, error) {
return nil, nil
}
func (n *noop) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
wOpts := NewWatchOptions(opts...)
return &watcher{wo: wOpts}, nil
}
func (n *noop) String() string {
return "noop"
}
type watcher struct {
wo WatchOptions
}
func (m *watcher) Next() (*Result, error) {
return nil, nil
}
func (m *watcher) Stop() {}

View File

@@ -4,7 +4,6 @@ package register // import "go.unistack.org/micro/v4/register"
import (
"context"
"errors"
"go.unistack.org/micro/v4/metadata"
)

View File

@@ -65,6 +65,8 @@ type Options struct {
DeregisterAttempts int
// Hooks may contains HandleWrapper or Server func wrapper
Hooks options.Hooks
// GracefulTimeout timeout for graceful stop server
GracefulTimeout time.Duration
}
// NewOptions returns new options struct with default or passed values
@@ -84,6 +86,7 @@ func NewOptions(opts ...options.Option) Options {
Name: DefaultName,
Version: DefaultVersion,
ID: id.Must(),
GracefulTimeout: DefaultGracefulTimeout,
}
for _, o := range opts {
@@ -162,6 +165,12 @@ func Listener(nl net.Listener) options.Option {
}
}
func GracefulTimeout(td time.Duration) options.Option {
return func(src interface{}) error {
return options.Set(src, td, ".GracefulTimeout")
}
}
// HandleOptions struct
type HandleOptions struct {
// Context holds external options

View File

@@ -32,6 +32,8 @@ var (
DefaultMaxMsgRecvSize = 1024 * 1024 * 4 // 4Mb
// DefaultMaxMsgSendSize holds default max send size
DefaultMaxMsgSendSize = 1024 * 1024 * 4 // 4Mb
// DefaultGracefulTimeout default time for graceful stop
DefaultGracefulTimeout = 5 * time.Second
)
// Server is a simple micro server abstraction

View File

@@ -372,19 +372,71 @@ func (s *service) Run() error {
return s.Stop()
}
type nameIface interface {
Name() string
}
func getNameIndex(n string, ifaces interface{}) int {
values, ok := ifaces.([]interface{})
if !ok {
return 0
}
for idx, iface := range values {
if ifc, ok := iface.(nameIface); ok && ifc.Name() == n {
return idx
switch values := ifaces.(type) {
case []router.Router:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []register.Register:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []store.Store:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []tracer.Tracer:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []server.Server:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []config.Config:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []meter.Meter:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []broker.Broker:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
case []client.Client:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
/*
case []logger.Logger:
for idx, iface := range values {
if iface.Name() == n {
return idx
}
}
*/
}
return 0
}

View File

@@ -1,6 +1,7 @@
package micro
import (
"go.unistack.org/micro/v4/register/memory"
"reflect"
"testing"
@@ -17,20 +18,18 @@ import (
"go.unistack.org/micro/v4/tracer"
)
type testItem struct {
name string
}
func TestClient(t *testing.T) {
c1 := client.NewClient(options.Name("test1"))
c2 := client.NewClient(options.Name("test2"))
func (ti *testItem) Name() string {
return ti.name
}
svc := NewService(Client(c1, c2))
if err := svc.Init(); err != nil {
t.Fatal(err)
}
func TestGetNameIndex(t *testing.T) {
item1 := &testItem{name: "first"}
item2 := &testItem{name: "second"}
items := []interface{}{item1, item2}
if idx := getNameIndex("second", items); idx != 1 {
t.Fatalf("getNameIndex func error, item not found")
x1 := svc.Client("test2")
if x1.Name() != "test2" {
t.Fatal("invalid client")
}
}
@@ -427,7 +426,7 @@ func Test_service_Store(t *testing.T) {
}
func Test_service_Register(t *testing.T) {
r := register.NewRegister()
r := memory.NewRegister()
type fields struct {
opts Options
}
@@ -446,7 +445,7 @@ func Test_service_Register(t *testing.T) {
opts: Options{Registers: []register.Register{r}},
},
args: args{
names: []string{"noop"},
names: []string{"memory"},
},
want: r,
},

143
tracer/memory/memory.go Normal file
View File

@@ -0,0 +1,143 @@
package memory
import (
"context"
"time"
"go.unistack.org/micro/v4/options"
"go.unistack.org/micro/v4/tracer"
"go.unistack.org/micro/v4/util/id"
)
var _ tracer.Tracer = (*Tracer)(nil)
type Tracer struct {
opts tracer.Options
spans []tracer.Span
}
func (t *Tracer) Spans() []tracer.Span {
return t.spans
}
func (t *Tracer) Start(ctx context.Context, name string, opts ...options.Option) (context.Context, tracer.Span) {
options := tracer.NewSpanOptions(opts...)
span := &Span{
name: name,
ctx: ctx,
tracer: t,
kind: options.Kind,
startTime: time.Now(),
}
span.spanID.s, _ = id.New()
span.traceID.s, _ = id.New()
if span.ctx == nil {
span.ctx = context.Background()
}
t.spans = append(t.spans, span)
return tracer.NewSpanContext(ctx, span), span
}
func (t *Tracer) Flush(_ context.Context) error {
return nil
}
func (t *Tracer) Init(opts ...options.Option) error {
var err error
for _, o := range opts {
if err = o(&t.opts); err != nil {
return err
}
}
return nil
}
func (t *Tracer) Name() string {
return t.opts.Name
}
type noopStringer struct {
s string
}
func (s noopStringer) String() string {
return s.s
}
type Span struct {
ctx context.Context
tracer tracer.Tracer
name string
statusMsg string
startTime time.Time
finishTime time.Time
traceID noopStringer
spanID noopStringer
events []*Event
labels []interface{}
logs []interface{}
kind tracer.SpanKind
status tracer.SpanStatus
}
func (s *Span) Finish(opts ...options.Option) {
s.finishTime = time.Now()
}
func (s *Span) Context() context.Context {
return s.ctx
}
func (s *Span) Tracer() tracer.Tracer {
return s.tracer
}
type Event struct {
name string
labels []interface{}
}
func (s *Span) AddEvent(name string, opts ...options.Option) {
options := tracer.NewEventOptions(opts...)
s.events = append(s.events, &Event{name: name, labels: options.Labels})
}
func (s *Span) SetName(name string) {
s.name = name
}
func (s *Span) AddLogs(kv ...interface{}) {
s.logs = append(s.logs, kv...)
}
func (s *Span) AddLabels(kv ...interface{}) {
s.labels = append(s.labels, kv...)
}
func (s *Span) Kind() tracer.SpanKind {
return s.kind
}
func (s *Span) TraceID() string {
return s.traceID.String()
}
func (s *Span) SpanID() string {
return s.spanID.String()
}
func (s *Span) Status() (tracer.SpanStatus, string) {
return s.status, s.statusMsg
}
func (s *Span) SetStatus(st tracer.SpanStatus, msg string) {
s.status = st
s.statusMsg = msg
}
// NewTracer returns new memory tracer
func NewTracer(opts ...options.Option) *Tracer {
return &Tracer{
opts: tracer.NewOptions(opts...),
}
}

View File

@@ -0,0 +1,38 @@
package memory
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/logger/slog"
"go.unistack.org/micro/v4/tracer"
)
func TestLoggerWithTracer(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
logger.DefaultLogger = slog.NewLogger(logger.WithOutput(buf))
if err := logger.Init(); err != nil {
t.Fatal(err)
}
var span tracer.Span
tr := NewTracer()
ctx, span = tr.Start(ctx, "test1")
logger.Error(ctx, "my test error", fmt.Errorf("error"))
if !strings.Contains(buf.String(), span.TraceID()) {
t.Fatalf("log does not contains trace id: %s", buf.Bytes())
}
_, _ = tr.Start(ctx, "test2")
for _, s := range tr.Spans() {
_ = s
}
}

View File

@@ -24,11 +24,10 @@ func (t *noopTracer) Start(ctx context.Context, name string, opts ...options.Opt
name: name,
ctx: ctx,
tracer: t,
labels: options.Labels,
kind: options.Kind,
}
span.spanID, _ = id.New()
span.traceID, _ = id.New()
span.spanID.s, _ = id.New()
span.traceID.s, _ = id.New()
if span.ctx == nil {
span.ctx = context.Background()
}
@@ -36,7 +35,7 @@ func (t *noopTracer) Start(ctx context.Context, name string, opts ...options.Opt
return NewSpanContext(ctx, span), span
}
func (t *noopTracer) Flush(ctx context.Context) error {
func (t *noopTracer) Flush(_ context.Context) error {
return nil
}
@@ -51,9 +50,12 @@ func (t *noopTracer) Name() string {
return t.opts.Name
}
type noopEvent struct {
name string
labels []interface{}
type noopStringer struct {
s string
}
func (s noopStringer) String() string {
return s.s
}
type noopSpan struct {
@@ -61,16 +63,13 @@ type noopSpan struct {
tracer Tracer
name string
statusMsg string
events []*noopEvent
labels []interface{}
logs []interface{}
traceID noopStringer
spanID noopStringer
kind SpanKind
status SpanStatus
traceID string
spanID string
}
func (s *noopSpan) Finish(opts ...options.Option) {
func (s *noopSpan) Finish(_ ...options.Option) {
}
func (s *noopSpan) Context() context.Context {
@@ -81,21 +80,17 @@ func (s *noopSpan) Tracer() Tracer {
return s.tracer
}
func (s *noopSpan) AddEvent(name string, opts ...options.Option) {
options := NewEventOptions(opts...)
s.events = append(s.events, &noopEvent{name: name, labels: options.Labels})
func (s *noopSpan) AddEvent(_ string, _ ...options.Option) {
}
func (s *noopSpan) SetName(name string) {
s.name = name
}
func (s *noopSpan) AddLogs(kv ...interface{}) {
s.logs = append(s.logs, kv...)
func (s *noopSpan) AddLogs(_ ...interface{}) {
}
func (s *noopSpan) AddLabels(kv ...interface{}) {
s.labels = append(s.labels, kv...)
func (s *noopSpan) AddLabels(_ ...interface{}) {
}
func (s *noopSpan) Kind() SpanKind {
@@ -103,11 +98,11 @@ func (s *noopSpan) Kind() SpanKind {
}
func (s *noopSpan) TraceID() string {
return s.traceID
return s.traceID.String()
}
func (s *noopSpan) SpanID() string {
return s.spanID
return s.spanID.String()
}
func (s *noopSpan) Status() (SpanStatus, string) {

View File

@@ -13,6 +13,26 @@ import (
// DefaultTracer is the global default tracer
var DefaultTracer = NewTracer()
var (
// TraceIDKey is the key used for the trace id in the log call
TraceIDKey = "trace-id"
// SpanIDKey is the key used for the span id in the log call
SpanIDKey = "span-id"
)
func init() {
logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs,
func(ctx context.Context) []interface{} {
if span, ok := SpanFromContext(ctx); ok {
return []interface{}{
TraceIDKey, span.TraceID(),
SpanIDKey, span.SpanID(),
}
}
return nil
})
}
// Tracer is an interface for distributed tracing
type Tracer interface {
// Name return tracer name
@@ -52,16 +72,6 @@ type Span interface {
SpanID() string
}
func init() {
logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, func(ctx context.Context) []interface{} {
span, ok := SpanFromContext(ctx)
if !ok || span == nil {
return nil
}
return []interface{}{"trace", span.TraceID(), "span", span.SpanID()}
})
}
// sort labels alphabeticaly by label name
type byKey []interface{}
@@ -76,6 +86,7 @@ func UniqLabels(labels []interface{}) []interface{} {
if len(labels)%2 == 1 {
labels = labels[:len(labels)-1]
}
if len(labels) > 2 {
sort.Sort(byKey(labels))

28
tracer/tracer_test.go Normal file
View File

@@ -0,0 +1,28 @@
package tracer_test
import (
"bytes"
"context"
"strings"
"testing"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/logger/slog"
"go.unistack.org/micro/v4/tracer"
)
func TestLoggerWithTracer(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
logger.DefaultLogger = slog.NewLogger(logger.WithOutput(buf))
if err := logger.Init(); err != nil {
t.Fatal(err)
}
var span tracer.Span
ctx, span = tracer.DefaultTracer.Start(ctx, "test")
logger.Info(ctx, "msg")
if !strings.Contains(buf.String(), span.TraceID()) {
t.Fatalf("log does not contains tracer id")
}
}