✨ Enhance observability and error handling in NATS integration. Introduce comprehensive OpenTelemetry support with environment-based sampling and semantic attributes for tracing and metrics. Implement an advisory dead-letter queue (DLQ) handler for managing message delivery failures. Update NATS consumer to utilize structured logging and improve error handling strategies. Refactor configuration files for OpenTelemetry collector in both development and production environments, ensuring robust telemetry integration. Enhance documentation to reflect new features and best practices for observability.
This commit is contained in:
@@ -2,9 +2,7 @@ package nats
|
||||
|
||||
import (
|
||||
"caatsm/internal/infra/config"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -65,22 +63,7 @@ func ProvideJetStream(nc *nats.Conn, cfg *config.Config, logger *zap.Logger) (na
|
||||
return nil, fmt.Errorf("failed to get JetStream context: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the stream exists and is minimally aligned with configuration.
|
||||
if err := EnsureStream(js, cfg, logger); err != nil {
|
||||
nc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return js, nil
|
||||
}
|
||||
|
||||
// EnsureStream ensures that the configured JetStream stream exists and has
|
||||
// at least the expected subjects bound. It is safe to call multiple times.
|
||||
//
|
||||
// In dev/test environments (see shouldBootstrapStream), the stream will be
|
||||
// auto-created if it does not exist. In production, a missing stream results
|
||||
// in an error so that operators can intervene.
|
||||
func EnsureStream(js nats.JetStreamContext, cfg *config.Config, logger *zap.Logger) error {
|
||||
// Ensure the stream exists using StreamManager
|
||||
streamName := cfg.NATS.Stream
|
||||
consumerSubject := cfg.EffectiveSubscriptionTopic()
|
||||
publisherSubject := strings.TrimSpace(cfg.Publisher.Topic)
|
||||
@@ -92,130 +75,15 @@ func EnsureStream(js nats.JetStreamContext, cfg *config.Config, logger *zap.Logg
|
||||
zap.String("consumer_subject", consumerSubject),
|
||||
zap.String("publisher_subject", publisherSubject),
|
||||
)
|
||||
return fmt.Errorf("no subjects configured for JetStream stream %s", streamName)
|
||||
nc.Close()
|
||||
return nil, fmt.Errorf("no subjects configured for JetStream stream %s", streamName)
|
||||
}
|
||||
|
||||
streamLimits := cfg.NATS.StreamLimits
|
||||
storage := nats.FileStorage
|
||||
switch strings.ToLower(streamLimits.Storage) {
|
||||
case "memory":
|
||||
storage = nats.MemoryStorage
|
||||
case "file":
|
||||
storage = nats.FileStorage
|
||||
streamManager := NewStreamManagerWithConfig(js, streamName, streamSubjects, &cfg.NATS.StreamLimits, logger)
|
||||
if err := streamManager.EnsureStream(); err != nil {
|
||||
nc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
discard := nats.DiscardOld
|
||||
if strings.EqualFold(streamLimits.Discard, "new") {
|
||||
discard = nats.DiscardNew
|
||||
}
|
||||
|
||||
streamConfig := &nats.StreamConfig{
|
||||
Name: streamName,
|
||||
Subjects: streamSubjects,
|
||||
Retention: nats.LimitsPolicy,
|
||||
MaxMsgs: streamLimits.MaxMsgs,
|
||||
MaxBytes: streamLimits.MaxBytes,
|
||||
MaxAge: streamLimits.MaxAge,
|
||||
Discard: discard,
|
||||
Storage: storage,
|
||||
Replicas: streamLimits.Replicas,
|
||||
}
|
||||
|
||||
info, err := js.StreamInfo(streamName)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrStreamNotFound) {
|
||||
if shouldBootstrapStream() {
|
||||
if _, err = js.AddStream(streamConfig); err != nil {
|
||||
logger.Error("failed to create stream",
|
||||
zap.String("stream", streamName),
|
||||
zap.Strings("subjects", streamSubjects),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to create stream %s: %w", streamName, err)
|
||||
}
|
||||
logger.Info("Created JetStream stream",
|
||||
zap.String("stream", streamName),
|
||||
zap.Strings("subjects", streamSubjects),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
logger.Error("stream not found and auto-creation disabled",
|
||||
zap.String("stream", streamName),
|
||||
zap.Strings("expected_subjects", streamSubjects),
|
||||
)
|
||||
return fmt.Errorf("stream %s not found and auto-creation disabled", streamName)
|
||||
}
|
||||
logger.Error("failed to fetch stream info",
|
||||
zap.String("stream", streamName),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to fetch stream info for %s: %w", streamName, err)
|
||||
}
|
||||
|
||||
// Stream exists: validate subjects but do not fail hard if they differ.
|
||||
validateStreamConfig(info, streamSubjects, logger)
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldBootstrapStream() bool {
|
||||
switch strings.ToLower(os.Getenv("GO_ENV")) {
|
||||
case "", "dev", "development", "test", "testing":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateStreamConfig(info *nats.StreamInfo, expectedSubjects []string, logger *zap.Logger) {
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if len(expectedSubjects) == 0 {
|
||||
expectedSubjects = []string{"<none>"}
|
||||
}
|
||||
}()
|
||||
|
||||
missing := make([]string, 0)
|
||||
for _, subj := range expectedSubjects {
|
||||
if subj == "" {
|
||||
continue
|
||||
}
|
||||
if !containsSubject(info.Config.Subjects, subj) {
|
||||
missing = append(missing, subj)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
logger.Warn("JetStream stream subjects missing expected entries",
|
||||
zap.String("stream", info.Config.Name),
|
||||
zap.Strings("stream_subjects", info.Config.Subjects),
|
||||
zap.Strings("missing_subjects", missing),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func containsSubject(subjects []string, target string) bool {
|
||||
for _, s := range subjects {
|
||||
if s == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func dedupeSubjects(subjects []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
result := make([]string, 0, len(subjects))
|
||||
for _, subj := range subjects {
|
||||
subj = strings.TrimSpace(subj)
|
||||
if subj == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[subj]; ok {
|
||||
continue
|
||||
}
|
||||
seen[subj] = struct{}{}
|
||||
result = append(result, subj)
|
||||
}
|
||||
return result
|
||||
return js, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user