mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* tweak a bunch of logging and config make overall logs less verbose and chatty 1 minute computer check-in interval 3 minute snapshot interval remove CaptureLogger as it has same functionality as buffer logger add a WithPrefix to the Logger interface so sub-services can have different prefixes * fix some lint * fix lint... confused why this is coming up now
238 lines
8.5 KiB
Go
238 lines
8.5 KiB
Go
package kinesis
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/sqs"
|
|
"github.com/aws/aws-sdk-go/service/sqs/sqsiface"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
)
|
|
|
|
// ErrorType signifies the type of error encountered.
|
|
type ErrorType string
|
|
|
|
// These are the currently supported ErrorType values that can be emitted
|
|
// to an ErrorStore.
|
|
const (
|
|
RecoverableErrorType = ErrorType("Error")
|
|
PanicErrorType = ErrorType("Panic") // Runtime errors.
|
|
)
|
|
|
|
// ErrorStore is an abstraction over a resource like external storage, a database,
|
|
// a queue, etc. that can receive and store error messages.
|
|
type ErrorStore interface {
|
|
// Available checks if the backing resource can receive error
|
|
// messages via Push.
|
|
Available() bool
|
|
|
|
// Push emits an error message of type ErrorType to the backing resource.
|
|
//
|
|
// The caller should NOT assume that Available is called implicitly
|
|
// to check for ErrorStore availability.
|
|
Push(ErrorType, string, logger.Logger) error
|
|
}
|
|
|
|
// SinkErrorPayload contains all data about a single IDK error message that will be
|
|
// emitted to an ErrorStore; includes a timestamp and a valid sink ID.
|
|
//
|
|
// This payload is not meant to be used outside the context of error propogation to
|
|
// an ErrorStore.
|
|
type SinkErrorPayload struct {
|
|
SinkId string `json:"sink_id"`
|
|
ErrorType ErrorType `json:"error_type"`
|
|
ErrorMessage string `json:"error_msg"`
|
|
Timestamp string `json:"time"`
|
|
}
|
|
|
|
// SinkErrorQueue is an ErrorStore implementation that uses an SQS queue as its backing
|
|
// resource to emit error and panic messages to.
|
|
//
|
|
// It also maps 1-to-1 to a Kinesis stream via a unique sink ID.
|
|
type SinkErrorQueue struct {
|
|
sinkId string
|
|
name string
|
|
url string
|
|
queue sqsiface.SQSAPI
|
|
}
|
|
|
|
// NewSinkErrorQueue attempts to construct a SinkErrorQueue instance from an AWS SQS client,
|
|
// a queue name, and a sink ID.
|
|
//
|
|
// On success, callers can assume that a backing SQS queue resource exists and is fully initialized.
|
|
//
|
|
// Returns nil and an SQS error if an SQS queue URL cannot be resolved from the queue name and/or
|
|
// the AWS SQS client.
|
|
//
|
|
// This method assumes the sink ID argument is valid.
|
|
func NewSinkErrorQueue(queue sqsiface.SQSAPI, queueName, sinkId string) (*SinkErrorQueue, error) {
|
|
input := &sqs.GetQueueUrlInput{QueueName: &queueName}
|
|
output, err := queue.GetQueueUrl(input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &SinkErrorQueue{sinkId, queueName, *output.QueueUrl, queue}, nil
|
|
}
|
|
|
|
// SinkErrorQueueFrom always constructs a SinkErrorQueue instance from an AWS SQS client and a
|
|
// kinesis.Source.
|
|
//
|
|
// Unlike NewSinkErrorQueue, this does NOT return an error if a queue URL cannot be resolved
|
|
// from the queue name and/or the AWS SQS client. Instead it will collapse to a SinkErrorQueue
|
|
// instance with a backing SQS resource that is ALWAYS unavailable. Attempting to invoke Push
|
|
// on this instance will not result in an error; instead it will just emit a warning that no
|
|
// backing SQS resource could be written to.
|
|
//
|
|
// This does check if the sink ID has a valid form: 'PREFIX'-VALID_UUID. If not, this collapses
|
|
// to a SinkErrorQueue instance that is ALWAYS unavailable.
|
|
func SinkErrorQueueFrom(queue sqsiface.SQSAPI, source *Source) *SinkErrorQueue {
|
|
// The below failure conditions are handled by collapsing to a no-op ErrorStore.Push implementation.
|
|
// - A missing SQS queue name.
|
|
// - An invalid sink ID (i.e. not a valid UUID); valid sink ID: "PREFIX"-UUID.
|
|
// - Unable to resolve queue URL from queue name.
|
|
//
|
|
// Means downstream ErrorStreamLogger behaves identical to its embedded Logger.
|
|
if source.ErrorQueueName == "" {
|
|
return &SinkErrorQueue{}
|
|
}
|
|
|
|
sinkId := strings.Join(strings.Split(source.StreamName, "-")[1:], "-")
|
|
_, err := uuid.Parse(sinkId)
|
|
if err != nil {
|
|
// Keep invalid sink ID around in case something downstream wants to log.
|
|
return &SinkErrorQueue{sinkId, source.ErrorQueueName, "", nil}
|
|
}
|
|
|
|
sinkErrorQueue, err := NewSinkErrorQueue(queue, source.ErrorQueueName, sinkId)
|
|
if err != nil {
|
|
return &SinkErrorQueue{sinkId, source.ErrorQueueName, "", nil}
|
|
}
|
|
|
|
return sinkErrorQueue
|
|
}
|
|
|
|
// Available checks that a valid SQS queue resource exists.
|
|
func (seq *SinkErrorQueue) Available() bool {
|
|
return seq.url != "" && seq.queue != nil
|
|
}
|
|
|
|
// Push attempts to emit a single error message of type ErrorType to a SQS queue resource.
|
|
//
|
|
// If the backing SQS queue resource is not available, this function is a no-op and does NOT
|
|
// return an error. Instead it emits a warning to the logger.Logger instance specified by the log
|
|
// argument.
|
|
//
|
|
// The warning can be ignored entirely by passing a nil log argument.
|
|
func (seq *SinkErrorQueue) Push(errorType ErrorType, message string, log logger.Logger) error {
|
|
if !seq.Available() {
|
|
if log != nil {
|
|
log.Warnf("Not pushing errors to an SQS queue='%+v' due to unavailability.", seq)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
payload := SinkErrorPayload{
|
|
SinkId: seq.sinkId,
|
|
ErrorType: errorType,
|
|
ErrorMessage: message,
|
|
Timestamp: time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(&payload)
|
|
if err != nil {
|
|
msgTemplate := "Unable to marshal payload='%+v' to send message='%s' to queue='%+v'."
|
|
return errors.Wrap(err, fmt.Sprintf(msgTemplate, payload, message, seq.queue))
|
|
}
|
|
|
|
encodedPayload := base64.URLEncoding.EncodeToString(payloadBytes)
|
|
input := &sqs.SendMessageInput{
|
|
DelaySeconds: aws.Int64(10),
|
|
MessageBody: aws.String(encodedPayload),
|
|
QueueUrl: &seq.url,
|
|
}
|
|
|
|
_, err = seq.queue.SendMessage(input)
|
|
if err != nil {
|
|
msgTemplate := "Unable to send message='%s' to queue='%+v' with input='%+v'."
|
|
return errors.Wrap(err, fmt.Sprintf(msgTemplate, message, seq.queue, input))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ErrorStreamLogger is a logger.Logger implementation that decorates a base logger.Logger instance
|
|
// and emits error and panic messages to an ErrorStore.
|
|
//
|
|
// All other log levels delegate to the base logger.Logger implementation.
|
|
type ErrorStreamLogger struct {
|
|
base logger.Logger
|
|
store ErrorStore
|
|
}
|
|
|
|
// NewErrorStreamLogger constructs an ErrorStreamLogger from a logger.Logger and an ErrorStore.
|
|
func NewErrorStreamLogger(base logger.Logger, store ErrorStore) *ErrorStreamLogger {
|
|
return &ErrorStreamLogger{base, store}
|
|
}
|
|
|
|
// Printf just delegates to the wrapped/base logger.Logger's Printf implementation.
|
|
func (esl *ErrorStreamLogger) Printf(format string, v ...interface{}) {
|
|
esl.base.Printf(format, v...)
|
|
}
|
|
|
|
// Debugf just delegates to the wrapped/base logger.Logger's Debugf implementation.
|
|
func (esl *ErrorStreamLogger) Debugf(format string, v ...interface{}) {
|
|
esl.base.Debugf(format, v...)
|
|
}
|
|
|
|
// Infof just delegates to the wrapped/base logger.Logger's Infof implementation.
|
|
func (esl *ErrorStreamLogger) Infof(format string, v ...interface{}) {
|
|
esl.base.Infof(format, v...)
|
|
}
|
|
|
|
// Warnf just delegates to the wrapped/base logger.Logger's Warnf implementation.
|
|
func (esl *ErrorStreamLogger) Warnf(format string, v ...interface{}) {
|
|
esl.base.Warnf(format, v...)
|
|
}
|
|
|
|
// Errorf delegates to the wrapped/base logger.Logger's Errorf implementation and additionally/
|
|
// pushes an error message with RecoverableErrorType ErrorType to the ErrorStore.
|
|
//
|
|
// If an error occurs during a Push to the ErrorStore, the error is logged to the wrapped/base
|
|
// logger.Logger using Errorf.
|
|
func (esl *ErrorStreamLogger) Errorf(format string, v ...interface{}) {
|
|
esl.base.Errorf(format, v...)
|
|
|
|
err := esl.store.Push(RecoverableErrorType, fmt.Sprintf(format, v...), esl)
|
|
if err != nil {
|
|
errMsg := fmt.Sprintf("Failed during push to store='%+v'", esl.store)
|
|
esl.base.Errorf(errors.Wrap(err, errMsg).Error())
|
|
}
|
|
}
|
|
|
|
// Panicf delegates to the wrapped/base logger.Logger's Panicf implementation and additionally
|
|
// pushes an error message with PanicErrorType ErrorType to the ErrorStore.
|
|
//
|
|
// If an error occurs during a Push to the ErrorStore, the error is logged to the wrapped/base
|
|
// logger.Logger using Errorf NOT Panicf.
|
|
func (esl *ErrorStreamLogger) Panicf(format string, v ...interface{}) {
|
|
esl.base.Panicf(format, v...)
|
|
|
|
err := esl.store.Push(PanicErrorType, fmt.Sprintf(format, v...), esl)
|
|
if err != nil {
|
|
// A (recoverable) error occurred during a push to the store, so
|
|
// log that error using `Errorf` not `Panicf`.
|
|
errMsg := fmt.Sprintf("Failed during push to store='%+v'", esl.store)
|
|
esl.base.Errorf(errors.Wrap(err, errMsg).Error())
|
|
}
|
|
}
|
|
|
|
func (esl *ErrorStreamLogger) WithPrefix(prefix string) logger.Logger {
|
|
return NewErrorStreamLogger(esl.base.WithPrefix(prefix), esl.store)
|
|
}
|