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:
windyboy
2025-11-18 11:47:35 +08:00
parent 5d05237283
commit 7f44b5389d
31 changed files with 2546 additions and 476 deletions
+8 -140
View File
@@ -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
}