featurebase/idk/kinesis/logger.go
Matthew Jaffee 903e234c69
tweak a bunch of logging and config (#2234)
* 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
2023-02-03 14:59:07 -06:00

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