mirror of
https://github.com/hanzozt/zt.git
synced 2026-10-30 15:04:55 +00:00
314 lines
8.1 KiB
Go
314 lines
8.1 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package events
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus"
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var contentType = "application/json"
|
|
|
|
type ServiceBusEventLoggerFactory struct{}
|
|
|
|
func (ServiceBusEventLoggerFactory) NewEventHandler(config map[interface{}]interface{}) (interface{}, error) {
|
|
return NewServiceBusEventLogger(fabricFormatterFactory{}, config)
|
|
}
|
|
|
|
type serviceBusWriteCloser struct {
|
|
client *azservicebus.Client
|
|
sender *azservicebus.Sender
|
|
config *serviceBusConfig
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
messages chan []byte
|
|
}
|
|
|
|
func newServiceBusWriteCloser(config *serviceBusConfig) serviceBusWriteCloser {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
ret := serviceBusWriteCloser{
|
|
config: config,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
messages: make(chan []byte, config.bufferSize),
|
|
}
|
|
go func() {
|
|
ret.connect()
|
|
for {
|
|
select {
|
|
case m := <-ret.messages:
|
|
{
|
|
ret.sendMessage(m)
|
|
}
|
|
case <-ret.ctx.Done():
|
|
{
|
|
var retErr error
|
|
if ret.sender != nil {
|
|
if err := ret.sender.Close(context.Background()); err != nil {
|
|
retErr = err
|
|
}
|
|
}
|
|
if ret.client != nil {
|
|
if err := ret.client.Close(context.Background()); err != nil {
|
|
if retErr != nil {
|
|
retErr = fmt.Errorf("%v; %w", retErr, err)
|
|
} else {
|
|
retErr = err
|
|
}
|
|
}
|
|
}
|
|
if retErr != nil {
|
|
logrus.Errorf("error closing service bus connections: %v", retErr)
|
|
return
|
|
}
|
|
logrus.Info("closed connection to service bus")
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return ret
|
|
}
|
|
|
|
func (wc *serviceBusWriteCloser) sendMessage(message []byte) {
|
|
for {
|
|
select {
|
|
case <-wc.ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
sbMessage := &azservicebus.Message{
|
|
ContentType: &contentType,
|
|
Body: message,
|
|
}
|
|
|
|
err := wc.sender.SendMessage(context.Background(), sbMessage, nil)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
// Check if it's a connection error that requires reconnection
|
|
if isConnectionError(err) {
|
|
logrus.Info("Need to attempt reconnect to service bus")
|
|
wc.connect()
|
|
continue
|
|
}
|
|
|
|
// Enhanced error logging with more details
|
|
logEntry := logrus.WithFields(logrus.Fields{
|
|
"body": string(message),
|
|
"error": err.Error(),
|
|
"error_type": fmt.Sprintf("%T", err),
|
|
})
|
|
|
|
// Add destination information if available
|
|
if wc.config.topicName != "" {
|
|
logEntry = logEntry.WithField("destination", "topic:"+wc.config.topicName)
|
|
} else if wc.config.queueName != "" {
|
|
logEntry = logEntry.WithField("destination", "queue:"+wc.config.queueName)
|
|
}
|
|
|
|
// Add response error details if available
|
|
var respErr *azcore.ResponseError
|
|
if errors.As(err, &respErr) {
|
|
logEntry = logEntry.WithFields(logrus.Fields{
|
|
"status_code": respErr.StatusCode,
|
|
"raw_response": respErr.RawResponse,
|
|
})
|
|
}
|
|
|
|
logEntry.Error("error sending message to service bus")
|
|
}
|
|
}
|
|
|
|
func isConnectionError(err error) bool {
|
|
// Check for common connection-related errors
|
|
var respErr *azcore.ResponseError
|
|
if errors.As(err, &respErr) {
|
|
// Check for connection-related status codes
|
|
switch respErr.StatusCode {
|
|
case 401, 403, 404, 408, 500, 502, 503, 504:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check for context errors
|
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
|
return true
|
|
}
|
|
|
|
// Check for network-related errors
|
|
// Note: azservicebus doesn't export ErrConnectionClosed, so we check for specific error types
|
|
if err != nil {
|
|
// Check if it's a connection-related error by examining the error message
|
|
errMsg := err.Error()
|
|
if strings.Contains(errMsg, "connection") || strings.Contains(errMsg, "network") || strings.Contains(errMsg, "timeout") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (wc *serviceBusWriteCloser) connect() {
|
|
expBackoff := backoff.NewExponentialBackOff()
|
|
expBackoff.InitialInterval = 1 * time.Second
|
|
expBackoff.MaxInterval = 5 * time.Minute
|
|
expBackoff.MaxElapsedTime = 0
|
|
|
|
operation := func() error {
|
|
select {
|
|
case <-wc.ctx.Done():
|
|
return backoff.Permanent(nil)
|
|
default:
|
|
}
|
|
|
|
// Create client options
|
|
clientOptions := &azservicebus.ClientOptions{}
|
|
|
|
// Create the client
|
|
client, err := azservicebus.NewClientFromConnectionString(wc.config.connectionString, clientOptions)
|
|
if err != nil {
|
|
logrus.Errorf("unable to create service bus client: %v", err)
|
|
return err
|
|
}
|
|
wc.client = client
|
|
|
|
// Create sender - support both topic and queue
|
|
var sender *azservicebus.Sender
|
|
if wc.config.topicName != "" {
|
|
sender, err = client.NewSender(wc.config.topicName, nil)
|
|
} else if wc.config.queueName != "" {
|
|
sender, err = client.NewSender(wc.config.queueName, nil)
|
|
} else {
|
|
return fmt.Errorf("either topic or queue must be specified")
|
|
}
|
|
if err != nil {
|
|
logrus.Errorf("error creating service bus sender: %v", err)
|
|
return err
|
|
}
|
|
wc.sender = sender
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := backoff.Retry(operation, expBackoff); err != nil {
|
|
logrus.Errorf("service bus connection failed after exponential backoff: %v", err)
|
|
return
|
|
}
|
|
|
|
// Log the connection success with appropriate destination
|
|
if wc.config.topicName != "" {
|
|
logrus.Infof("connected to service bus topic: %s", wc.config.topicName)
|
|
} else {
|
|
logrus.Infof("connected to service bus queue: %s", wc.config.queueName)
|
|
}
|
|
}
|
|
|
|
func (wc serviceBusWriteCloser) Write(data []byte) (int, error) {
|
|
select {
|
|
case wc.messages <- data:
|
|
return len(data), nil
|
|
default:
|
|
return 0, fmt.Errorf("service bus queue full. Message: %s", string(data))
|
|
}
|
|
}
|
|
|
|
func (wc serviceBusWriteCloser) Close() error {
|
|
wc.cancel()
|
|
return nil
|
|
}
|
|
|
|
type serviceBusConfig struct {
|
|
connectionString string
|
|
topicName string
|
|
queueName string
|
|
bufferSize int
|
|
}
|
|
|
|
func parseServiceBusConfig(config map[interface{}]interface{}) (*serviceBusConfig, error) {
|
|
ret := &serviceBusConfig{
|
|
bufferSize: 50,
|
|
}
|
|
|
|
if value, found := config["connectionString"]; !found {
|
|
return nil, fmt.Errorf("missing service bus connection string")
|
|
} else {
|
|
if u, ok := value.(string); ok {
|
|
ret.connectionString = u
|
|
} else {
|
|
return nil, fmt.Errorf("invalid service bus connection string")
|
|
}
|
|
}
|
|
|
|
// Check for topic or queue configuration
|
|
if value, found := config["topic"]; found {
|
|
if u, ok := value.(string); ok {
|
|
ret.topicName = u
|
|
} else {
|
|
return nil, fmt.Errorf("invalid service bus topic name")
|
|
}
|
|
} else if value, found := config["queue"]; found {
|
|
if u, ok := value.(string); ok {
|
|
ret.queueName = u
|
|
} else {
|
|
return nil, fmt.Errorf("invalid service bus queue name")
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("either topic or queue must be specified for service bus")
|
|
}
|
|
|
|
if value, found := config["bufferSize"]; found {
|
|
if u, ok := value.(int); ok {
|
|
ret.bufferSize = u
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func NewServiceBusEventLogger(formatterFactory LoggingHandlerFactory, config map[interface{}]interface{}) (interface{}, error) {
|
|
bufferSize := 10
|
|
if value, found := config["bufferSize"]; found {
|
|
if size, ok := value.(int); ok {
|
|
bufferSize = size
|
|
}
|
|
}
|
|
|
|
conf, err := parseServiceBusConfig(config)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to parse service bus config")
|
|
}
|
|
|
|
if value, found := config["format"]; found {
|
|
if format, ok := value.(string); ok {
|
|
return formatterFactory.NewLoggingHandler(format, bufferSize, newServiceBusWriteCloser(conf))
|
|
}
|
|
return nil, errors.New("invalid 'format' for event service bus log")
|
|
}
|
|
return nil, errors.New("'format' must be specified for event handler")
|
|
}
|