From bad0dc1373b16b2fd473c1f53c1dd902812d0e4c Mon Sep 17 00:00:00 2001 From: Nilesh Chate Date: Sun, 1 Mar 2026 17:27:03 +0100 Subject: [PATCH] add configurable Prometheus metrics with panic recovery and tests --- .coverage | 73 ++++---- dynamo_global_index.go | 6 +- dynamo_global_index_interface.go | 4 +- dynamo_repository.go | 6 +- dynamo_repository_interface.go | 4 +- metrics_prometheus.go | 133 +++++++++++++-- metrics_prometheus_test.go | 44 ++--- mock/dynamo_global_index_interface.go | 36 ++-- mock/dynamo_repository_interface.go | 60 ++++--- mock/log_interface.go | 20 ++- mock/metrics_interface.go | 10 +- prometheus_config_test.go | 230 ++++++++++++++++++++++++++ 12 files changed, 502 insertions(+), 124 deletions(-) create mode 100644 prometheus_config_test.go diff --git a/.coverage b/.coverage index b02f0c6..7518c21 100644 --- a/.coverage +++ b/.coverage @@ -1,7 +1,7 @@ mode: set github.com/adjoeio/djoemo/dynamo_global_index.go:22.50,24.2 1 0 github.com/adjoeio/djoemo/dynamo_global_index.go:27.71,29.2 1 0 -github.com/adjoeio/djoemo/dynamo_global_index.go:32.98,36.2 3 0 +github.com/adjoeio/djoemo/dynamo_global_index.go:32.121,36.2 3 1 github.com/adjoeio/djoemo/dynamo_global_index.go:39.105,43.39 3 1 github.com/adjoeio/djoemo/dynamo_global_index.go:43.39,45.3 1 1 github.com/adjoeio/djoemo/dynamo_global_index.go:47.2,48.16 2 1 @@ -52,7 +52,7 @@ github.com/adjoeio/djoemo/dynamo_global_index.go:168.16,170.3 1 1 github.com/adjoeio/djoemo/dynamo_repository.go:25.80,31.2 1 1 github.com/adjoeio/djoemo/dynamo_repository.go:34.57,36.2 1 1 github.com/adjoeio/djoemo/dynamo_repository.go:39.78,41.2 1 1 -github.com/adjoeio/djoemo/dynamo_repository.go:44.104,48.2 3 0 +github.com/adjoeio/djoemo/dynamo_repository.go:44.127,48.2 3 1 github.com/adjoeio/djoemo/dynamo_repository.go:53.120,57.39 3 1 github.com/adjoeio/djoemo/dynamo_repository.go:57.39,59.3 1 1 github.com/adjoeio/djoemo/dynamo_repository.go:61.2,62.16 2 1 @@ -280,32 +280,49 @@ github.com/adjoeio/djoemo/metrics.go:84.124,85.26 1 0 github.com/adjoeio/djoemo/metrics.go:85.26,87.3 1 0 github.com/adjoeio/djoemo/metrics_cloudwatch.go:10.126,13.2 0 0 github.com/adjoeio/djoemo/metrics_cloudwatch.go:16.111,17.2 0 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:24.78,30.53 3 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:30.53,31.61 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:31.61,33.4 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:34.3,34.13 1 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:36.2,36.16 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:39.85,47.55 3 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:47.55,48.61 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:48.61,50.4 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:51.3,51.13 1 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:53.2,53.18 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:63.77,70.2 2 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:72.128,78.32 5 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:78.32,80.34 2 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:80.34,82.4 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:83.3,83.37 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:83.37,85.4 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:86.3,88.16 3 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:91.2,92.13 2 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:92.13,94.3 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:96.2,97.41 2 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:97.41,99.3 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:101.2,106.31 3 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:106.31,108.46 1 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:108.46,112.4 2 1 -github.com/adjoeio/djoemo/metrics_prometheus.go:112.9,114.4 1 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:117.2,118.52 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:33.50,40.2 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:52.78,61.53 3 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:61.53,62.61 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:62.61,64.4 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:65.3,65.13 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:67.2,67.16 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:70.85,72.23 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:72.23,74.3 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:75.2,85.55 3 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:85.55,86.61 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:86.61,88.4 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:89.3,89.13 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:91.2,91.18 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:102.100,103.16 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:103.16,105.3 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:106.2,112.10 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:115.128,116.15 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:116.15,117.31 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:117.31,119.24 2 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:119.24,121.5 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:121.10,123.5 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:127.2,132.32 5 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:132.32,134.34 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:134.34,136.4 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:137.3,137.37 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:137.37,139.4 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:140.3,142.16 3 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:145.2,146.13 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:146.13,148.3 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:150.2,151.41 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:151.41,153.3 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:155.2,160.31 3 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:160.31,162.3 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:164.2,165.52 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:177.13,179.8 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:179.8,181.3 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:199.30,206.6 5 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:206.6,208.34 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:208.34,211.4 2 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:212.3,212.12 1 1 +github.com/adjoeio/djoemo/metrics_prometheus.go:212.12,213.9 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:217.2,217.18 1 0 +github.com/adjoeio/djoemo/metrics_prometheus.go:222.39,224.2 1 1 github.com/adjoeio/djoemo/model.go:11.35,13.2 1 1 github.com/adjoeio/djoemo/model.go:16.35,18.2 1 1 github.com/adjoeio/djoemo/model.go:21.33,22.24 1 1 diff --git a/dynamo_global_index.go b/dynamo_global_index.go index b740de5..9b78463 100644 --- a/dynamo_global_index.go +++ b/dynamo_global_index.go @@ -28,9 +28,9 @@ func (gi *GlobalIndex) WithMetrics(metricsInterface MetricsInterface) { gi.metrics.Add(metricsInterface) } -// WithPrometheusMetrics enables prometheus metrics -func (gi *GlobalIndex) WithPrometheusMetrics(registry *prometheus.Registry) GlobalIndexInterface { - prommetrics := NewPrometheusMetrics(registry) +// WithPrometheusMetrics enables prometheus metrics with the given config +func (gi *GlobalIndex) WithPrometheusMetrics(registry *prometheus.Registry, cfg *PrometheusConfig) GlobalIndexInterface { + prommetrics := NewPrometheusMetrics(registry, cfg) gi.metrics.Add(prommetrics) return gi } diff --git a/dynamo_global_index_interface.go b/dynamo_global_index_interface.go index 20a9dd3..e0b1b50 100644 --- a/dynamo_global_index_interface.go +++ b/dynamo_global_index_interface.go @@ -15,8 +15,8 @@ type GlobalIndexInterface interface { // WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher WithMetrics(metricsInterface MetricsInterface) - // WithPrometheusMetrics enables prometheus metrics - WithPrometheusMetrics(registry *prometheus.Registry) GlobalIndexInterface + // WithPrometheusMetrics enables prometheus metrics with the given config + WithPrometheusMetrics(registry *prometheus.Registry, cfg *PrometheusConfig) GlobalIndexInterface // GetItemWithContext get item from index; it accepts a key interface that is used to get the table name, hash key and range key if it exists; // context which used to enable log with context; the output will be given in item diff --git a/dynamo_repository.go b/dynamo_repository.go index 2e1ea77..8b6c0df 100644 --- a/dynamo_repository.go +++ b/dynamo_repository.go @@ -40,9 +40,9 @@ func (repository *Repository) WithMetrics(metricsInterface MetricsInterface) { repository.metrics.Add(metricsInterface) } -// WithPrometheusMetrics enables prometheus metrics -func (repository *Repository) WithPrometheusMetrics(registry *prometheus.Registry) RepositoryInterface { - prommetrics := NewPrometheusMetrics(registry) +// WithPrometheusMetrics enables prometheus metrics with the given config +func (repository *Repository) WithPrometheusMetrics(registry *prometheus.Registry, cfg *PrometheusConfig) RepositoryInterface { + prommetrics := NewPrometheusMetrics(registry, cfg) repository.metrics.Add(prommetrics) return repository } diff --git a/dynamo_repository_interface.go b/dynamo_repository_interface.go index f31744e..3184710 100644 --- a/dynamo_repository_interface.go +++ b/dynamo_repository_interface.go @@ -17,8 +17,8 @@ type RepositoryInterface interface { // WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher WithMetrics(metricsInterface MetricsInterface) - // WithPrometheusMetrics enables prometheus metrics - WithPrometheusMetrics(registry *prometheus.Registry) RepositoryInterface + // WithPrometheusMetrics enables prometheus metrics with the given config + WithPrometheusMetrics(registry *prometheus.Registry, cfg *PrometheusConfig) RepositoryInterface // GetItemWithContext get item; it accepts a key interface that is used to get the table name, hash key and range key if it exists; the output will be given in item // returns true if item is found, returns false and nil if no item found, returns false and an error in case of error diff --git a/metrics_prometheus.go b/metrics_prometheus.go index f0a8f64..c9752a0 100644 --- a/metrics_prometheus.go +++ b/metrics_prometheus.go @@ -2,6 +2,8 @@ package djoemo import ( "context" + "fmt" + "log" "maps" "path" "runtime" @@ -12,8 +14,34 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +// PrometheusConfig holds configuration for Prometheus metrics. +type PrometheusConfig struct { + // Namespace is the metric namespace (e.g., "adjoe"). + Namespace string + // Subsystem is the metric subsystem (e.g., "djoemo"). + Subsystem string + // HistogramBuckets defines the histogram bucket boundaries in seconds. + // If nil, defaults to ExponentialBuckets(0.001, 2.5, 12) (1ms to ~60s). + HistogramBuckets []float64 + // ConstLabels are labels added to all metrics. + ConstLabels prometheus.Labels + // Log is an optional logger for panic recovery. If nil, uses standard log. + Log LogInterface +} + +// DefaultPrometheusConfig returns a config with sensible defaults. +func DefaultPrometheusConfig() *PrometheusConfig { + return &PrometheusConfig{ + Namespace: "adjoe", + Subsystem: "djoemo", + HistogramBuckets: prometheus.ExponentialBuckets(0.001, 2.5, 12), + Log: NewNopLog(), + } +} + type prometheusmetrics struct { registry *prometheus.Registry + cfg *PrometheusConfig mu sync.RWMutex queryCount map[string]*prometheus.CounterVec queryDuration map[string]*prometheus.HistogramVec @@ -23,8 +51,11 @@ var metricLabelNames = []string{statusLabel, tableLabel, sourceLabel} func (m *prometheusmetrics) newCounter(caller string) *prometheus.CounterVec { opts := prometheus.CounterOpts{ - Name: strings.ToLower(caller), - Help: "counter for function " + caller, + Namespace: m.cfg.Namespace, + Subsystem: m.cfg.Subsystem, + Name: strings.ToLower(caller), + Help: "counter for function " + caller, + ConstLabels: m.cfg.ConstLabels, } counter := prometheus.NewCounterVec(opts, metricLabelNames) if err := m.registry.Register(counter); err != nil { @@ -37,10 +68,17 @@ func (m *prometheusmetrics) newCounter(caller string) *prometheus.CounterVec { } func (m *prometheusmetrics) newHistogramVec(caller string) *prometheus.HistogramVec { + buckets := m.cfg.HistogramBuckets + if len(buckets) == 0 { + buckets = prometheus.ExponentialBuckets(0.001, 2.5, 12) + } opts := prometheus.HistogramOpts{ - Name: strings.ToLower(caller) + "_duration_seconds", - Help: "histogram duration for function " + caller + " in seconds", - Buckets: prometheus.ExponentialBuckets(0.001, 2.5, 5), + Namespace: m.cfg.Namespace, + Subsystem: m.cfg.Subsystem, + Name: strings.ToLower(caller) + "_duration_seconds", + Help: "histogram duration for function " + caller + " in seconds", + Buckets: buckets, + ConstLabels: m.cfg.ConstLabels, } // WARNING: add high cardinality labels like sdkhash, etc with caution histogram := prometheus.NewHistogramVec(opts, metricLabelNames) @@ -60,9 +98,14 @@ const ( tableLabel = "table" ) -func NewPrometheusMetrics(registry *prometheus.Registry) *prometheusmetrics { +// NewPrometheusMetrics creates Prometheus metrics with default config. +func NewPrometheusMetrics(registry *prometheus.Registry, cfg *PrometheusConfig) *prometheusmetrics { + if cfg == nil { + cfg = DefaultPrometheusConfig() + } m := &prometheusmetrics{ registry: registry, + cfg: cfg, queryCount: make(map[string]*prometheus.CounterVec), queryDuration: make(map[string]*prometheus.HistogramVec), } @@ -70,6 +113,17 @@ func NewPrometheusMetrics(registry *prometheus.Registry) *prometheusmetrics { } func (m *prometheusmetrics) Record(ctx context.Context, caller string, key KeyInterface, duration time.Duration, success bool) { + defer func() { + if r := recover(); r != nil { + msg := fmt.Sprintf("prometheus metrics Record panic recovered: caller=%q panic=%v", caller, r) + if m.cfg.Log != nil { + m.cfg.Log.WithContext(ctx).Error(msg) + } else { + log.Printf("djoemo: %s", msg) + } + } + }() + m.mu.RLock() counter, counterOk := m.queryCount[caller] histogram, histogramOk := m.queryDuration[caller] @@ -104,16 +158,67 @@ func (m *prometheusmetrics) Record(ctx context.Context, caller string, key KeyIn } maps.Copy(labels, GetLabelsFromContext(ctx)) if labels[sourceLabel] == "" { - // Set to the filename of the calling function rather than the caller string - if _, file, _, ok := runtime.Caller(2); ok { - // Extract just the file name, not the full path - _, filename := path.Split(file) - labels[sourceLabel] = filename - } else { - labels[sourceLabel] = "unknown" - } + labels[sourceLabel] = externalCaller() } counter.With(labels).Inc() histogram.With(labels).Observe(duration.Seconds()) } + +// libraryDir is the directory on disk that contains this library's source files, +// determined once at init time via runtime.Caller(0). We compare frame file paths +// against this directory to decide whether a frame belongs to this library. +// +// This approach is immune to: +// - go.mod replace directives that change the module path. Ex: replace github.com/adjoeio/djoemo => github.com/pm-nilesh-chate/djoemo v0.2.1-0.20260301162703-078983d9e34a +// - callers that happen to have a package also named "djoemo". Ex. package djoemo in main backend repo +var libraryDir string + +func init() { + _, file, _, ok := runtime.Caller(0) + if ok { + libraryDir = path.Dir(file) + } +} + +// externalCaller walks the call stack and returns the first caller outside this +// library as "filename:line". This gives the user-facing source location that +// triggered the DynamoDB operation rather than an internal library frame. +// +// Example stack when Repository.GetItemWithContext is called from user code: +// +// frame 0: runtime.Callers (runtime) +// frame 1: djoemo.externalCaller (metrics_prometheus.go) +// frame 2: djoemo.(*prometheusmetrics).Record (metrics_prometheus.go) +// frame 3: djoemo.(*Metrics).Record (metrics.go) +// frame 4: djoemo.Repository.GetItemWithContext.recordMetrics.func1 (dynamo_repository.go) <- deferred closure +// frame 5: djoemo.Repository.GetItemWithContext (dynamo_repository.go) +// frame 6: main/service.(*UserService).GetUser (user_service.go:42) <- first external caller ✓ +// +// The returned value would be "user_service.go:42". +func externalCaller() string { + const maxDepth = 15 + var pcs [maxDepth]uintptr + // skip 0=Callers, 1=externalCaller, start from 2 + n := runtime.Callers(2, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + + for { + frame, more := frames.Next() + if !isLibraryFrame(frame.File) { + _, filename := path.Split(frame.File) + return fmt.Sprintf("%s:%d", filename, frame.Line) + } + if !more { + break + } + } + + return "unknown" +} + +// isLibraryFrame reports whether the given file path belongs to this library +// by checking if it resides in the same directory as this package's source files. +func isLibraryFrame(file string) bool { + return libraryDir != "" && path.Dir(file) == libraryDir +} diff --git a/metrics_prometheus_test.go b/metrics_prometheus_test.go index 3f24d89..bb8c07f 100644 --- a/metrics_prometheus_test.go +++ b/metrics_prometheus_test.go @@ -20,7 +20,7 @@ var _ = Describe("Prometheus Metrics", func() { BeforeEach(func() { registry = prometheus.NewRegistry() - promMetrics := djoemo.NewPrometheusMetrics(registry) + promMetrics := djoemo.NewPrometheusMetrics(registry, nil) metrics = djoemo.New() metrics.Add(promMetrics) }) @@ -34,19 +34,19 @@ var _ = Describe("Prometheus Metrics", func() { Expect(err).NotTo(HaveOccurred()) Expect(mfs).NotTo(BeEmpty(), "expected metrics from Gather, got names: %v", metricFamilyNames(mfs)) - // Find read counter and read_duration_seconds histogram (Prometheus adds _total to counters) + // Find read counter and read_duration_seconds histogram (with namespace/subsystem: adjoe_djoemo_*) var readTotal *dto.MetricFamily var readDuration *dto.MetricFamily for _, mf := range mfs { switch mf.GetName() { - case "read": + case "adjoe_djoemo_read": readTotal = mf - case "read_duration_seconds": + case "adjoe_djoemo_read_duration_seconds": readDuration = mf } } - Expect(readTotal).NotTo(BeNil(), "read_total counter should be registered, got: %v", metricFamilyNames(mfs)) - Expect(readDuration).NotTo(BeNil(), "read_duration_seconds histogram should be registered, got: %v", metricFamilyNames(mfs)) + Expect(readTotal).NotTo(BeNil(), "adjoe_djoemo_read_total counter should be registered, got: %v", metricFamilyNames(mfs)) + Expect(readDuration).NotTo(BeNil(), "adjoe_djoemo_read_duration_seconds histogram should be registered, got: %v", metricFamilyNames(mfs)) // Counter: expect status=success, table=usertable Expect(readTotal.GetMetric()).To(HaveLen(1)) @@ -54,7 +54,9 @@ var _ = Describe("Prometheus Metrics", func() { labels := readTotal.GetMetric()[0].GetLabel() Expect(getLabelValue(labels, "status")).To(Equal("success")) Expect(getLabelValue(labels, "table")).To(Equal("usertable")) - Expect(getLabelValue(labels, "source")).To(Equal("metrics_prometheus_test.go")) + // sourceLabel is resolved to "filename:line" of the first caller outside + // the djoemo library. In tests that caller is the ginkgo test runner. + Expect(getLabelValue(labels, "source")).To(MatchRegexp(`^.+\.go:\d+$`)) // Histogram: expect duration in seconds (50ms = 0.05) Expect(readDuration.GetMetric()).To(HaveLen(1)) @@ -71,10 +73,10 @@ var _ = Describe("Prometheus Metrics", func() { var commitTotal *dto.MetricFamily var commitDuration *dto.MetricFamily for _, mf := range mfs { - if mf.GetName() == "commit" { + if mf.GetName() == "adjoe_djoemo_commit" { commitTotal = mf } - if mf.GetName() == "commit_duration_seconds" { + if mf.GetName() == "adjoe_djoemo_commit_duration_seconds" { commitDuration = mf } } @@ -92,10 +94,10 @@ var _ = Describe("Prometheus Metrics", func() { var readTotal *dto.MetricFamily var readDuration *dto.MetricFamily for _, mf := range mfs { - if mf.GetName() == "read" { + if mf.GetName() == "adjoe_djoemo_read" { readTotal = mf } - if mf.GetName() == "read_duration_seconds" { + if mf.GetName() == "adjoe_djoemo_read_duration_seconds" { readDuration = mf } } @@ -115,10 +117,10 @@ var _ = Describe("Prometheus Metrics", func() { var updateTotal *dto.MetricFamily var updateDuration *dto.MetricFamily for _, mf := range mfs { - if mf.GetName() == "update" { + if mf.GetName() == "adjoe_djoemo_update" { updateTotal = mf } - if mf.GetName() == "update_duration_seconds" { + if mf.GetName() == "adjoe_djoemo_update_duration_seconds" { updateDuration = mf } } @@ -135,8 +137,8 @@ var _ = Describe("Prometheus Metrics", func() { sharedRegistry := prometheus.NewRegistry() // Create two prometheus metrics instances with same registry - pm1 := djoemo.NewPrometheusMetrics(sharedRegistry) - pm2 := djoemo.NewPrometheusMetrics(sharedRegistry) + pm1 := djoemo.NewPrometheusMetrics(sharedRegistry, nil) + pm2 := djoemo.NewPrometheusMetrics(sharedRegistry, nil) m := djoemo.New() m.Add(pm1) @@ -153,16 +155,16 @@ var _ = Describe("Prometheus Metrics", func() { var readTotal *dto.MetricFamily var readDuration *dto.MetricFamily for _, mf := range mfs { - if mf.GetName() == "read" { + if mf.GetName() == "adjoe_djoemo_read" { readTotal = mf } - if mf.GetName() == "read_duration_seconds" { + if mf.GetName() == "adjoe_djoemo_read_duration_seconds" { readDuration = mf } } Expect(readTotal).NotTo(BeNil()) - // Both Record calls should have incremented the same counter - Expect(readTotal.GetMetric()[0].GetCounter().GetValue()).To(Equal(4.0)) // Count of 2 calls * 2 metrics instances + // Both Record calls should have incremented the same counter (2 calls * 2 metrics instances) + Expect(readTotal.GetMetric()[0].GetCounter().GetValue()).To(Equal(4.0)) Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.06, 0.001)) Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(4))) }) @@ -187,10 +189,10 @@ var _ = Describe("Prometheus Metrics", func() { var readTotal *dto.MetricFamily var readDuration *dto.MetricFamily for _, mf := range mfs { - if mf.GetName() == "read" { + if mf.GetName() == "adjoe_djoemo_read" { readTotal = mf } - if mf.GetName() == "read_duration_seconds" { + if mf.GetName() == "adjoe_djoemo_read_duration_seconds" { readDuration = mf } } diff --git a/mock/dynamo_global_index_interface.go b/mock/dynamo_global_index_interface.go index d7c2f17..2f7f6a3 100644 --- a/mock/dynamo_global_index_interface.go +++ b/mock/dynamo_global_index_interface.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: dynamo_global_index_interface.go +// +// Generated by this command: +// +// mockgen -source=dynamo_global_index_interface.go -destination=./mock/dynamo_global_index_interface.go -package=mock . +// // Package mock is a generated GoMock package. package mock @@ -10,13 +15,14 @@ import ( djoemo "github.com/adjoeio/djoemo" prometheus "github.com/prometheus/client_golang/prometheus" - "go.uber.org/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockGlobalIndexInterface is a mock of GlobalIndexInterface interface. type MockGlobalIndexInterface struct { ctrl *gomock.Controller recorder *MockGlobalIndexInterfaceMockRecorder + isgomock struct{} } // MockGlobalIndexInterfaceMockRecorder is the mock recorder for MockGlobalIndexInterface. @@ -37,7 +43,7 @@ func (m *MockGlobalIndexInterface) EXPECT() *MockGlobalIndexInterfaceMockRecorde } // GetItemWithContext mocks base method. -func (m *MockGlobalIndexInterface) GetItemWithContext(ctx context.Context, key djoemo.KeyInterface, item interface{}) (bool, error) { +func (m *MockGlobalIndexInterface) GetItemWithContext(ctx context.Context, key djoemo.KeyInterface, item any) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetItemWithContext", ctx, key, item) ret0, _ := ret[0].(bool) @@ -46,13 +52,13 @@ func (m *MockGlobalIndexInterface) GetItemWithContext(ctx context.Context, key d } // GetItemWithContext indicates an expected call of GetItemWithContext. -func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemWithContext(ctx, key, item interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemWithContext(ctx, key, item any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItemWithContext), ctx, key, item) } // GetItemsWithContext mocks base method. -func (m *MockGlobalIndexInterface) GetItemsWithContext(ctx context.Context, key djoemo.KeyInterface, items interface{}) (bool, error) { +func (m *MockGlobalIndexInterface) GetItemsWithContext(ctx context.Context, key djoemo.KeyInterface, items any) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetItemsWithContext", ctx, key, items) ret0, _ := ret[0].(bool) @@ -61,13 +67,13 @@ func (m *MockGlobalIndexInterface) GetItemsWithContext(ctx context.Context, key } // GetItemsWithContext indicates an expected call of GetItemsWithContext. -func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemsWithContext(ctx, key, items interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemsWithContext(ctx, key, items any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemsWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItemsWithContext), ctx, key, items) } // GetItemsWithRangeWithContext mocks base method. -func (m *MockGlobalIndexInterface) GetItemsWithRangeWithContext(ctx context.Context, key djoemo.KeyInterface, items interface{}) (bool, error) { +func (m *MockGlobalIndexInterface) GetItemsWithRangeWithContext(ctx context.Context, key djoemo.KeyInterface, items any) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetItemsWithRangeWithContext", ctx, key, items) ret0, _ := ret[0].(bool) @@ -76,13 +82,13 @@ func (m *MockGlobalIndexInterface) GetItemsWithRangeWithContext(ctx context.Cont } // GetItemsWithRangeWithContext indicates an expected call of GetItemsWithRangeWithContext. -func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemsWithRangeWithContext(ctx, key, items interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemsWithRangeWithContext(ctx, key, items any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemsWithRangeWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItemsWithRangeWithContext), ctx, key, items) } // QueryWithContext mocks base method. -func (m *MockGlobalIndexInterface) QueryWithContext(ctx context.Context, query djoemo.QueryInterface, item interface{}) error { +func (m *MockGlobalIndexInterface) QueryWithContext(ctx context.Context, query djoemo.QueryInterface, item any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "QueryWithContext", ctx, query, item) ret0, _ := ret[0].(error) @@ -90,7 +96,7 @@ func (m *MockGlobalIndexInterface) QueryWithContext(ctx context.Context, query d } // QueryWithContext indicates an expected call of QueryWithContext. -func (mr *MockGlobalIndexInterfaceMockRecorder) QueryWithContext(ctx, query, item interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) QueryWithContext(ctx, query, item any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).QueryWithContext), ctx, query, item) } @@ -102,7 +108,7 @@ func (m *MockGlobalIndexInterface) WithLog(log djoemo.LogInterface) { } // WithLog indicates an expected call of WithLog. -func (mr *MockGlobalIndexInterfaceMockRecorder) WithLog(log interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) WithLog(log any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithLog", reflect.TypeOf((*MockGlobalIndexInterface)(nil).WithLog), log) } @@ -114,21 +120,21 @@ func (m *MockGlobalIndexInterface) WithMetrics(metricsInterface djoemo.MetricsIn } // WithMetrics indicates an expected call of WithMetrics. -func (mr *MockGlobalIndexInterfaceMockRecorder) WithMetrics(metricsInterface interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) WithMetrics(metricsInterface any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithMetrics", reflect.TypeOf((*MockGlobalIndexInterface)(nil).WithMetrics), metricsInterface) } // WithPrometheusMetrics mocks base method. -func (m *MockGlobalIndexInterface) WithPrometheusMetrics(registry *prometheus.Registry) djoemo.GlobalIndexInterface { +func (m *MockGlobalIndexInterface) WithPrometheusMetrics(registry *prometheus.Registry, cfg *djoemo.PrometheusConfig) djoemo.GlobalIndexInterface { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WithPrometheusMetrics", registry) + ret := m.ctrl.Call(m, "WithPrometheusMetrics", registry, cfg) ret0, _ := ret[0].(djoemo.GlobalIndexInterface) return ret0 } // WithPrometheusMetrics indicates an expected call of WithPrometheusMetrics. -func (mr *MockGlobalIndexInterfaceMockRecorder) WithPrometheusMetrics(registry interface{}) *gomock.Call { +func (mr *MockGlobalIndexInterfaceMockRecorder) WithPrometheusMetrics(registry, cfg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPrometheusMetrics", reflect.TypeOf((*MockGlobalIndexInterface)(nil).WithPrometheusMetrics), registry) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPrometheusMetrics", reflect.TypeOf((*MockGlobalIndexInterface)(nil).WithPrometheusMetrics), registry, cfg) } diff --git a/mock/dynamo_repository_interface.go b/mock/dynamo_repository_interface.go index 7b91872..82d94f0 100644 --- a/mock/dynamo_repository_interface.go +++ b/mock/dynamo_repository_interface.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: dynamo_repository_interface.go +// +// Generated by this command: +// +// mockgen -source=dynamo_repository_interface.go -destination=./mock/dynamo_repository_interface.go -package=mock . +// // Package mock is a generated GoMock package. package mock @@ -10,13 +15,14 @@ import ( djoemo "github.com/adjoeio/djoemo" prometheus "github.com/prometheus/client_golang/prometheus" - "go.uber.org/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockRepositoryInterface is a mock of RepositoryInterface interface. type MockRepositoryInterface struct { ctrl *gomock.Controller recorder *MockRepositoryInterfaceMockRecorder + isgomock struct{} } // MockRepositoryInterfaceMockRecorder is the mock recorder for MockRepositoryInterface. @@ -46,7 +52,7 @@ func (m *MockRepositoryInterface) BatchGetItemsWithContext(ctx context.Context, } // BatchGetItemsWithContext indicates an expected call of BatchGetItemsWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) BatchGetItemsWithContext(ctx, keys, out interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) BatchGetItemsWithContext(ctx, keys, out any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItemsWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).BatchGetItemsWithContext), ctx, keys, out) } @@ -54,7 +60,7 @@ func (mr *MockRepositoryInterfaceMockRecorder) BatchGetItemsWithContext(ctx, key // ConditionalUpdateWithContext mocks base method. func (m *MockRepositoryInterface) ConditionalUpdateWithContext(ctx context.Context, key djoemo.KeyInterface, item any, expression string, expressionArgs ...any) (bool, error) { m.ctrl.T.Helper() - varargs := []interface{}{ctx, key, item, expression} + varargs := []any{ctx, key, item, expression} for _, a := range expressionArgs { varargs = append(varargs, a) } @@ -65,16 +71,16 @@ func (m *MockRepositoryInterface) ConditionalUpdateWithContext(ctx context.Conte } // ConditionalUpdateWithContext indicates an expected call of ConditionalUpdateWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdateWithContext(ctx, key, item, expression interface{}, expressionArgs ...interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdateWithContext(ctx, key, item, expression any, expressionArgs ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, key, item, expression}, expressionArgs...) + varargs := append([]any{ctx, key, item, expression}, expressionArgs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConditionalUpdateWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).ConditionalUpdateWithContext), varargs...) } // ConditionalUpdateWithUpdateExpressionsAndReturnValue mocks base method. func (m *MockRepositoryInterface) ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key djoemo.KeyInterface, item any, updateExpressions djoemo.UpdateExpressions, conditionExpression string, conditionArgs ...any) (bool, error) { m.ctrl.T.Helper() - varargs := []interface{}{ctx, key, item, updateExpressions, conditionExpression} + varargs := []any{ctx, key, item, updateExpressions, conditionExpression} for _, a := range conditionArgs { varargs = append(varargs, a) } @@ -85,9 +91,9 @@ func (m *MockRepositoryInterface) ConditionalUpdateWithUpdateExpressionsAndRetur } // ConditionalUpdateWithUpdateExpressionsAndReturnValue indicates an expected call of ConditionalUpdateWithUpdateExpressionsAndReturnValue. -func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx, key, item, updateExpressions, conditionExpression interface{}, conditionArgs ...interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx, key, item, updateExpressions, conditionExpression any, conditionArgs ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, key, item, updateExpressions, conditionExpression}, conditionArgs...) + varargs := append([]any{ctx, key, item, updateExpressions, conditionExpression}, conditionArgs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConditionalUpdateWithUpdateExpressionsAndReturnValue", reflect.TypeOf((*MockRepositoryInterface)(nil).ConditionalUpdateWithUpdateExpressionsAndReturnValue), varargs...) } @@ -100,7 +106,7 @@ func (m *MockRepositoryInterface) DeleteItemWithContext(ctx context.Context, key } // DeleteItemWithContext indicates an expected call of DeleteItemWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) DeleteItemWithContext(ctx, key interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) DeleteItemWithContext(ctx, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItemWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteItemWithContext), ctx, key) } @@ -114,7 +120,7 @@ func (m *MockRepositoryInterface) DeleteItemsWithContext(ctx context.Context, ke } // DeleteItemsWithContext indicates an expected call of DeleteItemsWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) DeleteItemsWithContext(ctx, key interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) DeleteItemsWithContext(ctx, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItemsWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteItemsWithContext), ctx, key) } @@ -128,7 +134,7 @@ func (m *MockRepositoryInterface) GIndex(name string) djoemo.GlobalIndexInterfac } // GIndex indicates an expected call of GIndex. -func (mr *MockRepositoryInterfaceMockRecorder) GIndex(name interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) GIndex(name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GIndex", reflect.TypeOf((*MockRepositoryInterface)(nil).GIndex), name) } @@ -143,7 +149,7 @@ func (m *MockRepositoryInterface) GetItemWithContext(ctx context.Context, key dj } // GetItemWithContext indicates an expected call of GetItemWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) GetItemWithContext(ctx, key, item interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) GetItemWithContext(ctx, key, item any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).GetItemWithContext), ctx, key, item) } @@ -158,7 +164,7 @@ func (m *MockRepositoryInterface) GetItemsWithContext(ctx context.Context, key d } // GetItemsWithContext indicates an expected call of GetItemsWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) GetItemsWithContext(ctx, key, out interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) GetItemsWithContext(ctx, key, out any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemsWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).GetItemsWithContext), ctx, key, out) } @@ -173,7 +179,7 @@ func (m *MockRepositoryInterface) OptimisticLockSaveWithContext(ctx context.Cont } // OptimisticLockSaveWithContext indicates an expected call of OptimisticLockSaveWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) OptimisticLockSaveWithContext(ctx, key, item interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) OptimisticLockSaveWithContext(ctx, key, item any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OptimisticLockSaveWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).OptimisticLockSaveWithContext), ctx, key, item) } @@ -187,7 +193,7 @@ func (m *MockRepositoryInterface) QueryWithContext(ctx context.Context, query dj } // QueryWithContext indicates an expected call of QueryWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) QueryWithContext(ctx, query, item interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) QueryWithContext(ctx, query, item any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).QueryWithContext), ctx, query, item) } @@ -201,7 +207,7 @@ func (m *MockRepositoryInterface) SaveItemWithContext(ctx context.Context, key d } // SaveItemWithContext indicates an expected call of SaveItemWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) SaveItemWithContext(ctx, key, item interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) SaveItemWithContext(ctx, key, item any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveItemWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).SaveItemWithContext), ctx, key, item) } @@ -215,7 +221,7 @@ func (m *MockRepositoryInterface) SaveItemsWithContext(ctx context.Context, key } // SaveItemsWithContext indicates an expected call of SaveItemsWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) SaveItemsWithContext(ctx, key, items interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) SaveItemsWithContext(ctx, key, items any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveItemsWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).SaveItemsWithContext), ctx, key, items) } @@ -230,7 +236,7 @@ func (m *MockRepositoryInterface) ScanIteratorWithContext(ctx context.Context, k } // ScanIteratorWithContext indicates an expected call of ScanIteratorWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) ScanIteratorWithContext(ctx, key, searchLimit interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) ScanIteratorWithContext(ctx, key, searchLimit any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanIteratorWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).ScanIteratorWithContext), ctx, key, searchLimit) } @@ -244,7 +250,7 @@ func (m *MockRepositoryInterface) UpdateWithContext(ctx context.Context, express } // UpdateWithContext indicates an expected call of UpdateWithContext. -func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithContext(ctx, expression, key, values interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithContext(ctx, expression, key, values any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).UpdateWithContext), ctx, expression, key, values) } @@ -258,7 +264,7 @@ func (m *MockRepositoryInterface) UpdateWithUpdateExpressions(ctx context.Contex } // UpdateWithUpdateExpressions indicates an expected call of UpdateWithUpdateExpressions. -func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithUpdateExpressions(ctx, key, updateExpressions interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithUpdateExpressions(ctx, key, updateExpressions any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWithUpdateExpressions", reflect.TypeOf((*MockRepositoryInterface)(nil).UpdateWithUpdateExpressions), ctx, key, updateExpressions) } @@ -272,7 +278,7 @@ func (m *MockRepositoryInterface) UpdateWithUpdateExpressionsAndReturnValue(ctx } // UpdateWithUpdateExpressionsAndReturnValue indicates an expected call of UpdateWithUpdateExpressionsAndReturnValue. -func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithUpdateExpressionsAndReturnValue(ctx, key, item, updateExpressions interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithUpdateExpressionsAndReturnValue(ctx, key, item, updateExpressions any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWithUpdateExpressionsAndReturnValue", reflect.TypeOf((*MockRepositoryInterface)(nil).UpdateWithUpdateExpressionsAndReturnValue), ctx, key, item, updateExpressions) } @@ -284,7 +290,7 @@ func (m *MockRepositoryInterface) WithLog(log djoemo.LogInterface) { } // WithLog indicates an expected call of WithLog. -func (mr *MockRepositoryInterfaceMockRecorder) WithLog(log interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) WithLog(log any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithLog", reflect.TypeOf((*MockRepositoryInterface)(nil).WithLog), log) } @@ -296,21 +302,21 @@ func (m *MockRepositoryInterface) WithMetrics(metricsInterface djoemo.MetricsInt } // WithMetrics indicates an expected call of WithMetrics. -func (mr *MockRepositoryInterfaceMockRecorder) WithMetrics(metricsInterface interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) WithMetrics(metricsInterface any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithMetrics", reflect.TypeOf((*MockRepositoryInterface)(nil).WithMetrics), metricsInterface) } // WithPrometheusMetrics mocks base method. -func (m *MockRepositoryInterface) WithPrometheusMetrics(registry *prometheus.Registry) djoemo.RepositoryInterface { +func (m *MockRepositoryInterface) WithPrometheusMetrics(registry *prometheus.Registry, cfg *djoemo.PrometheusConfig) djoemo.RepositoryInterface { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WithPrometheusMetrics", registry) + ret := m.ctrl.Call(m, "WithPrometheusMetrics", registry, cfg) ret0, _ := ret[0].(djoemo.RepositoryInterface) return ret0 } // WithPrometheusMetrics indicates an expected call of WithPrometheusMetrics. -func (mr *MockRepositoryInterfaceMockRecorder) WithPrometheusMetrics(registry interface{}) *gomock.Call { +func (mr *MockRepositoryInterfaceMockRecorder) WithPrometheusMetrics(registry, cfg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPrometheusMetrics", reflect.TypeOf((*MockRepositoryInterface)(nil).WithPrometheusMetrics), registry) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPrometheusMetrics", reflect.TypeOf((*MockRepositoryInterface)(nil).WithPrometheusMetrics), registry, cfg) } diff --git a/mock/log_interface.go b/mock/log_interface.go index f850849..c9a4669 100644 --- a/mock/log_interface.go +++ b/mock/log_interface.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: logger_interface.go +// +// Generated by this command: +// +// mockgen -source=logger_interface.go -destination=./mock/log_interface.go -package=mock . +// // Package mock is a generated GoMock package. package mock @@ -9,13 +14,14 @@ import ( reflect "reflect" djoemo "github.com/adjoeio/djoemo" - "go.uber.org/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockLogInterface is a mock of LogInterface interface. type MockLogInterface struct { ctrl *gomock.Controller recorder *MockLogInterfaceMockRecorder + isgomock struct{} } // MockLogInterfaceMockRecorder is the mock recorder for MockLogInterface. @@ -42,7 +48,7 @@ func (m *MockLogInterface) Error(message string) { } // Error indicates an expected call of Error. -func (mr *MockLogInterfaceMockRecorder) Error(message interface{}) *gomock.Call { +func (mr *MockLogInterfaceMockRecorder) Error(message any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogInterface)(nil).Error), message) } @@ -54,7 +60,7 @@ func (m *MockLogInterface) Info(message string) { } // Info indicates an expected call of Info. -func (mr *MockLogInterfaceMockRecorder) Info(message interface{}) *gomock.Call { +func (mr *MockLogInterfaceMockRecorder) Info(message any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogInterface)(nil).Info), message) } @@ -66,7 +72,7 @@ func (m *MockLogInterface) Warn(message string) { } // Warn indicates an expected call of Warn. -func (mr *MockLogInterfaceMockRecorder) Warn(message interface{}) *gomock.Call { +func (mr *MockLogInterfaceMockRecorder) Warn(message any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogInterface)(nil).Warn), message) } @@ -80,7 +86,7 @@ func (m *MockLogInterface) WithContext(ctx context.Context) djoemo.LogInterface } // WithContext indicates an expected call of WithContext. -func (mr *MockLogInterfaceMockRecorder) WithContext(ctx interface{}) *gomock.Call { +func (mr *MockLogInterfaceMockRecorder) WithContext(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithContext", reflect.TypeOf((*MockLogInterface)(nil).WithContext), ctx) } @@ -94,7 +100,7 @@ func (m *MockLogInterface) WithField(key string, value any) djoemo.LogInterface } // WithField indicates an expected call of WithField. -func (mr *MockLogInterfaceMockRecorder) WithField(key, value interface{}) *gomock.Call { +func (mr *MockLogInterfaceMockRecorder) WithField(key, value any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithField", reflect.TypeOf((*MockLogInterface)(nil).WithField), key, value) } @@ -108,7 +114,7 @@ func (m *MockLogInterface) WithFields(fields map[string]any) djoemo.LogInterface } // WithFields indicates an expected call of WithFields. -func (mr *MockLogInterfaceMockRecorder) WithFields(fields interface{}) *gomock.Call { +func (mr *MockLogInterfaceMockRecorder) WithFields(fields any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithFields", reflect.TypeOf((*MockLogInterface)(nil).WithFields), fields) } diff --git a/mock/metrics_interface.go b/mock/metrics_interface.go index 824b6b6..73b89eb 100644 --- a/mock/metrics_interface.go +++ b/mock/metrics_interface.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: metrics.go +// +// Generated by this command: +// +// mockgen -source=metrics.go -destination=./mock/metrics_interface.go -package=mock .\ +// // Package mock is a generated GoMock package. package mock @@ -10,13 +15,14 @@ import ( time "time" djoemo "github.com/adjoeio/djoemo" - "go.uber.org/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockMetricsInterface is a mock of MetricsInterface interface. type MockMetricsInterface struct { ctrl *gomock.Controller recorder *MockMetricsInterfaceMockRecorder + isgomock struct{} } // MockMetricsInterfaceMockRecorder is the mock recorder for MockMetricsInterface. @@ -43,7 +49,7 @@ func (m *MockMetricsInterface) Record(ctx context.Context, caller string, key dj } // Record indicates an expected call of Record. -func (mr *MockMetricsInterfaceMockRecorder) Record(ctx, caller, key, duration, success interface{}) *gomock.Call { +func (mr *MockMetricsInterfaceMockRecorder) Record(ctx, caller, key, duration, success any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Record", reflect.TypeOf((*MockMetricsInterface)(nil).Record), ctx, caller, key, duration, success) } diff --git a/prometheus_config_test.go b/prometheus_config_test.go new file mode 100644 index 0000000..755d774 --- /dev/null +++ b/prometheus_config_test.go @@ -0,0 +1,230 @@ +package djoemo_test + +import ( + "context" + "errors" + + "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo/mock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "go.uber.org/mock/gomock" +) + +var _ = Describe("Prometheus Config", func() { + const ( + UserTableName = "UserTable" + IndexName = "gindex" + ) + + Describe("Repository WithPrometheusMetrics", func() { + It("uses custom namespace and subsystem from config", func() { + mockCtrl := gomock.NewController(GinkgoT()) + dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) + dMock := mock.NewDynamoMock(dAPIMock) + logMock := mock.NewMockLogInterface(mockCtrl) + + registry := prometheus.NewRegistry() + cfg := djoemo.DefaultPrometheusConfig() + cfg.Namespace = "myapp" + cfg.Subsystem = "dynamodb" + cfg.Log = logMock + + repository := djoemo.NewRepository(dAPIMock) + repository.WithLog(logMock) + repository.WithPrometheusMetrics(registry, cfg) + + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + userDBOutput := map[string]interface{}{ + "UUID": "uuid", + } + + dMock.Should(). + Get( + dMock.WithTable(key.TableName()), + dMock.WithHash(*key.HashKeyName(), key.HashKey()), + dMock.WithGetOutput(userDBOutput), + ).Exec() + + user := &User{} + found, err := repository.GetItemWithContext(context.Background(), key, user) + + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + + mfs, gatherErr := registry.Gather() + Expect(gatherErr).NotTo(HaveOccurred()) + Expect(mfs).NotTo(BeEmpty()) + + // Custom config should produce myapp_dynamodb_* metrics + var readCounter *dto.MetricFamily + var readDuration *dto.MetricFamily + for _, mf := range mfs { + switch mf.GetName() { + case "myapp_dynamodb_read": + readCounter = mf + case "myapp_dynamodb_read_duration_seconds": + readDuration = mf + } + } + Expect(readCounter).NotTo(BeNil(), "expected myapp_dynamodb_read metric, got: %v", metricFamilyNames(mfs)) + Expect(readDuration).NotTo(BeNil(), "expected myapp_dynamodb_read_duration_seconds metric, got: %v", metricFamilyNames(mfs)) + Expect(readCounter.GetMetric()[0].GetCounter().GetValue()).To(Equal(1.0)) + }) + + It("uses custom ConstLabels from config", func() { + mockCtrl := gomock.NewController(GinkgoT()) + dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) + dMock := mock.NewDynamoMock(dAPIMock) + logMock := mock.NewMockLogInterface(mockCtrl) + + registry := prometheus.NewRegistry() + cfg := djoemo.DefaultPrometheusConfig() + cfg.ConstLabels = prometheus.Labels{"env": "test", "service": "api"} + cfg.Log = logMock + + repository := djoemo.NewRepository(dAPIMock) + repository.WithLog(logMock) + repository.WithPrometheusMetrics(registry, cfg) + + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + dMock.Should(). + Get( + dMock.WithTable(key.TableName()), + dMock.WithHash(*key.HashKeyName(), key.HashKey()), + dMock.WithGetOutput(nil), + ).Exec() + + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info(djoemo.ErrNoItemFound.Error()) + + user := &User{} + _, _ = repository.GetItemWithContext(context.Background(), key, user) + + mfs, err := registry.Gather() + Expect(err).NotTo(HaveOccurred()) + var readCounter *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "adjoe_djoemo_read" { + readCounter = mf + break + } + } + Expect(readCounter).NotTo(BeNil()) + Expect(getLabelValue(readCounter.GetMetric()[0].GetLabel(), "env")).To(Equal("test")) + Expect(getLabelValue(readCounter.GetMetric()[0].GetLabel(), "service")).To(Equal("api")) + }) + }) + + Describe("GlobalIndex WithPrometheusMetrics", func() { + It("uses custom namespace and subsystem from config", func() { + mockCtrl := gomock.NewController(GinkgoT()) + dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) + dMock := mock.NewDynamoMock(dAPIMock) + logMock := mock.NewMockLogInterface(mockCtrl) + + registry := prometheus.NewRegistry() + cfg := djoemo.DefaultPrometheusConfig() + cfg.Namespace = "myapp" + cfg.Subsystem = "gindex" + cfg.Log = logMock + + repository := djoemo.NewRepository(dAPIMock) + repository.WithLog(logMock) + repository.GIndex(IndexName).WithPrometheusMetrics(registry, cfg) + + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + userDBOutput := map[string]interface{}{ + "UUID": "uuid", + } + + dMock.Should(). + Query( + dMock.WithTable(key.TableName()), + dMock.WithIndex(IndexName), + dMock.WithCondition(*key.HashKeyName(), key.HashKey(), "EQ"), + dMock.WithQueryOutput(userDBOutput), + ).Exec() + + user := &User{} + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) + + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + + mfs, gatherErr := registry.Gather() + Expect(gatherErr).NotTo(HaveOccurred()) + Expect(mfs).NotTo(BeEmpty()) + + var readCounter *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "myapp_gindex_read" { + readCounter = mf + break + } + } + Expect(readCounter).NotTo(BeNil(), "expected myapp_gindex_read metric, got: %v", metricFamilyNames(mfs)) + Expect(readCounter.GetMetric()[0].GetCounter().GetValue()).To(Equal(1.0)) + }) + + It("records failure when GetItem returns error", func() { + mockCtrl := gomock.NewController(GinkgoT()) + dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) + dMock := mock.NewDynamoMock(dAPIMock) + logMock := mock.NewMockLogInterface(mockCtrl) + + registry := prometheus.NewRegistry() + cfg := djoemo.DefaultPrometheusConfig() + cfg.Namespace = "test" + cfg.Subsystem = "index" + cfg.Log = logMock + + repository := djoemo.NewRepository(dAPIMock) + repository.WithLog(logMock) + repository.GIndex(IndexName).WithPrometheusMetrics(registry, cfg) + + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + dynamoErr := errors.New("dynamodb error") + dMock.Should(). + Query( + dMock.WithTable(key.TableName()), + dMock.WithIndex(IndexName), + dMock.WithCondition(*key.HashKeyName(), key.HashKey(), "EQ"), + dMock.WithError(dynamoErr), + ).Exec() + + user := &User{} + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(dynamoErr)) + Expect(found).To(BeFalse()) + + mfs, gatherErr := registry.Gather() + Expect(gatherErr).NotTo(HaveOccurred()) + var readCounter *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "test_index_read" { + readCounter = mf + break + } + } + Expect(readCounter).NotTo(BeNil()) + Expect(getLabelValue(readCounter.GetMetric()[0].GetLabel(), "status")).To(Equal("failure")) + }) + }) +})