✨ Update dependencies, enhance NATS consumer configuration, and improve error handling in message processing. Introduce telemetry support with OpenTelemetry for tracing and metrics. Refactor README to include new configuration options and update tests for improved coverage of error scenarios.
This commit is contained in:
+90
-18
@@ -3,11 +3,16 @@ package app
|
||||
import (
|
||||
"caatsm/internal/adapter"
|
||||
"caatsm/internal/adapter/parser"
|
||||
"caatsm/internal/domain"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -40,11 +45,20 @@ func (p *MessageProcessor) Handle(ctx context.Context, raw []byte, msgID string)
|
||||
return Permanent(fmt.Errorf("empty message"))
|
||||
}
|
||||
|
||||
tracer := otel.Tracer("caatsm/app")
|
||||
ctx, span := tracer.Start(ctx, "MessageProcessor.Handle")
|
||||
defer span.End()
|
||||
span.SetAttributes(attribute.String("nats.msg_id", msgID))
|
||||
|
||||
receivedAt := time.Now()
|
||||
|
||||
parsed := p.parser.Parse(string(raw))
|
||||
parsed, parseErr := p.parser.Parse(string(raw))
|
||||
if parsed == nil {
|
||||
return Permanent(fmt.Errorf("parser returned nil"))
|
||||
parsed = domain.NewParsedMessage()
|
||||
parsed.Content = string(raw)
|
||||
parsed.ErrorReason = "parser returned nil"
|
||||
parsed.Status = domain.MessageStatusBodyError
|
||||
parseErr = fmt.Errorf("parser returned nil")
|
||||
}
|
||||
|
||||
if msgID != "" {
|
||||
@@ -60,44 +74,102 @@ func (p *MessageProcessor) Handle(ctx context.Context, raw []byte, msgID string)
|
||||
if parsed.ParsedAt.IsZero() {
|
||||
parsed.ParsedAt = time.Now()
|
||||
}
|
||||
if parsed.Status == domain.MessageStatusUnknown {
|
||||
if parseErr == nil {
|
||||
parsed.Status = domain.MessageStatusParsed
|
||||
} else {
|
||||
parsed.Status = domain.MessageStatusBodyError
|
||||
}
|
||||
}
|
||||
|
||||
if parseErr != nil || !parsed.Parsed {
|
||||
if parsed.ErrorReason == "" && parseErr != nil {
|
||||
parsed.ErrorReason = parseErr.Error()
|
||||
}
|
||||
span.RecordError(parseErr)
|
||||
span.SetStatus(codes.Error, parseErr.Error())
|
||||
p.persistRaw(ctx, parsed)
|
||||
p.logger.Warn("Message failed to parse",
|
||||
zap.String("msg_id", msgID),
|
||||
zap.String("status", string(parsed.Status)),
|
||||
zap.String("content_preview", truncateContent(parsed.Content, 256)),
|
||||
zap.Error(parseErr),
|
||||
)
|
||||
return Permanent(fmt.Errorf("parser error: %w", parseErr))
|
||||
}
|
||||
parsed.ErrorReason = ""
|
||||
|
||||
// Log parsing result
|
||||
if !parsed.Parsed {
|
||||
p.logger.Warn("Message not parsed",
|
||||
zap.String("msg_id", msgID),
|
||||
zap.String("message_id", parsed.MessageID),
|
||||
zap.String("category", parsed.Category),
|
||||
zap.String("content_preview", truncateContent(parsed.Content, 256)),
|
||||
)
|
||||
} else {
|
||||
p.logger.Info("Message parsed successfully",
|
||||
zap.String("msg_id", msgID),
|
||||
zap.String("message_id", parsed.MessageID),
|
||||
zap.String("category", parsed.Category),
|
||||
zap.Time("received_at", parsed.ReceivedAt),
|
||||
zap.Time("parsed_at", parsed.ParsedAt),
|
||||
)
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.String("telegram.status", string(parsed.Status)),
|
||||
attribute.Bool("telegram.parsed", parsed.Parsed),
|
||||
attribute.String("telegram.category", parsed.Category),
|
||||
)
|
||||
|
||||
p.logger.Info("Message parsed successfully",
|
||||
zap.String("msg_id", msgID),
|
||||
zap.String("message_id", parsed.MessageID),
|
||||
zap.String("category", parsed.Category),
|
||||
zap.Time("received_at", parsed.ReceivedAt),
|
||||
zap.Time("parsed_at", parsed.ParsedAt),
|
||||
)
|
||||
|
||||
// Insert into database
|
||||
if err := p.repository.InsertOne(ctx, parsed); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return fmt.Errorf("failed to insert message: %w", err)
|
||||
}
|
||||
|
||||
// Publish parsed message
|
||||
_, pubSpan := tracer.Start(ctx, "Publisher.Publish")
|
||||
if err := p.publisher.Publish(parsed); err != nil {
|
||||
// Log error but don't fail the entire operation
|
||||
p.logger.Error("Failed to publish message",
|
||||
zap.String("msg_id", msgID),
|
||||
zap.Error(err),
|
||||
)
|
||||
pubSpan.RecordError(err)
|
||||
pubSpan.SetStatus(codes.Error, err.Error())
|
||||
parsed.Status = domain.MessageStatusPublishFail
|
||||
parsed.ErrorReason = err.Error()
|
||||
p.persistRaw(ctx, parsed)
|
||||
// Mark as permanent so the consumer will ack instead of retrying
|
||||
pubSpan.End()
|
||||
return Permanent(fmt.Errorf("failed to publish message: %w", err))
|
||||
}
|
||||
pubSpan.End()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MessageProcessor) persistRaw(ctx context.Context, msg *domain.ParsedMessage) {
|
||||
if msg == nil || p.repository == nil {
|
||||
return
|
||||
}
|
||||
if msg.Content == "" && msg.BodyData != nil {
|
||||
msg.Content = fmt.Sprintf("%v", msg.BodyData)
|
||||
}
|
||||
if msg.ReceivedAt.IsZero() {
|
||||
msg.ReceivedAt = time.Now()
|
||||
}
|
||||
if err := p.repository.InsertRaw(ctx, msg); err != nil {
|
||||
p.logger.Error("Failed to persist raw telegram",
|
||||
zap.String("message_id", msg.MessageID),
|
||||
zap.String("status", string(msg.Status)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
if span := trace.SpanFromContext(ctx); span.IsRecording() {
|
||||
span.AddEvent("raw telegram persisted",
|
||||
trace.WithAttributes(
|
||||
attribute.String("telegram.status", string(msg.Status)),
|
||||
attribute.String("telegram.message_id", msg.MessageID),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func truncateContent(content string, limit int) string {
|
||||
if limit <= 0 || len(content) <= limit {
|
||||
return content
|
||||
|
||||
Reference in New Issue
Block a user