Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
1db017d966 | |||
debf8cb03d | |||
1dc9c1891f | |||
930859a537 | |||
3141f1ed8b | |||
47943cfb05 | |||
ed4e9d54b1 | |||
b4b8583594 | |||
fb43e8c58c | |||
8863c10ef4 | |||
8058095bcc | |||
092f5d96b1 | |||
84552513f7 | |||
80a2db264e | |||
0be09c8b3e | |||
047f479e1b | |||
8f757c953e | |||
5f1c673a24 | |||
6794ea9871 | |||
089e7b6812 | |||
1c703f0f0c | |||
d167c8c67c | |||
df4f96a2d8 | |||
fac3b20bd4 | |||
7c6bd98498 | |||
23e1174f25 | |||
52bed214cf |
@@ -3,6 +3,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v3/broker"
|
"go.unistack.org/micro/v3/broker"
|
||||||
@@ -485,21 +486,34 @@ func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishO
|
|||||||
|
|
||||||
msgs := make([]*broker.Message, 0, len(ps))
|
msgs := make([]*broker.Message, 0, len(ps))
|
||||||
|
|
||||||
|
// get proxy
|
||||||
|
exchange := ""
|
||||||
|
if v, ok := os.LookupEnv("MICRO_PROXY"); ok {
|
||||||
|
exchange = v
|
||||||
|
}
|
||||||
|
// get the exchange
|
||||||
|
if len(options.Exchange) > 0 {
|
||||||
|
exchange = options.Exchange
|
||||||
|
}
|
||||||
|
|
||||||
|
omd, ok := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
omd = metadata.New(0)
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range ps {
|
for _, p := range ps {
|
||||||
md, ok := metadata.FromOutgoingContext(ctx)
|
md := metadata.Copy(omd)
|
||||||
if !ok {
|
|
||||||
md = metadata.New(0)
|
|
||||||
}
|
|
||||||
md[metadata.HeaderContentType] = p.ContentType()
|
md[metadata.HeaderContentType] = p.ContentType()
|
||||||
|
|
||||||
topic := p.Topic()
|
topic := p.Topic()
|
||||||
|
if len(exchange) > 0 {
|
||||||
// get the exchange
|
topic = exchange
|
||||||
if len(options.Exchange) > 0 {
|
|
||||||
topic = options.Exchange
|
|
||||||
}
|
}
|
||||||
|
|
||||||
md[metadata.HeaderTopic] = topic
|
md[metadata.HeaderTopic] = topic
|
||||||
|
iter := p.Metadata().Iterator()
|
||||||
|
var k, v string
|
||||||
|
for iter.Next(&k, &v) {
|
||||||
|
md.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
var body []byte
|
var body []byte
|
||||||
|
|
||||||
|
@@ -39,6 +39,10 @@ func (c *defaultConfig) Init(opts ...Option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
func (c *defaultConfig) Load(ctx context.Context, opts ...LoadOption) error {
|
||||||
|
if c.opts.SkipLoad != nil && c.opts.SkipLoad(ctx, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := DefaultBeforeLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
if err := DefaultBeforeLoad(ctx, c); err != nil && !c.opts.AllowFail {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -291,7 +295,11 @@ func fillValues(valueOf reflect.Value, tname string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *defaultConfig) Save(ctx context.Context, opts ...SaveOption) error {
|
func (c *defaultConfig) Save(ctx context.Context, _ ...SaveOption) error {
|
||||||
|
if c.opts.SkipSave != nil && c.opts.SkipSave(ctx, c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := DefaultBeforeSave(ctx, c); err != nil {
|
if err := DefaultBeforeSave(ctx, c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -42,6 +42,10 @@ type Options struct {
|
|||||||
AfterInit []func(context.Context, Config) error
|
AfterInit []func(context.Context, Config) error
|
||||||
// AllowFail flag to allow fail in config source
|
// AllowFail flag to allow fail in config source
|
||||||
AllowFail bool
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option function signature
|
// Option function signature
|
||||||
@@ -68,9 +72,9 @@ type LoadOption func(o *LoadOptions)
|
|||||||
// LoadOptions struct
|
// LoadOptions struct
|
||||||
type LoadOptions struct {
|
type LoadOptions struct {
|
||||||
Struct interface{}
|
Struct interface{}
|
||||||
|
Context context.Context
|
||||||
Override bool
|
Override bool
|
||||||
Append bool
|
Append bool
|
||||||
Context context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoadOptions create LoadOptions struct with provided opts
|
// NewLoadOptions create LoadOptions struct with provided opts
|
||||||
|
157
database/dsn.go
Normal file
157
database/dsn.go
Normal 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¶mN=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&...¶mN=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
31
database/dsn_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
138
errors/errors.go
138
errors/errors.go
@@ -4,11 +4,17 @@ package errors // import "go.unistack.org/micro/v3/errors"
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -340,3 +346,135 @@ func addslashes(str string) string {
|
|||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type retryableError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retryable returns error that can be retried later
|
||||||
|
func Retryable(err error) error {
|
||||||
|
return &retryableError{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsRetryableFunc func(error) bool
|
||||||
|
|
||||||
|
var (
|
||||||
|
RetrayableOracleErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
errmsg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(errmsg, `ORA-`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `can not assign`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `can't assign`):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetrayablePostgresErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
errmsg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(errmsg, `number of field descriptions must equal number of`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `not a pointer`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `values, but dst struct has only`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `struct doesn't have corresponding row field`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `cannot find field`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `cannot scan`) || strings.Contains(errmsg, `cannot convert`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(errmsg, `failed to connect to`):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetryableMicroErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
switch verr := err.(type) {
|
||||||
|
case *Error:
|
||||||
|
switch verr.Code {
|
||||||
|
case 401, 403, 408, 500, 501, 502, 503, 504:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case *retryableError:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetryableGoErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
switch verr := err.(type) {
|
||||||
|
case interface{ SafeToRetry() bool }:
|
||||||
|
return verr.SafeToRetry()
|
||||||
|
case interface{ Timeout() bool }:
|
||||||
|
return verr.Timeout()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, context.DeadlineExceeded):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, io.ErrClosedPipe), errors.Is(err, io.ErrShortBuffer), errors.Is(err, io.ErrShortWrite):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
RetryableGrpcErrors = []IsRetryableFunc{
|
||||||
|
func(err error) bool {
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch st.Code() {
|
||||||
|
case codes.Unavailable, codes.ResourceExhausted:
|
||||||
|
return true
|
||||||
|
case codes.DeadlineExceeded:
|
||||||
|
return true
|
||||||
|
case codes.Internal:
|
||||||
|
switch {
|
||||||
|
case strings.Contains(st.Message(), `transport: received the unexpected content-type "text/html; charset=UTF-8"`):
|
||||||
|
return true
|
||||||
|
case strings.Contains(st.Message(), io.ErrUnexpectedEOF.Error()):
|
||||||
|
return true
|
||||||
|
case strings.Contains(st.Message(), `stream terminated by RST_STREAM with error code: INTERNAL_ERROR`):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unwrap provides error wrapping
|
||||||
|
func (e *retryableError) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error string
|
||||||
|
func (e *retryableError) Error() string {
|
||||||
|
if e.err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRetryable checks error for ability to retry later
|
||||||
|
func IsRetryable(err error, fns ...IsRetryableFunc) bool {
|
||||||
|
for _, fn := range fns {
|
||||||
|
if ok := fn(err); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@@ -8,6 +8,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestIsRetrayable(t *testing.T) {
|
||||||
|
err := fmt.Errorf("ORA-")
|
||||||
|
if !IsRetryable(err, RetrayableOracleErrors...) {
|
||||||
|
t.Fatalf("IsRetrayable not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalJSON(t *testing.T) {
|
func TestMarshalJSON(t *testing.T) {
|
||||||
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
|
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
|
||||||
_, err := json.Marshal(e)
|
_, err := json.Marshal(e)
|
||||||
|
5
go.mod
5
go.mod
@@ -7,7 +7,7 @@ require (
|
|||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/imdario/mergo v0.3.15
|
github.com/imdario/mergo v0.3.15
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35
|
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.3.0
|
||||||
google.golang.org/grpc v1.57.0
|
google.golang.org/grpc v1.57.0
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
@@ -15,5 +15,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
|
golang.org/x/net v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e // indirect
|
||||||
)
|
)
|
||||||
|
15
go.sum
15
go.sum
@@ -11,16 +11,17 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
|||||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35 h1:4mohWoM/UGg1BvFFiqSPRl5uwJY3rVV0HQX0ETqauqQ=
|
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
|
||||||
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||||
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e h1:NumxXLPfHSndr3wBBdeKiVHjGVFzi9RX2HwwQke94iY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
@@ -1,230 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type defaultLogger struct {
|
|
||||||
enc *json.Encoder
|
|
||||||
opts Options
|
|
||||||
sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init(opts...) should only overwrite provided options
|
|
||||||
func (l *defaultLogger) Init(opts ...Option) error {
|
|
||||||
l.Lock()
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&l.opts)
|
|
||||||
}
|
|
||||||
l.enc = json.NewEncoder(l.opts.Out)
|
|
||||||
// wrap the Log func
|
|
||||||
l.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) String() string {
|
|
||||||
return "micro"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Clone(opts ...Option) Logger {
|
|
||||||
newopts := NewOptions(opts...)
|
|
||||||
oldopts := l.opts
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&newopts)
|
|
||||||
o(&oldopts)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Lock()
|
|
||||||
cl := &defaultLogger{opts: oldopts, enc: json.NewEncoder(l.opts.Out)}
|
|
||||||
l.Unlock()
|
|
||||||
|
|
||||||
return cl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) V(level Level) bool {
|
|
||||||
l.RLock()
|
|
||||||
ok := l.opts.Level.Enabled(level)
|
|
||||||
l.RUnlock()
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Level(level Level) {
|
|
||||||
l.Lock()
|
|
||||||
l.opts.Level = level
|
|
||||||
l.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Fields(fields ...interface{}) Logger {
|
|
||||||
l.RLock()
|
|
||||||
nl := &defaultLogger{opts: l.opts, enc: l.enc}
|
|
||||||
if len(fields) == 0 {
|
|
||||||
l.RUnlock()
|
|
||||||
return nl
|
|
||||||
} else if len(fields)%2 != 0 {
|
|
||||||
fields = fields[:len(fields)-1]
|
|
||||||
}
|
|
||||||
nl.opts.Fields = copyFields(l.opts.Fields)
|
|
||||||
nl.opts.Fields = append(nl.opts.Fields, fields...)
|
|
||||||
l.RUnlock()
|
|
||||||
return nl
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFields(src []interface{}) []interface{} {
|
|
||||||
dst := make([]interface{}, len(src))
|
|
||||||
copy(dst, src)
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// logCallerfilePath returns a package/file:line description of the caller,
|
|
||||||
// preserving only the leaf directory name and file name.
|
|
||||||
func logCallerfilePath(loggingFilePath string) string {
|
|
||||||
// To make sure we trim the path correctly on Windows too, we
|
|
||||||
// counter-intuitively need to use '/' and *not* os.PathSeparator here,
|
|
||||||
// because the path given originates from Go stdlib, specifically
|
|
||||||
// runtime.Caller() which (as of Mar/17) returns forward slashes even on
|
|
||||||
// Windows.
|
|
||||||
//
|
|
||||||
// See https://github.com/golang/go/issues/3335
|
|
||||||
// and https://github.com/golang/go/issues/18151
|
|
||||||
//
|
|
||||||
// for discussion on the issue on Go side.
|
|
||||||
idx := strings.LastIndexByte(loggingFilePath, '/')
|
|
||||||
if idx == -1 {
|
|
||||||
return loggingFilePath
|
|
||||||
}
|
|
||||||
idx = strings.LastIndexByte(loggingFilePath[:idx], '/')
|
|
||||||
if idx == -1 {
|
|
||||||
return loggingFilePath
|
|
||||||
}
|
|
||||||
return loggingFilePath[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Info(ctx context.Context, args ...interface{}) {
|
|
||||||
l.Log(ctx, InfoLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Error(ctx context.Context, args ...interface{}) {
|
|
||||||
l.Log(ctx, ErrorLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Debug(ctx context.Context, args ...interface{}) {
|
|
||||||
l.Log(ctx, DebugLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Warn(ctx context.Context, args ...interface{}) {
|
|
||||||
l.Log(ctx, WarnLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Trace(ctx context.Context, args ...interface{}) {
|
|
||||||
l.Log(ctx, TraceLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Fatal(ctx context.Context, args ...interface{}) {
|
|
||||||
l.Log(ctx, FatalLevel, args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Infof(ctx context.Context, msg string, args ...interface{}) {
|
|
||||||
l.Logf(ctx, InfoLevel, msg, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Errorf(ctx context.Context, msg string, args ...interface{}) {
|
|
||||||
l.Logf(ctx, ErrorLevel, msg, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Debugf(ctx context.Context, msg string, args ...interface{}) {
|
|
||||||
l.Logf(ctx, DebugLevel, msg, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Warnf(ctx context.Context, msg string, args ...interface{}) {
|
|
||||||
l.Logf(ctx, WarnLevel, msg, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Tracef(ctx context.Context, msg string, args ...interface{}) {
|
|
||||||
l.Logf(ctx, TraceLevel, msg, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) {
|
|
||||||
l.Logf(ctx, FatalLevel, msg, args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Log(ctx context.Context, level Level, args ...interface{}) {
|
|
||||||
if !l.V(level) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l.RLock()
|
|
||||||
fields := copyFields(l.opts.Fields)
|
|
||||||
l.RUnlock()
|
|
||||||
|
|
||||||
fields = append(fields, "level", level.String())
|
|
||||||
|
|
||||||
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
|
|
||||||
fields = append(fields, "caller", fmt.Sprintf("%s:%d", logCallerfilePath(file), line))
|
|
||||||
}
|
|
||||||
fields = append(fields, "timestamp", time.Now().Format("2006-01-02 15:04:05"))
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
fields = append(fields, "msg", fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make(map[string]interface{}, len(fields)/2)
|
|
||||||
for i := 0; i < len(fields); i += 2 {
|
|
||||||
out[fields[i].(string)] = fields[i+1]
|
|
||||||
}
|
|
||||||
l.RLock()
|
|
||||||
_ = l.enc.Encode(out)
|
|
||||||
l.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Logf(ctx context.Context, level Level, msg string, args ...interface{}) {
|
|
||||||
if !l.V(level) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l.RLock()
|
|
||||||
fields := copyFields(l.opts.Fields)
|
|
||||||
l.RUnlock()
|
|
||||||
|
|
||||||
fields = append(fields, "level", level.String())
|
|
||||||
|
|
||||||
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
|
|
||||||
fields = append(fields, "caller", fmt.Sprintf("%s:%d", logCallerfilePath(file), line))
|
|
||||||
}
|
|
||||||
|
|
||||||
fields = append(fields, "timestamp", time.Now().Format("2006-01-02 15:04:05"))
|
|
||||||
if len(args) > 0 {
|
|
||||||
fields = append(fields, "msg", fmt.Sprintf(msg, args...))
|
|
||||||
} else if msg != "" {
|
|
||||||
fields = append(fields, "msg", msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make(map[string]interface{}, len(fields)/2)
|
|
||||||
for i := 0; i < len(fields); i += 2 {
|
|
||||||
out[fields[i].(string)] = fields[i+1]
|
|
||||||
}
|
|
||||||
l.RLock()
|
|
||||||
_ = l.enc.Encode(out)
|
|
||||||
l.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *defaultLogger) Options() Options {
|
|
||||||
return l.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLogger builds a new logger based on options
|
|
||||||
func NewLogger(opts ...Option) Logger {
|
|
||||||
l := &defaultLogger{
|
|
||||||
opts: NewOptions(opts...),
|
|
||||||
}
|
|
||||||
l.enc = json.NewEncoder(l.opts.Out)
|
|
||||||
return l
|
|
||||||
}
|
|
90
logger/noop.go
Normal file
90
logger/noop.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type noopLogger struct {
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(opts ...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 ...Option) error {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&l.opts)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Clone(opts ...Option) Logger {
|
||||||
|
nl := &noopLogger{opts: l.opts}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&nl.opts)
|
||||||
|
}
|
||||||
|
return nl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Fields(attrs ...interface{}) Logger {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Options() Options {
|
||||||
|
return l.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) String() string {
|
||||||
|
return "noop"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Log(ctx context.Context, lvl Level, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Info(ctx context.Context, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Debug(ctx context.Context, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Error(ctx context.Context, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Trace(ctx context.Context, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Warn(ctx context.Context, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Fatal(ctx context.Context, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Logf(ctx context.Context, lvl Level, msg string, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Infof(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Debugf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Errorf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Tracef(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Warnf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *noopLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
}
|
27
logger/slog/options.go
Normal file
27
logger/slog/options.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package slog
|
||||||
|
|
||||||
|
import "go.unistack.org/micro/v3/logger"
|
||||||
|
|
||||||
|
type sourceKey struct{}
|
||||||
|
|
||||||
|
func WithSourceKey(v string) logger.Option {
|
||||||
|
return logger.SetOption(sourceKey{}, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type timeKey struct{}
|
||||||
|
|
||||||
|
func WithTimeKey(v string) logger.Option {
|
||||||
|
return logger.SetOption(timeKey{}, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageKey struct{}
|
||||||
|
|
||||||
|
func WithMessageKey(v string) logger.Option {
|
||||||
|
return logger.SetOption(messageKey{}, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type levelKey struct{}
|
||||||
|
|
||||||
|
func WithLevelKey(v string) logger.Option {
|
||||||
|
return logger.SetOption(levelKey{}, v)
|
||||||
|
}
|
419
logger/slog/slog.go
Normal file
419
logger/slog/slog.go
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
package slog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/logger"
|
||||||
|
"go.unistack.org/micro/v3/tracer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultSourceKey string = slog.SourceKey
|
||||||
|
DefaultTimeKey string = slog.TimeKey
|
||||||
|
DefaultMessageKey string = slog.MessageKey
|
||||||
|
DefaultLevelKey string = slog.LevelKey
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
traceValue = slog.StringValue("trace")
|
||||||
|
debugValue = slog.StringValue("debug")
|
||||||
|
infoValue = slog.StringValue("info")
|
||||||
|
warnValue = slog.StringValue("warn")
|
||||||
|
errorValue = slog.StringValue("error")
|
||||||
|
fatalValue = slog.StringValue("fatal")
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = s.sourceKey
|
||||||
|
case slog.TimeKey:
|
||||||
|
a.Key = s.timeKey
|
||||||
|
case slog.MessageKey:
|
||||||
|
a.Key = s.messageKey
|
||||||
|
case slog.LevelKey:
|
||||||
|
level := a.Value.Any().(slog.Level)
|
||||||
|
lvl := slogToLoggerLevel(level)
|
||||||
|
a.Key = s.levelKey
|
||||||
|
switch {
|
||||||
|
case lvl < logger.DebugLevel:
|
||||||
|
a.Value = traceValue
|
||||||
|
case lvl < logger.InfoLevel:
|
||||||
|
a.Value = debugValue
|
||||||
|
case lvl < logger.WarnLevel:
|
||||||
|
a.Value = infoValue
|
||||||
|
case lvl < logger.ErrorLevel:
|
||||||
|
a.Value = warnValue
|
||||||
|
case lvl < logger.FatalLevel:
|
||||||
|
a.Value = errorValue
|
||||||
|
case lvl >= logger.FatalLevel:
|
||||||
|
a.Value = fatalValue
|
||||||
|
default:
|
||||||
|
a.Value = infoValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
type slogLogger struct {
|
||||||
|
slog *slog.Logger
|
||||||
|
leveler *slog.LevelVar
|
||||||
|
levelKey string
|
||||||
|
messageKey string
|
||||||
|
sourceKey string
|
||||||
|
timeKey string
|
||||||
|
opts logger.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Clone(opts ...logger.Option) logger.Logger {
|
||||||
|
options := s.opts
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &slogLogger{
|
||||||
|
opts: options,
|
||||||
|
levelKey: s.levelKey,
|
||||||
|
messageKey: s.messageKey,
|
||||||
|
sourceKey: s.sourceKey,
|
||||||
|
timeKey: s.timeKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := l.opts.Context.Value(levelKey{}).(string); ok && v != "" {
|
||||||
|
l.levelKey = v
|
||||||
|
}
|
||||||
|
if v, ok := l.opts.Context.Value(messageKey{}).(string); ok && v != "" {
|
||||||
|
l.messageKey = v
|
||||||
|
}
|
||||||
|
if v, ok := l.opts.Context.Value(sourceKey{}).(string); ok && v != "" {
|
||||||
|
l.sourceKey = v
|
||||||
|
}
|
||||||
|
if v, ok := l.opts.Context.Value(timeKey{}).(string); ok && v != "" {
|
||||||
|
l.timeKey = v
|
||||||
|
}
|
||||||
|
|
||||||
|
l.leveler = new(slog.LevelVar)
|
||||||
|
handleOpt := &slog.HandlerOptions{
|
||||||
|
ReplaceAttr: s.renameAttr,
|
||||||
|
Level: l.leveler,
|
||||||
|
AddSource: true,
|
||||||
|
}
|
||||||
|
l.leveler.Set(loggerToSlogLevel(l.opts.Level))
|
||||||
|
handler := slog.NewJSONHandler(options.Out, handleOpt)
|
||||||
|
l.slog = slog.New(handler).With(options.Fields...)
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) V(level logger.Level) bool {
|
||||||
|
return s.opts.Level.Enabled(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Level(level logger.Level) {
|
||||||
|
s.leveler.Set(loggerToSlogLevel(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Options() logger.Options {
|
||||||
|
return s.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Fields(attrs ...interface{}) logger.Logger {
|
||||||
|
nl := &slogLogger{
|
||||||
|
opts: s.opts,
|
||||||
|
levelKey: s.levelKey,
|
||||||
|
messageKey: s.messageKey,
|
||||||
|
sourceKey: s.sourceKey,
|
||||||
|
timeKey: s.timeKey,
|
||||||
|
}
|
||||||
|
nl.leveler = new(slog.LevelVar)
|
||||||
|
nl.leveler.Set(s.leveler.Level())
|
||||||
|
|
||||||
|
handleOpt := &slog.HandlerOptions{
|
||||||
|
ReplaceAttr: nl.renameAttr,
|
||||||
|
Level: s.leveler,
|
||||||
|
AddSource: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := slog.NewJSONHandler(s.opts.Out, handleOpt)
|
||||||
|
nl.slog = slog.New(handler).With(attrs...)
|
||||||
|
|
||||||
|
return nl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Init(opts ...logger.Option) error {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&s.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := s.opts.Context.Value(levelKey{}).(string); ok && v != "" {
|
||||||
|
s.levelKey = v
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(messageKey{}).(string); ok && v != "" {
|
||||||
|
s.messageKey = v
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(sourceKey{}).(string); ok && v != "" {
|
||||||
|
s.sourceKey = v
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(timeKey{}).(string); ok && v != "" {
|
||||||
|
s.timeKey = v
|
||||||
|
}
|
||||||
|
|
||||||
|
s.leveler = new(slog.LevelVar)
|
||||||
|
handleOpt := &slog.HandlerOptions{
|
||||||
|
ReplaceAttr: s.renameAttr,
|
||||||
|
Level: s.leveler,
|
||||||
|
AddSource: true,
|
||||||
|
}
|
||||||
|
s.leveler.Set(loggerToSlogLevel(s.opts.Level))
|
||||||
|
handler := slog.NewJSONHandler(s.opts.Out, handleOpt)
|
||||||
|
s.slog = slog.New(handler).With(s.opts.Fields...)
|
||||||
|
|
||||||
|
slog.SetDefault(s.slog)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Log(ctx context.Context, lvl logger.Level, attrs ...interface{}) {
|
||||||
|
if !s.V(lvl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var pcs [1]uintptr
|
||||||
|
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
|
||||||
|
r := slog.NewRecord(time.Now(), loggerToSlogLevel(lvl), fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Logf(ctx context.Context, lvl logger.Level, msg string, attrs ...interface{}) {
|
||||||
|
if !s.V(lvl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var pcs [1]uintptr
|
||||||
|
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
|
||||||
|
r := slog.NewRecord(time.Now(), loggerToSlogLevel(lvl), fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Info(ctx context.Context, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Infof(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Debug(ctx context.Context, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Debugf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Trace(ctx context.Context, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Tracef(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Error(ctx context.Context, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
|
if a.Key == "error" {
|
||||||
|
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) Errorf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
|
if a.Key == "error" {
|
||||||
|
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, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Warn(ctx context.Context, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf("%s", attrs[0]), pcs[0])
|
||||||
|
// r.Add(attrs[1:]...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) Warnf(ctx context.Context, msg string, attrs ...interface{}) {
|
||||||
|
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, fmt.Sprintf(msg, attrs...), pcs[0])
|
||||||
|
// r.Add(attrs...)
|
||||||
|
_ = s.slog.Handler().Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slogLogger) String() string {
|
||||||
|
return "slog"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(opts ...logger.Option) logger.Logger {
|
||||||
|
s := &slogLogger{
|
||||||
|
opts: logger.NewOptions(opts...),
|
||||||
|
sourceKey: DefaultSourceKey,
|
||||||
|
timeKey: DefaultTimeKey,
|
||||||
|
messageKey: DefaultMessageKey,
|
||||||
|
levelKey: DefaultLevelKey,
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(levelKey{}).(string); ok && v != "" {
|
||||||
|
s.levelKey = v
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(messageKey{}).(string); ok && v != "" {
|
||||||
|
s.messageKey = v
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(sourceKey{}).(string); ok && v != "" {
|
||||||
|
s.sourceKey = v
|
||||||
|
}
|
||||||
|
if v, ok := s.opts.Context.Value(timeKey{}).(string); ok && v != "" {
|
||||||
|
s.timeKey = v
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggerToSlogLevel(level logger.Level) slog.Level {
|
||||||
|
switch level {
|
||||||
|
case logger.DebugLevel:
|
||||||
|
return slog.LevelDebug
|
||||||
|
case logger.WarnLevel:
|
||||||
|
return slog.LevelWarn
|
||||||
|
case logger.ErrorLevel:
|
||||||
|
return slog.LevelError
|
||||||
|
case logger.TraceLevel:
|
||||||
|
return slog.LevelDebug - 1
|
||||||
|
case logger.FatalLevel:
|
||||||
|
return slog.LevelError + 1
|
||||||
|
default:
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func slogToLoggerLevel(level slog.Level) logger.Level {
|
||||||
|
switch level {
|
||||||
|
case slog.LevelDebug:
|
||||||
|
return logger.DebugLevel
|
||||||
|
case slog.LevelWarn:
|
||||||
|
return logger.WarnLevel
|
||||||
|
case slog.LevelError:
|
||||||
|
return logger.ErrorLevel
|
||||||
|
case slog.LevelDebug - 1:
|
||||||
|
return logger.TraceLevel
|
||||||
|
case slog.LevelError + 1:
|
||||||
|
return logger.FatalLevel
|
||||||
|
default:
|
||||||
|
return logger.InfoLevel
|
||||||
|
}
|
||||||
|
}
|
141
logger/slog/slog_test.go
Normal file
141
logger/slog/slog_test.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package slog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContext(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nl, ok := logger.FromContext(logger.NewContext(ctx, l.Fields("key", "val")))
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("context without logger")
|
||||||
|
}
|
||||||
|
nl.Info(ctx, "message")
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) {
|
||||||
|
t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFields(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nl := l.Fields("key", "val")
|
||||||
|
|
||||||
|
nl.Info(ctx, "message")
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) {
|
||||||
|
t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromContextWithFields(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
var ok bool
|
||||||
|
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nl := l.Fields("key", "val")
|
||||||
|
|
||||||
|
ctx = logger.NewContext(ctx, nl)
|
||||||
|
|
||||||
|
l, ok = logger.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("context does not have logger")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info(ctx, "message")
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) {
|
||||||
|
t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClone(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nl := l.Clone(logger.WithLevel(logger.ErrorLevel))
|
||||||
|
if err := nl.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nl.Info(ctx, "info message")
|
||||||
|
if len(buf.Bytes()) != 0 {
|
||||||
|
t.Fatal("message must not be logged")
|
||||||
|
}
|
||||||
|
l.Info(ctx, "info message")
|
||||||
|
if len(buf.Bytes()) == 0 {
|
||||||
|
t.Fatal("message must be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirectStdLogger(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
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"`))) {
|
||||||
|
t.Fatalf("logger error, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdLogger(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf))
|
||||||
|
if err := l.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
l.Trace(ctx, "trace_msg1")
|
||||||
|
l.Warn(ctx, "warn_msg1")
|
||||||
|
l.Fields("error", "test").Info(ctx, "error message")
|
||||||
|
l.Warn(ctx, "first second")
|
||||||
|
|
||||||
|
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"trace"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"trace_msg1"`))) {
|
||||||
|
t.Fatalf("logger tracer, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"warn"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"warn_msg1"`))) {
|
||||||
|
t.Fatalf("logger warn, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"info"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"error message","error":"test"`))) {
|
||||||
|
t.Fatalf("logger info, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
if !(bytes.Contains(buf.Bytes(), []byte(`"level":"warn"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"first second"`))) {
|
||||||
|
t.Fatalf("logger warn, buf %s", buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
@@ -342,9 +342,6 @@ func (n *noopServer) createSubHandler(sb *subscriber, opts Options) broker.Handl
|
|||||||
|
|
||||||
hdr := metadata.New(len(msg.Header))
|
hdr := metadata.New(len(msg.Header))
|
||||||
for k, v := range msg.Header {
|
for k, v := range msg.Header {
|
||||||
if k == "Content-Type" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hdr.Set(k, v)
|
hdr.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
75
service.go
75
service.go
@@ -88,6 +88,7 @@ func (s *service) Name() string {
|
|||||||
// Init initialises options. Additionally it calls cmd.Init
|
// Init initialises options. Additionally it calls cmd.Init
|
||||||
// which parses command line flags. cmd.Init is only called
|
// which parses command line flags. cmd.Init is only called
|
||||||
// on first Init.
|
// on first Init.
|
||||||
|
//
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (s *service) Init(opts ...Option) error {
|
func (s *service) Init(opts ...Option) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -375,19 +376,71 @@ func (s *service) Run() error {
|
|||||||
return s.Stop()
|
return s.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
type nameIface interface {
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNameIndex(n string, ifaces interface{}) int {
|
func getNameIndex(n string, ifaces interface{}) int {
|
||||||
values, ok := ifaces.([]interface{})
|
switch values := ifaces.(type) {
|
||||||
if !ok {
|
case []router.Router:
|
||||||
return 0
|
for idx, iface := range values {
|
||||||
}
|
if iface.Name() == n {
|
||||||
for idx, iface := range values {
|
return idx
|
||||||
if ifc, ok := iface.(nameIface); ok && ifc.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
|
return 0
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,21 @@ import (
|
|||||||
"go.unistack.org/micro/v3/tracer"
|
"go.unistack.org/micro/v3/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestClient(t *testing.T) {
|
||||||
|
c1 := client.NewClient(client.Name("test1"))
|
||||||
|
c2 := client.NewClient(client.Name("test2"))
|
||||||
|
|
||||||
|
svc := NewService(Client(c1, c2))
|
||||||
|
if err := svc.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
x1 := svc.Client("test2")
|
||||||
|
if x1.Name() != "test2" {
|
||||||
|
t.Fatal("invalid client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testItem struct {
|
type testItem struct {
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
@@ -7,19 +7,27 @@ import (
|
|||||||
var _ Tracer = (*noopTracer)(nil)
|
var _ Tracer = (*noopTracer)(nil)
|
||||||
|
|
||||||
type noopTracer struct {
|
type noopTracer struct {
|
||||||
opts Options
|
opts Options
|
||||||
|
spans []Span
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) Spans() []Span {
|
||||||
|
return t.spans
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *noopTracer) Start(ctx context.Context, name string, opts ...SpanOption) (context.Context, Span) {
|
func (t *noopTracer) Start(ctx context.Context, name string, opts ...SpanOption) (context.Context, Span) {
|
||||||
|
options := NewSpanOptions(opts...)
|
||||||
span := &noopSpan{
|
span := &noopSpan{
|
||||||
name: name,
|
name: name,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
tracer: t,
|
tracer: t,
|
||||||
opts: NewSpanOptions(opts...),
|
labels: options.Labels,
|
||||||
|
kind: options.Kind,
|
||||||
}
|
}
|
||||||
if span.ctx == nil {
|
if span.ctx == nil {
|
||||||
span.ctx = context.Background()
|
span.ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
t.spans = append(t.spans, span)
|
||||||
return NewSpanContext(ctx, span), span
|
return NewSpanContext(ctx, span), span
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,13 +46,21 @@ func (t *noopTracer) Name() string {
|
|||||||
return t.opts.Name
|
return t.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type noopEvent struct {
|
||||||
|
name string
|
||||||
|
labels []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
type noopSpan struct {
|
type noopSpan struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
tracer Tracer
|
tracer Tracer
|
||||||
name string
|
name string
|
||||||
opts SpanOptions
|
|
||||||
status SpanStatus
|
|
||||||
statusMsg string
|
statusMsg string
|
||||||
|
events []*noopEvent
|
||||||
|
labels []interface{}
|
||||||
|
logs []interface{}
|
||||||
|
kind SpanKind
|
||||||
|
status SpanStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) Finish(opts ...SpanOption) {
|
func (s *noopSpan) Finish(opts ...SpanOption) {
|
||||||
@@ -59,22 +75,24 @@ func (s *noopSpan) Tracer() Tracer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) AddEvent(name string, opts ...EventOption) {
|
func (s *noopSpan) AddEvent(name string, opts ...EventOption) {
|
||||||
|
options := NewEventOptions(opts...)
|
||||||
|
s.events = append(s.events, &noopEvent{name: name, labels: options.Labels})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) SetName(name string) {
|
func (s *noopSpan) SetName(name string) {
|
||||||
s.name = name
|
s.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) SetLabels(labels ...interface{}) {
|
func (s *noopSpan) AddLogs(kv ...interface{}) {
|
||||||
s.opts.Labels = labels
|
s.logs = append(s.logs, kv...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) AddLabels(labels ...interface{}) {
|
func (s *noopSpan) AddLabels(kv ...interface{}) {
|
||||||
s.opts.Labels = append(s.opts.Labels, labels...)
|
s.labels = append(s.labels, kv...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) Kind() SpanKind {
|
func (s *noopSpan) Kind() SpanKind {
|
||||||
return s.opts.Kind
|
return s.kind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *noopSpan) Status() (SpanStatus, string) {
|
func (s *noopSpan) Status() (SpanStatus, string) {
|
||||||
|
@@ -98,15 +98,15 @@ type EventOptions struct {
|
|||||||
// EventOption func signature
|
// EventOption func signature
|
||||||
type EventOption func(o *EventOptions)
|
type EventOption func(o *EventOptions)
|
||||||
|
|
||||||
func WithEventLabels(labels ...interface{}) EventOption {
|
func WithEventLabels(kv ...interface{}) EventOption {
|
||||||
return func(o *EventOptions) {
|
return func(o *EventOptions) {
|
||||||
o.Labels = labels
|
o.Labels = kv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithSpanLabels(labels ...interface{}) SpanOption {
|
func WithSpanLabels(kv ...interface{}) SpanOption {
|
||||||
return func(o *SpanOptions) {
|
return func(o *SpanOptions) {
|
||||||
o.Labels = labels
|
o.Labels = kv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +136,15 @@ func Logger(l logger.Logger) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewEventOptions returns default EventOptions
|
||||||
|
func NewEventOptions(opts ...EventOption) EventOptions {
|
||||||
|
options := EventOptions{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
// NewSpanOptions returns default SpanOptions
|
// NewSpanOptions returns default SpanOptions
|
||||||
func NewSpanOptions(opts ...SpanOption) SpanOptions {
|
func NewSpanOptions(opts ...SpanOption) SpanOptions {
|
||||||
options := SpanOptions{
|
options := SpanOptions{
|
||||||
|
@@ -27,8 +27,6 @@ type Span interface {
|
|||||||
Tracer() Tracer
|
Tracer() Tracer
|
||||||
// Finish complete and send span
|
// Finish complete and send span
|
||||||
Finish(opts ...SpanOption)
|
Finish(opts ...SpanOption)
|
||||||
// AddEvent add event to span
|
|
||||||
AddEvent(name string, opts ...EventOption)
|
|
||||||
// Context return context with span
|
// Context return context with span
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
// SetName set the span name
|
// SetName set the span name
|
||||||
@@ -37,10 +35,12 @@ type Span interface {
|
|||||||
SetStatus(status SpanStatus, msg string)
|
SetStatus(status SpanStatus, msg string)
|
||||||
// Status returns span status and msg
|
// Status returns span status and msg
|
||||||
Status() (SpanStatus, string)
|
Status() (SpanStatus, string)
|
||||||
// SetLabels set the span labels
|
// AddLabels append labels to span
|
||||||
SetLabels(labels ...interface{})
|
AddLabels(kv ...interface{})
|
||||||
// AddLabels append the span labels
|
// AddEvent append event to span
|
||||||
AddLabels(labels ...interface{})
|
AddEvent(name string, opts ...EventOption)
|
||||||
|
// AddEvent append event to span
|
||||||
|
AddLogs(kv ...interface{})
|
||||||
// Kind returns span kind
|
// Kind returns span kind
|
||||||
Kind() SpanKind
|
Kind() SpanKind
|
||||||
}
|
}
|
||||||
|
271
util/grpc/tracer.go
Normal file
271
util/grpc/tracer.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package grpc_util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/tracer"
|
||||||
|
grpc_codes "google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/peer"
|
||||||
|
"google.golang.org/grpc/stats"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gRPCContextKey struct{}
|
||||||
|
|
||||||
|
type gRPCContext struct {
|
||||||
|
messagesReceived int64
|
||||||
|
messagesSent int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Tracer tracer.Tracer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
func Tracer(tr tracer.Tracer) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Tracer = tr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServerHandler creates a stats.Handler for gRPC server.
|
||||||
|
func NewServerHandler(opts ...Option) stats.Handler {
|
||||||
|
options := Options{Tracer: tracer.DefaultTracer}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
h := &serverHandler{
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverHandler struct {
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagRPC can attach some information to the given context.
|
||||||
|
func (h *serverHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
|
||||||
|
name, attrs := parseFullMethod(info.FullMethodName)
|
||||||
|
attrs = append(attrs, "rpc.system", "grpc")
|
||||||
|
ctx, _ = h.opts.Tracer.Start(
|
||||||
|
ctx,
|
||||||
|
name,
|
||||||
|
tracer.WithSpanKind(tracer.SpanKindServer),
|
||||||
|
tracer.WithSpanLabels(attrs...),
|
||||||
|
)
|
||||||
|
|
||||||
|
gctx := gRPCContext{}
|
||||||
|
return context.WithValue(ctx, gRPCContextKey{}, &gctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRPC processes the RPC stats.
|
||||||
|
func (h *serverHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
|
||||||
|
handleRPC(ctx, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagConn can attach some information to the given context.
|
||||||
|
func (h *serverHandler) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
|
||||||
|
if span, ok := tracer.SpanFromContext(ctx); ok {
|
||||||
|
attrs := peerAttr(peerFromCtx(ctx))
|
||||||
|
span.AddLabels(attrs...)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleConn processes the Conn stats.
|
||||||
|
func (h *serverHandler) HandleConn(ctx context.Context, info stats.ConnStats) {
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientHandler struct {
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientHandler creates a stats.Handler for gRPC client.
|
||||||
|
func NewClientHandler(opts ...Option) stats.Handler {
|
||||||
|
options := Options{Tracer: tracer.DefaultTracer}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
h := &clientHandler{
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagRPC can attach some information to the given context.
|
||||||
|
func (h *clientHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
|
||||||
|
name, attrs := parseFullMethod(info.FullMethodName)
|
||||||
|
attrs = append(attrs, "rpc.system", "grpc", "rpc.flavor", "grpc", "rpc.call", info.FullMethodName)
|
||||||
|
ctx, _ = h.opts.Tracer.Start(
|
||||||
|
ctx,
|
||||||
|
name,
|
||||||
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
||||||
|
tracer.WithSpanLabels(attrs...),
|
||||||
|
)
|
||||||
|
|
||||||
|
gctx := gRPCContext{}
|
||||||
|
|
||||||
|
return context.WithValue(ctx, gRPCContextKey{}, &gctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRPC processes the RPC stats.
|
||||||
|
func (h *clientHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
|
||||||
|
handleRPC(ctx, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagConn can attach some information to the given context.
|
||||||
|
func (h *clientHandler) TagConn(ctx context.Context, cti *stats.ConnTagInfo) context.Context {
|
||||||
|
// TODO
|
||||||
|
if span, ok := tracer.SpanFromContext(ctx); ok {
|
||||||
|
attrs := peerAttr(cti.RemoteAddr.String())
|
||||||
|
span.AddLabels(attrs...)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleConn processes the Conn stats.
|
||||||
|
func (h *clientHandler) HandleConn(context.Context, stats.ConnStats) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRPC(ctx context.Context, rs stats.RPCStats) {
|
||||||
|
span, ok := tracer.SpanFromContext(ctx)
|
||||||
|
gctx, _ := ctx.Value(gRPCContextKey{}).(*gRPCContext)
|
||||||
|
var messageID int64
|
||||||
|
if rs.IsClient() {
|
||||||
|
span.AddLabels("span.kind", "client")
|
||||||
|
} else {
|
||||||
|
span.AddLabels("span.kind", "server")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rs := rs.(type) {
|
||||||
|
case *stats.Begin:
|
||||||
|
if rs.IsClientStream || rs.IsServerStream {
|
||||||
|
span.AddLabels("rpc.call_type", "stream")
|
||||||
|
} else {
|
||||||
|
span.AddLabels("rpc.call_type", "unary")
|
||||||
|
}
|
||||||
|
span.AddEvent("message",
|
||||||
|
tracer.WithEventLabels(
|
||||||
|
"message.begin_time", rs.BeginTime.Format(time.RFC3339),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
case *stats.InPayload:
|
||||||
|
if gctx != nil {
|
||||||
|
messageID = atomic.AddInt64(&gctx.messagesReceived, 1)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
span.AddEvent("message",
|
||||||
|
tracer.WithEventLabels(
|
||||||
|
"message.recv_time", rs.RecvTime.Format(time.RFC3339),
|
||||||
|
"message.type", "RECEIVED",
|
||||||
|
"message.id", messageID,
|
||||||
|
"message.compressed_size", rs.CompressedLength,
|
||||||
|
"message.uncompressed_size", rs.Length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case *stats.OutPayload:
|
||||||
|
if gctx != nil {
|
||||||
|
messageID = atomic.AddInt64(&gctx.messagesSent, 1)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
span.AddEvent("message",
|
||||||
|
tracer.WithEventLabels(
|
||||||
|
"message.sent_time", rs.SentTime.Format(time.RFC3339),
|
||||||
|
"message.type", "SENT",
|
||||||
|
"message.id", messageID,
|
||||||
|
"message.compressed_size", rs.CompressedLength,
|
||||||
|
"message.uncompressed_size", rs.Length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case *stats.End:
|
||||||
|
if ok {
|
||||||
|
span.AddEvent("message",
|
||||||
|
tracer.WithEventLabels(
|
||||||
|
"message.begin_time", rs.BeginTime.Format(time.RFC3339),
|
||||||
|
"message.end_time", rs.EndTime.Format(time.RFC3339),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if rs.Error != nil {
|
||||||
|
s, _ := status.FromError(rs.Error)
|
||||||
|
span.SetStatus(tracer.SpanStatusError, s.Message())
|
||||||
|
span.AddLabels("rpc.grpc.status_code", s.Code())
|
||||||
|
} else {
|
||||||
|
span.AddLabels("rpc.grpc.status_code", grpc_codes.OK)
|
||||||
|
}
|
||||||
|
span.Finish()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFullMethod(fullMethod string) (string, []interface{}) {
|
||||||
|
if !strings.HasPrefix(fullMethod, "/") {
|
||||||
|
// Invalid format, does not follow `/package.service/method`.
|
||||||
|
return fullMethod, nil
|
||||||
|
}
|
||||||
|
name := fullMethod[1:]
|
||||||
|
pos := strings.LastIndex(name, "/")
|
||||||
|
if pos < 0 {
|
||||||
|
// Invalid format, does not follow `/package.service/method`.
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
service, method := name[:pos], name[pos+1:]
|
||||||
|
|
||||||
|
var attrs []interface{}
|
||||||
|
if service != "" {
|
||||||
|
attrs = append(attrs, "rpc.service", service)
|
||||||
|
}
|
||||||
|
if method != "" {
|
||||||
|
attrs = append(attrs, "rpc.method", method)
|
||||||
|
}
|
||||||
|
return name, attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerAttr(addr string) []interface{} {
|
||||||
|
host, p, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var attr []interface{}
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
attr = []interface{}{
|
||||||
|
"net.sock.peer.addr", host,
|
||||||
|
"net.sock.peer.port", port,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attr = []interface{}{
|
||||||
|
"net.peer.name", host,
|
||||||
|
"net.peer.port", port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerFromCtx(ctx context.Context) string {
|
||||||
|
p, ok := peer.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return p.Addr.String()
|
||||||
|
}
|
253
util/http/clienttracer.go
Normal file
253
util/http/clienttracer.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
//
|
||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
//
|
||||||
|
// 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 http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v3/tracer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpStatus = "http.status"
|
||||||
|
httpHeaderMIME = "http.mime"
|
||||||
|
httpRemoteAddr = "http.remote"
|
||||||
|
httpLocalAddr = "http.local"
|
||||||
|
httpHost = "http.host"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hookMap = map[string]string{
|
||||||
|
"http.dns": "http.getconn",
|
||||||
|
"http.connect": "http.getconn",
|
||||||
|
"http.tls": "http.getconn",
|
||||||
|
}
|
||||||
|
|
||||||
|
func parentHook(hook string) string {
|
||||||
|
if strings.HasPrefix(hook, "http.connect") {
|
||||||
|
return hookMap["http.connect"]
|
||||||
|
}
|
||||||
|
return hookMap[hook]
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientTracer struct {
|
||||||
|
context.Context
|
||||||
|
tr tracer.Tracer
|
||||||
|
activeHooks map[string]context.Context
|
||||||
|
root tracer.Span
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientTrace(ctx context.Context, tr tracer.Tracer) *httptrace.ClientTrace {
|
||||||
|
ct := &clientTracer{
|
||||||
|
Context: ctx,
|
||||||
|
activeHooks: make(map[string]context.Context),
|
||||||
|
tr: tr,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &httptrace.ClientTrace{
|
||||||
|
GetConn: ct.getConn,
|
||||||
|
GotConn: ct.gotConn,
|
||||||
|
PutIdleConn: ct.putIdleConn,
|
||||||
|
GotFirstResponseByte: ct.gotFirstResponseByte,
|
||||||
|
Got100Continue: ct.got100Continue,
|
||||||
|
Got1xxResponse: ct.got1xxResponse,
|
||||||
|
DNSStart: ct.dnsStart,
|
||||||
|
DNSDone: ct.dnsDone,
|
||||||
|
ConnectStart: ct.connectStart,
|
||||||
|
ConnectDone: ct.connectDone,
|
||||||
|
TLSHandshakeStart: ct.tlsHandshakeStart,
|
||||||
|
TLSHandshakeDone: ct.tlsHandshakeDone,
|
||||||
|
WroteHeaderField: ct.wroteHeaderField,
|
||||||
|
WroteHeaders: ct.wroteHeaders,
|
||||||
|
Wait100Continue: ct.wait100Continue,
|
||||||
|
WroteRequest: ct.wroteRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) start(hook, spanName string, attrs ...interface{}) {
|
||||||
|
ct.mtx.Lock()
|
||||||
|
defer ct.mtx.Unlock()
|
||||||
|
|
||||||
|
if hookCtx, found := ct.activeHooks[hook]; !found {
|
||||||
|
var sp tracer.Span
|
||||||
|
ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, tracer.WithSpanLabels(attrs...), tracer.WithSpanKind(tracer.SpanKindClient))
|
||||||
|
if ct.root == nil {
|
||||||
|
ct.root = sp
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// end was called before start finished, add the start attributes and end the span here
|
||||||
|
if span, ok := tracer.SpanFromContext(hookCtx); ok {
|
||||||
|
span.AddLabels(attrs...)
|
||||||
|
span.Finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ct.activeHooks, hook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) end(hook string, err error, attrs ...interface{}) {
|
||||||
|
ct.mtx.Lock()
|
||||||
|
defer ct.mtx.Unlock()
|
||||||
|
if ctx, ok := ct.activeHooks[hook]; ok { // nolint:nestif
|
||||||
|
if span, ok := tracer.SpanFromContext(ctx); ok {
|
||||||
|
if err != nil {
|
||||||
|
span.SetStatus(tracer.SpanStatusError, err.Error())
|
||||||
|
}
|
||||||
|
span.AddLabels(attrs...)
|
||||||
|
span.Finish()
|
||||||
|
}
|
||||||
|
delete(ct.activeHooks, hook)
|
||||||
|
} else {
|
||||||
|
// start is not finished before end is called.
|
||||||
|
// Start a span here with the ending attributes that will be finished when start finishes.
|
||||||
|
// Yes, it's backwards. v0v
|
||||||
|
ctx, span := ct.tr.Start(ct.getParentContext(hook), hook, tracer.WithSpanLabels(attrs...), tracer.WithSpanKind(tracer.SpanKindClient))
|
||||||
|
if err != nil {
|
||||||
|
span.SetStatus(tracer.SpanStatusError, err.Error())
|
||||||
|
}
|
||||||
|
ct.activeHooks[hook] = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) getParentContext(hook string) context.Context {
|
||||||
|
ctx, ok := ct.activeHooks[parentHook(hook)]
|
||||||
|
if !ok {
|
||||||
|
return ct.Context
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) span(hook string) (tracer.Span, bool) {
|
||||||
|
ct.mtx.Lock()
|
||||||
|
defer ct.mtx.Unlock()
|
||||||
|
if ctx, ok := ct.activeHooks[hook]; ok {
|
||||||
|
return tracer.SpanFromContext(ctx)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) getConn(host string) {
|
||||||
|
ct.start("http.getconn", "http.getconn", httpHost, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) gotConn(info httptrace.GotConnInfo) {
|
||||||
|
ct.end("http.getconn",
|
||||||
|
nil,
|
||||||
|
httpRemoteAddr, info.Conn.RemoteAddr().String(),
|
||||||
|
httpLocalAddr, info.Conn.LocalAddr().String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) putIdleConn(err error) {
|
||||||
|
ct.end("http.receive", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) gotFirstResponseByte() {
|
||||||
|
ct.start("http.receive", "http.receive")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) dnsStart(info httptrace.DNSStartInfo) {
|
||||||
|
ct.start("http.dns", "http.dns", httpHost, info.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) {
|
||||||
|
ct.end("http.dns", info.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) connectStart(network, addr string) {
|
||||||
|
_ = network
|
||||||
|
ct.start("http.connect."+addr, "http.connect", httpRemoteAddr, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) connectDone(network, addr string, err error) {
|
||||||
|
_ = network
|
||||||
|
ct.end("http.connect."+addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) tlsHandshakeStart() {
|
||||||
|
ct.start("http.tls", "http.tls")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) {
|
||||||
|
ct.end("http.tls", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) wroteHeaderField(k string, v []string) {
|
||||||
|
if sp, ok := ct.span("http.headers"); !ok || sp == nil {
|
||||||
|
ct.start("http.headers", "http.headers")
|
||||||
|
}
|
||||||
|
ct.root.AddLabels("http."+strings.ToLower(k), sliceToString(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) wroteHeaders() {
|
||||||
|
ct.start("http.send", "http.send")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) {
|
||||||
|
if info.Err != nil {
|
||||||
|
ct.root.SetStatus(tracer.SpanStatusError, info.Err.Error())
|
||||||
|
}
|
||||||
|
ct.end("http.send", info.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) got100Continue() {
|
||||||
|
if sp, ok := ct.span("http.receive"); ok && sp != nil {
|
||||||
|
sp.AddEvent("GOT 100 - Continue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) wait100Continue() {
|
||||||
|
if sp, ok := ct.span("http.receive"); ok && sp != nil {
|
||||||
|
sp.AddEvent("GOT 100 - Wait")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *clientTracer) got1xxResponse(code int, header textproto.MIMEHeader) error {
|
||||||
|
if sp, ok := ct.span("http.receive"); ok && sp != nil {
|
||||||
|
sp.AddEvent("GOT 1xx",
|
||||||
|
tracer.WithEventLabels(
|
||||||
|
httpStatus, code,
|
||||||
|
httpHeaderMIME, sm2s(header),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceToString(value []string) string {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return "undefined"
|
||||||
|
}
|
||||||
|
return strings.Join(value, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sm2s(value map[string][]string) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
for k, v := range value {
|
||||||
|
if buf.Len() != 0 {
|
||||||
|
buf.WriteString(",")
|
||||||
|
}
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteString("=")
|
||||||
|
buf.WriteString(sliceToString(v))
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
@@ -508,3 +508,74 @@ func FieldName(name string) string {
|
|||||||
|
|
||||||
return string(newstr)
|
return string(newstr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Equal(src interface{}, dst interface{}, excptFields ...string) bool {
|
||||||
|
srcVal := reflect.ValueOf(src)
|
||||||
|
dstVal := reflect.ValueOf(dst)
|
||||||
|
|
||||||
|
switch srcVal.Kind() {
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
for i := 0; i < srcVal.Len(); i++ {
|
||||||
|
e := srcVal.Index(i).Interface()
|
||||||
|
a := dstVal.Index(i).Interface()
|
||||||
|
if !Equal(e, a, excptFields...) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case reflect.Map:
|
||||||
|
for i := 0; i < len(srcVal.MapKeys()); i++ {
|
||||||
|
key := srcVal.MapKeys()[i]
|
||||||
|
keyStr := fmt.Sprintf("%v", key.Interface())
|
||||||
|
if stringContains(keyStr, excptFields) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := srcVal.MapIndex(key)
|
||||||
|
d := dstVal.MapIndex(key)
|
||||||
|
if !Equal(s.Interface(), d.Interface(), excptFields...) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case reflect.Struct, reflect.Interface:
|
||||||
|
for i := 0; i < srcVal.NumField(); i++ {
|
||||||
|
typeField := srcVal.Type().Field(i)
|
||||||
|
if stringContains(typeField.Name, excptFields) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := srcVal.Field(i)
|
||||||
|
d := dstVal.FieldByName(typeField.Name)
|
||||||
|
if s.CanInterface() && d.CanInterface() {
|
||||||
|
if !Equal(s.Interface(), d.Interface(), excptFields...) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case reflect.Ptr:
|
||||||
|
if srcVal.IsNil() {
|
||||||
|
return dstVal.IsNil()
|
||||||
|
}
|
||||||
|
s := srcVal.Elem()
|
||||||
|
d := reflect.Indirect(dstVal)
|
||||||
|
if s.CanInterface() && d.CanInterface() {
|
||||||
|
return Equal(s.Interface(), d.Interface(), excptFields...)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case reflect.String, reflect.Int, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Bool:
|
||||||
|
return src == dst
|
||||||
|
default:
|
||||||
|
return srcVal.Interface() == dstVal.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringContains(a string, list []string) bool {
|
||||||
|
for _, b := range list {
|
||||||
|
if b == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@@ -133,3 +133,16 @@ func TestMergeNested(t *testing.T) {
|
|||||||
t.Fatalf("merge error: %#+v", dst.Nested)
|
t.Fatalf("merge error: %#+v", dst.Nested)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEqual(t *testing.T) {
|
||||||
|
type tstr struct {
|
||||||
|
Key1 string
|
||||||
|
Key2 string
|
||||||
|
}
|
||||||
|
|
||||||
|
src := &tstr{Key1: "val1", Key2: "micro:generate"}
|
||||||
|
dst := &tstr{Key1: "val1", Key2: "val2"}
|
||||||
|
if !Equal(src, dst, "Key2") {
|
||||||
|
t.Fatal("invalid Equal test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user