Compare commits

..

83 Commits

Author SHA1 Message Date
b5f8316b57 semconv: fix broker group lag metric name
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-13 02:38:58 +03:00
d7ddd912a8 Merge pull request 'semconv: add broker group lag' (#336) from brokerlag into v3
Reviewed-on: #336
2024-04-13 02:07:53 +03:00
c020d90cb4 semconv: add broker group lag
Some checks failed
pr / test (pull_request) Failing after 1m39s
lint / lint (pull_request) Successful in 10m49s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-13 02:06:51 +03:00
db47b62159 Merge pull request 'add options in broker' (#334) from devstigneev/micro:v3 into v3
Reviewed-on: #334
2024-04-08 23:12:59 +03:00
8254456c8b rename path to sync
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-04-07 21:16:50 +03:00
c2808679c3 add options in broker
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-04-07 20:53:01 +03:00
f418235c16 Merge pull request 'cluster: initial import' (#332) from cluster into v3
Reviewed-on: #332
2024-04-06 23:29:04 +03:00
67ba7b3753 cluster: initial import
Some checks failed
pr / test (pull_request) Failing after 1m36s
lint / lint (pull_request) Successful in 10m48s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-06 23:28:01 +03:00
e48d7cadf9 Merge pull request 'add semconv package' (#331) from semconv into v3
Reviewed-on: #331
2024-04-06 22:04:47 +03:00
c906186011 add semconv package
Some checks failed
pr / test (pull_request) Failing after 1m39s
lint / lint (pull_request) Successful in 10m24s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-06 22:03:56 +03:00
dc0ff91b83 Merge pull request 'util/reflect: detect json.Unmarshaler' (#328) from utilsort into v3
Reviewed-on: #328
2024-04-02 08:52:11 +03:00
e739c2d438 util/reflect: detect json.Unmarshaler
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Failing after 2m3s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-02 08:51:06 +03:00
bf4a036652 Merge pull request 'move sort.Uniq to dedicated package' (#327) from utilsort into v3
Reviewed-on: #327
2024-03-27 11:25:50 +03:00
f83a29eb67 move sort.Uniq to dedicated package
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-27 11:24:28 +03:00
aef7f53d88 Merge pull request 'tracer: append labels' (#326) from tracerfix into v3
Reviewed-on: #326
2024-03-17 00:18:23 +03:00
02c8e4fb7f tracer: append labels
All checks were successful
pr / test (pull_request) Successful in 1m35s
lint / lint (pull_request) Successful in 10m38s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-17 00:17:10 +03:00
f5693bd940 Merge pull request 'v3 update WaitGroup Options' (#325) from devstigneev/micro:v3 into v3
Reviewed-on: #325
2024-03-13 11:03:29 +03:00
701afb7bea sort imports
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
2024-03-13 10:51:03 +03:00
019b407e74 update WaitOptions 2024-03-13 10:49:58 +03:00
f29a346434 Merge pull request 'tracer: add Context init to NewOptions' (#323) from tracerctx into v3
Reviewed-on: #323
2024-03-11 01:13:01 +03:00
27db1876c0 tracer: add Context init to NewOptions
All checks were successful
pr / test (pull_request) Successful in 1m30s
lint / lint (pull_request) Successful in 10m33s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-11 01:12:20 +03:00
f66ac9736b metadata: allow to exclude some keys in Copy func (#321)
Reviewed-on: #321
Co-authored-by: Vasiliy Tolstov <v.tolstov@unistack.org>
Co-committed-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-09 23:50:40 +03:00
ed7972a1fa Merge pull request 'sync/waitgroup: backport from master' (#320) from waitgroup into v3
Reviewed-on: #320
2024-03-09 23:37:39 +03:00
2cc004b01c sync/waitgroup: backport from master
All checks were successful
pr / test (pull_request) Successful in 1m40s
lint / lint (pull_request) Successful in 10m42s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-09 23:36:39 +03:00
df951e5daf Merge pull request 'logger/slog: fix slog' (#317) from slogfix2 into v3
Reviewed-on: #317
2024-03-07 08:22:37 +03:00
5bec0cef03 logger/slog: fix slog
All checks were successful
pr / test (pull_request) Successful in 1m24s
lint / lint (pull_request) Successful in 10m24s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-07 08:19:14 +03:00
34940b68d7 Merge pull request 'logger/slog: fix race condition' (#316) from slogfix into v3
Reviewed-on: #316
2024-03-07 07:45:07 +03:00
1c57127128 logger/slog: fix race condition
All checks were successful
pr / test (pull_request) Successful in 1m34s
lint / lint (pull_request) Successful in 10m36s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-07 07:43:52 +03:00
a4dd1a494c Merge pull request 'logger: add TimeFunc option' (#315) from logger-timefunc into v3
Reviewed-on: #315
2024-03-07 00:02:53 +03:00
60e5e42167 logger: add TimeFunc option
All checks were successful
pr / test (pull_request) Successful in 1m36s
lint / lint (pull_request) Successful in 10m45s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-07 00:02:00 +03:00
b519b61fff Merge pull request 'fixup interfaces' (#314) from iface-v3 into v3
Reviewed-on: #314
2024-03-06 18:49:03 +03:00
f62b26eda3 fixup interfaces
Some checks failed
pr / test (pull_request) Failing after 1m30s
lint / lint (pull_request) Successful in 10m37s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-06 18:45:32 +03:00
13eda451da Merge pull request 'fixup deps' (#313) from deps into v3
Reviewed-on: #313
2024-03-06 16:46:33 +03:00
89cad06121 fixup deps
Some checks failed
pr / test (pull_request) Failing after 1m35s
lint / lint (pull_request) Successful in 10m44s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-06 16:45:19 +03:00
0bebf3d59f Merge pull request 'tracer and logger improvements' (#312) from tracer-logger into v3
Reviewed-on: #312
2024-03-06 00:57:01 +03:00
01e05e8df6 tracer and logger improvements
Some checks failed
pr / test (pull_request) Failing after 1m27s
lint / lint (pull_request) Successful in 10m33s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-06 00:53:20 +03:00
2b69a4f51c Merge pull request 'logger/slog: backport default logger keys from master' (#311) from v3-logger into v3
Reviewed-on: #311
2024-03-05 01:54:17 +03:00
4af2b077dd logger/slog: backport default logger keys from master
All checks were successful
pr / test (pull_request) Successful in 1m45s
lint / lint (pull_request) Successful in 10m43s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 23:58:31 +03:00
de4418189d Merge pull request 'add missing option' (#309) from logger-stacktrace into v3
Reviewed-on: #309
2024-03-04 23:04:50 +03:00
2c44550897 add missing option
All checks were successful
pr / test (pull_request) Successful in 1m46s
lint / lint (pull_request) Successful in 10m49s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 23:03:55 +03:00
99b8a3c950 Merge pull request 'logger/slog: add stacktrace support' (#308) from logger-stacktrace into v3
Reviewed-on: #308
2024-03-04 23:00:35 +03:00
4c7e1607d4 logger/slog: add stacktrace support
Some checks failed
pr / test (pull_request) Failing after 1m28s
lint / lint (pull_request) Successful in 10m40s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 22:54:11 +03:00
897be419b4 Merge pull request 'broker noop implementation' (#307) from noops into v3
Reviewed-on: #307
2024-03-04 01:15:16 +03:00
81b9a4341f logger: extend interface, fix tests
All checks were successful
pr / test (pull_request) Successful in 1m35s
lint / lint (pull_request) Successful in 10m40s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 01:09:08 +03:00
d3bb2f7236 broker/noop: add initial implementation
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-03-04 01:05:40 +03:00
97fd62cb21 Merge pull request 'register/noop: add noop register' (#306) from register-noop into v3
Reviewed-on: #306
2024-03-01 21:40:01 +03:00
3cd8bc33d6 fixup test
Some checks failed
pr / test (pull_request) Failing after 1m31s
lint / lint (pull_request) Successful in 10m44s
2024-03-01 21:39:31 +03:00
f6f67af8d0 register/noop: add noop register
Some checks failed
pr / test (pull_request) Failing after 1m34s
lint / lint (pull_request) Successful in 11m0s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-29 23:58:11 +03:00
2d5acaca2f Merge pull request 'server: add GracefulTimeout option' (#304) from graceful into v3
Reviewed-on: #304
2024-02-29 23:24:43 +03:00
0674df3d9f update workflow
Some checks failed
pr / test (pull_request) Failing after 1m40s
lint / lint (pull_request) Successful in 11m5s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-29 23:23:51 +03:00
2c282825ce fixup
Some checks failed
pr / test (pull_request) Failing after 1m38s
lint / lint (pull_request) Failing after 1m47s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-29 23:17:36 +03:00
e87ff942bb bump gomod
Some checks failed
lint / lint (pull_request) Failing after 1m40s
pr / test (pull_request) Failing after 1m44s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-29 23:03:39 +03:00
0459ea0613 fixup
Some checks failed
lint / lint (pull_request) Failing after 1m38s
pr / test (pull_request) Failing after 1m38s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-29 22:40:05 +03:00
d44a75d074 add gracefultimeout in server 2024-02-29 22:35:55 +03:00
Кирилл Горбунов
ccf92eb84d As for interface casting
Co-authored-by: Gorbunov Kirill Andreevich <kgorbunov@mtsbank.ru>
Reviewed-on: #299
Co-authored-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
Co-committed-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
2024-02-27 23:35:49 +03:00
6baf1f2744 Merge pull request 'logger/slog: fixup race condition' (#292) from log into v3
Reviewed-on: #292
2024-02-22 08:58:40 +03:00
8e2eafde9c logger/slog: fixup race condition
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-22 08:57:21 +03:00
c2b97b0f20 fixup logger/slog
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-15 10:01:41 +03:00
1db017d966 Merge pull request 'logger/slog: fixup old format' (#291) from fixupslog into v3
Reviewed-on: #291
2024-02-08 08:44:23 +03:00
debf8cb03d logger/slog: fixup old format
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-08 08:43:53 +03:00
1dc9c1891f Merge pull request 'logger/slog: initial import' (#290) from slog into v3
Reviewed-on: #290
2024-02-08 08:18:57 +03:00
930859a537 logger/slog: initial import
Some checks failed
lint / lint (pull_request) Has been cancelled
pr / test (pull_request) Has been cancelled
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-02-08 08:17:53 +03:00
3141f1ed8b Merge pull request 'config: add conditions' (#286) from cond-config into v3
Reviewed-on: #286
2024-01-15 00:46:37 +03:00
47943cfb05 config: add conditions
Some checks failed
lint / lint (pull_request) Successful in 1m28s
pr / test (pull_request) Failing after 1m5s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-01-15 00:46:00 +03:00
ed4e9d54b1 Merge pull request 'client/noop: fixup md' (#285) from noopfix into v3
Reviewed-on: #285
2023-12-21 00:14:54 +03:00
b4b8583594 client/noop: fixup md
Some checks failed
lint / lint (pull_request) Failing after 1m28s
pr / test (pull_request) Failing after 2m45s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-12-21 00:13:08 +03:00
fb43e8c58c Merge pull request 'client/noop: fix metadata overwrite' (#284) from noopfix into v3
Reviewed-on: #284
2023-12-21 00:07:22 +03:00
8863c10ef4 client/noop: fix metadata overwrite
Some checks failed
lint / lint (pull_request) Failing after 1m29s
pr / test (pull_request) Failing after 2m36s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-12-21 00:06:56 +03:00
8058095bcc Merge pull request 'copy incoming content-type' (#283) from ct into v3
Reviewed-on: #283
2023-12-20 09:35:33 +03:00
092f5d96b1 copy incoming content-type
Some checks failed
lint / lint (pull_request) Failing after 1m28s
pr / test (pull_request) Failing after 2m33s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-12-20 09:35:01 +03:00
84552513f7 Merge pull request 'fixup multiple client handling' (#280) from multiple into v3
Reviewed-on: #280
2023-11-13 08:20:52 +03:00
80a2db264e fixup multiple client handling
Some checks failed
lint / lint (pull_request) Failing after 1m29s
pr / test (pull_request) Failing after 2m35s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-11-13 08:19:44 +03:00
0be09c8b3e Merge pull request 'database: add FormatDSN' (#278) from database-newv3 into v3
Reviewed-on: #278
2023-11-02 01:35:25 +03:00
047f479e1b database: add FormatDSN
Some checks failed
lint / lint (pull_request) Failing after 1m27s
pr / test (pull_request) Failing after 2m39s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-11-02 01:32:26 +03:00
8f757c953e Merge pull request 'database: initial import for dsn parsing' (#276) from databasev3 into v3
Reviewed-on: #276
2023-11-01 23:44:17 +03:00
5f1c673a24 database: initial import for dsn parsing
Some checks failed
lint / lint (pull_request) Failing after 1m28s
pr / test (pull_request) Failing after 2m36s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-11-01 23:42:48 +03:00
6794ea9871 Merge pull request 'client/noop: fix MessageMetadata option' (#274) from client-noop-metadata into v3
Reviewed-on: #274
2023-10-26 03:07:12 +03:00
089e7b6812 client/noop: fix MessageMetadata option
All checks were successful
lint / lint (pull_request) Successful in 1m18s
pr / test (pull_request) Successful in 1m1s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-26 03:06:39 +03:00
1c703f0f0c Merge pull request 'errors: add IsRetrayable func' (#273) from errors into v3
Reviewed-on: #273
2023-10-25 10:24:58 +03:00
d167c8c67c cleanup
All checks were successful
lint / lint (pull_request) Successful in 1m7s
pr / test (pull_request) Successful in 1m2s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-25 02:36:52 +03:00
df4f96a2d8 errors: add IsRetrayable func
All checks were successful
lint / lint (pull_request) Successful in 1m18s
pr / test (pull_request) Successful in 1m3s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-23 02:35:10 +03:00
fac3b20bd4 Merge pull request 'util/reflect: add Equal func with ability to skip some fields' (#244) from util-reflect into v3
Reviewed-on: #244
2023-09-12 11:45:26 +03:00
7c6bd98498 util/reflect: add Equal func with ability to skip some fields
All checks were successful
pr / test (pull_request) Successful in 1m4s
lint / lint (pull_request) Successful in 1m10s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-09-12 10:31:45 +03:00
57 changed files with 2514 additions and 607 deletions

View File

@@ -10,15 +10,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: setup-go
uses: https://gitea.com/actions/setup-go@v3
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.21
- name: checkout
uses: https://gitea.com/actions/checkout@v3
uses: actions/checkout@v3
- name: deps
run: go get -v -d ./...
- name: lint
uses: https://github.com/golangci/golangci-lint-action@v3.4.0
continue-on-error: true
with:
version: v1.52
version: v1.52

View File

@@ -10,14 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: https://gitea.com/actions/checkout@v3
uses: actions/checkout@v3
- name: setup-go
uses: https://gitea.com/actions/setup-go@v3
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.21
- name: deps
run: go get -v -t -d ./...
- name: test
env:
INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./...
run: go test -mod readonly -v ./...

3
.gitignore vendored
View File

@@ -1,6 +1,8 @@
# Develop tools
/.vscode/
/.idea/
.idea
.vscode
# Binaries for programs and plugins
*.exe
@@ -13,6 +15,7 @@
_obj
_test
_build
.DS_Store
# Architecture specific extensions/prefixes
*.[568vq]

View File

@@ -4,19 +4,22 @@ package broker // import "go.unistack.org/micro/v3/broker"
import (
"context"
"errors"
"time"
"go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v3/metadata"
)
// DefaultBroker default memory broker
var DefaultBroker = NewBroker()
var DefaultBroker Broker = NewBroker()
var (
// ErrNotConnected returns when broker used but not connected yet
ErrNotConnected = errors.New("broker not connected")
// ErrDisconnected returns when broker disconnected
ErrDisconnected = errors.New("broker disconnected")
// DefaultGracefulTimeout
DefaultGracefulTimeout = 5 * time.Second
)
// Broker is an interface used for asynchronous messaging.

View File

@@ -4,6 +4,7 @@ import (
"context"
"sync"
"go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/metadata"
maddr "go.unistack.org/micro/v3/util/addr"
@@ -15,7 +16,7 @@ import (
type memoryBroker struct {
subscribers map[string][]*memorySubscriber
addr string
opts Options
opts broker.Options
sync.RWMutex
connected bool
}
@@ -24,20 +25,20 @@ type memoryEvent struct {
err error
message interface{}
topic string
opts Options
opts broker.Options
}
type memorySubscriber struct {
ctx context.Context
exit chan bool
handler Handler
batchhandler BatchHandler
handler broker.Handler
batchhandler broker.BatchHandler
id string
topic string
opts SubscribeOptions
opts broker.SubscribeOptions
}
func (m *memoryBroker) Options() Options {
func (m *memoryBroker) Options() broker.Options {
return m.opts
}
@@ -46,6 +47,12 @@ func (m *memoryBroker) Address() string {
}
func (m *memoryBroker) Connect(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
m.Lock()
defer m.Unlock()
@@ -70,6 +77,12 @@ func (m *memoryBroker) Connect(ctx context.Context) error {
}
func (m *memoryBroker) Disconnect(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
m.Lock()
defer m.Unlock()
@@ -81,27 +94,27 @@ func (m *memoryBroker) Disconnect(ctx context.Context) error {
return nil
}
func (m *memoryBroker) Init(opts ...Option) error {
func (m *memoryBroker) Init(opts ...broker.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *broker.Message, opts ...broker.PublishOption) error {
msg.Header.Set(metadata.HeaderTopic, topic)
return m.publish(ctx, []*Message{msg}, opts...)
return m.publish(ctx, []*broker.Message{msg}, opts...)
}
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
return m.publish(ctx, msgs, opts...)
}
func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
func (m *memoryBroker) publish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
m.RLock()
if !m.connected {
m.RUnlock()
return ErrNotConnected
return broker.ErrNotConnected
}
m.RUnlock()
@@ -111,9 +124,9 @@ func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...Pub
case <-ctx.Done():
return ctx.Err()
default:
options := NewPublishOptions(opts...)
options := broker.NewPublishOptions(opts...)
msgTopicMap := make(map[string]Events)
msgTopicMap := make(map[string]broker.Events)
for _, v := range msgs {
p := &memoryEvent{opts: m.opts}
@@ -188,11 +201,11 @@ func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...Pub
return nil
}
func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
m.RLock()
if !m.connected {
m.RUnlock()
return nil, ErrNotConnected
return nil, broker.ErrNotConnected
}
m.RUnlock()
@@ -201,7 +214,7 @@ func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler
return nil, err
}
options := NewSubscribeOptions(opts...)
options := broker.NewSubscribeOptions(opts...)
sub := &memorySubscriber{
exit: make(chan bool, 1),
@@ -233,11 +246,11 @@ func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler
return sub, nil
}
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
m.RLock()
if !m.connected {
m.RUnlock()
return nil, ErrNotConnected
return nil, broker.ErrNotConnected
}
m.RUnlock()
@@ -246,7 +259,7 @@ func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Hand
return nil, err
}
options := NewSubscribeOptions(opts...)
options := broker.NewSubscribeOptions(opts...)
sub := &memorySubscriber{
exit: make(chan bool, 1),
@@ -290,12 +303,12 @@ func (m *memoryEvent) Topic() string {
return m.topic
}
func (m *memoryEvent) Message() *Message {
func (m *memoryEvent) Message() *broker.Message {
switch v := m.message.(type) {
case *Message:
case *broker.Message:
return v
case []byte:
msg := &Message{}
msg := &broker.Message{}
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, "[memory]: failed to unmarshal: %v", err)
@@ -320,7 +333,7 @@ func (m *memoryEvent) SetError(err error) {
m.err = err
}
func (m *memorySubscriber) Options() SubscribeOptions {
func (m *memorySubscriber) Options() broker.SubscribeOptions {
return m.opts
}
@@ -334,9 +347,9 @@ func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
}
// NewBroker return new memory broker
func NewBroker(opts ...Option) Broker {
func NewBroker(opts ...broker.Option) broker.Broker {
return &memoryBroker{
opts: NewOptions(opts...),
opts: broker.NewOptions(opts...),
subscribers: make(map[string][]*memorySubscriber),
}
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"testing"
"go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v3/metadata"
)
@@ -19,7 +20,7 @@ func TestMemoryBatchBroker(t *testing.T) {
topic := "test"
count := 10
fn := func(evts Events) error {
fn := func(evts broker.Events) error {
return evts.Ack()
}
@@ -28,9 +29,9 @@ func TestMemoryBatchBroker(t *testing.T) {
t.Fatalf("Unexpected error subscribing %v", err)
}
msgs := make([]*Message, 0, count)
msgs := make([]*broker.Message, 0, count)
for i := 0; i < count; i++ {
message := &Message{
message := &broker.Message{
Header: map[string]string{
metadata.HeaderTopic: topic,
"foo": "bar",
@@ -65,7 +66,7 @@ func TestMemoryBroker(t *testing.T) {
topic := "test"
count := 10
fn := func(p Event) error {
fn := func(p broker.Event) error {
return nil
}
@@ -74,9 +75,9 @@ func TestMemoryBroker(t *testing.T) {
t.Fatalf("Unexpected error subscribing %v", err)
}
msgs := make([]*Message, 0, count)
msgs := make([]*broker.Message, 0, count)
for i := 0; i < count; i++ {
message := &Message{
message := &broker.Message{
Header: map[string]string{
metadata.HeaderTopic: topic,
"foo": "bar",

82
broker/noop.go Normal file
View File

@@ -0,0 +1,82 @@
package broker
import (
"context"
"strings"
)
type NoopBroker struct {
opts Options
}
func NewBroker(opts ...Option) *NoopBroker {
b := &NoopBroker{opts: NewOptions(opts...)}
return b
}
func (b *NoopBroker) Name() string {
return b.opts.Name
}
func (b *NoopBroker) String() string {
return "noop"
}
func (b *NoopBroker) Options() Options {
return b.opts
}
func (b *NoopBroker) Init(opts ...Option) error {
for _, opt := range opts {
opt(&b.opts)
}
return nil
}
func (b *NoopBroker) Connect(_ context.Context) error {
return nil
}
func (b *NoopBroker) Disconnect(_ context.Context) error {
return nil
}
func (b *NoopBroker) Address() string {
return strings.Join(b.opts.Addrs, ",")
}
func (b *NoopBroker) BatchPublish(_ context.Context, _ []*Message, _ ...PublishOption) error {
return nil
}
func (b *NoopBroker) Publish(_ context.Context, _ string, _ *Message, _ ...PublishOption) error {
return nil
}
type NoopSubscriber struct {
ctx context.Context
topic string
handler Handler
batchHandler BatchHandler
opts SubscribeOptions
}
func (b *NoopBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), batchHandler: handler}, nil
}
func (b *NoopBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), handler: handler}, nil
}
func (s *NoopSubscriber) Options() SubscribeOptions {
return s.opts
}
func (s *NoopSubscriber) Topic() string {
return s.topic
}
func (s *NoopSubscriber) Unsubscribe(ctx context.Context) error {
return nil
}

View File

@@ -9,6 +9,7 @@ import (
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v3/sync"
"go.unistack.org/micro/v3/tracer"
)
@@ -36,17 +37,22 @@ type Options struct {
Name string
// Addrs holds the broker address
Addrs []string
Wait *sync.WaitGroup
GracefulTimeout time.Duration
}
// NewOptions create new Options
func NewOptions(opts ...Option) Options {
options := Options{
Register: register.DefaultRegister,
Logger: logger.DefaultLogger,
Context: context.Background(),
Meter: meter.DefaultMeter,
Codec: codec.DefaultCodec,
Tracer: tracer.DefaultTracer,
Register: register.DefaultRegister,
Logger: logger.DefaultLogger,
Context: context.Background(),
Meter: meter.DefaultMeter,
Codec: codec.DefaultCodec,
Tracer: tracer.DefaultTracer,
GracefulTimeout: DefaultGracefulTimeout,
}
for _, o := range opts {
o(&options)

View File

@@ -3,6 +3,7 @@ package client
import (
"context"
"fmt"
"os"
"time"
"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))
// 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 {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = metadata.New(0)
}
md := metadata.Copy(omd)
md[metadata.HeaderContentType] = p.ContentType()
topic := p.Topic()
// get the exchange
if len(options.Exchange) > 0 {
topic = options.Exchange
if len(exchange) > 0 {
topic = exchange
}
md[metadata.HeaderTopic] = topic
iter := p.Metadata().Iterator()
var k, v string
for iter.Next(&k, &v) {
md.Set(k, v)
}
var body []byte

41
cluster/cluster.go Normal file
View File

@@ -0,0 +1,41 @@
package cluster
import (
"context"
"go.unistack.org/micro/v3/metadata"
)
// Message sent to member in cluster
type Message interface {
// Header returns message headers
Header() metadata.Metadata
// Body returns broker message may be []byte slice or some go struct or interface
Body() interface{}
}
type Node interface {
// Name returns node name
Name() string
// Address returns node address
Address() string
// Metadata returns node metadata
Metadata() metadata.Metadata
}
// Cluster interface used for cluster communication across nodes
type Cluster interface {
// Join is used to take an existing members and performing state sync
Join(ctx context.Context, addr ...string) error
// Leave broadcast a leave message and stop listeners
Leave(ctx context.Context) error
// Ping is used to probe live status of the node
Ping(ctx context.Context, node Node, payload []byte) error
// Members returns the cluster members
Members() ([]Node, error)
// Broadcast send message for all members in cluster, if filter is not nil, nodes may be filtered
// by key/value pairs
Broadcast(ctx context.Context, msg Message, filter ...string) error
// Unicast send message to single member in cluster
Unicast(ctx context.Context, node Node, msg Message) error
}

View File

@@ -13,7 +13,7 @@ type Validator interface {
}
// DefaultConfig default config
var DefaultConfig = NewConfig()
var DefaultConfig Config = NewConfig()
// DefaultWatcherMinInterval default min interval for poll changes
var DefaultWatcherMinInterval = 5 * time.Second

View File

@@ -7,8 +7,8 @@ import (
"strings"
"time"
"dario.cat/mergo"
"github.com/google/uuid"
"github.com/imdario/mergo"
mid "go.unistack.org/micro/v3/util/id"
rutil "go.unistack.org/micro/v3/util/reflect"
mtime "go.unistack.org/micro/v3/util/time"
@@ -39,6 +39,10 @@ func (c *defaultConfig) Init(opts ...Option) 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 {
return err
}
@@ -291,7 +295,11 @@ func fillValues(valueOf reflect.Value, tname string) error {
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 {
return err
}

View File

@@ -42,6 +42,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
}
// Option function signature
@@ -68,9 +72,9 @@ type LoadOption func(o *LoadOptions)
// LoadOptions struct
type LoadOptions struct {
Struct interface{}
Context context.Context
Override bool
Append bool
Context context.Context
}
// NewLoadOptions create LoadOptions struct with provided opts

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

@@ -4,11 +4,17 @@ package errors // import "go.unistack.org/micro/v3/errors"
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
@@ -340,3 +346,135 @@ func addslashes(str string) 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
}

View File

@@ -8,6 +8,13 @@ import (
"testing"
)
func TestIsRetrayable(t *testing.T) {
err := fmt.Errorf("ORA-")
if !IsRetryable(err, RetrayableOracleErrors...) {
t.Fatalf("IsRetrayable not works")
}
}
func TestMarshalJSON(t *testing.T) {
e := InternalServerError("id", "err: %v", fmt.Errorf("err: %v", `xxx: "UNIX_TIMESTAMP": invalid identifier`))
_, err := json.Marshal(e)

4
go.mod
View File

@@ -1,11 +1,11 @@
module go.unistack.org/micro/v3
go 1.19
go 1.20
require (
dario.cat/mergo v1.0.0
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/google/uuid v1.3.0
github.com/imdario/mergo v0.3.15
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5
golang.org/x/sync v0.3.0

4
go.sum
View File

@@ -1,3 +1,5 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -7,8 +9,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=

View File

@@ -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
}

View File

@@ -3,12 +3,15 @@ package logger // import "go.unistack.org/micro/v3/logger"
import (
"context"
"os"
)
type ContextAttrFunc func(ctx context.Context) []interface{}
var DefaultContextAttrFuncs []ContextAttrFunc
var (
// DefaultLogger variable
DefaultLogger = NewLogger(WithLevel(ParseLevel(os.Getenv("MICRO_LOG_LEVEL"))))
DefaultLogger Logger = NewLogger()
// DefaultLevel used by logger
DefaultLevel = InfoLevel
// DefaultCallerSkipCount used by logger
@@ -57,7 +60,9 @@ type Logger interface {
Log(ctx context.Context, level Level, args ...interface{})
// Logf logs message with needed level
Logf(ctx context.Context, level Level, msg string, args ...interface{})
// String returns the name of logger
// Name returns broker instance name
Name() string
// String returns the type of logger
String() string
}
@@ -65,76 +70,106 @@ type Logger interface {
type Field interface{}
// Info writes msg to default logger on info level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Info(ctx context.Context, args ...interface{}) {
DefaultLogger.Info(ctx, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Info(ctx, args...)
}
// Error writes msg to default logger on error level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Error(ctx context.Context, args ...interface{}) {
DefaultLogger.Error(ctx, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Error(ctx, args...)
}
// Debug writes msg to default logger on debug level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Debug(ctx context.Context, args ...interface{}) {
DefaultLogger.Debug(ctx, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Debug(ctx, args...)
}
// Warn writes msg to default logger on warn level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Warn(ctx context.Context, args ...interface{}) {
DefaultLogger.Warn(ctx, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Warn(ctx, args...)
}
// Trace writes msg to default logger on trace level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Trace(ctx context.Context, args ...interface{}) {
DefaultLogger.Trace(ctx, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Trace(ctx, args...)
}
// Fatal writes msg to default logger on fatal level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Fatal(ctx context.Context, args ...interface{}) {
DefaultLogger.Fatal(ctx, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Fatal(ctx, args...)
}
// Infof writes formatted msg to default logger on info level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Infof(ctx context.Context, msg string, args ...interface{}) {
DefaultLogger.Infof(ctx, msg, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Infof(ctx, msg, args...)
}
// Errorf writes formatted msg to default logger on error level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Errorf(ctx context.Context, msg string, args ...interface{}) {
DefaultLogger.Errorf(ctx, msg, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Errorf(ctx, msg, args...)
}
// Debugf writes formatted msg to default logger on debug level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Debugf(ctx context.Context, msg string, args ...interface{}) {
DefaultLogger.Debugf(ctx, msg, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Debugf(ctx, msg, args...)
}
// Warnf writes formatted msg to default logger on warn level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Warnf(ctx context.Context, msg string, args ...interface{}) {
DefaultLogger.Warnf(ctx, msg, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Warnf(ctx, msg, args...)
}
// Tracef writes formatted msg to default logger on trace level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Tracef(ctx context.Context, msg string, args ...interface{}) {
DefaultLogger.Tracef(ctx, msg, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Tracef(ctx, msg, args...)
}
// Fatalf writes formatted msg to default logger on fatal level
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Fatalf(ctx context.Context, msg string, args ...interface{}) {
DefaultLogger.Fatalf(ctx, msg, args...)
DefaultLogger.Clone(WithCallerSkipCount(DefaultCallerSkipCount+1)).Fatalf(ctx, msg, args...)
}
// V returns true if passed level enabled in default logger
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func V(level Level) bool {
return DefaultLogger.V(level)
}
// Init initialize logger
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Init(opts ...Option) error {
return DefaultLogger.Init(opts...)
}
// Fields create logger with specific fields
//
// Deprecated: Dont use logger methods directly, use instance of logger to avoid additional allocations
func Fields(fields ...interface{}) Logger {
return DefaultLogger.Fields(fields...)
}

View File

@@ -1,138 +0,0 @@
package logger
import (
"bytes"
"context"
"log"
"testing"
)
func TestContext(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
nl, ok := FromContext(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(WithLevel(TraceLevel), 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(WithLevel(TraceLevel), WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
nl := l.Fields("key", "val")
ctx = NewContext(ctx, nl)
l, ok = 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(WithLevel(TraceLevel), WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
nl := l.Clone(WithLevel(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(WithLevel(TraceLevel), WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
fn := RedirectStdLogger(l, ErrorLevel)
defer fn()
log.Print("test")
if !bytes.Contains(buf.Bytes(), []byte(`"level":"error","msg":"test","timestamp"`)) {
t.Fatalf("logger error, buf %s", buf.Bytes())
}
}
func TestStdLogger(t *testing.T) {
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), WithOutput(buf))
if err := l.Init(); err != nil {
t.Fatal(err)
}
lg := NewStdLogger(l, ErrorLevel)
lg.Print("test")
if !bytes.Contains(buf.Bytes(), []byte(`"level":"error","msg":"test","timestamp"`)) {
t.Fatalf("logger error, buf %s", buf.Bytes())
}
}
func TestLogger(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(WithLevel(TraceLevel), 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","msg":"trace_msg1"`)) {
t.Fatalf("logger error, buf %s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`"warn","msg":"warn_msg1"`)) {
t.Fatalf("logger error, buf %s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`"error":"test","level":"info","msg":"error message"`)) {
t.Fatalf("logger error, buf %s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`"level":"warn","msg":"first second"`)) {
t.Fatalf("logger error, buf %s", buf.Bytes())
}
}

94
logger/noop.go Normal file
View File

@@ -0,0 +1,94 @@
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(_ Level) bool {
return false
}
func (l *noopLogger) Level(_ Level) {
}
func (l *noopLogger) Name() string {
return l.opts.Name
}
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(_ ...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{}) {
}

View File

@@ -3,10 +3,12 @@ package logger
import (
"context"
"io"
"log/slog"
"os"
"time"
)
// Option func
// Option func signature
type Option func(*Options)
// Options holds logger options
@@ -15,31 +17,65 @@ type Options struct {
Out io.Writer
// Context holds exernal options
Context context.Context
// Fields holds additional metadata
Fields []interface{}
// Name holds the logger name
Name string
// The logging level the logger should log
Level Level
// Fields holds additional metadata
Fields []interface{}
// CallerSkipCount number of frmaes to skip
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
// ErroreKey is the key used for the error of the log call
ErrorKey string
// MessageKey is the key used for the message of the log call
MessageKey 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
// The logging level the logger should log
Level Level
// TimeFunc used to obtain current time
TimeFunc func() time.Time
}
// NewOptions creates new options struct
func NewOptions(opts ...Option) Options {
options := Options{
Level: DefaultLevel,
Fields: make([]interface{}, 0, 6),
Out: os.Stderr,
CallerSkipCount: DefaultCallerSkipCount,
Context: context.Background(),
Level: DefaultLevel,
Fields: make([]interface{}, 0, 6),
Out: os.Stderr,
CallerSkipCount: DefaultCallerSkipCount,
Context: context.Background(),
ContextAttrFuncs: DefaultContextAttrFuncs,
AddSource: true,
TimeFunc: time.Now,
}
WithMicroKeys()(&options)
for _, o := range opts {
o(&options)
}
return options
}
// WithContextAttrFuncs appends default funcs for the context arrts filler
func WithContextAttrFuncs(fncs ...ContextAttrFunc) Option {
return func(o *Options) {
o.ContextAttrFuncs = append(o.ContextAttrFuncs, fncs...)
}
}
// WithFields set default fields for the logger
func WithFields(fields ...interface{}) Option {
return func(o *Options) {
@@ -61,6 +97,20 @@ func WithOutput(out io.Writer) Option {
}
}
// WitAddStacktrace controls writing stacktrace on error
func WithAddStacktrace(v bool) Option {
return func(o *Options) {
o.AddStacktrace = v
}
}
// WitAddSource controls writing source file and pos in log
func WithAddSource(v bool) Option {
return func(o *Options) {
o.AddSource = v
}
}
// WithCallerSkipCount set frame count to skip
func WithCallerSkipCount(c int) Option {
return func(o *Options) {
@@ -81,3 +131,54 @@ func WithName(n string) Option {
o.Name = n
}
}
// WithTimeFunc sets the func to obtain current time
func WithTimeFunc(fn func() time.Time) Option {
return func(o *Options) {
o.TimeFunc = fn
}
}
func WithZapKeys() Option {
return func(o *Options) {
o.TimeKey = "@timestamp"
o.LevelKey = "level"
o.MessageKey = "msg"
o.SourceKey = "caller"
o.StacktraceKey = "stacktrace"
o.ErrorKey = "error"
}
}
func WithZerologKeys() Option {
return func(o *Options) {
o.TimeKey = "time"
o.LevelKey = "level"
o.MessageKey = "message"
o.SourceKey = "caller"
o.StacktraceKey = "stacktrace"
o.ErrorKey = "error"
}
}
func WithSlogKeys() Option {
return func(o *Options) {
o.TimeKey = slog.TimeKey
o.LevelKey = slog.LevelKey
o.MessageKey = slog.MessageKey
o.SourceKey = slog.SourceKey
o.StacktraceKey = "stacktrace"
o.ErrorKey = "error"
}
}
func WithMicroKeys() Option {
return func(o *Options) {
o.TimeKey = "timestamp"
o.LevelKey = "level"
o.MessageKey = "msg"
o.SourceKey = "caller"
o.StacktraceKey = "stacktrace"
o.ErrorKey = "error"
}
}

568
logger/slog/slog.go Normal file
View File

@@ -0,0 +1,568 @@
package slog
import (
"context"
"fmt"
"log/slog"
"os"
"regexp"
"runtime"
"strconv"
"sync"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/tracer"
)
var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`)
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.opts.SourceKey
case slog.TimeKey:
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 < 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 {
leveler *slog.LevelVar
handler slog.Handler
opts logger.Options
mu sync.RWMutex
}
func (s *slogLogger) Clone(opts ...logger.Option) logger.Logger {
s.mu.RLock()
options := s.opts
s.mu.RUnlock()
for _, o := range opts {
o(&options)
}
l := &slogLogger{
opts: options,
}
l.leveler = new(slog.LevelVar)
handleOpt := &slog.HandlerOptions{
ReplaceAttr: l.renameAttr,
Level: l.leveler,
AddSource: l.opts.AddSource,
}
l.leveler.Set(loggerToSlogLevel(l.opts.Level))
l.handler = slog.New(slog.NewJSONHandler(options.Out, handleOpt)).With(options.Fields...).Handler()
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 {
s.mu.RLock()
level := s.leveler.Level()
options := s.opts
s.mu.RUnlock()
l := &slogLogger{opts: options}
l.leveler = new(slog.LevelVar)
l.leveler.Set(level)
handleOpt := &slog.HandlerOptions{
ReplaceAttr: l.renameAttr,
Level: l.leveler,
AddSource: l.opts.AddSource,
}
l.handler = slog.New(slog.NewJSONHandler(l.opts.Out, handleOpt)).With(attrs...).Handler()
return l
}
func (s *slogLogger) Init(opts ...logger.Option) error {
s.mu.Lock()
if len(s.opts.ContextAttrFuncs) == 0 {
s.opts.ContextAttrFuncs = logger.DefaultContextAttrFuncs
}
for _, o := range opts {
o(&s.opts)
}
s.leveler = new(slog.LevelVar)
handleOpt := &slog.HandlerOptions{
ReplaceAttr: s.renameAttr,
Level: s.leveler,
AddSource: s.opts.AddSource,
}
s.leveler.Set(loggerToSlogLevel(s.opts.Level))
s.handler = slog.New(slog.NewJSONHandler(s.opts.Out, handleOpt)).With(s.opts.Fields...).Handler()
s.mu.Unlock()
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(s.opts.TimeFunc(), loggerToSlogLevel(lvl), fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = 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[1:]...)
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.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(s.opts.TimeFunc(), loggerToSlogLevel(lvl), msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = 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[1:]...)
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.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(s.opts.TimeFunc(), slog.LevelInfo, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.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(s.opts.TimeFunc(), slog.LevelInfo, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs...)
_ = s.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(s.opts.TimeFunc(), slog.LevelDebug, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.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(s.opts.TimeFunc(), slog.LevelDebug, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs...)
_ = s.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(s.opts.TimeFunc(), slog.LevelDebug-1, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.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(s.opts.TimeFunc(), slog.LevelDebug-1, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.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(s.opts.TimeFunc(), slog.LevelError, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = 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("stacktrace", traceLines[len(traceLines)-1]))
}
}
}
r.Add(attrs[1:]...)
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.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(s.opts.TimeFunc(), slog.LevelError, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = 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("stacktrace", 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.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(s.opts.TimeFunc(), slog.LevelError+1, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.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(s.opts.TimeFunc(), slog.LevelError+1, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs...)
_ = s.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(s.opts.TimeFunc(), slog.LevelWarn, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.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(s.opts.TimeFunc(), slog.LevelWarn, msg, pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.handler.Handle(ctx, r)
}
func (s *slogLogger) Name() string {
return s.opts.Name
}
func (s *slogLogger) String() string {
return "slog"
}
func NewLogger(opts ...logger.Option) logger.Logger {
s := &slogLogger{
opts: logger.NewOptions(opts...),
}
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
}
}

176
logger/slog/slog_test.go Normal file
View File

@@ -0,0 +1,176 @@
package slog
import (
"bytes"
"context"
"fmt"
"log"
"testing"
"go.unistack.org/micro/v3/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, "message", fmt.Errorf("error message"))
if !bytes.Contains(buf.Bytes(), []byte(`"stacktrace":"`)) {
t.Fatalf("logger stacktrace not works, buf contains: %s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`"error":"`)) {
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
}
}
func TestErrorf(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.Errorf(ctx, "message", fmt.Errorf("error message"))
if !bytes.Contains(buf.Bytes(), []byte(`"stacktrace":"`)) {
t.Fatalf("logger stacktrace not works, buf contains: %s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`"error":"`)) {
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
}
}
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())
}
}

View File

@@ -98,11 +98,12 @@ func (md Metadata) Del(keys ...string) {
}
// Copy makes a copy of the metadata
func Copy(md Metadata) Metadata {
func Copy(md Metadata, exclude ...string) Metadata {
nmd := New(len(md))
for key, val := range md {
nmd.Set(key, val)
}
nmd.Del(exclude...)
return nmd
}

View File

@@ -190,3 +190,14 @@ func TestMetadataContext(t *testing.T) {
t.Errorf("Expected metadata length 1 got %d", i)
}
}
func TestCopy(t *testing.T) {
md := New(2)
md.Set("key1", "val1", "key2", "val2")
nmd := Copy(md, "key2")
if len(nmd) != 1 {
t.Fatal("Copy exclude not works")
} else if nmd["Key1"] != "val1" {
t.Fatal("Copy exclude not works")
}
}

View File

@@ -1,5 +1,5 @@
// Package meter is for instrumentation
package meter // import "go.unistack.org/micro/v3/meter"
package meter
import (
"io"
@@ -11,7 +11,7 @@ import (
var (
// DefaultMeter is the default meter
DefaultMeter = NewMeter()
DefaultMeter Meter = NewMeter()
// DefaultAddress data will be made available on this host:port
DefaultAddress = ":9090"
// DefaultPath the meter endpoint where the Meter data will be made available

94
micro.go Normal file
View File

@@ -0,0 +1,94 @@
package micro
import (
"reflect"
"go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v3/flow"
"go.unistack.org/micro/v3/fsm"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v3/resolver"
"go.unistack.org/micro/v3/router"
"go.unistack.org/micro/v3/selector"
"go.unistack.org/micro/v3/server"
"go.unistack.org/micro/v3/store"
"go.unistack.org/micro/v3/sync"
"go.unistack.org/micro/v3/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()

115
micro_test.go Normal file
View File

@@ -0,0 +1,115 @@
package micro
import (
"context"
"fmt"
"reflect"
"testing"
"go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v3/fsm"
)
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 ...broker.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, topic string, msg *broker.Message, opts ...broker.PublishOption) error {
return nil
}
// BatchPublish messages to broker with multiple topics
func (p *bro) BatchPublish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
return nil
}
// BatchSubscribe subscribes to topic messages via handler
func (p *bro) BatchSubscribe(ctx context.Context, topic string, h broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
return nil, nil
}
// Subscribe subscribes to topic message via handler
func (p *bro) Subscribe(ctx context.Context, topic string, handler broker.Handler, opts ...broker.SubscribeOption) (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

@@ -6,6 +6,7 @@ import (
"time"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v3/util/id"
)
@@ -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.Debugf(m.opts.Context, "Register added new service: %s, version: %s", s.Name, 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.Debugf(m.opts.Context, "Register added new node to service: %s, version: %s", s.Name, 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.Debugf(m.opts.Context, "Register removed service: %s", 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.Debugf(m.opts.Context, "Register removed service: %s, version: %s", s.Name, 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 {
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

@@ -6,14 +6,16 @@ import (
"sync"
"testing"
"time"
"go.unistack.org/micro/v3/register"
)
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 +29,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 +39,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 +51,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 +65,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 +80,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 +157,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 +173,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 +202,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 +251,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 +286,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()

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

@@ -18,7 +18,7 @@ var DefaultDomain = "micro"
var (
// DefaultRegister is the global default register
DefaultRegister = NewRegister()
DefaultRegister Register = NewRegister()
// ErrNotFound returned when LookupService is called and no services found
ErrNotFound = errors.New("service not found")
// ErrWatcherStopped returned when when watcher is stopped

View File

@@ -1,5 +1,5 @@
// Package resolver resolves network names to addresses
package resolver // import "go.unistack.org/micro/v3/resolver"
package resolver
// Resolver is network resolver. It's used to find network nodes
// via the name to connect to. This is done based on Network.Name().

View File

@@ -7,7 +7,7 @@ import (
var (
// DefaultRouter is the global default router
DefaultRouter = NewRouter()
DefaultRouter Router = NewRouter()
// DefaultNetwork is default micro network
DefaultNetwork = "micro"
// ErrRouteNotFound is returned when no route was found in the routing table

22
semconv/broker.go Normal file
View File

@@ -0,0 +1,22 @@
package semconv
var (
// PublishMessageDurationSeconds specifies meter metric name
PublishMessageDurationSeconds = "publish_message_duration_seconds"
// PublishMessageLatencyMicroseconds specifies meter metric name
PublishMessageLatencyMicroseconds = "publish_message_latency_microseconds"
// PublishMessageTotal specifies meter metric name
PublishMessageTotal = "publish_message_total"
// PublishMessageInflight specifies meter metric name
PublishMessageInflight = "publish_message_inflight"
// SubscribeMessageDurationSeconds specifies meter metric name
SubscribeMessageDurationSeconds = "subscribe_message_duration_seconds"
// SubscribeMessageLatencyMicroseconds specifies meter metric name
SubscribeMessageLatencyMicroseconds = "subscribe_message_latency_microseconds"
// SubscribeMessageTotal specifies meter metric name
SubscribeMessageTotal = "subscribe_message_total"
// SubscribeMessageInflight specifies meter metric name
SubscribeMessageInflight = "subscribe_message_inflight"
// BrokerGroupLag specifies broker lag
BrokerGroupLag = "broker_group_lag"
)

12
semconv/client.go Normal file
View File

@@ -0,0 +1,12 @@
package semconv
var (
// ClientRequestDurationSeconds specifies meter metric name
ClientRequestDurationSeconds = "client_request_duration_seconds"
// ClientRequestLatencyMicroseconds specifies meter metric name
ClientRequestLatencyMicroseconds = "client_request_latency_microseconds"
// ClientRequestTotal specifies meter metric name
ClientRequestTotal = "client_request_total"
// ClientRequestInflight specifies meter metric name
ClientRequestInflight = "client_request_inflight"
)

12
semconv/server.go Normal file
View File

@@ -0,0 +1,12 @@
package semconv
var (
// ServerRequestDurationSeconds specifies meter metric name
ServerRequestDurationSeconds = "server_request_duration_seconds"
// ServerRequestLatencyMicroseconds specifies meter metric name
ServerRequestLatencyMicroseconds = "server_request_latency_microseconds"
// ServerRequestTotal specifies meter metric name
ServerRequestTotal = "server_request_total"
// ServerRequestInflight specifies meter metric name
ServerRequestInflight = "server_request_inflight"
)

View File

@@ -15,6 +15,7 @@ import (
"go.unistack.org/micro/v3/network/transport"
"go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v3/register"
msync "go.unistack.org/micro/v3/sync"
"go.unistack.org/micro/v3/tracer"
"go.unistack.org/micro/v3/util/id"
)
@@ -47,7 +48,7 @@ type Options struct {
// Listener may be passed if already created
Listener net.Listener
// Wait group
Wait *sync.WaitGroup
Wait *msync.WaitGroup
// TLSConfig specifies tls.Config for secure serving
TLSConfig *tls.Config
// Metadata holds the server metadata
@@ -86,6 +87,8 @@ type Options struct {
DeregisterAttempts int
// Hooks may contains SubscriberWrapper, HandlerWrapper 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
@@ -108,6 +111,7 @@ func NewOptions(opts ...Option) Options {
Version: DefaultVersion,
ID: id.Must(),
Namespace: DefaultNamespace,
GracefulTimeout: DefaultGracefulTimeout,
}
for _, o := range opts {
@@ -279,7 +283,7 @@ func Wait(wg *sync.WaitGroup) Option {
if wg == nil {
wg = new(sync.WaitGroup)
}
o.Wait = wg
o.Wait = msync.WrapWaitGroup(wg)
}
}
@@ -321,6 +325,13 @@ func Listener(l net.Listener) Option {
// HandlerOption func
type HandlerOption func(*HandlerOptions)
// GracefulTimeout duration
func GracefulTimeout(td time.Duration) Option {
return func(o *Options) {
o.GracefulTimeout = td
}
}
// HandlerOptions struct
type HandlerOptions struct {
// Context holds external options

View File

@@ -11,7 +11,7 @@ import (
)
// DefaultServer default server
var DefaultServer = NewServer()
var DefaultServer Server = NewServer()
var (
// DefaultAddress will be used if no address passed, use secure localhost
@@ -34,6 +34,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

@@ -342,9 +342,6 @@ func (n *noopServer) createSubHandler(sb *subscriber, opts Options) broker.Handl
hdr := metadata.New(len(msg.Header))
for k, v := range msg.Header {
if k == "Content-Type" {
continue
}
hdr.Set(k, v)
}

View File

@@ -72,8 +72,8 @@ func RegisterSubscriber(topic string, s server.Server, h interface{}, opts ...se
}
type service struct {
sync.RWMutex
opts Options
sync.RWMutex
}
// NewService creates and returns a new Service based on the packages within.
@@ -88,6 +88,7 @@ func (s *service) Name() string {
// Init initialises options. Additionally it calls cmd.Init
// which parses command line flags. cmd.Init is only called
// on first Init.
//
//nolint:gocyclo
func (s *service) Init(opts ...Option) error {
var err error
@@ -375,17 +376,13 @@ func (s *service) Run() error {
return s.Stop()
}
type nameIface interface {
type Namer 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 {
func getNameIndex[T Namer](n string, ifaces []T) int {
for idx, iface := range ifaces {
if iface.Name() == n {
return idx
}
}

View File

@@ -17,6 +17,22 @@ import (
"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.Fatalf("invalid client %#+v", svc.Options().Clients)
}
}
type testItem struct {
name string
}
@@ -25,12 +41,11 @@ func (ti *testItem) Name() string {
return ti.name
}
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")
func Test_getNameIndex(t *testing.T) {
items := []*testItem{{name: "test1"}, {name: "test2"}}
idx := getNameIndex("test2", items)
if items[idx].Name() != "test2" {
t.Fatal("getNameIndex wrong")
}
}

View File

@@ -12,7 +12,7 @@ var (
// ErrInvalidKey is returned when a key has empty or have invalid format
ErrInvalidKey = errors.New("invalid key")
// DefaultStore is the global default store
DefaultStore = NewStore()
DefaultStore Store = NewStore()
// DefaultSeparator is the gloabal default key parts separator
DefaultSeparator = "/"
)

69
sync/waitgroup.go Normal file
View File

@@ -0,0 +1,69 @@
package sync
import (
"context"
"sync"
)
type WaitGroup struct {
wg *sync.WaitGroup
c int
mu sync.Mutex
}
func WrapWaitGroup(wg *sync.WaitGroup) *WaitGroup {
g := &WaitGroup{
wg: wg,
}
return g
}
func NewWaitGroup() *WaitGroup {
var wg sync.WaitGroup
return WrapWaitGroup(&wg)
}
func (g *WaitGroup) Add(n int) {
g.mu.Lock()
g.c += n
g.wg.Add(n)
g.mu.Unlock()
}
func (g *WaitGroup) Done() {
g.mu.Lock()
g.c += -1
g.wg.Add(-1)
g.mu.Unlock()
}
func (g *WaitGroup) Wait() {
g.wg.Wait()
}
func (g *WaitGroup) WaitContext(ctx context.Context) {
done := make(chan struct{})
go func() {
g.wg.Wait()
close(done)
}()
select {
case <-ctx.Done():
g.mu.Lock()
g.wg.Add(-g.c)
<-done
g.wg.Add(g.c)
g.mu.Unlock()
return
case <-done:
return
}
}
func (g *WaitGroup) Waiters() int {
g.mu.Lock()
c := g.c
g.mu.Unlock()
return c
}

37
sync/waitgroup_test.go Normal file
View File

@@ -0,0 +1,37 @@
package sync
import (
"context"
"testing"
"time"
)
func TestWaitGroupContext(t *testing.T) {
wg := NewWaitGroup()
_ = t
wg.Add(1)
ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
defer cancel()
wg.WaitContext(ctx)
}
func TestWaitGroupReuse(t *testing.T) {
wg := NewWaitGroup()
defer func() {
if wg.Waiters() != 0 {
t.Fatal("lost goroutines")
}
}()
wg.Add(1)
defer wg.Done()
ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
defer cancel()
wg.WaitContext(ctx)
wg.Add(1)
defer wg.Done()
ctx, cancel = context.WithTimeout(context.TODO(), 1*time.Second)
defer cancel()
wg.WaitContext(ctx)
}

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

@@ -0,0 +1,139 @@
package memory
import (
"context"
"time"
"go.unistack.org/micro/v3/tracer"
"go.unistack.org/micro/v3/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 ...tracer.SpanOption) (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 ...tracer.Option) error {
for _, o := range opts {
o(&t.opts)
}
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(_ ...tracer.SpanOption) {
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 ...tracer.EventOption) {
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 ...tracer.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/v3/logger"
"go.unistack.org/micro/v3/logger/slog"
"go.unistack.org/micro/v3/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

@@ -2,6 +2,8 @@ package tracer
import (
"context"
"go.unistack.org/micro/v3/util/id"
)
var _ Tracer = (*noopTracer)(nil)
@@ -24,6 +26,8 @@ func (t *noopTracer) Start(ctx context.Context, name string, opts ...SpanOption)
labels: options.Labels,
kind: options.Kind,
}
span.spanID.s, _ = id.New()
span.traceID.s, _ = id.New()
if span.ctx == nil {
span.ctx = context.Background()
}
@@ -31,6 +35,14 @@ func (t *noopTracer) Start(ctx context.Context, name string, opts ...SpanOption)
return NewSpanContext(ctx, span), span
}
type noopStringer struct {
s string
}
func (s noopStringer) String() string {
return s.s
}
func (t *noopTracer) Flush(ctx context.Context) error {
return nil
}
@@ -56,6 +68,8 @@ type noopSpan struct {
tracer Tracer
name string
statusMsg string
traceID noopStringer
spanID noopStringer
events []*noopEvent
labels []interface{}
logs []interface{}
@@ -63,7 +77,15 @@ type noopSpan struct {
status SpanStatus
}
func (s *noopSpan) Finish(opts ...SpanOption) {
func (s *noopSpan) TraceID() string {
return s.traceID.String()
}
func (s *noopSpan) SpanID() string {
return s.spanID.String()
}
func (s *noopSpan) Finish(_ ...SpanOption) {
}
func (s *noopSpan) Context() context.Context {

View File

@@ -100,13 +100,13 @@ type EventOption func(o *EventOptions)
func WithEventLabels(kv ...interface{}) EventOption {
return func(o *EventOptions) {
o.Labels = kv
o.Labels = append(o.Labels, kv...)
}
}
func WithSpanLabels(kv ...interface{}) SpanOption {
return func(o *SpanOptions) {
o.Labels = kv
o.Labels = append(o.Labels, kv...)
}
}
@@ -159,7 +159,8 @@ func NewSpanOptions(opts ...SpanOption) SpanOptions {
// NewOptions returns default options
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
Logger: logger.DefaultLogger,
Context: context.Background(),
}
for _, o := range opts {
o(&options)

View File

@@ -3,12 +3,32 @@ package tracer // import "go.unistack.org/micro/v3/tracer"
import (
"context"
"fmt"
"sort"
"go.unistack.org/micro/v3/logger"
)
// DefaultTracer is the global default tracer
var DefaultTracer = NewTracer()
var DefaultTracer Tracer = 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 {
@@ -43,38 +63,8 @@ type Span interface {
AddLogs(kv ...interface{})
// Kind returns span kind
Kind() SpanKind
}
// sort labels alphabeticaly by label name
type byKey []interface{}
func (k byKey) Len() int { return len(k) / 2 }
func (k byKey) Less(i, j int) bool { return fmt.Sprintf("%s", k[i*2]) < fmt.Sprintf("%s", k[j*2]) }
func (k byKey) Swap(i, j int) {
k[i*2], k[j*2] = k[j*2], k[i*2]
k[i*2+1], k[j*2+1] = k[j*2+1], k[i*2+1]
}
func UniqLabels(labels []interface{}) []interface{} {
if len(labels)%2 == 1 {
labels = labels[:len(labels)-1]
}
if len(labels) > 2 {
sort.Sort(byKey(labels))
idx := 0
for {
if labels[idx] == labels[idx+2] {
copy(labels[idx:], labels[idx+2:])
labels = labels[:len(labels)-2]
} else {
idx += 2
}
if idx+2 >= len(labels) {
break
}
}
}
return labels
// TraceID returns trace id
TraceID() string
// SpanID returns span id
SpanID() string
}

View File

@@ -1,6 +1,7 @@
package reflect // import "go.unistack.org/micro/v3/util/reflect"
package reflect
import (
"encoding/json"
"errors"
"fmt"
"reflect"
@@ -45,15 +46,23 @@ func SliceAppend(b bool) Option {
// Merge merges map[string]interface{} to destination struct
func Merge(dst interface{}, mp map[string]interface{}, opts ...Option) error {
var err error
var sval reflect.Value
var fname string
options := Options{}
for _, o := range opts {
o(&options)
}
if unmarshaler, ok := dst.(json.Unmarshaler); ok {
buf, err := json.Marshal(mp)
if err == nil {
err = unmarshaler.UnmarshalJSON(buf)
}
return err
}
var err error
var sval reflect.Value
var fname string
dviface := reflect.ValueOf(dst)
if dviface.Kind() == reflect.Ptr {
dviface = dviface.Elem()
@@ -508,3 +517,74 @@ func FieldName(name string) string {
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
}

View File

@@ -133,3 +133,16 @@ func TestMergeNested(t *testing.T) {
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")
}
}

40
util/sort/sort.go Normal file
View File

@@ -0,0 +1,40 @@
package sort
import (
"fmt"
"sort"
)
// sort labels alphabeticaly by label name
type byKey []interface{}
func (k byKey) Len() int { return len(k) / 2 }
func (k byKey) Less(i, j int) bool { return fmt.Sprintf("%s", k[i*2]) < fmt.Sprintf("%s", k[j*2]) }
func (k byKey) Swap(i, j int) {
k[i*2], k[j*2] = k[j*2], k[i*2]
k[i*2+1], k[j*2+1] = k[j*2+1], k[i*2+1]
}
func Uniq(labels []interface{}) []interface{} {
if len(labels)%2 == 1 {
labels = labels[:len(labels)-1]
}
if len(labels) > 2 {
sort.Sort(byKey(labels))
idx := 0
for {
if labels[idx] == labels[idx+2] {
copy(labels[idx:], labels[idx+2:])
labels = labels[:len(labels)-2]
} else {
idx += 2
}
if idx+2 >= len(labels) {
break
}
}
}
return labels
}

View File

@@ -35,8 +35,8 @@ func TestUnmarshalJSON(t *testing.T) {
err = json.Unmarshal([]byte(`{"ttl":"1y"}`), v)
if err != nil {
t.Fatal(err)
} else if v.TTL != 31536000000000000 {
t.Fatalf("invalid duration %v != 31536000000000000", v.TTL)
} else if v.TTL != 31622400000000000 {
t.Fatalf("invalid duration %v != 31622400000000000", v.TTL)
}
}
@@ -55,7 +55,7 @@ func TestParseDuration(t *testing.T) {
if err != nil {
t.Fatalf("ParseDuration error: %v", err)
}
if td.String() != "8760h0m0s" {
t.Fatalf("ParseDuration 1y != 8760h0m0s : %s", td.String())
if td.String() != "8784h0m0s" {
t.Fatalf("ParseDuration 1y != 8784h0m0s : %s", td.String())
}
}