Update Go module dependencies and enhance NATS consumer metrics. Add github.com/kylelemons/godebug as an indirect dependency. Upgrade github.com/prometheus/common to version 0.67.3. Change NATS consumer mode to "jetstream" in the development configuration. Improve observability by ensuring metrics for pending messages are initialized and recorded correctly in JetStream mode. Update documentation to reflect changes in metrics handling and consumer behavior.

This commit is contained in:
windyboy
2025-11-19 16:49:53 +08:00
parent f01dd3bf1f
commit 8914156a58
10 changed files with 261 additions and 49 deletions
+15 -1
View File
@@ -255,7 +255,21 @@ func RecordJSAPICall(operation string) {
// JetStream consumer as a gauge, enabling backlog / lag alerts.
func RecordNATSConsumerPending(stream, consumer string, pending uint64) {
ensureCollectors()
natsConsumerPending.WithLabelValues(labelValue(stream), labelValue(consumer)).Set(float64(pending))
// Validate inputs to ensure metric is recorded correctly
streamLabel := labelValue(stream)
consumerLabel := labelValue(consumer)
// Ensure metric is always set, even with empty labels (will be "unknown")
if streamLabel == "" {
streamLabel = "unknown"
}
if consumerLabel == "" {
consumerLabel = "unknown"
}
// Set the metric value
natsConsumerPending.WithLabelValues(streamLabel, consumerLabel).Set(float64(pending))
}
func labelValue(value string) string {
+64 -13
View File
@@ -3,9 +3,12 @@ package metrics
import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus/testutil"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -16,6 +19,10 @@ func TestMetrics(t *testing.T) {
}
var _ = Describe("Metrics", func() {
BeforeEach(func() {
resetMetricsForTest()
})
Describe("Handler", func() {
It("serves metrics with the correct content type", func() {
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
@@ -29,25 +36,69 @@ var _ = Describe("Metrics", func() {
})
Describe("labelValue helper", func() {
It("returns unknown for empty values and lowercases input", func() {
It("normalizes empty and uppercase values", func() {
Expect(labelValue(" ")).To(Equal("unknown"))
Expect(labelValue("SOME_VALUE")).To(Equal("some_value"))
})
})
Describe("record helpers", func() {
It("can be invoked without panicking", func() {
Expect(func() {
RecordProcessed("parsed", "ARR", 150*time.Millisecond)
RecordFailure("parser")
RecordMessageHandled("TEST", "consumer", ResultOK, 205*time.Millisecond)
RecordRetry("TEST", "consumer", RetryReasonProcessorError)
RecordDLQMessage("TEST", "consumer")
RecordDLQPublishFailure("TEST", "consumer")
RecordDBQuery("insert", DBResultOK, 10*time.Millisecond)
RecordJSAPICall("publish")
RecordNATSConsumerPending("TEST", "consumer", 7)
}).NotTo(Panic())
It("record counters and histograms for message processing", func() {
RecordProcessed("PARSED", "ARR", 150*time.Millisecond)
RecordFailure("parser")
RecordMessageHandled("stream1", "consumer1", ResultOK, 80*time.Millisecond)
RecordRetry("stream1", "consumer1", RetryReasonProcessorError)
RecordDLQMessage("stream1", "consumer1")
RecordDLQPublishFailure("stream1", "consumer1")
RecordPublishFailure("flightPlan")
RecordDBQuery("insert_one", DBResultOK, 10*time.Millisecond)
RecordJSAPICall("publish")
Expect(testutil.ToFloat64(processedCounter.WithLabelValues("parsed", "arr"))).To(BeNumerically("==", 1))
Expect(testutil.CollectAndCount(parseLatency, MetricParseLatencySeconds)).To(Equal(1))
Expect(testutil.ToFloat64(failureCounter.WithLabelValues("parser"))).To(BeNumerically("==", 1))
Expect(testutil.ToFloat64(messagesTotal.WithLabelValues("stream1", "consumer1", ResultOK))).To(BeNumerically("==", 1))
Expect(testutil.CollectAndCount(handleLatency, MetricHandleLatencySeconds)).To(Equal(1))
Expect(testutil.ToFloat64(retriesTotal.WithLabelValues("stream1", "consumer1", RetryReasonProcessorError))).To(BeNumerically("==", 1))
Expect(testutil.ToFloat64(dlqMessagesTotal.WithLabelValues("stream1", "consumer1"))).To(BeNumerically("==", 1))
Expect(testutil.ToFloat64(dlqPublishFailures.WithLabelValues("stream1", "consumer1"))).To(BeNumerically("==", 1))
Expect(testutil.ToFloat64(publishFailuresTotal.WithLabelValues("flightplan"))).To(BeNumerically("==", 1))
Expect(testutil.ToFloat64(dbQueriesTotal.WithLabelValues("insert_one", DBResultOK))).To(BeNumerically("==", 1))
Expect(testutil.CollectAndCount(dbQueryLatency, MetricDBQueryLatencySeconds)).To(Equal(1))
Expect(testutil.ToFloat64(jsAPICallsTotal.WithLabelValues("publish"))).To(BeNumerically("==", 1))
})
It("records gauge values for pending messages", func() {
RecordNATSConsumerPending("STREAM_A", "consumerA", 42)
Expect(testutil.ToFloat64(natsConsumerPending.WithLabelValues("stream_a", "consumera"))).To(BeNumerically("==", 42))
RecordNATSConsumerPending("STREAM_A", "consumerA", 5)
Expect(testutil.ToFloat64(natsConsumerPending.WithLabelValues("stream_a", "consumera"))).To(BeNumerically("==", 5))
})
})
})
func resetMetricsForTest() {
once = sync.Once{}
registry = nil
processedCounter = nil
failureCounter = nil
parseLatency = nil
messagesTotal = nil
handleLatency = nil
retriesTotal = nil
jsAPICallsTotal = nil
dlqMessagesTotal = nil
dlqPublishFailures = nil
publishFailuresTotal = nil
dbQueriesTotal = nil
dbQueryLatency = nil
natsConsumerPending = nil
}