Skip to content

Commit 900f24f

Browse files
author
Guy Baron
authored
v1.1.4 rollup into master (#179)
* inceased outbox VARCHAR column length to 2048 (#155) * added reply to initiator functionality to sagas (#157) * added generic handler metrics with message type as the label (#144) * added generic handler metrics with message type as the label * add handler name label to the metrics * adding new metrics to the read me * Fix handle empty body (#156) * set the correct Type and Content-Type headers on out going messages (#160) * set the correct Type and Content-Type headers on out going messages * refactoring * fixing ReplyToInitiator not working when initiator sends a message via the RPC interface (#163) * fixing ReplyToInitiator not working when initiator sends a message via the RPC interface * Improved wording of saga documentation article (#164) * better wording for documentation * added golangcli lint configuration and fixed linting failures (#165) * fixed logging issues (#167) * allow getting the saga id of the current invoked saga (#168) * setting the target saga id on the saga correlation id field (#171) * support emperror (#174) * setting the target saga id on the saga correlation id field * added emperror support * Fix logging and added logging documentation (#176) * fixed logging issues and added documentation logging via the invocation interface was broken and did not add contextual data related to the invocation due to a bug in the way the Glogged structure is currently implemented. Also added documentation on how logging should be done within a handler including adding context to returned errors so that data gets logged * added missing documentation file * added documentation on serialization support (#177) * fixed emperror url format * added serialization documentation
1 parent d13f2c2 commit 900f24f

34 files changed

Lines changed: 950 additions & 139 deletions

.golangci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
linters-settings:
2+
golint:
3+
# minimal confidence for issues, default is 0.8
4+
min-confidence: 0.8
5+
gocyclo:
6+
min-complexity: 15
7+
8+
govet:
9+
# report about shadowed variables
10+
check-shadowing: false
11+
12+
# settings per analyzer
13+
settings:
14+
printf: # analyzer name, run `go tool vet help` to see all analyzers
15+
funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
16+
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
17+
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
18+
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
19+
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
20+
linters:
21+
disable-all: true
22+
enable:
23+
- deadcode
24+
# - errcheck
25+
- gosimple
26+
- govet
27+
- ineffassign
28+
# - staticcheck
29+
# - typecheck
30+
- unused
31+
# - varcheck
32+
# - deadcode
33+
# - dupl
34+
# - gocritic
35+
- gocyclo
36+
- golint
37+
- misspell

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ A lightweight transactional message bus on top of RabbitMQ supporting:
2323
7) [Structured logging](https://github.com/wework/grabbit/blob/master/docs/LOGGING.md)
2424
8) Reporting [Metrics](https://github.com/wework/grabbit/blob/master/docs/METRICS.md) via Prometheus
2525
9) Distributed [Tracing](https://github.com/wework/grabbit/blob/master/docs/TRACING.md) via OpenTracing
26+
10) [Extensible serialization](https://github.com/wework/grabbit/blob/master/docs/SERIALIZATION.md) with
27+
default support for gob, protobuf and avro
2628

2729
## Stable release
2830
the v1.x branch contains the latest stable releases of grabbit and one should track that branch to get point and minor release updates.

docs/LOGGING.md

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,100 @@
11
# Logging
22

3+
### Logging within a handler
4+
35
grabbit supports structured logging via the [logrus](https://github.com/sirupsen/logrus) logging package.
46
The logger is accessible to message handlers via the past in invocation instance.
7+
When logging via the logger exposed on the passed invocation each entry you log will be
8+
annotated with the following contextual data (added as logrus fields to the log entry)
9+
allowing for a better debugging experience.
10+
11+
- _service: the service name
12+
- handler_name: the name of the handler being invoked
13+
- message_id: the id of the processed message
14+
- message_name: the type of the message that is being processed
15+
- routing_key: the routing_key of the message
16+
- saga_id: the id of the saga instance being invoked
17+
- saga_def: the type of the saga that is being invoked
518

619
```go
720

821
func SomeHandler(invocation gbus.Invocation, message *gbus.BusMessage) error{
9-
invocation.Log().WithField("name", "rhinof").Info("handler invoked")
22+
invocation.Log().WithField("name", "rhinof").Info("performing some business logic")
1023
return nil
1124
}
1225

1326
```
27+
### Logging contextual data when a handler errors
28+
29+
In cases a message handler errors it is common to log custom contextual data allowing
30+
service developers to diagnose the root cause of the error.
31+
32+
```go
33+
package my_handlers
34+
35+
import (
36+
"gitub.com/wework/grabbit/gbus"
37+
)
38+
39+
func SomeHandler(invocation gbus.Invocation, message *gbus.BusMessage) error{
40+
invocation.Log().WithField("name", "rhinof").Info("performing some business logic")
41+
PlaceOrderCommand := message.Payload.(*PlaceOrderCommand)
42+
e := placeOrder(PlaceOrderCommand.CustomerID, PlaceOrderCommand.LineItems)
43+
if e != nil{
44+
invocation.Log().
45+
WithField("customer_id", PlaceOrderCommand.CustomerID).
46+
Error("failed placing order for customer")
47+
return e
48+
}
49+
return nil
50+
}
51+
```
52+
grabbit makes it easier handling these cases and reduce the repetitive task of logging
53+
these custom contextual attributes in cases of errors by integrating the [emperror errors package](https://github.com/emperror/errors).
54+
emperror drop-in replacement for the default errors package allows developers to add the needed contextual data on the error instance and have graabit log the error with all contextual attribute.
55+
56+
```go
57+
package my_handlers
58+
59+
import (
60+
"emperror.dev/errors"
61+
"gitub.com/wework/grabbit/gbus"
62+
)
63+
64+
func SomeHandler(invocation gbus.Invocation, message *gbus.BusMessage) error{
65+
invocation.Log().WithField("name", "rhinof").Info("performing some business logic")
66+
PlaceOrderCommand := message.Payload.(*PlaceOrderCommand)
67+
return placeOrder(PlaceOrderCommand.CustomerID, PlaceOrderCommand.LineItems)
68+
}
69+
70+
func placeOrder(customerID string, lineItems LineItems[]) error{
71+
72+
if(someErrorCondition()){
73+
return errors.WithDetails("failed placing order for customer", "customer_id", customerID)
74+
}
75+
76+
return nil
77+
}
78+
79+
```
80+
81+
82+
83+
### Setting a custom logger instance
1484

1585
grabbit will create a default instance of logrus FieldLogger if no such logger is set when the bus is created.
16-
In order to set a custom logger when creating the bus you need to call the Builder.WithLogger method passing it
86+
To set a custom logger when creating the bus you need to call the Builder.WithLogger method passing it
1787
a logrus.FieldLogger instance.
1888

1989
```go
20-
90+
logger := logrus.New().WithField("my_custom_key", "my_custom_value")
91+
bus := builder.
92+
New().
93+
Bus("rabbitmq connection string").
94+
WithLogger(logger).
95+
WorkerNum(3, 1).
96+
WithConfirms().
97+
Txnl("mysql", "").
98+
Build("your service name")
2199

22100
```

docs/METRICS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ grabbit exposes and reports the following metrics to Prometheus
44

55
| Namespace | Subsystem | Name | Description |
66
| ------------- | ------------- | ----------------------------------| --------------------------------------------------------------------------- |
7-
| grabbit | handler | [name of message handler]_result | records and counts each succesfull or failed execution of a message handler |
8-
| grabbit | handler | [name of message handler]_latency | records the execution time of each handler |
7+
| grabbit | handlers | [name of message handler]_result | records and counts each successful or failed execution of a message handler |
8+
| grabbit | handlers | [name of message handler]_latency | records the execution time of each handler |
9+
| grabbit | handlers | result | records and counts each run of a handler, having the handler's name, message type and the result as labels|
10+
| grabbit | handlers | latency | records the execution time of each run of a handler, having the handler's name, message type as labels|
911
| grabbit | messages | rejected_messages | increments each time a message gets rejected |
1012
| grabbit | saga | timedout_sagas | counting the number of timedout saga instances |

docs/SAGA.md

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type BookVacationSaga struct {
2222
BookingId string
2323
GotCarSvcResponse bool
2424
GotHotelSvcResponse bool
25+
SomeConfigData string
2526
}
2627
```
2728

@@ -171,7 +172,7 @@ func (s *BookVacationSaga) HandleBookFlightResponse(invocation gbus.Invocation,
171172

172173
}
173174
```
174-
### Step 4 - Handling the timeout requirement
175+
### Step 4 - Handling the timeout requirement
175176

176177
In order to define a timeout for the saga and have grabbit call the saga instance once that timeout is reached (assuming the saga hasn't completed yet) the saga needs to implement the gbus.RequestSagaTimeout interface
177178

@@ -191,7 +192,7 @@ func (s *BookVacationSaga) TimeoutDuration() time.Duration {
191192
return time.Minute * 15
192193
}
193194

194-
func (s *BookVacationSaga) Timeout(invocation gbus.Invocation, message *gbus.BusMessage) error {
195+
func (s *BookVacationSaga) Timeout(tx *sql.Tx, bus Messaging) error {
195196
return bus.Publish(context.Background(), "some_exchange", "some.topic.1", gbus.NewBusMessage(VacationBookingTimedOut{}))
196197
}
197198

@@ -202,7 +203,7 @@ func (s *BookVacationSaga) Timeout(invocation gbus.Invocation, message *gbus.Bus
202203
```go
203204

204205
gb := getBus("vacationSvc")
205-
gb.RegisterSaga(BookVacationSaga{})
206+
gb.RegisterSaga(&BookVacationSaga{})
206207

207208
```
208209

@@ -218,3 +219,54 @@ It is recommended to follow [semantic versioning](https://semver.org/) of the go
218219

219220
grabbit automatically implements an optimistic concurrency model when processing a message and persisting saga instances, detecting when the saga state turns stale due to processing concurrently a different message.
220221
When the above is detected grabbit will rollback the bounded transaction and retry the execution of the saga.
222+
223+
### Configuring a Saga Instance
224+
225+
It is sometimes necessary to configure a saga instance with some data before it gets executed.
226+
grrabit allows you to do so by providing a saga configuration function when registering the saga.
227+
Each time a saga instance gets created or inflated from the persistent store the configuration function will be executed.
228+
229+
The saga configuration function accepts a single gbus.Saga parameter and returns a single gbus.Saga return value.
230+
The passed in gbus.Saga is the instance that will be executed and will be the type of the saga being registered meaning it can safely be casted to your specific saga type.
231+
Once you casted to the specific saga type you can configure the instance and access its fields as needed.
232+
After the instance is configured the function returns the configured saga instance so grabbit can proceed and execute it.
233+
234+
The following snippet is an example of how to pass in a saga configuration function
235+
```go
236+
configSaga := func(saga gbus.Saga) gbus.Saga {
237+
s := saga.(*BookVacationSaga)
238+
s.SomeConfigData = "config value"
239+
return s
240+
}
241+
svc1.RegisterSaga(&BookVacationSaga{}, configSaga)
242+
243+
```
244+
245+
### Replying to the saga initiator
246+
247+
It is common that during its life cycle a saga will need to report back and send messages with the service that initiated it (sent the command that started the saga).
248+
In the example above when the booking has completed we would like to send a message to the service which initiated the booking saga.
249+
The way we have implemented this in the example above is by publishing an event which the service which initiated the saga would need to subscribe to and handle to get notified when the booking is complete.
250+
251+
Although the above would work it won't be an elegant solution especially if the initiator of the saga is another saga since it means that the initiating saga will need to filter all events and select the single event that correlates to that particular instance.
252+
To relive client code to do so grabbit provides a way for a saga to directly send a message to its initiator, and if the initiator is another saga grabbit will automatically correlate the message with the correct saga instance and invoke the relevant handler.
253+
254+
To send a message to the saga initiator the message handler attached to the saga instance will need to cast the passed in gbus.Invocation argument to a gbus.SagaInvocation and then invoke the ReplyToInitiator function.
255+
We can replace the following code from the above example
256+
257+
```go
258+
if s.IsComplete(){
259+
event := gbus.NewBusMessage(VacationBookingComplete{})
260+
invocation.Bus().Publish(invocation.Ctx(), "some_exchange", "some.topic", event)
261+
}
262+
```
263+
264+
to this:
265+
266+
```go
267+
sagaInvocation := invocation.(gbus.SagaInvocation)
268+
if s.IsComplete(){
269+
msg := gbus.NewBusMessage(VacationBookingComplete{})
270+
sagaInvocation.ReplyToInitiator(invocation.Ctx(), msg)
271+
}
272+
```

docs/SERIALIZATION.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Serialization
2+
3+
grabbit supports out of the box three serializers to format messages over the wire
4+
5+
- gob (defualt)
6+
- protobuf
7+
- avro (experimental)
8+
9+
To configure a bus to work with a different serializer than the default one you must call the WithSerializer function call on the builder interface passing it an instance of a gbus.Serializer.
10+
11+
The following example configures the bus to work with the protobuf serializer
12+
13+
```go
14+
package main
15+
16+
import (
17+
"github.com/sirupsen/logrus"
18+
"github.com/wework/grabbit/gbus"
19+
"github.com/wework/grabbit/gbus/builder"
20+
"github.com/wework/grabbit/gbus/serialization"
21+
)
22+
23+
logger := logrus.New()
24+
bus := builder.
25+
New().
26+
Bus("rabbitmq connection string").
27+
WithLogger(logger).
28+
WithSerializer(serialization.NewProtoSerializer(logger)).
29+
WorkerNum(3, 1).
30+
WithConfirms().
31+
Txnl("mysql", "database connection string").
32+
Build("your service name")
33+
34+
```
35+

gbus/abstractions.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,26 @@ type Invocation interface {
226226
Bus() Messaging
227227
Tx() *sql.Tx
228228
Ctx() context.Context
229+
InvokingSvc() string
229230
Routing() (exchange, routingKey string)
230231
DeliveryInfo() DeliveryInfo
231232
}
232233

234+
/*
235+
SagaInvocation allows saga instances to reply to their creator even when not in the conext of handling
236+
the message that starts the saga.
237+
A message handler that is attached to a saga instance can safly cast the passed in invocation to SagaInvocation
238+
and use the ReplyToInitiator function to send a message to the originating service that sent the message that started the saga
239+
*/
240+
type SagaInvocation interface {
241+
ReplyToInitiator(ctx context.Context, message *BusMessage) error
242+
//HostingSvc returns the svc name that is executing the service
243+
HostingSvc() string
244+
245+
//SagaID returns the saga id of the currently invoked saga instance
246+
SagaID() string
247+
}
248+
233249
//Serializer is the base interface for all message serializers
234250
type Serializer interface {
235251
Name() string
@@ -247,6 +263,7 @@ type TxProvider interface {
247263

248264
//TxOutbox abstracts the transactional outgoing channel type
249265
type TxOutbox interface {
266+
Logged
250267
Save(tx *sql.Tx, exchange, routingKey string, amqpMessage amqp.Publishing) error
251268
Start(amqpOut *AMQPOutbox) error
252269
Stop() error

gbus/builder/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func (builder *defaultBuilder) Build(svcName string) gbus.Bus {
8989
}
9090
}
9191
gb.Outbox = mysql.NewOutbox(gb.SvcName, mysqltx, builder.purgeOnStartup)
92+
gb.Outbox.SetLogger(gb.Log())
9293
timeoutManager = mysql.NewTimeoutManager(gb, gb.TxProvider, gb.Log, svcName, builder.purgeOnStartup)
9394

9495
default:
@@ -107,6 +108,7 @@ func (builder *defaultBuilder) Build(svcName string) gbus.Bus {
107108
}
108109
}
109110
glue := saga.NewGlue(gb, sagaStore, svcName, gb.TxProvider, gb.Log, timeoutManager)
111+
glue.SetLogger(gb.Log())
110112
gb.Glue = glue
111113
return gb
112114
}

gbus/bus.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -655,12 +655,13 @@ func (b *DefaultBus) sendImpl(sctx context.Context, tx *sql.Tx, toService, reply
655655
}
656656

657657
msg := amqp.Publishing{
658-
Body: buffer,
659-
ReplyTo: replyTo,
660-
MessageId: message.ID,
661-
CorrelationId: message.CorrelationID,
662-
ContentEncoding: b.Serializer.Name(),
663-
Headers: headers,
658+
Type: message.PayloadFQN,
659+
Body: buffer,
660+
ReplyTo: replyTo,
661+
MessageId: message.ID,
662+
CorrelationId: message.CorrelationID,
663+
ContentType: b.Serializer.Name(),
664+
Headers: headers,
664665
}
665666
span.LogFields(message.GetTraceLog()...)
666667

gbus/invocation.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var _ Messaging = &defaultInvocationContext{}
1313

1414
type defaultInvocationContext struct {
1515
*Glogged
16-
invocingSvc string
16+
invokingSvc string
1717
bus *DefaultBus
1818
inboundMsg *BusMessage
1919
tx *sql.Tx
@@ -29,8 +29,8 @@ type DeliveryInfo struct {
2929
MaxRetryCount uint
3030
}
3131

32-
func (dfi *defaultInvocationContext) Log() logrus.FieldLogger {
33-
return dfi.Glogged.Log().WithFields(logrus.Fields{"routing_key": dfi.routingKey, "message_id": dfi.inboundMsg.ID})
32+
func (dfi *defaultInvocationContext) InvokingSvc() string {
33+
return dfi.invokingSvc
3434
}
3535

3636
//Reply implements the Invocation.Reply signature
@@ -43,9 +43,9 @@ func (dfi *defaultInvocationContext) Reply(ctx context.Context, replyMessage *Bu
4343
var err error
4444

4545
if dfi.tx != nil {
46-
return dfi.bus.sendWithTx(ctx, dfi.tx, dfi.invocingSvc, replyMessage)
46+
return dfi.bus.sendWithTx(ctx, dfi.tx, dfi.invokingSvc, replyMessage)
4747
}
48-
if err = dfi.bus.Send(ctx, dfi.invocingSvc, replyMessage); err != nil {
48+
if err = dfi.bus.Send(ctx, dfi.invokingSvc, replyMessage); err != nil {
4949
//TODO: add logs?
5050
logrus.WithError(err).Error("could not send reply")
5151

0 commit comments

Comments
 (0)