From c7a70b005794846c9423d84b5a9884da3cb23034 Mon Sep 17 00:00:00 2001 From: Nilesh Chate Date: Fri, 2 Jan 2026 23:36:43 +0100 Subject: [PATCH] support prometheus metrics and refactor logger --- .coverage | 46 ++- .gitignore | 1 + README.md | 362 +++++++++++----------- djoemo_suite_test.go | 119 +------ global_index.go => dynamo_global_index.go | 90 +++--- dynamo_global_index_interface.go | 23 +- dynamo_repository.go | 316 ++++++++----------- dynamo_repository_interface.go | 71 ++--- errors.go | 15 +- examples/example.go | 39 ++- go.mod | 38 ++- go.sum | 105 ++++--- helper.go | 4 +- key_interface.go | 6 +- log.go | 46 --- log_interface.go => logger_interface.go | 6 +- logger_noplog.go | 38 +++ metrics.go | 89 +++++- metrics_cloudwatch.go | 17 + metrics_interface.go | 11 - metrics_prometheus.go | 119 +++++++ metrics_prometheus_test.go | 224 +++++++++++++ mock/dynamo_api_mock_helper.go | 3 - mock/dynamo_api_mock_input_matcher.go | 37 +-- mock/dynamo_global_index_interface.go | 83 +++-- mock/dynamo_repository_interface.go | 188 ++--------- mock/dynamodb_api_mock.go | 5 +- mock/log_interface.go | 109 ++++--- mock/metrics_interface.go | 46 ++- model.go | 2 +- query.go | 6 +- query_interface.go | 2 +- reflect_helper.go | 2 +- reflect_helper_test.go | 16 +- repository_delete_test.go | 191 +++++++----- repository_get_test.go | 267 ++++++++++++---- repository_global_index_test.go | 224 ++++++++----- repository_query_test.go | 141 ++++++--- repository_save_test.go | 223 +++++-------- repository_update_test.go | 131 ++++---- time.go | 10 +- 41 files changed, 1924 insertions(+), 1547 deletions(-) rename global_index.go => dynamo_global_index.go (50%) delete mode 100644 log.go rename log_interface.go => logger_interface.go (60%) create mode 100644 logger_noplog.go create mode 100644 metrics_cloudwatch.go delete mode 100644 metrics_interface.go create mode 100644 metrics_prometheus.go create mode 100644 metrics_prometheus_test.go diff --git a/.coverage b/.coverage index 9afd795..b02f0c6 100644 --- a/.coverage +++ b/.coverage @@ -269,27 +269,43 @@ github.com/adjoeio/djoemo/metrics.go:37.73,39.9 2 1 github.com/adjoeio/djoemo/metrics.go:39.9,44.3 2 1 github.com/adjoeio/djoemo/metrics.go:46.2,50.12 4 1 github.com/adjoeio/djoemo/metrics.go:54.73,56.2 1 1 -github.com/adjoeio/djoemo/metrics.go:58.63,60.32 2 1 -github.com/adjoeio/djoemo/metrics.go:60.32,62.3 1 0 +github.com/adjoeio/djoemo/metrics.go:58.66,60.32 2 1 +github.com/adjoeio/djoemo/metrics.go:60.32,62.3 1 1 github.com/adjoeio/djoemo/metrics.go:63.2,63.28 1 1 -github.com/adjoeio/djoemo/metrics.go:66.21,68.2 1 0 +github.com/adjoeio/djoemo/metrics.go:66.21,68.2 1 1 github.com/adjoeio/djoemo/metrics.go:74.48,76.2 1 1 -github.com/adjoeio/djoemo/metrics.go:78.118,79.35 1 1 +github.com/adjoeio/djoemo/metrics.go:78.114,79.35 1 1 github.com/adjoeio/djoemo/metrics.go:79.35,81.3 1 1 -github.com/adjoeio/djoemo/metrics.go:84.128,85.26 1 0 +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:19.78,27.2 4 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:29.85,40.2 4 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:49.77,56.2 2 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:58.128,59.67 1 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:59.67,62.3 2 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:64.2,65.13 2 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:65.13,67.3 1 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:69.2,70.27 2 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:70.27,72.3 1 0 -github.com/adjoeio/djoemo/metrics_prometheus.go:74.2,77.65 3 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/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/.gitignore b/.gitignore index 632f2ec..48d9864 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea vendor Gopkg.* +.coverage diff --git a/README.md b/README.md index 35948ae..3d87af4 100644 --- a/README.md +++ b/README.md @@ -3,231 +3,215 @@ import "github.com/adjoeio/djoemo" ``` is a facade library for [guregu/dynamo](https://github.com/guregu/dynamo) and uses the [repository pattern](https://deviq.com/repository-pattern) to simplify dynamodb operations (save, update , delete and retrieve) on go structs + ## Factories -``` +```go // NewRepository factory method for dynamo repository NewRepository(dynamoClient dynamodbiface.DynamoDBAPI) RepositoryInterface - ``` -``` +```go // Key factory method to create struct implement key interface func Key() *key { return &key{} } // usage - - key := dynamo.Key(). - WithTableName("user"). - WithHashKeyName("UserUUID"). - WithHashKey("123"). - WithRangeKeyName("CreatedAt"). - WithRangeKey(time.Now().Day()) - +key := djoemo.Key(). + WithTableName("user"). + WithHashKeyName("UserUUID"). + WithHashKey("123"). + WithRangeKeyName("CreatedAt"). + WithRangeKey(time.Now().Day()) ``` - ## Interfaces **RepositoryInterface:** -``` - // WithLog enables logging; it accepts LogInterface as logger - WithLog(log LogInterface) - - // WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher - WithMetrics(metricsInterface MetricsInterface) - - // Get 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 - Get(key KeyInterface, item interface{}) (bool, error) - - // Save item; it accepts a key interface, that is used to get the table name; item is the item to be saved - // returns error in case of error - Save(key KeyInterface, item interface{}) error - - // Update updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; - // values contains the values that should be used in the update; - // returns error in case of error - Update(expression UpdateExpression, key KeyInterface, values map[string]interface{}) error - - // Delete item by key; returns error in case of error - Delete(key KeyInterface) error - - // SaveItems batch save a slice of items by key; returns error in case of error - SaveItems(key KeyInterface, items interface{}) error - - // DeleteItems deletes items matching the keys; returns error in case of error - DeleteItems(key []KeyInterface) error - - // GetItems by key; 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 items - // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItems(key KeyInterface, items interface{}) (bool, error) - - // GetWithContext 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 - GetWithContext(key KeyInterface, item interface{}, ctx context.Context) (bool, error) - - // SaveWithContext it accepts a key interface, that is used to get the table name; item is the item to be saved; context which used to enable log with context - // returns error in case of error - SaveWithContext(key KeyInterface, item interface{}, ctx context.Context) error - - // Update updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; - // values contains the values that should be used in the update; context which used to enable log with context - // returns error in case of error - UpdateWithContext(expression UpdateExpression, key KeyInterface, values map[string]interface{}, ctx context.Context) error - - // Delete item by its key; it accepts key of item to be deleted; context which used to enable log with context - // returns error in case of error - DeleteWithContext(key KeyInterface, ctx context.Context) error - - // SaveItems batch save a slice of items by key; it accepts key of item to be saved; item to be saved; context which used to enable log with context - // returns error in case of error - SaveItemsWithContext(key KeyInterface, items interface{}, ctx context.Context) error - - // DeleteItems deletes items matching the keys; it accepts array of keys to be deleted; context which used to enable log with context - // returns error in case of error - DeleteItemsWithContext(key []KeyInterface, ctx context.Context) error - - // GetItems by key; it accepts key of item to get it; context which used to enable log with context - // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItemsWithContext(key KeyInterface, out interface{}, ctx context.Context) (bool, error) - - // QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; - // context which used to enable log with context, the output will be given in items - // returns error in case of error - QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error - - // Query by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; - // returns error in case of error - Query(query QueryInterface, item interface{}) error - - // GIndex returns index repository - GIndex(name string) GlobalIndexInterface +```go +// WithLog enables logging; it accepts LogInterface as logger +WithLog(log LogInterface) + +// WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher +WithMetrics(metricsInterface MetricsInterface) + +// WithPrometheusMetrics enables prometheus metrics +WithPrometheusMetrics(registry *prometheus.Registry) + +// 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 +GetItemWithContext(ctx context.Context, key KeyInterface, item any) (bool, error) + +// SaveItemWithContext it accepts a key interface, that is used to get the table name; item is the item to be saved; context which used to enable log with context +// returns error in case of error +SaveItemWithContext(ctx context.Context, key KeyInterface, item any) error + +// UpdateWithContext updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; +// values contains the values that should be used in the update; context which used to enable log with context +// returns error in case of error +UpdateWithContext(ctx context.Context, expression UpdateExpression, key KeyInterface, values map[string]any) error + +// UpdateWithUpdateExpressions updates an item with update expressions defined at field level, enabling you to set +// different update expressions for each field. The first key of the updateMap specifies the Update expression to use +// for the expressions in the map +UpdateWithUpdateExpressions(ctx context.Context, key KeyInterface, updateExpressions UpdateExpressions) error + +// UpdateWithUpdateExpressionsAndReturnValue updates an item with update expressions defined at field level and returns +// the item, as it appears after the update, enabling you to set different update expressions for each field. The first +// key of the updateMap specifies the Update expression to use for the expressions in the map +UpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key KeyInterface, item any, updateExpressions UpdateExpressions) error + +// ConditionalUpdateWithUpdateExpressionsAndReturnValue updates an item with update expressions and a condition. +// If the condition is met, the item will be updated and returned as it appears after the update. +// The first key of the updateMap specifies the Update expression to use for the expressions in the map +ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key KeyInterface, item any, updateExpressions UpdateExpressions, conditionExpression string, conditionArgs ...any) (conditionMet bool, err error) + +// DeleteItemWithContext item by its key; it accepts key of item to be deleted; context which used to enable log with context +// returns error in case of error +DeleteItemWithContext(ctx context.Context, key KeyInterface) error + +// SaveItemsWithContext batch save a slice of items by key; it accepts key of item to be saved; item to be saved; context which used to enable log with context +// returns error in case of error +SaveItemsWithContext(ctx context.Context, key KeyInterface, items any) error + +// DeleteItemsWithContext deletes items matching the keys; it accepts array of keys to be deleted; context which used to enable log with context +// returns error in case of error +DeleteItemsWithContext(ctx context.Context, key []KeyInterface) error + +// GetItemsWithContext by key; it accepts key of item to get it; context which used to enable log with context +// returns true if items are found, returns false and nil if no items found, returns false and error in case of error +GetItemsWithContext(ctx context.Context, key KeyInterface, out any) (bool, error) + +// QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; +// context which used to enable log with context, the output will be given in items +// returns error in case of error +QueryWithContext(ctx context.Context, query QueryInterface, item any) error + +// GIndex returns index repository +GIndex(name string) GlobalIndexInterface + +// OptimisticLockSaveWithContext saves an item if the version attribute on the server matches the version of the object +OptimisticLockSaveWithContext(ctx context.Context, key KeyInterface, item any) (bool, error) + +// ScanIteratorWithContext returns an instance of an iterator that provides methods to use for scanning tables +ScanIteratorWithContext(ctx context.Context, key KeyInterface, searchLimit int64) (IteratorInterface, error) + +// ConditionalUpdateWithContext updates an item if the passed expression and condition evaluates to true +ConditionalUpdateWithContext(ctx context.Context, key KeyInterface, item any, expression string, expressionArgs ...any) (bool, error) + +// BatchGetItemsWithContext gets multiple items by their keys; it accepts a slice of keys (all from the same table) +// and fills out (pointer to a slice) with any found items. +// returns true if at least one item is found, returns false and nil if no items found, returns false and error in case of error +BatchGetItemsWithContext(ctx context.Context, keys []KeyInterface, out any) (bool, error) ``` **GlobalIndexInterface:** -``` - // GetWithContext 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 - // returns true if item is found, returns false and nil if no item found, returns false and an error in case of error - GetWithContext(key KeyInterface, item interface{}, ctx context.Context) (bool, error) - - // GetItemsWithContext by key 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 items - // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItemsWithContext(key KeyInterface, items interface{}, ctx context.Context) (bool, error) - - // GetItems by key; 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 items - // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItems(key KeyInterface, items interface{}) (bool, error) - - // Get 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 - Get(key KeyInterface, item interface{}) (bool, error) - - // QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; - // context which used to enable log with context, the output will be given in items - // returns error in case of error - QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error - - // Query by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; - // returns error in case of error - Query(query QueryInterface, item interface{}) error - +```go +// 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 +// returns true if item is found, returns false and nil if no item found, returns false and an error in case of error +GetItemWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) + +// GetItemsWithContext by key 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 items +// returns true if items are found, returns false and nil if no items found, returns false and error in case of error +GetItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) + +// GetItemsWithRangeWithContext same as GetItemsWithContext, but also respects range key +GetItemsWithRangeWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) + +// QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; +// context which used to enable log with context, the output will be given in items +// returns error in case of error +QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error ``` **KeyInterface:** -act as adapter between dynamo db table key and golang model -``` - // TableName returns dynamo table name - TableName() string - - // HashKeyName returns the name of hash key if it exists - HashKeyName() *string - - // RangeKeyName returns the name of range key if it exists - RangeKeyName() *string - - // HashKey returns the hash key value - HashKey() interface{} - - // HashKey returns the range key value - RangeKey() interface{} - +Acts as adapter between dynamo db table key and golang model. +```go +// TableName returns dynamo table name +TableName() string + +// HashKeyName returns the name of hash key if it exists +HashKeyName() *string + +// RangeKeyName returns the name of range key if it exists +RangeKeyName() *string + +// HashKey returns the hash key value +HashKey() interface{} + +// RangeKey returns the range key value +RangeKey() interface{} ``` **LogInterface:** -to support debug, it's necessary to provide a logger with this interface -``` - // WithFields adds fields from map string interface to logger - WithFields(fields map[string]interface{}) LogInterface - - // Warn logs info - Info(message string ) - - // Warn logs warning - Warn(message string ) - - // Error logs error - Error(message string ) -``` +To support debug, it's necessary to provide a logger with this interface. +```go +// WithFields adds fields from map string interface to logger +WithFields(fields map[string]interface{}) LogInterface +// WithField adds a single field to logger +WithField(field string, value any) LogInterface -**MetricsInterface:** -to support metrics, it's necessary to provide a metrics publisher with this interface -``` - // Publish publishes metrics - Publish(key string, metricName string, metricValue float64) error +// WithContext adds context to logger +WithContext(ctx context.Context) LogInterface + +// Info logs info +Info(message string ) + +// Warn logs warning +Warn(message string ) + +// Error logs error +Error(message string ) ``` +**MetricsInterface:** +To support metrics, it's necessary to provide a metrics publisher with this interface. +```go +// Record publishes metrics for an operation +Record(ctx context.Context, caller string, key KeyInterface, duration time.Duration, err *error) +``` -##Usage +## Usage **Get example:** -``` +```go // enable log by passing logger interface - repository.WithLog(logInterface) - - // enable metrics by passing metrics interface - repository.WithMetrics(metricsInterface) - - user := &User{} - // use factory to create dynamo key interface - key := djoemo.Key(). - WithTableName("user"). - WithHashKeyName("UserUUID"). - WithHashKey("123") - - // get item - found, err := repository.Get(key, user) - if err != nil { - fmt.Println(err.Error()) - } - - if !found { - fmt.Println("user not found") - } - - // context is optional param, which used to enable log with context - ctx := context.Background() - type TraceInfo string - ctx = context.WithValue(ctx, TraceInfo("TraceInfo"), map[string]interface{}{"TraceID": "trace-id"}) - - // get item with extra to allow trace fields in logger - found, err = repository.GetWithContext(key, user, ctx) - if err != nil { - fmt.Println(err.Error()) - } - - if !found { - fmt.Println("user not found") - } +repository.WithLog(logInterface) + +// enable metrics by passing metrics interface +repository.WithMetrics(metricsInterface) + +user := &User{} +// use factory to create dynamo key interface +key := djoemo.Key(). + WithTableName("user"). + WithHashKeyName("UserUUID"). + WithHashKey("123") + +// optional: +// create context with source label for metrics (increase observability) +ctx = WithSourceLabel(ctx, "FooBarAPI") + +// get item +found, err := repository.GetItemWithContext( + ctx, + key, + user) +if err != nil { + fmt.Println(err.Error()) +} + +if !found { + fmt.Println("user not found") +} ``` -**notes** - * The operation will not fail, if publish of metrics returns an error. If the logger is enabled, it will just log the error. +**notes** +* The operation will not fail, if publish of metrics returns an error. If the logger is enabled, it will just log the error. For more examples see examples/example.go . \ No newline at end of file diff --git a/djoemo_suite_test.go b/djoemo_suite_test.go index f72b9d4..fde31d2 100644 --- a/djoemo_suite_test.go +++ b/djoemo_suite_test.go @@ -1,12 +1,11 @@ package djoemo_test import ( - "context" "testing" "time" - "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) func TestDjoemo(t *testing.T) { @@ -14,114 +13,6 @@ func TestDjoemo(t *testing.T) { RunSpecs(t, "Djoemo Suite") } -// Declarations for Ginkgo DSL -type Done ginkgo.Done -type Benchmarker ginkgo.Benchmarker - -var GinkgoWriter = ginkgo.GinkgoWriter -var GinkgoRandomSeed = ginkgo.GinkgoRandomSeed -var GinkgoParallelNode = ginkgo.GinkgoParallelNode -var GinkgoT = ginkgo.GinkgoT -var CurrentGinkgoTestDescription = ginkgo.CurrentGinkgoTestDescription -var RunSpecs = ginkgo.RunSpecs -var RunSpecsWithDefaultAndCustomReporters = ginkgo.RunSpecsWithDefaultAndCustomReporters -var RunSpecsWithCustomReporters = ginkgo.RunSpecsWithCustomReporters -var Skip = ginkgo.Skip -var Fail = ginkgo.Fail -var GinkgoRecover = ginkgo.GinkgoRecover -var Describe = ginkgo.Describe -var FDescribe = ginkgo.FDescribe -var PDescribe = ginkgo.PDescribe -var XDescribe = ginkgo.XDescribe -var Context = ginkgo.Context -var FContext = ginkgo.FContext -var PContext = ginkgo.PContext -var XContext = ginkgo.XContext -var When = ginkgo.When -var FWhen = ginkgo.FWhen -var PWhen = ginkgo.PWhen -var XWhen = ginkgo.XWhen -var It = ginkgo.It -var FIt = ginkgo.FIt -var PIt = ginkgo.PIt -var XIt = ginkgo.XIt -var Specify = ginkgo.Specify -var FSpecify = ginkgo.FSpecify -var PSpecify = ginkgo.PSpecify -var XSpecify = ginkgo.XSpecify -var By = ginkgo.By -var BeforeSuite = ginkgo.BeforeSuite -var AfterSuite = ginkgo.AfterSuite -var SynchronizedBeforeSuite = ginkgo.SynchronizedBeforeSuite -var SynchronizedAfterSuite = ginkgo.SynchronizedAfterSuite -var BeforeEach = ginkgo.BeforeEach -var JustBeforeEach = ginkgo.JustBeforeEach -var AfterEach = ginkgo.AfterEach - -// Declarations for Gomega DSL -var RegisterFailHandler = gomega.RegisterFailHandler -var RegisterTestingT = gomega.RegisterTestingT -var InterceptGomegaFailures = gomega.InterceptGomegaFailures -var Ω = gomega.Ω -var ExpectWithOffset = gomega.ExpectWithOffset -var EventuallyWithOffset = gomega.EventuallyWithOffset -var ConsistentlyWithOffset = gomega.ConsistentlyWithOffset -var SetDefaultEventuallyTimeout = gomega.SetDefaultEventuallyTimeout -var SetDefaultEventuallyPollingInterval = gomega.SetDefaultEventuallyPollingInterval -var SetDefaultConsistentlyDuration = gomega.SetDefaultConsistentlyDuration -var SetDefaultConsistentlyPollingInterval = gomega.SetDefaultConsistentlyPollingInterval -var NewGomegaWithT = gomega.NewGomegaWithT -var Expect = gomega.Expect -var Eventually = gomega.Eventually -var Consistently = gomega.Consistently - -// Declarations for Gomega Matchers -// Equal changed to Equal to avoid conflict -var BeEqualTo = gomega.Equal -var BeEquivalentTo = gomega.BeEquivalentTo -var BeIdenticalTo = gomega.BeIdenticalTo -var BeNil = gomega.BeNil -var BeTrue = gomega.BeTrue -var BeFalse = gomega.BeFalse -var HaveOccurred = gomega.HaveOccurred -var Succeed = gomega.Succeed -var MatchError = gomega.MatchError -var BeClosed = gomega.BeClosed -var Receive = gomega.Receive -var BeSent = gomega.BeSent -var MatchRegexp = gomega.MatchRegexp -var ContainSubstring = gomega.ContainSubstring -var HavePrefix = gomega.HavePrefix -var HaveSuffix = gomega.HaveSuffix -var MatchJSON = gomega.MatchJSON -var MatchXML = gomega.MatchXML -var MatchYAML = gomega.MatchYAML -var BeEmpty = gomega.BeEmpty -var HaveLen = gomega.HaveLen -var HaveCap = gomega.HaveCap -var BeZero = gomega.BeZero -var ContainElement = gomega.ContainElement -var ConsistOf = gomega.ConsistOf -var HaveKey = gomega.HaveKey -var HaveKeyWithValue = gomega.HaveKeyWithValue -var BeNumerically = gomega.BeNumerically -var BeTemporally = gomega.BeTemporally -var BeAssignableToTypeOf = gomega.BeAssignableToTypeOf -var Panic = gomega.Panic -var BeAnExistingFile = gomega.BeAnExistingFile -var BeARegularFile = gomega.BeARegularFile -var BeADirectory = gomega.BeADirectory -var And = gomega.And -var SatisfyAll = gomega.SatisfyAll -var Or = gomega.Or -var SatisfyAny = gomega.SatisfyAny -var Not = gomega.Not -var WithTransform = gomega.WithTransform - -type ContextFieldsKey = string - -const ContextFields ContextFieldsKey = "ContextFields" - // User model with hash key only type User struct { UUID string @@ -141,9 +32,3 @@ type Profile struct { CreatedAt time.Time TraceID string } - -// WithFields returns context with fields -func WithFields(fields map[string]interface{}) context.Context { - ctx := context.Background() - return context.WithValue(ctx, ContextFields, fields) -} diff --git a/global_index.go b/dynamo_global_index.go similarity index 50% rename from global_index.go rename to dynamo_global_index.go index b5f0f87..b740de5 100644 --- a/global_index.go +++ b/dynamo_global_index.go @@ -2,68 +2,77 @@ package djoemo import ( "context" + "errors" "reflect" + "time" "github.com/guregu/dynamo" + "github.com/prometheus/client_golang/prometheus" ) // GlobalIndex models a global secondary index used in a query type GlobalIndex struct { name string dynamoClient *dynamo.DB - log logger + log LogInterface + metrics *Metrics } -// GetItems by key; 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 items -// returns true if items are found, returns false and nil if no items found, returns false and error in case of error -func (gi GlobalIndex) GetItems(key KeyInterface, items interface{}) (bool, error) { - return gi.GetItemsWithContext(context.TODO(), key, items) +// WithLog enables logging; it accepts LogInterface as logger +func (gi *GlobalIndex) WithLog(log LogInterface) { + gi.log = log } -// GetItem 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 -func (gi GlobalIndex) GetItem(key KeyInterface, item interface{}) (bool, error) { - return gi.GetItemWithContext(context.TODO(), key, item) +// WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher +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) + gi.metrics.Add(prommetrics) + return gi } // GetItemWithContext item; it needs a key interface that is used to get the table name, hash key, and the range key if it exists; output will be contained in item; context is optional param, which used to enable log with context -func (gi GlobalIndex) GetItemWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) { +func (gi GlobalIndex) GetItemWithContext(ctx context.Context, key KeyInterface, item any) (bool, error) { + var err error + defer gi.recordMetrics(ctx, OpRead, key, &err)() - if err := isValidKey(key); err != nil { - gi.log.error(ctx, key.TableName(), err.Error()) + if err = isValidKey(key); err != nil { return false, err } - err := buildTableKeyCondition(gi.table(key.TableName()), key).Index(gi.name).OneWithContext(ctx, item) + err = buildTableKeyCondition(gi.table(key.TableName()), key).Index(gi.name).OneWithContext(ctx, item) if err != nil { - if err == dynamo.ErrNotFound { - gi.log.info(ctx, key.TableName(), ErrNoItemFound.Error()) + if errors.Is(err, dynamo.ErrNotFound) { + gi.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(ErrNoItemFound.Error()) return false, nil } - gi.log.error(ctx, key.TableName(), err.Error()) return false, err } return true, nil - } // GetItemsWithContext queries multiple items by key (hash key) and returns it in the slice of items items -func (gi GlobalIndex) GetItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) { - if err := isValidKey(key); err != nil { - gi.log.error(ctx, key.TableName(), err.Error()) +func (gi GlobalIndex) GetItemsWithContext(ctx context.Context, key KeyInterface, items any) (bool, error) { + var err error + defer gi.recordMetrics(ctx, OpRead, key, &err)() + + if err = isValidKey(key); err != nil { return false, err } - err := gi.table(key.TableName()).Get(*key.HashKeyName(), key.HashKey()).Index(gi.name).AllWithContext(ctx, items) + err = gi.table(key.TableName()).Get(*key.HashKeyName(), key.HashKey()).Index(gi.name).AllWithContext(ctx, items) if err != nil { - if err == dynamo.ErrNotFound { - gi.log.info(ctx, key.TableName(), ErrNoItemFound.Error()) + if errors.Is(err, dynamo.ErrNotFound) { + gi.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(ErrNoItemFound.Error()) return false, nil } - gi.log.error(ctx, key.TableName(), err.Error()) return false, err } @@ -82,20 +91,21 @@ func (gi GlobalIndex) GetItemsWithContext(ctx context.Context, key KeyInterface, } // GetItemsWithRangeWithContext queries multiple items by key (hash key) and returns it in the slice of items respecting the range key -func (gi GlobalIndex) GetItemsWithRangeWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) { - if err := isValidKey(key); err != nil { - gi.log.error(ctx, key.TableName(), err.Error()) +func (gi GlobalIndex) GetItemsWithRangeWithContext(ctx context.Context, key KeyInterface, items any) (bool, error) { + var err error + defer gi.recordMetrics(ctx, OpRead, key, &err)() + + if err = isValidKey(key); err != nil { return false, err } - err := buildTableKeyCondition(gi.table(key.TableName()), key).Index(gi.name).AllWithContext(ctx, items) + err = buildTableKeyCondition(gi.table(key.TableName()), key).Index(gi.name).AllWithContext(ctx, items) if err != nil { - if err == dynamo.ErrNotFound { - gi.log.info(ctx, key.TableName(), ErrNoItemFound.Error()) + if errors.Is(err, dynamo.ErrNotFound) { + gi.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(ErrNoItemFound.Error()) return false, nil } - gi.log.error(ctx, key.TableName(), err.Error()) return false, err } @@ -120,13 +130,13 @@ func (gi GlobalIndex) table(tableName string) dynamo.Table { // QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; // context which used to enable log with context, the output will be given in items // returns error in case of error -func (gi GlobalIndex) QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error { +func (gi GlobalIndex) QueryWithContext(ctx context.Context, query QueryInterface, item any) (err error) { + defer gi.recordMetrics(ctx, OpRead, query, &err)() if !IsPointerOFSlice(item) { return ErrInvalidPointerSliceType } - if err := isValidKey(query); err != nil { - gi.log.error(ctx, query.TableName(), err.Error()) + if err = isValidKey(query); err != nil { return err } @@ -145,17 +155,17 @@ func (gi GlobalIndex) QueryWithContext(ctx context.Context, query QueryInterface q = q.Order(dynamo.Descending) } - err := q.AllWithContext(ctx, item) + err = q.AllWithContext(ctx, item) if err != nil { - gi.log.error(ctx, query.TableName(), err.Error()) return err } return nil } -// Query by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; -// returns error in case of error -func (gi GlobalIndex) Query(query QueryInterface, item interface{}) error { - return gi.QueryWithContext(context.TODO(), query, item) +func (gi GlobalIndex) recordMetrics(ctx context.Context, op string, key KeyInterface, err *error) func() { + start := time.Now() + return func() { + gi.metrics.Record(ctx, op, key, time.Since(start), isOpSuccess(err)) + } } diff --git a/dynamo_global_index_interface.go b/dynamo_global_index_interface.go index ad6947d..20a9dd3 100644 --- a/dynamo_global_index_interface.go +++ b/dynamo_global_index_interface.go @@ -2,11 +2,22 @@ package djoemo import ( "context" + + "github.com/prometheus/client_golang/prometheus" ) //go:generate mockgen -source=dynamo_global_index_interface.go -destination=./mock/dynamo_global_index_interface.go -package=mock . type GlobalIndexInterface interface { + // WithLog enables logging; it accepts LogInterface as logger + WithLog(log LogInterface) + + // WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher + WithMetrics(metricsInterface MetricsInterface) + + // WithPrometheusMetrics enables prometheus metrics + WithPrometheusMetrics(registry *prometheus.Registry) 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 // returns true if item is found, returns false and nil if no item found, returns false and an error in case of error @@ -20,20 +31,8 @@ type GlobalIndexInterface interface { // GetItemsWithRangeWithContext same as GetItemsWithContext, but also respects range key GetItemsWithRangeWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) - // GetItems by key; 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 items - // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItems(key KeyInterface, items interface{}) (bool, error) - - // GetItem 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 - GetItem(key KeyInterface, item interface{}) (bool, error) - // QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; // context which used to enable log with context, the output will be given in items // returns error in case of error QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error - - // Query by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; - // returns error in case of error - Query(query QueryInterface, item interface{}) error } diff --git a/dynamo_repository.go b/dynamo_repository.go index 831c821..2e1ea77 100644 --- a/dynamo_repository.go +++ b/dynamo_repository.go @@ -4,9 +4,11 @@ import ( "context" "errors" "reflect" + "time" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/prometheus/client_golang/prometheus" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" "github.com/guregu/dynamo" @@ -15,45 +17,54 @@ import ( // Repository facade for github.com/guregu/djoemo type Repository struct { dynamoClient *dynamo.DB - log logger - metrics metrics + log LogInterface + metrics *Metrics } // NewRepository factory method for djoemo repository func NewRepository(dynamoClient dynamodbiface.DynamoDBAPI) RepositoryInterface { return &Repository{ dynamoClient: dynamo.NewFromIface(dynamoClient), - log: logger{log: nopLog{}}, + log: NewNopLog(), + metrics: &Metrics{}, } } // WithLog enables logging; it accepts LogInterface as logger func (repository *Repository) WithLog(log LogInterface) { - repository.log = logger{log: log} + repository.log = log } // WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher func (repository *Repository) WithMetrics(metricsInterface MetricsInterface) { - repository.metrics = metrics{metrics: metricsInterface} + repository.metrics.Add(metricsInterface) +} + +// WithPrometheusMetrics enables prometheus metrics +func (repository *Repository) WithPrometheusMetrics(registry *prometheus.Registry) RepositoryInterface { + prommetrics := NewPrometheusMetrics(registry) + repository.metrics.Add(prommetrics) + return repository } // GetItemWithContext get item; 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 // returns true if item is found, returns false and nil if no item found, returns false and an error in case of error -func (repository *Repository) GetItemWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) { - if err := isValidKey(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) +func (repository Repository) GetItemWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) { + var err error + defer repository.recordMetrics(ctx, OpRead, key, &err)() + + if err = isValidKey(key); err != nil { return false, err } - err := buildTableKeyCondition(repository.table(key.TableName()), key).OneWithContext(ctx, item) + err = buildTableKeyCondition(repository.table(key.TableName()), key).OneWithContext(ctx, item) if err != nil { - if err == dynamo.ErrNotFound { - repository.log.info(ctx, key.TableName(), ErrNoItemFound.Error()) + if errors.Is(err, dynamo.ErrNotFound) { + repository.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(ErrNoItemFound.Error()) return false, nil } - repository.log.error(ctx, key.TableName(), err.Error()) return false, err } @@ -62,21 +73,17 @@ func (repository *Repository) GetItemWithContext(ctx context.Context, key KeyInt // SaveItemWithContext it accepts a key interface, that is used to get the table name; item is the item to be saved; context which used to enable log with context // returns error in case of error -func (repository *Repository) SaveItemWithContext(ctx context.Context, key KeyInterface, item interface{}) error { - if err := isValidKey(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) - return err - } +func (repository Repository) SaveItemWithContext(ctx context.Context, key KeyInterface, item interface{}) error { + var err error + defer repository.recordMetrics(ctx, OpCommit, key, &err)() - err := repository.table(key.TableName()).Put(item).RunWithContext(ctx) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) + if err = isValidKey(key); err != nil { return err } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameSavedItemsCount, float64(1)) + err = repository.table(key.TableName()).Put(item).RunWithContext(ctx) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) + return err } return nil @@ -85,9 +92,11 @@ func (repository *Repository) SaveItemWithContext(ctx context.Context, key KeyIn // UpdateWithContext updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; // values contains the values that should be used in the update; context which used to enable log with context // returns error in case of error -func (repository *Repository) UpdateWithContext(ctx context.Context, expression UpdateExpression, key KeyInterface, values map[string]interface{}) error { - if err := isValidKey(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) +func (repository Repository) UpdateWithContext(ctx context.Context, expression UpdateExpression, key KeyInterface, values map[string]interface{}) error { + var err error + defer repository.recordMetrics(ctx, OpUpdate, key, &err)() + + if err = isValidKey(key); err != nil { return err } @@ -115,29 +124,22 @@ func (repository *Repository) UpdateWithContext(ctx context.Context, expression if expression == SetExpr { valueSlice, err := InterfaceToArrayOfInterface(value) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } update.SetExpr(expr, valueSlice...) } } - err := update.RunWithContext(ctx) + err = update.RunWithContext(ctx) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameUpdatedItemsCount, float64(1)) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) - } - return nil } -func (repository *Repository) prepareUpdateWithUpdateExpressions( - ctx context.Context, +func (repository Repository) prepareUpdateWithUpdateExpressions( + _ context.Context, key KeyInterface, updateExpressions UpdateExpressions, ) (*dynamo.Update, error) { @@ -172,7 +174,6 @@ func (repository *Repository) prepareUpdateWithUpdateExpressions( if expression == SetExpr { valueSlice, err := InterfaceToArrayOfInterface(value) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return nil, err } update.SetExpr(expr, valueSlice...) @@ -186,74 +187,68 @@ func (repository *Repository) prepareUpdateWithUpdateExpressions( // UpdateWithUpdateExpressions updates an item with update expressions defined at field level, enabling you to set // different update expressions for each field. The first key of the updateMap specifies the Update expression to use // for the expressions in the map -func (repository *Repository) UpdateWithUpdateExpressions( +func (repository Repository) UpdateWithUpdateExpressions( ctx context.Context, key KeyInterface, updateExpressions UpdateExpressions, ) error { + var err error + defer repository.recordMetrics(ctx, OpUpdate, key, &err)() + update, err := repository.prepareUpdateWithUpdateExpressions(ctx, key, updateExpressions) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } err = update.RunWithContext(ctx) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameUpdatedItemsCount, float64(1)) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) - } - return nil } // UpdateWithUpdateExpressionsAndReturnValue updates an item with update expressions defined at field level and returns // the item, as it appears after the update, enabling you to set different update expressions for each field. The first // key of the updateMap specifies the Update expression to use for the expressions in the map -func (repository *Repository) UpdateWithUpdateExpressionsAndReturnValue( +func (repository Repository) UpdateWithUpdateExpressionsAndReturnValue( ctx context.Context, key KeyInterface, item interface{}, updateExpressions UpdateExpressions, ) error { + var err error + defer repository.recordMetrics(ctx, OpUpdate, key, &err)() + update, err := repository.prepareUpdateWithUpdateExpressions(ctx, key, updateExpressions) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } err = update.ValueWithContext(ctx, item) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameUpdatedItemsCount, float64(1)) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) - } - return nil } // ConditionalUpdateWithUpdateExpressionsAndReturnValue updates an item with update expressions and a condition. // If the condition is met, the item will be updated and returned as it appears after the update. // The first key of the updateMap specifies the Update expression to use for the expressions in the map -func (repository *Repository) ConditionalUpdateWithUpdateExpressionsAndReturnValue( +func (repository Repository) ConditionalUpdateWithUpdateExpressionsAndReturnValue( ctx context.Context, key KeyInterface, item interface{}, updateExpressions UpdateExpressions, conditionExpression string, conditionArgs ...interface{}, -) (conditionMet bool, err error) { +) (bool, error) { + var err error + defer repository.recordMetrics(ctx, OpUpdate, key, &err)() + update, err := repository.prepareUpdateWithUpdateExpressions(ctx, key, updateExpressions) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return false, err } @@ -262,16 +257,11 @@ func (repository *Repository) ConditionalUpdateWithUpdateExpressionsAndReturnVal err = update.ValueWithContext(ctx, item) if err != nil { if awsError, ok := err.(awserr.Error); ok && awsError.Code() == dynamodb.ErrCodeConditionalCheckFailedException { - repository.log.info(ctx, key.TableName(), dynamodb.ErrCodeConditionalCheckFailedException) + repository.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(dynamodb.ErrCodeConditionalCheckFailedException) return false, nil } - repository.log.error(ctx, key.TableName(), err.Error()) - return false, err - } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameUpdatedItemsCount, float64(1)) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) + return false, err } return true, nil @@ -279,10 +269,11 @@ func (repository *Repository) ConditionalUpdateWithUpdateExpressionsAndReturnVal // DeleteItemWithContext item by its key; it accepts key of item to be deleted; context which used to enable log with context // returns error in case of error -func (repository *Repository) DeleteItemWithContext(ctx context.Context, key KeyInterface) error { +func (repository Repository) DeleteItemWithContext(ctx context.Context, key KeyInterface) error { + var err error + defer repository.recordMetrics(ctx, OpDelete, key, &err)() - if err := isValidKey(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) + if err = isValidKey(key); err != nil { return err } // by hash @@ -293,26 +284,21 @@ func (repository *Repository) DeleteItemWithContext(ctx context.Context, key Key delete = delete.Range(*key.RangeKeyName(), key.RangeKey()) } - err := delete.RunWithContext(ctx) + err = delete.RunWithContext(ctx) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameDeleteItemsCount, float64(1)) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) - } - return nil } // SaveItemsWithContext batch save a slice of items by key; it accepts key of item to be saved; item to be saved; context which used to enable log with context // returns error in case of error -func (repository *Repository) SaveItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) error { +func (repository Repository) SaveItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) error { + var err error + defer repository.recordMetrics(ctx, OpCommit, key, &err)() - if err := isValidKey(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) + if err = isValidKey(key); err != nil { return err } @@ -325,33 +311,28 @@ func (repository *Repository) SaveItemsWithContext(ctx context.Context, key KeyI itemSlice, err := InterfaceToArrayOfInterface(items) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } - count, err := batch.Write().Put(itemSlice...).RunWithContext(ctx) + _, err = batch.Write().Put(itemSlice...).RunWithContext(ctx) if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) return err } - err = repository.metrics.Publish(ctx, key.TableName(), MetricNameSavedItemsCount, float64(count)) - if err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) - } - return nil } // DeleteItemsWithContext deletes items matching the keys; it accepts array of keys to be deleted; context which used to enable log with context // returns error in case of error -func (repository *Repository) DeleteItemsWithContext(ctx context.Context, keys []KeyInterface) error { +func (repository Repository) DeleteItemsWithContext(ctx context.Context, keys []KeyInterface) error { + var err error + defer repository.recordMultipleMetrics(ctx, OpDelete, keys, &err)() + if len(keys) == 0 { return nil } for i := 0; i < len(keys); i++ { - if err := isValidKey(keys[i]); err != nil { - repository.log.error(ctx, keys[i].TableName(), err.Error()) + if err = isValidKey(keys[i]); err != nil { return err } } @@ -368,37 +349,32 @@ func (repository *Repository) DeleteItemsWithContext(ctx context.Context, keys [ dynamoKeys[i] = dynamo.Keyed(keys[i]) } - count, err := batch.Write().Delete(dynamoKeys...).RunWithContext(ctx) + _, err = batch.Write().Delete(dynamoKeys...).RunWithContext(ctx) if err != nil { - repository.log.error(ctx, keys[0].TableName(), err.Error()) return err } - err = repository.metrics.Publish(ctx, keys[0].TableName(), MetricNameDeleteItemsCount, float64(count)) - if err != nil { - repository.log.error(ctx, keys[0].TableName(), err.Error()) - } - return nil } // GetItemsWithContext by key; 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 items // returns true if items are found, returns false and nil if no items found, returns false and error in case of error -func (repository *Repository) GetItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) { - if err := isValidKey(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) +func (repository Repository) GetItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) (bool, error) { + var err error + defer repository.recordMetrics(ctx, OpRead, key, &err)() + + if err = isValidKey(key); err != nil { return false, err } - err := repository.table(key.TableName()).Get(*key.HashKeyName(), key.HashKey()).AllWithContext(ctx, items) + err = repository.table(key.TableName()).Get(*key.HashKeyName(), key.HashKey()).AllWithContext(ctx, items) if err != nil { - if err == dynamo.ErrNotFound { - repository.log.info(ctx, key.TableName(), ErrNoItemFound.Error()) + if errors.Is(err, dynamo.ErrNotFound) { + repository.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(ErrNoItemFound.Error()) return false, nil } - repository.log.error(ctx, key.TableName(), err.Error()) return false, err } @@ -419,13 +395,13 @@ func (repository *Repository) GetItemsWithContext(ctx context.Context, key KeyIn // QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; // context which used to enable log with context, the output will be given in items // returns error in case of error -func (repository *Repository) QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error { +func (repository Repository) QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) (err error) { + defer repository.recordMetrics(ctx, OpRead, query, &err)() if !IsPointerOFSlice(item) { return ErrInvalidPointerSliceType } - if err := isValidKey(query); err != nil { - repository.log.error(ctx, query.TableName(), err.Error()) + if err = isValidKey(query); err != nil { return err } @@ -444,9 +420,8 @@ func (repository *Repository) QueryWithContext(ctx context.Context, query QueryI q = q.Order(dynamo.Descending) } - err := q.AllWithContext(ctx, item) + err = q.AllWithContext(ctx, item) if err != nil { - repository.log.error(ctx, query.TableName(), err.Error()) return err } @@ -455,6 +430,9 @@ func (repository *Repository) QueryWithContext(ctx context.Context, query QueryI // OptimisticLockSaveWithContext saves an item if the version attribute on the server matches the version of the object func (repository Repository) OptimisticLockSaveWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) { + var err error + defer repository.recordMetrics(ctx, OpCommit, key, &err)() + model, isDjoemoModel := item.(ModelInterface) if !isDjoemoModel { return false, errors.New("Items to use with OptimisticLock must implement the ModelInterface") @@ -467,13 +445,13 @@ func (repository Repository) OptimisticLockSaveWithContext(ctx context.Context, update := repository.table(key.TableName()).Put(item).If("attribute_not_exists(Version) OR Version = ?", currentVersion) - err := update.Run() + err = update.Run() if err != nil { if awserr, ok := err.(awserr.Error); ok && awserr.Code() == dynamodb.ErrCodeConditionalCheckFailedException { - repository.log.info(ctx, key.TableName(), dynamodb.ErrCodeConditionalCheckFailedException) + repository.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(dynamodb.ErrCodeConditionalCheckFailedException) return false, nil } - repository.log.error(ctx, key.TableName(), err.Error()) + return false, err } return true, nil @@ -481,93 +459,43 @@ func (repository Repository) OptimisticLockSaveWithContext(ctx context.Context, // ConditionalUpdateWithContext updates an item when the condition is met, otherwise the update will be rejected func (repository Repository) ConditionalUpdateWithContext(ctx context.Context, key KeyInterface, item interface{}, expression string, expressionArgs ...interface{}) (bool, error) { + var err error + defer repository.recordMetrics(ctx, OpUpdate, key, &err)() + update := repository.table(key.TableName()).Put(item).If(expression, expressionArgs...) - err := update.Run() + err = update.Run() if err != nil { if awserr, ok := err.(awserr.Error); ok && awserr.Code() == dynamodb.ErrCodeConditionalCheckFailedException { - repository.log.info(ctx, key.TableName(), dynamodb.ErrCodeConditionalCheckFailedException) + repository.log.WithContext(ctx).WithField(TableName, key.TableName()).Info(dynamodb.ErrCodeConditionalCheckFailedException) return false, nil } - repository.log.error(ctx, key.TableName(), err.Error()) + return false, err } return true, nil } -// GetItem 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 -func (repository Repository) GetItem(key KeyInterface, item interface{}) (bool, error) { - return repository.GetItemWithContext(context.TODO(), key, item) -} - -// SaveItem item; it accepts a key interface, that is used to get the table name; item is the item to be saved -// returns error in case of error -func (repository Repository) SaveItem(key KeyInterface, item interface{}) error { - return repository.SaveItemWithContext(context.TODO(), key, item) -} - -// Update updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; -// values contains the values that should be used in the update; -// returns error in case of error -func (repository Repository) Update(expression UpdateExpression, key KeyInterface, values map[string]interface{}) error { - return repository.UpdateWithContext(context.TODO(), expression, key, values) -} - -// DeleteItem item by key; returns error in case of error -func (repository Repository) DeleteItem(key KeyInterface) error { - return repository.DeleteItemWithContext(context.TODO(), key) -} - -// SaveItems batch save a slice of items by key -func (repository Repository) SaveItems(key KeyInterface, items interface{}) error { - return repository.SaveItemsWithContext(context.TODO(), key, items) -} - -// DeleteItems deletes items matching the keys -func (repository Repository) DeleteItems(keys []KeyInterface) error { - return repository.DeleteItemsWithContext(context.TODO(), keys) -} - -// GetItems by key; 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 items -// returns true if items are found, returns false and nil if no items found, returns false and error in case of error -func (repository Repository) GetItems(key KeyInterface, items interface{}) (bool, error) { - return repository.GetItemsWithContext(context.TODO(), key, items) -} - -// Query by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; -// returns error in case of error -func (repository Repository) Query(query QueryInterface, item interface{}) error { - return repository.QueryWithContext(context.TODO(), query, item) -} - -// OptimisticLockSave updates an item if the version attribute on the server matches the one of the object -func (repository Repository) OptimisticLockSave(key KeyInterface, item interface{}) (bool, error) { - return repository.OptimisticLockSaveWithContext(context.TODO(), key, item) -} - -// ConditionalUpdate updates an item when the condition is met, otherwise the update will be rejected -func (repository Repository) ConditionalUpdate(key KeyInterface, item interface{}, expression string, expressionArgs ...interface{}) (bool, error) { - return repository.ConditionalUpdateWithContext(context.TODO(), key, item, expression, expressionArgs) -} - // GIndex creates an index repository by name -func (repository Repository) GIndex(name string) GlobalIndexInterface { - return GlobalIndex{ +func (repository *Repository) GIndex(name string) GlobalIndexInterface { + return &GlobalIndex{ name: name, log: repository.log, dynamoClient: repository.dynamoClient, + metrics: repository.metrics, } } -func (repository Repository) table(tableName string) dynamo.Table { +func (repository *Repository) table(tableName string) dynamo.Table { return repository.dynamoClient.Table(tableName) } // ScanIteratorWithContext returns an instance of an Iterator that provides methods for scanning tables -func (repository Repository) ScanIteratorWithContext(ctx context.Context, key KeyInterface, searchLimit int64) (IteratorInterface, error) { - if err := isValidTableName(key); err != nil { - repository.log.error(ctx, key.TableName(), err.Error()) +func (repository *Repository) ScanIteratorWithContext(ctx context.Context, key KeyInterface, searchLimit int64) (IteratorInterface, error) { + var err error + defer repository.recordMetrics(ctx, OpRead, key, &err)() + + if err = isValidTableName(key); err != nil { return nil, err } @@ -589,7 +517,10 @@ func (repository Repository) ScanIteratorWithContext(ctx context.Context, key Ke // BatchGetItemsWithContext gets multiple items by their keys; all keys must refer to the same table. // out must be a pointer to a slice of your model type. // Returns (true, nil) if at least one item is found, (false, nil) if none found, or (false, err) on error. -func (repository *Repository) BatchGetItemsWithContext(ctx context.Context, keys []KeyInterface, out interface{}) (bool, error) { +func (repository Repository) BatchGetItemsWithContext(ctx context.Context, keys []KeyInterface, out interface{}) (bool, error) { + var err error + defer repository.recordMultipleMetrics(ctx, OpRead, keys, &err)() + if len(keys) == 0 { return false, nil } @@ -597,14 +528,11 @@ func (repository *Repository) BatchGetItemsWithContext(ctx context.Context, keys // Validate keys and ensure they all point to the same table tableName := keys[0].TableName() for i := 0; i < len(keys); i++ { - if err := isValidKey(keys[i]); err != nil { - repository.log.error(ctx, keys[i].TableName(), err.Error()) + if err = isValidKey(keys[i]); err != nil { return false, err } if keys[i].TableName() != tableName { - err := errors.New("BatchGetItemsWithContext: all keys must belong to the same table") - repository.log.error(ctx, tableName, err.Error()) - return false, err + return false, ErrInvalidBatchRequest } } @@ -622,14 +550,12 @@ func (repository *Repository) BatchGetItemsWithContext(ctx context.Context, keys } // Execute batch get - err := batch.Get(dKeys...).AllWithContext(ctx, out) + err = batch.Get(dKeys...).AllWithContext(ctx, out) if err != nil { if errors.Is(err, dynamo.ErrNotFound) { - repository.log.info(ctx, tableName, ErrNoItemFound.Error()) + repository.log.WithContext(ctx).WithField(TableName, tableName).Info(ErrNoItemFound.Error()) return false, nil } - - repository.log.error(ctx, tableName, err.Error()) return false, err } @@ -647,3 +573,27 @@ func (repository *Repository) BatchGetItemsWithContext(ctx context.Context, keys return true, nil } + +func (repository Repository) recordMetrics(ctx context.Context, op string, key KeyInterface, err *error) func() { + start := time.Now() + return func() { + repository.metrics.Record(ctx, op, key, time.Since(start), isOpSuccess(err)) + } +} + +func (repository Repository) recordMultipleMetrics(ctx context.Context, op string, keys []KeyInterface, err *error) func() { + start := time.Now() + return func() { + duration := time.Since(start) + for _, key := range keys { + repository.metrics.Record(ctx, op, key, duration, isOpSuccess(err)) + } + } +} + +func isOpSuccess(err *error) bool { + if err == nil || *err == nil { + return true + } + return errors.Is(*err, dynamo.ErrNotFound) +} diff --git a/dynamo_repository_interface.go b/dynamo_repository_interface.go index 1f840e0..f31744e 100644 --- a/dynamo_repository_interface.go +++ b/dynamo_repository_interface.go @@ -2,12 +2,14 @@ package djoemo import ( "context" -) -//go:generate mockgen -source=dynamo_repository_interface.go -destination=./mock/dynamo_repository_interface.go -package=mock . + "github.com/prometheus/client_golang/prometheus" +) // RepositoryInterface provides an interface to enable mocking the AWS dynamodb repository // for testing your code. +// +//go:generate mockgen -source=dynamo_repository_interface.go -destination=./mock/dynamo_repository_interface.go -package=mock . type RepositoryInterface interface { // WithLog enables logging; it accepts LogInterface as logger WithLog(log LogInterface) @@ -15,44 +17,21 @@ type RepositoryInterface interface { // WithMetrics enables metrics; it accepts MetricsInterface as metrics publisher WithMetrics(metricsInterface MetricsInterface) - // GetItem 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 - GetItem(key KeyInterface, item interface{}) (bool, error) - - // SaveItem item; it accepts a key interface, that is used to get the table name; item is the item to be saved - // returns error in case of error - SaveItem(key KeyInterface, item interface{}) error - - // Update updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; - // values contains the values that should be used in the update; - // returns error in case of error - Update(expression UpdateExpression, key KeyInterface, values map[string]interface{}) error - - // DeleteItem item by key; returns error in case of error - DeleteItem(key KeyInterface) error - - // SaveItems batch save a slice of items by key; returns error in case of error - SaveItems(key KeyInterface, items interface{}) error - - // DeleteItems deletes items matching the keys; returns error in case of error - DeleteItems(key []KeyInterface) error - - // GetItems by key; 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 items - // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItems(key KeyInterface, items interface{}) (bool, error) + // WithPrometheusMetrics enables prometheus metrics + WithPrometheusMetrics(registry *prometheus.Registry) 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 - GetItemWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) + GetItemWithContext(ctx context.Context, key KeyInterface, item any) (bool, error) // SaveItemWithContext it accepts a key interface, that is used to get the table name; item is the item to be saved; context which used to enable log with context // returns error in case of error - SaveItemWithContext(ctx context.Context, key KeyInterface, item interface{}) error + SaveItemWithContext(ctx context.Context, key KeyInterface, item any) error // UpdateWithContext updates item by key; it accepts an expression (Set, SetSet, SetIfNotExists, SetExpr); key is the key to be updated; // values contains the values that should be used in the update; context which used to enable log with context // returns error in case of error - UpdateWithContext(ctx context.Context, expression UpdateExpression, key KeyInterface, values map[string]interface{}) error + UpdateWithContext(ctx context.Context, expression UpdateExpression, key KeyInterface, values map[string]any) error // UpdateWithUpdateExpressions updates an item with update expressions defined at field level, enabling you to set // different update expressions for each field. The first key of the updateMap specifies the Update expression to use @@ -62,12 +41,12 @@ type RepositoryInterface interface { // UpdateWithUpdateExpressionsAndReturnValue updates an item with update expressions defined at field level and returns // the item, as it appears after the update, enabling you to set different update expressions for each field. The first // key of the updateMap specifies the Update expression to use for the expressions in the map - UpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key KeyInterface, item interface{}, updateExpressions UpdateExpressions) error + UpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key KeyInterface, item any, updateExpressions UpdateExpressions) error // ConditionalUpdateWithUpdateExpressionsAndReturnValue updates an item with update expressions and a condition. // If the condition is met, the item will be updated and returned as it appears after the update. // The first key of the updateMap specifies the Update expression to use for the expressions in the map - ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key KeyInterface, item interface{}, updateExpressions UpdateExpressions, conditionExpression string, conditionArgs ...interface{}) (conditionMet bool, err error) + ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key KeyInterface, item any, updateExpressions UpdateExpressions, conditionExpression string, conditionArgs ...any) (conditionMet bool, err error) // DeleteItemWithContext item by its key; it accepts key of item to be deleted; context which used to enable log with context // returns error in case of error @@ -75,7 +54,7 @@ type RepositoryInterface interface { // SaveItemsWithContext batch save a slice of items by key; it accepts key of item to be saved; item to be saved; context which used to enable log with context // returns error in case of error - SaveItemsWithContext(ctx context.Context, key KeyInterface, items interface{}) error + SaveItemsWithContext(ctx context.Context, key KeyInterface, items any) error // DeleteItemsWithContext deletes items matching the keys; it accepts array of keys to be deleted; context which used to enable log with context // returns error in case of error @@ -83,37 +62,27 @@ type RepositoryInterface interface { // GetItemsWithContext by key; it accepts key of item to get it; context which used to enable log with context // returns true if items are found, returns false and nil if no items found, returns false and error in case of error - GetItemsWithContext(ctx context.Context, key KeyInterface, out interface{}) (bool, error) + GetItemsWithContext(ctx context.Context, key KeyInterface, out any) (bool, error) // QueryWithContext by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; // context which used to enable log with context, the output will be given in items // returns error in case of error - QueryWithContext(ctx context.Context, query QueryInterface, item interface{}) error - - // Query by query; it accepts a query interface that is used to get the table name, hash key and range key with its operator if it exists; - // returns error in case of error - Query(query QueryInterface, item interface{}) error + QueryWithContext(ctx context.Context, query QueryInterface, item any) error // GIndex returns index repository GIndex(name string) GlobalIndexInterface - //OptimisticLockSaveWithContext saves an item if the version attribute on the server matches the version of the object - OptimisticLockSaveWithContext(ctx context.Context, key KeyInterface, item interface{}) (bool, error) + // OptimisticLockSaveWithContext saves an item if the version attribute on the server matches the version of the object + OptimisticLockSaveWithContext(ctx context.Context, key KeyInterface, item any) (bool, error) - //OptimisticLockSave ... - OptimisticLockSave(key KeyInterface, item interface{}) (bool, error) - - //ScanIteratorWithContext returns an instance of an iterator that provides methods to use for scanning tables + // ScanIteratorWithContext returns an instance of an iterator that provides methods to use for scanning tables ScanIteratorWithContext(ctx context.Context, key KeyInterface, searchLimit int64) (IteratorInterface, error) - //ConditionalUpdateWithContext updates an item if the passed expression and condition evaluates to true - ConditionalUpdateWithContext(ctx context.Context, key KeyInterface, item interface{}, expression string, expressionArgs ...interface{}) (bool, error) - - //ConditionalUpdate updates an item if the passed expression and condition evaluates to true - ConditionalUpdate(key KeyInterface, item interface{}, expression string, expressionArgs ...interface{}) (bool, error) + // ConditionalUpdateWithContext updates an item if the passed expression and condition evaluates to true + ConditionalUpdateWithContext(ctx context.Context, key KeyInterface, item any, expression string, expressionArgs ...any) (bool, error) // BatchGetItemsWithContext gets multiple items by their keys; it accepts a slice of keys (all from the same table) // and fills out (pointer to a slice) with any found items. // returns true if at least one item is found, returns false and nil if no items found, returns false and error in case of error - BatchGetItemsWithContext(ctx context.Context, keys []KeyInterface, out interface{}) (bool, error) + BatchGetItemsWithContext(ctx context.Context, keys []KeyInterface, out any) (bool, error) } diff --git a/errors.go b/errors.go index fe6e3f7..57bf0c7 100644 --- a/errors.go +++ b/errors.go @@ -2,20 +2,23 @@ package djoemo import "errors" -//ErrInvalidTableName table name is invalid error +// ErrInvalidTableName table name is invalid error var ErrInvalidTableName = errors.New("invalid table name") -//ErrInvalidHashKeyName hash key name is invalid error +// ErrInvalidHashKeyName hash key name is invalid error var ErrInvalidHashKeyName = errors.New("invalid hash key name") -//ErrInvalidHashKeyValue hash key value is invalid error +// ErrInvalidHashKeyValue hash key value is invalid error var ErrInvalidHashKeyValue = errors.New("invalid hash key value") -//ErrNoItemFound item not found error +// ErrNoItemFound item not found error var ErrNoItemFound = errors.New("no item found") -//ErrInvalidSliceType interface should be slice error +// ErrInvalidSliceType interface should be slice error var ErrInvalidSliceType = errors.New("invalid type expected slice") -//ErrInvalidPointerSliceType should be pointer of slice error +// ErrInvalidPointerSliceType should be pointer of slice error var ErrInvalidPointerSliceType = errors.New("invalid type expected pointer of slice") + +// ErrInvalidBatchRequest batch request should be for same table +var ErrInvalidBatchRequest = errors.New("batch request with multiple tables") diff --git a/examples/example.go b/examples/example.go index 2e71d8a..20f91b3 100644 --- a/examples/example.go +++ b/examples/example.go @@ -47,7 +47,7 @@ func Get() { WithHashKey("123") // get item - found, err := repository.GetItem(key, user) + found, err := repository.GetItemWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } @@ -88,7 +88,7 @@ func GetItems() { WithHashKey("123") // get item - found, err := repository.GetItems(key, users) + found, err := repository.GetItemsWithContext(context.Background(), key, users) if err != nil { fmt.Println(err.Error()) } @@ -98,7 +98,7 @@ func GetItems() { } // get item with context to allow trace fields in logger - found, err = repository.GetItem(key, users) + found, err = repository.GetItemWithContext(context.Background(), key, users) if err != nil { fmt.Println(err.Error()) } @@ -127,7 +127,7 @@ func Query() { WithRangeOp(djoemo.BeginsWith) // query items - err := repository.Query(q, users) + err := repository.QueryWithContext(context.Background(), q, users) if err != nil { fmt.Println(err.Error()) } @@ -149,20 +149,20 @@ func Save() { WithHashKey("123") // get item - err := repository.SaveItem(key, user) + err := repository.SaveItemWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } // SaveItem item with context to allow trace fields in logger - err = repository.SaveItem(key, user) + err = repository.SaveItemWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } } -// SaveItems shows an example, how to save multiple items -func SaveItems() { +// SaveItemsWithContext(context.Background(), shows an example, how to save multiple items +func SaveItemsWithContext() { // enable log by passing logger interface repository.WithLog(logInterface) @@ -179,13 +179,13 @@ func SaveItems() { WithRangeKey(time.Now().Day()) // get item - err := repository.SaveItems(key, user) + err := repository.SaveItemsWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } - // SaveItems item with context to allow trace fields in logger - err = repository.SaveItems(key, user) + // .SaveItemsWithContext(context.Background(), item with context to allow trace fields in logger + err = repository.SaveItemsWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } @@ -210,14 +210,13 @@ func Update() { "Message": "msg1", } - err := repository.Update(djoemo.Set, key, updates) - + err := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) if err != nil { fmt.Println(err.Error()) } // Update item with context to allow trace fields in logger - err = repository.Update(djoemo.Set, key, updates) + err = repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) if err != nil { fmt.Println(err.Error()) } @@ -238,13 +237,13 @@ func Delete() { WithHashKey("123") // get item - err := repository.DeleteItem(key) + err := repository.DeleteItemWithContext(context.Background(), key) if err != nil { fmt.Println(err.Error()) } // DeleteItem item with context to allow trace fields in logger - err = repository.DeleteItem(key) + err = repository.DeleteItemWithContext(context.Background(), key) if err != nil { fmt.Println(err.Error()) } @@ -265,13 +264,13 @@ func DeleteItems() { WithHashKey("123") // get item - err := repository.DeleteItems([]djoemo.KeyInterface{key}) + err := repository.DeleteItemsWithContext(context.Background(), []djoemo.KeyInterface{key}) if err != nil { fmt.Println(err.Error()) } // DeleteItems with context to allow trace fields in logger - err = repository.DeleteItems([]djoemo.KeyInterface{key}) + err = repository.DeleteItemsWithContext(context.Background(), []djoemo.KeyInterface{key}) if err != nil { fmt.Println(err.Error()) } @@ -293,7 +292,7 @@ func GetFromGlobalIndex() { WithHashKey("123") // get item - found, err := repository.GIndex("UserIndex").GetItem(key, user) + found, err := repository.GIndex("UserIndex").GetItemWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } @@ -334,7 +333,7 @@ func GetItemsFromGlobalIndex() { WithHashKey("123") // get item - found, err := repository.GIndex("UserIndex").GetItem(key, user) + found, err := repository.GIndex("UserIndex").GetItemWithContext(context.Background(), key, user) if err != nil { fmt.Println(err.Error()) } diff --git a/go.mod b/go.mod index 7986fe5..d1d05cb 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,37 @@ module github.com/adjoeio/djoemo require ( github.com/aws/aws-sdk-go v1.19.1 - github.com/bouk/monkey v1.0.1 - github.com/golang/mock v1.6.0 github.com/guregu/dynamo v1.2.1 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.34.2 + github.com/onsi/ginkgo/v2 v2.25.1 + github.com/onsi/gomega v1.38.2 github.com/pkg/errors v0.8.1 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 go.uber.org/mock v0.5.0 ) require ( - bou.ke/monkey v1.0.2 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff v2.1.1+incompatible // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/tools v0.26.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) -go 1.22.0 - -toolchain go1.23.1 +go 1.25 diff --git a/go.sum b/go.sum index 561adab..1142d50 100644 --- a/go.sum +++ b/go.sum @@ -1,80 +1,87 @@ -bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= -bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/aws/aws-sdk-go v1.18.5/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.19.1 h1:8kOP0/XGJwXIFlYoD1DAtA39cAjc15Iv/QiDMKitD9U= github.com/aws/aws-sdk-go v1.19.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/bouk/monkey v1.0.1 h1:82kWEtyEjyfkRZb0DaQ5+7O5dJfe3GzF/o97+yUo5d0= -github.com/bouk/monkey v1.0.1/go.mod h1:PG/63f4XEUlVyW1ttIeOJmJhhe1+t9EC/je3eTjvFhE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/guregu/dynamo v1.2.1 h1:1jKHg3GSTo4/JpmnlaLawqhh8XoYCrTCD5IrWs4ONp8= github.com/guregu/dynamo v1.2.1/go.mod h1:ZS3tuE64ykQlCnuGfOnAi+ztGZlq0Wo/z5EVQA1fwFY= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= +github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190318221613-d196dffd7c2b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helper.go b/helper.go index 2211db2..df01a2b 100644 --- a/helper.go +++ b/helper.go @@ -1,6 +1,8 @@ package djoemo -import "github.com/guregu/dynamo" +import ( + "github.com/guregu/dynamo" +) func valueFromPtr[T any](ptr *T) T { if ptr == nil { diff --git a/key_interface.go b/key_interface.go index 7717bc0..0878b3e 100644 --- a/key_interface.go +++ b/key_interface.go @@ -1,6 +1,6 @@ package djoemo -// KeyInterface provides an interface for djoemo key used to identify item in djoemo table +// Key provides an interface for djoemo key used to identify item in djoemo table type KeyInterface interface { // TableName returns the djoemo table name TableName() string @@ -9,7 +9,7 @@ type KeyInterface interface { // RangeKeyName returns the name of range key if exists RangeKeyName() *string // HashKey returns the hash key value - HashKey() interface{} + HashKey() any // HashKey returns the range key value - RangeKey() interface{} + RangeKey() any } diff --git a/log.go b/log.go deleted file mode 100644 index 0ed9469..0000000 --- a/log.go +++ /dev/null @@ -1,46 +0,0 @@ -package djoemo - -import ( - "context" -) - -type logger struct { - log LogInterface -} - -// info logs info -func (l logger) info(ctx context.Context, table string, message string) { - l.log.WithFields(map[string]interface{}{TableName: table}).WithContext(ctx).Info(message) -} - -// warn logs warning -func (l logger) warn(ctx context.Context, table string, message string) { - l.log.WithFields(map[string]interface{}{TableName: table}).WithContext(ctx).Warn(message) -} - -// error logs error -func (l logger) error(ctx context.Context, table string, message string) { - l.log.WithFields(map[string]interface{}{TableName: table}).WithContext(ctx).Error(message) -} - -//nopLog logger to turn off logging. -type nopLog struct{} - -// WithContext adds context to logger -func (l nopLog) WithContext(ctx context.Context) LogInterface { - return l -} - -// WithFields adds fields from map string interface to logger -func (l nopLog) WithFields(fields map[string]interface{}) LogInterface { - return l -} - -// info logs info -func (l nopLog) Info(message string) {} - -// warn logs warning -func (l nopLog) Warn(message string) {} - -// info logs info -func (l nopLog) Error(message string) {} diff --git a/log_interface.go b/logger_interface.go similarity index 60% rename from log_interface.go rename to logger_interface.go index 3522e8f..f2d1960 100644 --- a/log_interface.go +++ b/logger_interface.go @@ -5,11 +5,15 @@ import ( ) // LogInterface provides an interface for logging +// +//go:generate mockgen -source=logger_interface.go -destination=./mock/log_interface.go -package=mock . type LogInterface interface { // WithContext adds context to logger WithContext(ctx context.Context) LogInterface + // WithField adds fields from map string interface to logger + WithField(key string, value any) LogInterface // WithFields adds fields from map string interface to logger - WithFields(fields map[string]interface{}) LogInterface + WithFields(fields map[string]any) LogInterface // Info logs info Info(message string) // warn logs warning diff --git a/logger_noplog.go b/logger_noplog.go new file mode 100644 index 0000000..3270982 --- /dev/null +++ b/logger_noplog.go @@ -0,0 +1,38 @@ +package djoemo + +import ( + "context" +) + +var nolog = &nopLog{} + +func NewNopLog() LogInterface { + return nolog +} + +// nopLog logger to turn off logging. +type nopLog struct{} + +// WithContext adds context to logger +func (l *nopLog) WithContext(ctx context.Context) LogInterface { + return l +} + +// WithFields adds fields from map string interface to logger +func (l *nopLog) WithFields(fields map[string]any) LogInterface { + return l +} + +// WithFields adds field with value to logger +func (l *nopLog) WithField(key string, value any) LogInterface { + return l +} + +// info logs info +func (l nopLog) Info(message string) {} + +// warn logs warning +func (l nopLog) Warn(message string) {} + +// info logs info +func (l nopLog) Error(message string) {} diff --git a/metrics.go b/metrics.go index fc403f7..4ad4da9 100644 --- a/metrics.go +++ b/metrics.go @@ -1,25 +1,88 @@ package djoemo -import "context" +import ( + "context" + "sync" + "time" +) + +// MetricsInterface provides an interface for metrics publisher +// +//go:generate mockgen -source=metrics.go -destination=./mock/metrics_interface.go -package=mock .\ +type MetricsInterface interface { + Record(ctx context.Context, caller string, key KeyInterface, duration time.Duration, success bool) +} const ( - // MetricNameSavedItemsCount save count metrics key - MetricNameSavedItemsCount = "ItemsSavedCount" - // MetricNameUpdatedItemsCount update count metrics key - MetricNameUpdatedItemsCount = "ItemsUpdatedCount" - // MetricNameDeleteItemsCount delete count metrics key - MetricNameDeleteItemsCount = "ItemsDeleteCount" + labelSource = "source" + + StatusSuccess = "success" + StatusFailure = "failure" + + OpCommit = "commit" + OpUpdate = "update" + OpRead = "read" + OpDelete = "delete" ) -type metrics struct { - metrics MetricsInterface +type customMetricsLabelsContextKey int + +const customLabelsCtxKey customMetricsLabelsContextKey = iota + +type customLabels struct { + sync.RWMutex + Labels map[string]string } -// Publish publishes metrics -func (m metrics) Publish(ctx context.Context, key string, metricName string, metricValue float64) error { - if m.metrics == nil { +func AddMetrics(ctx context.Context, key, value string) context.Context { + labels, ok := ctx.Value(customLabelsCtxKey).(*customLabels) + if !ok { + labels = &customLabels{ + Labels: make(map[string]string), + } + ctx = context.WithValue(ctx, customLabelsCtxKey, labels) + } + + labels.Lock() + defer labels.Unlock() + labels.Labels[key] = value + + return ctx +} + +// WithSourceLabel is a label to tag buisness logic as default metrics are aggregated for CURD operations to reduce cardinality +func WithSourceLabel(ctx context.Context, value string) context.Context { + return AddMetrics(ctx, labelSource, value) +} + +func GetLabelsFromContext(ctx context.Context) map[string]string { + customLabels, ok := ctx.Value(customLabelsCtxKey).(*customLabels) + if !ok || customLabels == nil { return nil } + return customLabels.Labels +} + +func New() *Metrics { + return &Metrics{} +} + +type Metrics struct { + metrics []MetricsInterface +} + +func (m *Metrics) Add(metric MetricsInterface) { + m.metrics = append(m.metrics, metric) +} + +func (m *Metrics) Record(ctx context.Context, op string, key KeyInterface, duration time.Duration, success bool) { + for _, metric := range m.metrics { + metric.Record(ctx, op, key, duration, success) + } +} - return m.metrics.WithContext(ctx).Publish(key, metricName, metricValue) +func (m *Metrics) RecordMultiple(ctx context.Context, op string, key []KeyInterface, duration time.Duration, success bool) { + for _, key := range key { + m.Record(ctx, op, key, duration, success) + } } diff --git a/metrics_cloudwatch.go b/metrics_cloudwatch.go new file mode 100644 index 0000000..9838689 --- /dev/null +++ b/metrics_cloudwatch.go @@ -0,0 +1,17 @@ +package djoemo + +import ( + "context" + "time" +) + +type cloudwatchmetrics struct{} + +func (m *cloudwatchmetrics) Record(ctx context.Context, caller string, key KeyInterface, duration time.Duration, err error) { + // TODO: implement legacy CloudWatch if required. Return err, use WithCloudWatch/Add, etc + // m.Publish() +} + +// Publish publishes metrics +func (m *cloudwatchmetrics) Publish(ctx context.Context, key string, metricName string, metricValue float64) { +} diff --git a/metrics_interface.go b/metrics_interface.go deleted file mode 100644 index 80a6fa1..0000000 --- a/metrics_interface.go +++ /dev/null @@ -1,11 +0,0 @@ -package djoemo - -import "context" - -// MetricsInterface provides an interface for metrics publisher -type MetricsInterface interface { - // WithContext adds context to logger - WithContext(ctx context.Context) MetricsInterface - // Publish publishes metrics - Publish(key string, metricName string, metricValue float64) error -} diff --git a/metrics_prometheus.go b/metrics_prometheus.go new file mode 100644 index 0000000..f0a8f64 --- /dev/null +++ b/metrics_prometheus.go @@ -0,0 +1,119 @@ +package djoemo + +import ( + "context" + "maps" + "path" + "runtime" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +type prometheusmetrics struct { + registry *prometheus.Registry + mu sync.RWMutex + queryCount map[string]*prometheus.CounterVec + queryDuration map[string]*prometheus.HistogramVec +} + +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, + } + counter := prometheus.NewCounterVec(opts, metricLabelNames) + if err := m.registry.Register(counter); err != nil { + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + return are.ExistingCollector.(*prometheus.CounterVec) + } + panic(err) + } + return counter +} + +func (m *prometheusmetrics) newHistogramVec(caller string) *prometheus.HistogramVec { + 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), + } + // WARNING: add high cardinality labels like sdkhash, etc with caution + histogram := prometheus.NewHistogramVec(opts, metricLabelNames) + if err := m.registry.Register(histogram); err != nil { + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + return are.ExistingCollector.(*prometheus.HistogramVec) + } + panic(err) + } + return histogram +} + +const ( + statusLabel = "status" + callerLabel = "caller" // NOTE: used separate metrics for now + sourceLabel = "source" + tableLabel = "table" +) + +func NewPrometheusMetrics(registry *prometheus.Registry) *prometheusmetrics { + m := &prometheusmetrics{ + registry: registry, + queryCount: make(map[string]*prometheus.CounterVec), + queryDuration: make(map[string]*prometheus.HistogramVec), + } + return m +} + +func (m *prometheusmetrics) Record(ctx context.Context, caller string, key KeyInterface, duration time.Duration, success bool) { + m.mu.RLock() + counter, counterOk := m.queryCount[caller] + histogram, histogramOk := m.queryDuration[caller] + m.mu.RUnlock() + + if !counterOk || !histogramOk { + m.mu.Lock() + if m.queryCount[caller] == nil { + m.queryCount[caller] = m.newCounter(caller) + } + if m.queryDuration[caller] == nil { + m.queryDuration[caller] = m.newHistogramVec(caller) + } + counter = m.queryCount[caller] + histogram = m.queryDuration[caller] + m.mu.Unlock() + } + + status := StatusFailure + if success { + status = StatusSuccess + } + + table := "" + if key != nil && key.TableName() != "" { + table = strings.ToLower(key.TableName()) + } + + labels := prometheus.Labels{ + statusLabel: status, + tableLabel: table, + } + 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" + } + } + + counter.With(labels).Inc() + histogram.With(labels).Observe(duration.Seconds()) +} diff --git a/metrics_prometheus_test.go b/metrics_prometheus_test.go new file mode 100644 index 0000000..3f24d89 --- /dev/null +++ b/metrics_prometheus_test.go @@ -0,0 +1,224 @@ +package djoemo_test + +import ( + "context" + "sync" + "time" + + "github.com/adjoeio/djoemo" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" +) + +var _ = Describe("Prometheus Metrics", func() { + var ( + registry *prometheus.Registry + metrics *djoemo.Metrics + ) + + BeforeEach(func() { + registry = prometheus.NewRegistry() + promMetrics := djoemo.NewPrometheusMetrics(registry) + metrics = djoemo.New() + metrics.Add(promMetrics) + }) + + Describe("Record", func() { + It("records counter and histogram with success status", func() { + key := djoemo.Key().WithTableName("UserTable").WithHashKeyName("UUID").WithHashKey("id-1") + metrics.Record(context.Background(), djoemo.OpRead, key, 50*time.Millisecond, true) + + mfs, err := registry.Gather() + 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) + var readTotal *dto.MetricFamily + var readDuration *dto.MetricFamily + for _, mf := range mfs { + switch mf.GetName() { + case "read": + readTotal = mf + case "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)) + + // Counter: expect status=success, table=usertable + Expect(readTotal.GetMetric()).To(HaveLen(1)) + Expect(readTotal.GetMetric()[0].GetCounter().GetValue()).To(Equal(1.0)) + 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")) + + // Histogram: expect duration in seconds (50ms = 0.05) + Expect(readDuration.GetMetric()).To(HaveLen(1)) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.05, 0.001)) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(1))) + }) + + It("records with failure status", func() { + key := djoemo.Key().WithTableName("OrderTable").WithHashKeyName("ID").WithHashKey("order-1") + metrics.Record(context.Background(), djoemo.OpCommit, key, 100*time.Millisecond, false) + + mfs, err := registry.Gather() + Expect(err).NotTo(HaveOccurred()) + var commitTotal *dto.MetricFamily + var commitDuration *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "commit" { + commitTotal = mf + } + if mf.GetName() == "commit_duration_seconds" { + commitDuration = mf + } + } + Expect(commitTotal).NotTo(BeNil()) + Expect(commitTotal.GetMetric()[0].GetLabel()).To(ContainElement(&dto.LabelPair{Name: stringPtr("status"), Value: stringPtr("failure")})) + Expect(commitDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.1, 0.001)) + Expect(commitDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(1))) + }) + + It("handles nil key with empty table label", func() { + metrics.Record(context.Background(), djoemo.OpRead, nil, 10*time.Millisecond, true) + + mfs, err := registry.Gather() + Expect(err).NotTo(HaveOccurred()) + var readTotal *dto.MetricFamily + var readDuration *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "read" { + readTotal = mf + } + if mf.GetName() == "read_duration_seconds" { + readDuration = mf + } + } + Expect(readTotal).NotTo(BeNil()) + Expect(getLabelValue(readTotal.GetMetric()[0].GetLabel(), "table")).To(Equal("")) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.01, 0.001)) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(1))) + }) + + It("picks up source label from context", func() { + ctx := djoemo.WithSourceLabel(context.Background(), "checkout-service") + key := djoemo.Key().WithTableName("CartTable").WithHashKeyName("ID").WithHashKey("cart-1") + metrics.Record(ctx, djoemo.OpUpdate, key, 25*time.Millisecond, true) + + mfs, err := registry.Gather() + Expect(err).NotTo(HaveOccurred()) + var updateTotal *dto.MetricFamily + var updateDuration *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "update" { + updateTotal = mf + } + if mf.GetName() == "update_duration_seconds" { + updateDuration = mf + } + } + Expect(updateTotal).NotTo(BeNil()) + Expect(getLabelValue(updateTotal.GetMetric()[0].GetLabel(), "source")).To(Equal("checkout-service")) + Expect(updateDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.025, 0.001)) + Expect(updateDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(1))) + Expect(getLabelValue(updateDuration.GetMetric()[0].GetLabel(), "source")).To(Equal("checkout-service")) + }) + }) + + Describe("Duplicate registration safety", func() { + It("does not panic when multiple instances share the same registry", func() { + sharedRegistry := prometheus.NewRegistry() + + // Create two prometheus metrics instances with same registry + pm1 := djoemo.NewPrometheusMetrics(sharedRegistry) + pm2 := djoemo.NewPrometheusMetrics(sharedRegistry) + + m := djoemo.New() + m.Add(pm1) + m.Add(pm2) + + key := djoemo.Key().WithTableName("TestTable").WithHashKeyName("ID").WithHashKey("1") + Expect(func() { + m.Record(context.Background(), "read", key, 10*time.Millisecond, true) + m.Record(context.Background(), "read", key, 20*time.Millisecond, true) + }).NotTo(Panic()) + + mfs, err := sharedRegistry.Gather() + Expect(err).NotTo(HaveOccurred()) + var readTotal *dto.MetricFamily + var readDuration *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "read" { + readTotal = mf + } + if mf.GetName() == "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 + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.06, 0.001)) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(4))) + }) + }) + + Describe("Concurrent Record", func() { + It("safely handles concurrent Record calls for the same caller", func() { + key := djoemo.Key().WithTableName("UserTable").WithHashKeyName("UUID").WithHashKey("id") + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + metrics.Record(context.Background(), djoemo.OpRead, key, 5*time.Millisecond, true) + }() + } + wg.Wait() + + mfs, err := registry.Gather() + Expect(err).NotTo(HaveOccurred()) + var readTotal *dto.MetricFamily + var readDuration *dto.MetricFamily + for _, mf := range mfs { + if mf.GetName() == "read" { + readTotal = mf + } + if mf.GetName() == "read_duration_seconds" { + readDuration = mf + } + } + Expect(readTotal).NotTo(BeNil()) + Expect(readTotal.GetMetric()[0].GetCounter().GetValue()).To(Equal(100.0)) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleSum()).To(BeNumerically("~", 0.5, 0.001)) + Expect(readDuration.GetMetric()[0].GetHistogram().GetSampleCount()).To(Equal(uint64(100))) + }) + }) +}) + +func getLabelValue(labels []*dto.LabelPair, name string) string { + for _, l := range labels { + if l.GetName() == name { + return l.GetValue() + } + } + return "" +} + +func metricFamilyNames(mfs []*dto.MetricFamily) []string { + names := make([]string, len(mfs)) + for i, mf := range mfs { + names[i] = mf.GetName() + } + return names +} + +func stringPtr(s string) *string { + return &s +} diff --git a/mock/dynamo_api_mock_helper.go b/mock/dynamo_api_mock_helper.go index e8c26a2..ea92a55 100644 --- a/mock/dynamo_api_mock_helper.go +++ b/mock/dynamo_api_mock_helper.go @@ -417,7 +417,6 @@ func (d *DynamoMock) WithTable(name string) DynamoDBOption { return func(args *DynamoMock) { args.TableName = name } - } // WithError register error to call note its on mock scope @@ -425,7 +424,6 @@ func (d *DynamoMock) WithError(err error) DynamoDBOption { return func(args *DynamoMock) { args.Err = err } - } // Exec execute all registered calls with its options @@ -483,7 +481,6 @@ func (d *DynamoMock) batchGetInput() *dynamodb.BatchGetItemInput { } keys = append(keys, key) } - } req := &dynamodb.BatchGetItemInput{ RequestItems: map[string]*dynamodb.KeysAndAttributes{ diff --git a/mock/dynamo_api_mock_input_matcher.go b/mock/dynamo_api_mock_input_matcher.go index bfc6c46..db1ddd4 100644 --- a/mock/dynamo_api_mock_input_matcher.go +++ b/mock/dynamo_api_mock_input_matcher.go @@ -11,11 +11,13 @@ import ( "github.com/onsi/gomega" ) -/*InputMatcher matcher to match only specified fields and not pass all fields to mock +/* +InputMatcher matcher to match only specified fields and not pass all fields to mock ## Usage matcher := mocks.InputExpect(). - FieldEq("FIELD_NAME", "FIELD_VALUE"). - FieldEq("FIELD_NAME_1", "FIELD_VALUE_1"), + + FieldEq("FIELD_NAME", "FIELD_VALUE"). + FieldEq("FIELD_NAME_1", "FIELD_VALUE_1"), dynamoDBMock := mocks.NewMockDynamoDBAPI(mockCtrl) dynamoDBMock.EXPECT().PutItem() @@ -23,15 +25,16 @@ dynamoDBMock.EXPECT().PutItem() ## usage with dynamomock helper dynamoMock := mocks.NewMockDynamoDBAPI(mockCtrl) dmock := mocks.NewDynamoMock(dynamoMock) - dmock.Should(). - SaveItem( - dmock.WithTable("TABLENAME"), - dmock.WithMatch( - mocks.InputExpect(). - FieldEq("FIELD_NAME", "FIELD_VALUE"). - FieldEq("FIELD_NAME_1", "FIELD_VALUE_1"), - ), - ).Exec() + + dmock.Should(). + SaveItem( + dmock.WithTable("TABLENAME"), + dmock.WithMatch( + mocks.InputExpect(). + FieldEq("FIELD_NAME", "FIELD_VALUE"). + FieldEq("FIELD_NAME_1", "FIELD_VALUE_1"), + ), + ).Exec() */ type InputMatcher struct { Fields map[string]interface{} @@ -55,7 +58,6 @@ func (i InputMatcher) String() string { // InputExpect init matcher with empty fields func InputExpect() *InputMatcher { - i := &InputMatcher{} i.Fields = make(map[string]interface{}) return i @@ -89,7 +91,6 @@ func (i *InputMatcher) matchPutItemInput(x interface{}) bool { } func (i *InputMatcher) matchUpdateItemInput(x interface{}) bool { - inputItem := x.(*dynamodb.UpdateItemInput) // remove spaces & Set & if_not_exists(Field reg := regexp.MustCompile(`if_not_exists|\(([^ ]+)|\)|SET|ADD| `) @@ -101,11 +102,11 @@ func (i *InputMatcher) matchUpdateItemInput(x interface{}) bool { for _, expression := range updateExpressions { var keyValue []string - if strings.ContainsRune(expression, '='){ + if strings.ContainsRune(expression, '=') { keyValue = strings.Split(expression, "=") - }else{ - keyValue = strings.Split(expression,":") - if len(keyValue) > 0{ + } else { + keyValue = strings.Split(expression, ":") + if len(keyValue) > 0 { keyValue[1] = fmt.Sprintf(":%s", keyValue[1]) } } diff --git a/mock/dynamo_global_index_interface.go b/mock/dynamo_global_index_interface.go index a367d27..d7c2f17 100644 --- a/mock/dynamo_global_index_interface.go +++ b/mock/dynamo_global_index_interface.go @@ -9,7 +9,8 @@ import ( reflect "reflect" djoemo "github.com/adjoeio/djoemo" - gomock "github.com/golang/mock/gomock" + prometheus "github.com/prometheus/client_golang/prometheus" + "go.uber.org/mock/gomock" ) // MockGlobalIndexInterface is a mock of GlobalIndexInterface interface. @@ -35,21 +36,6 @@ func (m *MockGlobalIndexInterface) EXPECT() *MockGlobalIndexInterfaceMockRecorde return m.recorder } -// GetItem mocks base method. -func (m *MockGlobalIndexInterface) GetItem(key djoemo.KeyInterface, item interface{}) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetItem", key, item) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetItem indicates an expected call of GetItem. -func (mr *MockGlobalIndexInterfaceMockRecorder) GetItem(key, item interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItem", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItem), key, item) -} - // GetItemWithContext mocks base method. func (m *MockGlobalIndexInterface) GetItemWithContext(ctx context.Context, key djoemo.KeyInterface, item interface{}) (bool, error) { m.ctrl.T.Helper() @@ -65,21 +51,6 @@ func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemWithContext(ctx, key, ite return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItemWithContext), ctx, key, item) } -// GetItems mocks base method. -func (m *MockGlobalIndexInterface) GetItems(key djoemo.KeyInterface, items interface{}) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetItems", key, items) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetItems indicates an expected call of GetItems. -func (mr *MockGlobalIndexInterfaceMockRecorder) GetItems(key, items interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItems", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItems), key, items) -} - // GetItemsWithContext mocks base method. func (m *MockGlobalIndexInterface) GetItemsWithContext(ctx context.Context, key djoemo.KeyInterface, items interface{}) (bool, error) { m.ctrl.T.Helper() @@ -110,30 +81,54 @@ func (mr *MockGlobalIndexInterfaceMockRecorder) GetItemsWithRangeWithContext(ctx return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemsWithRangeWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).GetItemsWithRangeWithContext), ctx, key, items) } -// Query mocks base method. -func (m *MockGlobalIndexInterface) Query(query djoemo.QueryInterface, item interface{}) error { +// QueryWithContext mocks base method. +func (m *MockGlobalIndexInterface) QueryWithContext(ctx context.Context, query djoemo.QueryInterface, item interface{}) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Query", query, item) + ret := m.ctrl.Call(m, "QueryWithContext", ctx, query, item) ret0, _ := ret[0].(error) return ret0 } -// Query indicates an expected call of Query. -func (mr *MockGlobalIndexInterfaceMockRecorder) Query(query, item interface{}) *gomock.Call { +// QueryWithContext indicates an expected call of QueryWithContext. +func (mr *MockGlobalIndexInterfaceMockRecorder) QueryWithContext(ctx, query, item interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockGlobalIndexInterface)(nil).Query), query, item) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).QueryWithContext), ctx, query, item) } -// QueryWithContext mocks base method. -func (m *MockGlobalIndexInterface) QueryWithContext(ctx context.Context, query djoemo.QueryInterface, item interface{}) error { +// WithLog mocks base method. +func (m *MockGlobalIndexInterface) WithLog(log djoemo.LogInterface) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryWithContext", ctx, query, item) - ret0, _ := ret[0].(error) + m.ctrl.Call(m, "WithLog", log) +} + +// WithLog indicates an expected call of WithLog. +func (mr *MockGlobalIndexInterfaceMockRecorder) WithLog(log interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithLog", reflect.TypeOf((*MockGlobalIndexInterface)(nil).WithLog), log) +} + +// WithMetrics mocks base method. +func (m *MockGlobalIndexInterface) WithMetrics(metricsInterface djoemo.MetricsInterface) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WithMetrics", metricsInterface) +} + +// WithMetrics indicates an expected call of WithMetrics. +func (mr *MockGlobalIndexInterfaceMockRecorder) WithMetrics(metricsInterface interface{}) *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 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithPrometheusMetrics", registry) + ret0, _ := ret[0].(djoemo.GlobalIndexInterface) return ret0 } -// QueryWithContext indicates an expected call of QueryWithContext. -func (mr *MockGlobalIndexInterfaceMockRecorder) QueryWithContext(ctx, query, item interface{}) *gomock.Call { +// WithPrometheusMetrics indicates an expected call of WithPrometheusMetrics. +func (mr *MockGlobalIndexInterfaceMockRecorder) WithPrometheusMetrics(registry interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithContext", reflect.TypeOf((*MockGlobalIndexInterface)(nil).QueryWithContext), ctx, query, item) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPrometheusMetrics", reflect.TypeOf((*MockGlobalIndexInterface)(nil).WithPrometheusMetrics), registry) } diff --git a/mock/dynamo_repository_interface.go b/mock/dynamo_repository_interface.go index 6a7f221..7b91872 100644 --- a/mock/dynamo_repository_interface.go +++ b/mock/dynamo_repository_interface.go @@ -9,7 +9,8 @@ import ( reflect "reflect" djoemo "github.com/adjoeio/djoemo" - gomock "github.com/golang/mock/gomock" + prometheus "github.com/prometheus/client_golang/prometheus" + "go.uber.org/mock/gomock" ) // MockRepositoryInterface is a mock of RepositoryInterface interface. @@ -36,7 +37,7 @@ func (m *MockRepositoryInterface) EXPECT() *MockRepositoryInterfaceMockRecorder } // BatchGetItemsWithContext mocks base method. -func (m *MockRepositoryInterface) BatchGetItemsWithContext(ctx context.Context, keys []djoemo.KeyInterface, out interface{}) (bool, error) { +func (m *MockRepositoryInterface) BatchGetItemsWithContext(ctx context.Context, keys []djoemo.KeyInterface, out any) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BatchGetItemsWithContext", ctx, keys, out) ret0, _ := ret[0].(bool) @@ -50,28 +51,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) BatchGetItemsWithContext(ctx, key return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItemsWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).BatchGetItemsWithContext), ctx, keys, out) } -// ConditionalUpdate mocks base method. -func (m *MockRepositoryInterface) ConditionalUpdate(key djoemo.KeyInterface, item interface{}, expression string, expressionArgs ...interface{}) (bool, error) { - m.ctrl.T.Helper() - varargs := []interface{}{key, item, expression} - for _, a := range expressionArgs { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ConditionalUpdate", varargs...) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ConditionalUpdate indicates an expected call of ConditionalUpdate. -func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdate(key, item, expression interface{}, expressionArgs ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{key, item, expression}, expressionArgs...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConditionalUpdate", reflect.TypeOf((*MockRepositoryInterface)(nil).ConditionalUpdate), varargs...) -} - // ConditionalUpdateWithContext mocks base method. -func (m *MockRepositoryInterface) ConditionalUpdateWithContext(ctx context.Context, key djoemo.KeyInterface, item interface{}, expression string, expressionArgs ...interface{}) (bool, error) { +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} for _, a := range expressionArgs { @@ -91,7 +72,7 @@ func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdateWithContext(ctx, } // ConditionalUpdateWithUpdateExpressionsAndReturnValue mocks base method. -func (m *MockRepositoryInterface) ConditionalUpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key djoemo.KeyInterface, item interface{}, updateExpressions djoemo.UpdateExpressions, conditionExpression string, conditionArgs ...interface{}) (bool, error) { +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} for _, a := range conditionArgs { @@ -110,20 +91,6 @@ func (mr *MockRepositoryInterfaceMockRecorder) ConditionalUpdateWithUpdateExpres return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConditionalUpdateWithUpdateExpressionsAndReturnValue", reflect.TypeOf((*MockRepositoryInterface)(nil).ConditionalUpdateWithUpdateExpressionsAndReturnValue), varargs...) } -// DeleteItem mocks base method. -func (m *MockRepositoryInterface) DeleteItem(key djoemo.KeyInterface) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteItem", key) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteItem indicates an expected call of DeleteItem. -func (mr *MockRepositoryInterfaceMockRecorder) DeleteItem(key interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItem", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteItem), key) -} - // DeleteItemWithContext mocks base method. func (m *MockRepositoryInterface) DeleteItemWithContext(ctx context.Context, key djoemo.KeyInterface) error { m.ctrl.T.Helper() @@ -138,20 +105,6 @@ func (mr *MockRepositoryInterfaceMockRecorder) DeleteItemWithContext(ctx, key in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItemWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteItemWithContext), ctx, key) } -// DeleteItems mocks base method. -func (m *MockRepositoryInterface) DeleteItems(key []djoemo.KeyInterface) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteItems", key) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteItems indicates an expected call of DeleteItems. -func (mr *MockRepositoryInterfaceMockRecorder) DeleteItems(key interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItems", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteItems), key) -} - // DeleteItemsWithContext mocks base method. func (m *MockRepositoryInterface) DeleteItemsWithContext(ctx context.Context, key []djoemo.KeyInterface) error { m.ctrl.T.Helper() @@ -180,23 +133,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) GIndex(name interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GIndex", reflect.TypeOf((*MockRepositoryInterface)(nil).GIndex), name) } -// GetItem mocks base method. -func (m *MockRepositoryInterface) GetItem(key djoemo.KeyInterface, item interface{}) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetItem", key, item) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetItem indicates an expected call of GetItem. -func (mr *MockRepositoryInterfaceMockRecorder) GetItem(key, item interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItem", reflect.TypeOf((*MockRepositoryInterface)(nil).GetItem), key, item) -} - // GetItemWithContext mocks base method. -func (m *MockRepositoryInterface) GetItemWithContext(ctx context.Context, key djoemo.KeyInterface, item interface{}) (bool, error) { +func (m *MockRepositoryInterface) 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) @@ -210,23 +148,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) GetItemWithContext(ctx, key, item return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).GetItemWithContext), ctx, key, item) } -// GetItems mocks base method. -func (m *MockRepositoryInterface) GetItems(key djoemo.KeyInterface, items interface{}) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetItems", key, items) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetItems indicates an expected call of GetItems. -func (mr *MockRepositoryInterfaceMockRecorder) GetItems(key, items interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItems", reflect.TypeOf((*MockRepositoryInterface)(nil).GetItems), key, items) -} - // GetItemsWithContext mocks base method. -func (m *MockRepositoryInterface) GetItemsWithContext(ctx context.Context, key djoemo.KeyInterface, out interface{}) (bool, error) { +func (m *MockRepositoryInterface) GetItemsWithContext(ctx context.Context, key djoemo.KeyInterface, out any) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetItemsWithContext", ctx, key, out) ret0, _ := ret[0].(bool) @@ -240,23 +163,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) GetItemsWithContext(ctx, key, out return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemsWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).GetItemsWithContext), ctx, key, out) } -// OptimisticLockSave mocks base method. -func (m *MockRepositoryInterface) OptimisticLockSave(key djoemo.KeyInterface, item interface{}) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OptimisticLockSave", key, item) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// OptimisticLockSave indicates an expected call of OptimisticLockSave. -func (mr *MockRepositoryInterfaceMockRecorder) OptimisticLockSave(key, item interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OptimisticLockSave", reflect.TypeOf((*MockRepositoryInterface)(nil).OptimisticLockSave), key, item) -} - // OptimisticLockSaveWithContext mocks base method. -func (m *MockRepositoryInterface) OptimisticLockSaveWithContext(ctx context.Context, key djoemo.KeyInterface, item interface{}) (bool, error) { +func (m *MockRepositoryInterface) OptimisticLockSaveWithContext(ctx context.Context, key djoemo.KeyInterface, item any) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OptimisticLockSaveWithContext", ctx, key, item) ret0, _ := ret[0].(bool) @@ -270,22 +178,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) OptimisticLockSaveWithContext(ctx return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OptimisticLockSaveWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).OptimisticLockSaveWithContext), ctx, key, item) } -// Query mocks base method. -func (m *MockRepositoryInterface) Query(query djoemo.QueryInterface, item interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Query", query, item) - ret0, _ := ret[0].(error) - return ret0 -} - -// Query indicates an expected call of Query. -func (mr *MockRepositoryInterfaceMockRecorder) Query(query, item interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockRepositoryInterface)(nil).Query), query, item) -} - // QueryWithContext mocks base method. -func (m *MockRepositoryInterface) QueryWithContext(ctx context.Context, query djoemo.QueryInterface, item interface{}) error { +func (m *MockRepositoryInterface) 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) @@ -298,22 +192,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) QueryWithContext(ctx, query, item return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).QueryWithContext), ctx, query, item) } -// SaveItem mocks base method. -func (m *MockRepositoryInterface) SaveItem(key djoemo.KeyInterface, item interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveItem", key, item) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveItem indicates an expected call of SaveItem. -func (mr *MockRepositoryInterfaceMockRecorder) SaveItem(key, item interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveItem", reflect.TypeOf((*MockRepositoryInterface)(nil).SaveItem), key, item) -} - // SaveItemWithContext mocks base method. -func (m *MockRepositoryInterface) SaveItemWithContext(ctx context.Context, key djoemo.KeyInterface, item interface{}) error { +func (m *MockRepositoryInterface) SaveItemWithContext(ctx context.Context, key djoemo.KeyInterface, item any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveItemWithContext", ctx, key, item) ret0, _ := ret[0].(error) @@ -326,22 +206,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) SaveItemWithContext(ctx, key, ite return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveItemWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).SaveItemWithContext), ctx, key, item) } -// SaveItems mocks base method. -func (m *MockRepositoryInterface) SaveItems(key djoemo.KeyInterface, items interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveItems", key, items) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveItems indicates an expected call of SaveItems. -func (mr *MockRepositoryInterfaceMockRecorder) SaveItems(key, items interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveItems", reflect.TypeOf((*MockRepositoryInterface)(nil).SaveItems), key, items) -} - // SaveItemsWithContext mocks base method. -func (m *MockRepositoryInterface) SaveItemsWithContext(ctx context.Context, key djoemo.KeyInterface, items interface{}) error { +func (m *MockRepositoryInterface) SaveItemsWithContext(ctx context.Context, key djoemo.KeyInterface, items any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveItemsWithContext", ctx, key, items) ret0, _ := ret[0].(error) @@ -369,22 +235,8 @@ func (mr *MockRepositoryInterfaceMockRecorder) ScanIteratorWithContext(ctx, key, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanIteratorWithContext", reflect.TypeOf((*MockRepositoryInterface)(nil).ScanIteratorWithContext), ctx, key, searchLimit) } -// Update mocks base method. -func (m *MockRepositoryInterface) Update(expression djoemo.UpdateExpression, key djoemo.KeyInterface, values map[string]interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", expression, key, values) - ret0, _ := ret[0].(error) - return ret0 -} - -// Update indicates an expected call of Update. -func (mr *MockRepositoryInterfaceMockRecorder) Update(expression, key, values interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepositoryInterface)(nil).Update), expression, key, values) -} - // UpdateWithContext mocks base method. -func (m *MockRepositoryInterface) UpdateWithContext(ctx context.Context, expression djoemo.UpdateExpression, key djoemo.KeyInterface, values map[string]interface{}) error { +func (m *MockRepositoryInterface) UpdateWithContext(ctx context.Context, expression djoemo.UpdateExpression, key djoemo.KeyInterface, values map[string]any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateWithContext", ctx, expression, key, values) ret0, _ := ret[0].(error) @@ -412,7 +264,7 @@ func (mr *MockRepositoryInterfaceMockRecorder) UpdateWithUpdateExpressions(ctx, } // UpdateWithUpdateExpressionsAndReturnValue mocks base method. -func (m *MockRepositoryInterface) UpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key djoemo.KeyInterface, item interface{}, updateExpressions djoemo.UpdateExpressions) error { +func (m *MockRepositoryInterface) UpdateWithUpdateExpressionsAndReturnValue(ctx context.Context, key djoemo.KeyInterface, item any, updateExpressions djoemo.UpdateExpressions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateWithUpdateExpressionsAndReturnValue", ctx, key, item, updateExpressions) ret0, _ := ret[0].(error) @@ -448,3 +300,17 @@ func (mr *MockRepositoryInterfaceMockRecorder) WithMetrics(metricsInterface inte 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 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithPrometheusMetrics", registry) + ret0, _ := ret[0].(djoemo.RepositoryInterface) + return ret0 +} + +// WithPrometheusMetrics indicates an expected call of WithPrometheusMetrics. +func (mr *MockRepositoryInterfaceMockRecorder) WithPrometheusMetrics(registry interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPrometheusMetrics", reflect.TypeOf((*MockRepositoryInterface)(nil).WithPrometheusMetrics), registry) +} diff --git a/mock/dynamodb_api_mock.go b/mock/dynamodb_api_mock.go index e5ee63f..06ee37b 100644 --- a/mock/dynamodb_api_mock.go +++ b/mock/dynamodb_api_mock.go @@ -5,11 +5,12 @@ package mock import ( + reflect "reflect" + aws "github.com/aws/aws-sdk-go/aws" request "github.com/aws/aws-sdk-go/aws/request" dynamodb "github.com/aws/aws-sdk-go/service/dynamodb" - gomock "go.uber.org/mock/gomock" - reflect "reflect" + "go.uber.org/mock/gomock" ) // MockDynamoDBAPI is a mock of DynamoDBAPI interface diff --git a/mock/log_interface.go b/mock/log_interface.go index 822bef2..f850849 100644 --- a/mock/log_interface.go +++ b/mock/log_interface.go @@ -1,89 +1,114 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: log_interface.go +// Source: logger_interface.go -// Package mock_djoemo is a generated GoMock package. +// Package mock is a generated GoMock package. package mock import ( context "context" - djoemo "github.com/adjoeio/djoemo" - gomock "go.uber.org/mock/gomock" reflect "reflect" + + djoemo "github.com/adjoeio/djoemo" + "go.uber.org/mock/gomock" ) -// MockLogInterface is a mock of LogInterface interface +// MockLogInterface is a mock of LogInterface interface. type MockLogInterface struct { ctrl *gomock.Controller recorder *MockLogInterfaceMockRecorder } -// MockLogInterfaceMockRecorder is the mock recorder for MockLogInterface +// MockLogInterfaceMockRecorder is the mock recorder for MockLogInterface. type MockLogInterfaceMockRecorder struct { mock *MockLogInterface } -// NewMockLogInterface creates a new mock instance +// NewMockLogInterface creates a new mock instance. func NewMockLogInterface(ctrl *gomock.Controller) *MockLogInterface { mock := &MockLogInterface{ctrl: ctrl} mock.recorder = &MockLogInterfaceMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockLogInterface) EXPECT() *MockLogInterfaceMockRecorder { return m.recorder } -// WithContext mocks base method -func (m *MockLogInterface) WithContext(ctx context.Context) djoemo.LogInterface { - ret := m.ctrl.Call(m, "WithContext", ctx) - ret0, _ := ret[0].(djoemo.LogInterface) - return ret0 -} - -// WithContext indicates an expected call of WithContext -func (mr *MockLogInterfaceMockRecorder) WithContext(ctx interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithContext", reflect.TypeOf((*MockLogInterface)(nil).WithContext), ctx) -} - -// WithFields mocks base method -func (m *MockLogInterface) WithFields(fields map[string]interface{}) djoemo.LogInterface { - ret := m.ctrl.Call(m, "WithFields", fields) - ret0, _ := ret[0].(djoemo.LogInterface) - return ret0 +// Error mocks base method. +func (m *MockLogInterface) Error(message string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Error", message) } -// WithFields indicates an expected call of WithFields -func (mr *MockLogInterfaceMockRecorder) WithFields(fields interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithFields", reflect.TypeOf((*MockLogInterface)(nil).WithFields), fields) +// Error indicates an expected call of Error. +func (mr *MockLogInterfaceMockRecorder) Error(message interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogInterface)(nil).Error), message) } -// info mocks base method +// Info mocks base method. func (m *MockLogInterface) Info(message string) { - m.ctrl.Call(m, "info", message) + m.ctrl.T.Helper() + m.ctrl.Call(m, "Info", message) } -// info indicates an expected call of info +// Info indicates an expected call of Info. func (mr *MockLogInterfaceMockRecorder) Info(message interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "info", reflect.TypeOf((*MockLogInterface)(nil).Info), message) + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogInterface)(nil).Info), message) } -// warn mocks base method +// Warn mocks base method. func (m *MockLogInterface) Warn(message string) { - m.ctrl.Call(m, "warn", message) + m.ctrl.T.Helper() + m.ctrl.Call(m, "Warn", message) } -// warn indicates an expected call of warn +// Warn indicates an expected call of Warn. func (mr *MockLogInterfaceMockRecorder) Warn(message interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "warn", reflect.TypeOf((*MockLogInterface)(nil).Warn), message) + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogInterface)(nil).Warn), message) } -// error mocks base method -func (m *MockLogInterface) Error(message string) { - m.ctrl.Call(m, "error", message) +// WithContext mocks base method. +func (m *MockLogInterface) WithContext(ctx context.Context) djoemo.LogInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithContext", ctx) + ret0, _ := ret[0].(djoemo.LogInterface) + return ret0 } -// error indicates an expected call of error -func (mr *MockLogInterfaceMockRecorder) Error(message interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "error", reflect.TypeOf((*MockLogInterface)(nil).Error), message) +// WithContext indicates an expected call of WithContext. +func (mr *MockLogInterfaceMockRecorder) WithContext(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithContext", reflect.TypeOf((*MockLogInterface)(nil).WithContext), ctx) +} + +// WithField mocks base method. +func (m *MockLogInterface) WithField(key string, value any) djoemo.LogInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithField", key, value) + ret0, _ := ret[0].(djoemo.LogInterface) + return ret0 +} + +// WithField indicates an expected call of WithField. +func (mr *MockLogInterfaceMockRecorder) WithField(key, value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithField", reflect.TypeOf((*MockLogInterface)(nil).WithField), key, value) +} + +// WithFields mocks base method. +func (m *MockLogInterface) WithFields(fields map[string]any) djoemo.LogInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithFields", fields) + ret0, _ := ret[0].(djoemo.LogInterface) + return ret0 +} + +// WithFields indicates an expected call of WithFields. +func (mr *MockLogInterfaceMockRecorder) WithFields(fields interface{}) *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 114a941..824b6b6 100644 --- a/mock/metrics_interface.go +++ b/mock/metrics_interface.go @@ -1,59 +1,49 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: metrics_interface.go +// Source: metrics.go -// Package mock_djoemo is a generated GoMock package. +// Package mock is a generated GoMock package. package mock import ( context "context" - djoemo "github.com/adjoeio/djoemo" - gomock "go.uber.org/mock/gomock" reflect "reflect" + time "time" + + djoemo "github.com/adjoeio/djoemo" + "go.uber.org/mock/gomock" ) -// MockMetricsInterface is a mock of MetricsInterface interface +// MockMetricsInterface is a mock of MetricsInterface interface. type MockMetricsInterface struct { ctrl *gomock.Controller recorder *MockMetricsInterfaceMockRecorder } -// MockMetricsInterfaceMockRecorder is the mock recorder for MockMetricsInterface +// MockMetricsInterfaceMockRecorder is the mock recorder for MockMetricsInterface. type MockMetricsInterfaceMockRecorder struct { mock *MockMetricsInterface } -// NewMockMetricsInterface creates a new mock instance +// NewMockMetricsInterface creates a new mock instance. func NewMockMetricsInterface(ctrl *gomock.Controller) *MockMetricsInterface { mock := &MockMetricsInterface{ctrl: ctrl} mock.recorder = &MockMetricsInterfaceMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockMetricsInterface) EXPECT() *MockMetricsInterfaceMockRecorder { return m.recorder } -// WithContext mocks base method -func (m *MockMetricsInterface) WithContext(ctx context.Context) djoemo.MetricsInterface { - ret := m.ctrl.Call(m, "WithContext", ctx) - ret0, _ := ret[0].(djoemo.MetricsInterface) - return ret0 -} - -// WithContext indicates an expected call of WithContext -func (mr *MockMetricsInterfaceMockRecorder) WithContext(ctx interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithContext", reflect.TypeOf((*MockMetricsInterface)(nil).WithContext), ctx) -} - -// Publish mocks base method -func (m *MockMetricsInterface) Publish(key, metricName string, metricValue float64) error { - ret := m.ctrl.Call(m, "Publish", key, metricName, metricValue) - ret0, _ := ret[0].(error) - return ret0 +// Record mocks base method. +func (m *MockMetricsInterface) Record(ctx context.Context, caller string, key djoemo.KeyInterface, duration time.Duration, success bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Record", ctx, caller, key, duration, success) } -// Publish indicates an expected call of Publish -func (mr *MockMetricsInterfaceMockRecorder) Publish(key, metricName, metricValue interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockMetricsInterface)(nil).Publish), key, metricName, metricValue) +// Record indicates an expected call of Record. +func (mr *MockMetricsInterfaceMockRecorder) Record(ctx, caller, key, duration, success interface{}) *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/model.go b/model.go index 66dfccb..f012f4c 100644 --- a/model.go +++ b/model.go @@ -25,7 +25,7 @@ func (m *Model) InitCreatedAt() { } } -//InitUpdatedAt sets the UpdatedAt +// InitUpdatedAt sets the UpdatedAt func (m *Model) InitUpdatedAt() { now := Now() m.UpdatedAt = &now diff --git a/query.go b/query.go index 75deabd..1c4e0f2 100644 --- a/query.go +++ b/query.go @@ -17,9 +17,9 @@ const ( type query struct { key - rangeOp Operator + rangeOp Operator descending bool - limit *int64 + limit *int64 } // Key factory method to create struct that implements key interface @@ -91,4 +91,4 @@ func (q *query) Limit() *int64 { // Descending returns scan direction func (q *query) Descending() bool { return q.descending -} \ No newline at end of file +} diff --git a/query_interface.go b/query_interface.go index 807157d..21f5c6c 100644 --- a/query_interface.go +++ b/query_interface.go @@ -4,6 +4,6 @@ package djoemo type QueryInterface interface { KeyInterface RangeOp() Operator - Limit() *int64 + Limit() *int64 Descending() bool } diff --git a/reflect_helper.go b/reflect_helper.go index 08b390f..7b7e034 100644 --- a/reflect_helper.go +++ b/reflect_helper.go @@ -4,7 +4,7 @@ import ( "reflect" ) -//InterfaceToArrayOfInterface transforms interface of slice to slice of interfaces +// InterfaceToArrayOfInterface transforms interface of slice to slice of interfaces func InterfaceToArrayOfInterface(sliceOfItems interface{}) ([]interface{}, error) { s := reflect.ValueOf(sliceOfItems) if s.Kind() != reflect.Slice { diff --git a/reflect_helper_test.go b/reflect_helper_test.go index e97a52b..636c1ee 100644 --- a/reflect_helper_test.go +++ b/reflect_helper_test.go @@ -1,25 +1,27 @@ package djoemo_test import ( - . "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("ReflectHelper", func() { Describe("transform interface of slice to slice of interfaces", func() { It("should transform interface of slice to slice of interfaces", func() { items := []string{"a", "b"} - ret, err := InterfaceToArrayOfInterface(items) + ret, err := djoemo.InterfaceToArrayOfInterface(items) Expect(err).To(BeNil()) - Expect(len(ret)).To(BeEqualTo(2)) + Expect(len(ret)).To(BeEquivalentTo(2)) - Expect(ret[0].(string)).To(BeEqualTo("a")) - Expect(ret[1].(string)).To(BeEqualTo("b")) + Expect(ret[0].(string)).To(BeEquivalentTo("a")) + Expect(ret[1].(string)).To(BeEquivalentTo("b")) }) It("should return error when not pass interface of slice", func() { item := "a" - ret, err := InterfaceToArrayOfInterface(item) - Expect(err).To(BeEqualTo(ErrInvalidSliceType)) + ret, err := djoemo.InterfaceToArrayOfInterface(item) + Expect(err).To(BeEquivalentTo(djoemo.ErrInvalidSliceType)) Expect(ret).To(BeNil()) }) }) diff --git a/repository_delete_test.go b/repository_delete_test.go index f272b2f..478f2c7 100644 --- a/repository_delete_test.go +++ b/repository_delete_test.go @@ -3,20 +3,24 @@ package djoemo_test import ( "context" "errors" - . "github.com/adjoeio/djoemo" + + "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo/mock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "go.uber.org/mock/gomock" ) var _ = Describe("Repository", func() { - const ( UserTableName = "UserTable" ) var ( dMock mock.DynamoMock - repository RepositoryInterface + repository djoemo.RepositoryInterface logMock *mock.MockLogInterface metricsMock *mock.MockMetricsInterface ) @@ -27,32 +31,32 @@ var _ = Describe("Repository", func() { metricsMock = mock.NewMockMetricsInterface(mockCtrl) dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) dMock = mock.NewDynamoMock(dAPIMock) - repository = NewRepository(dAPIMock) + repository = djoemo.NewRepository(dAPIMock) }) Describe("DeleteItem", func() { Describe("DeleteItem Invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") - err := repository.DeleteItem(key) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + err := repository.DeleteItemWithContext(context.Background(), key) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") - err := repository.DeleteItem(key) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + err := repository.DeleteItemWithContext(context.Background(), key) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") - err := repository.DeleteItem(key) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + err := repository.DeleteItemWithContext(context.Background(), key) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) It("should delete item by hash key", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -66,11 +70,11 @@ var _ = Describe("Repository", func() { dMock.WithDeleteInput(deleteDBInput), ).Exec() - err := repository.DeleteItem(key) + err := repository.DeleteItemWithContext(context.Background(), key) Expect(err).To(BeNil()) }) It("should delete item by hash key and range key", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("UserName"). @@ -87,12 +91,12 @@ var _ = Describe("Repository", func() { dMock.WithDeleteInput(deleteDBInput), ).Exec() - err := repository.DeleteItem(key) + err := repository.DeleteItemWithContext(context.Background(), key) Expect(err).To(BeNil()) }) It("should return error in case of db error", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -107,48 +111,48 @@ var _ = Describe("Repository", func() { dMock.WithError(err), ).Exec() - ret := repository.DeleteItem(key) - Expect(ret).To(BeEqualTo(err)) + ret := repository.DeleteItemWithContext(context.Background(), key) + Expect(ret).To(Equal(err)) }) }) Describe("DeleteItems", func() { Describe("DeleteItem Invalid keys", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") - key1 := Key().WithHashKeyName("UUID").WithHashKey("uuid1") - keys := []KeyInterface{key, key1} + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") + key1 := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid1") + keys := []djoemo.KeyInterface{key, key1} - err := repository.DeleteItems(keys) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + err := repository.DeleteItemsWithContext(context.Background(), keys) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") - key1 := Key().WithTableName(UserTableName).WithHashKey("uuid1") - keys := []KeyInterface{key, key1} + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") + key1 := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid1") + keys := []djoemo.KeyInterface{key, key1} - err := repository.DeleteItems(keys) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + err := repository.DeleteItemsWithContext(context.Background(), keys) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") - key1 := Key().WithTableName(UserTableName).WithHashKeyName("UUID") - keys := []KeyInterface{key, key1} + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key1 := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") + keys := []djoemo.KeyInterface{key, key1} - err := repository.DeleteItems(keys) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + err := repository.DeleteItemsWithContext(context.Background(), keys) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) It("should delete items by hash key", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") - key1 := Key().WithTableName(UserTableName). + key1 := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid1") - keys := []KeyInterface{key, key1} + keys := []djoemo.KeyInterface{key, key1} deleteDBInput := []map[string]interface{}{ {"UUID": "uuid"}, {"UUID": "uuid1"}, @@ -160,23 +164,23 @@ var _ = Describe("Repository", func() { dMock.WithDeleteInputs(deleteDBInput), ).Exec() - err := repository.DeleteItems(keys) + err := repository.DeleteItemsWithContext(context.Background(), keys) Expect(err).To(BeNil()) }) It("should delete items by hash and range key ", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("UserName"). WithRangeKey("user") - key1 := Key().WithTableName(UserTableName). + key1 := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid1"). WithRangeKeyName("UserName"). WithRangeKey("user1") - keys := []KeyInterface{key, key1} + keys := []djoemo.KeyInterface{key, key1} deleteDBInput := []map[string]interface{}{ {"UUID": "uuid", "UserName": "user"}, {"UUID": "uuid1", "UserName": "user1"}, @@ -188,19 +192,19 @@ var _ = Describe("Repository", func() { dMock.WithDeleteInputs(deleteDBInput), ).Exec() - err := repository.DeleteItems(keys) + err := repository.DeleteItemsWithContext(context.Background(), keys) Expect(err).To(BeNil()) }) It("should return error in case of db error", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") - key1 := Key().WithTableName(UserTableName). + key1 := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid1") - keys := []KeyInterface{key, key1} + keys := []djoemo.KeyInterface{key, key1} deleteDBInput := []map[string]interface{}{ {"UUID": "uuid"}, {"UUID": "uuid1"}, @@ -214,26 +218,26 @@ var _ = Describe("Repository", func() { dMock.WithError(err), ).Exec() - ret := repository.DeleteItems(keys) - Expect(ret).To(BeEqualTo(err)) + ret := repository.DeleteItemsWithContext(context.Background(), keys) + Expect(ret).To(Equal(err)) }) It("should return nil if keys empty", func() { - var keys []KeyInterface + var keys []djoemo.KeyInterface - err := repository.DeleteItems(keys) + err := repository.DeleteItemsWithContext(context.Background(), keys) Expect(err).To(BeNil()) }) - It("should publish metrics if metric is supported", func() { - key := Key().WithTableName(UserTableName). + It("should record metrics", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") - key1 := Key().WithTableName(UserTableName). + key1 := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid1") - keys := []KeyInterface{key, key1} + keys := []djoemo.KeyInterface{key, key1} deleteDBInput := []map[string]interface{}{ {"UUID": "uuid"}, {"UUID": "uuid1"}, @@ -246,42 +250,91 @@ var _ = Describe("Repository", func() { ).Exec() repository.WithMetrics(metricsMock) - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameDeleteItemsCount, float64(2)).Return(nil) - err := repository.DeleteItems(keys) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpDelete, key, gomock.Any(), true) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpDelete, key1, gomock.Any(), true) + err := repository.DeleteItemsWithContext(context.Background(), keys) Expect(err).To(BeNil()) }) - It("should not affect save and log error if publish failed", func() { - key := Key().WithTableName(UserTableName). + It("should log on error", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") - key1 := Key().WithTableName(UserTableName). + key1 := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid1") - keys := []KeyInterface{key, key1} + keys := []djoemo.KeyInterface{key, key1} deleteDBInput := []map[string]interface{}{ {"UUID": "uuid"}, {"UUID": "uuid1"}, } + err := errors.New("failed to delete") dMock.Should(). DeleteAll( dMock.WithTable(key.TableName()), dMock.WithDeleteInputs(deleteDBInput), + dMock.WithError(err), ).Exec() + repository.WithLog(logMock) + repository.WithMetrics(metricsMock) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpDelete, key, gomock.Any(), false) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpDelete, key1, gomock.Any(), false) + + err = repository.DeleteItemsWithContext(context.Background(), keys) + Expect(err).To(Equal(err)) + }) + }) + + Describe("Log", func() { + It("should log with extra fields if log is supported for DeleteItemWithContext", func() { + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + deleteDBInput := map[string]interface{}{ + "UUID": "uuid", + } + err := errors.New("failed to delete") + dMock.Should(). + Delete( + dMock.WithTable(key.TableName()), + dMock.WithDeleteInput(deleteDBInput), + dMock.WithError(err), + ).Exec() + repository.WithLog(logMock) - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameDeleteItemsCount, float64(2)). - Return(errors.New("failed to publish")) - - logMock.EXPECT().WithFields(map[string]interface{}{"TableName": key.TableName()}).Return(logMock) - logMock.EXPECT().WithContext(context.TODO()).Return(logMock) - logMock.EXPECT().Error("failed to publish") - err := repository.DeleteItems(keys) + + repository.WithMetrics(metricsMock) + metricsMock.EXPECT().Record(gomock.Any(), "delete", key, gomock.Any(), false) + + ret := repository.DeleteItemWithContext(context.Background(), key) + Expect(ret).To(BeEquivalentTo(err)) + }) + }) + + Describe("Metrics", func() { + It("should record metrics if metric is supported for DeleteItemWithContext", func() { + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + deleteDBInput := map[string]interface{}{ + "UUID": "uuid", + } + + dMock.Should(). + Delete( + dMock.WithTable(key.TableName()), + dMock.WithDeleteInput(deleteDBInput), + ).Exec() + + repository.WithMetrics(metricsMock) + metricsMock.EXPECT().Record(gomock.Any(), "delete", key, gomock.Any(), true) + + err := repository.DeleteItemWithContext(context.Background(), key) Expect(err).To(BeNil()) }) }) diff --git a/repository_get_test.go b/repository_get_test.go index 3e2c6b1..317cddf 100644 --- a/repository_get_test.go +++ b/repository_get_test.go @@ -4,9 +4,11 @@ import ( "context" "errors" - . "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo" "github.com/adjoeio/djoemo/mock" "github.com/guregu/dynamo" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "go.uber.org/mock/gomock" ) @@ -17,74 +19,98 @@ var _ = Describe("Repository", func() { ) var ( - dMock mock.DynamoMock - repository RepositoryInterface + dMock mock.DynamoMock + repository djoemo.RepositoryInterface + logMock *mock.MockLogInterface + metricsMock *mock.MockMetricsInterface ) BeforeEach(func() { mockCtrl := gomock.NewController(GinkgoT()) dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) dMock = mock.NewDynamoMock(dAPIMock) - repository = NewRepository(dAPIMock) + metricsMock = mock.NewMockMetricsInterface(mockCtrl) + logMock = mock.NewMockLogInterface(mockCtrl) + repository = djoemo.NewRepository(dAPIMock) + repository.WithMetrics(metricsMock) + repository.WithLog(logMock) }) Describe("GetItem", func() { Describe("GetItem Invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") user := &User{} - found, err := repository.GetItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) Expect(found).To(BeFalse()) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") user := &User{} - found, err := repository.GetItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) Expect(found).To(BeFalse()) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") user := &User{} - found, err := repository.GetItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) Expect(found).To(BeFalse()) }) }) Describe("GetItems Invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") users := &[]User{} - found, err := repository.GetItems(key, users) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GetItemsWithContext(context.Background(), key, users) + + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) Expect(found).To(BeFalse()) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") users := &[]User{} - found, err := repository.GetItems(key, users) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GetItemsWithContext(context.Background(), key, users) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) Expect(found).To(BeFalse()) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") users := &[]User{} - found, err := repository.GetItems(key, users) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GetItemsWithContext(context.Background(), key, users) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) Expect(found).To(BeFalse()) }) }) Describe("GetItem", func() { It("should get item with Hash", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -92,6 +118,8 @@ var _ = Describe("Repository", func() { "UUID": "uuid", } + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + dMock.Should(). Get( dMock.WithTable(key.TableName()), @@ -100,15 +128,15 @@ var _ = Describe("Repository", func() { ).Exec() user := &User{} - found, err := repository.GetItem(key, user) + found, err := repository.GetItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) - Expect(user.UUID).To(BeEqualTo(userDBOutput["UUID"])) + Expect(user.UUID).To(Equal(userDBOutput["UUID"])) }) It("should get item with Hash and range", func() { - key := Key().WithTableName(ProfileTableName). + key := djoemo.Key().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("Email"). @@ -119,6 +147,8 @@ var _ = Describe("Repository", func() { "Email": "user@adjeo.io", } + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + dMock.Should(). Get( dMock.WithTable(key.TableName()), @@ -128,19 +158,24 @@ var _ = Describe("Repository", func() { ).Exec() profile := &Profile{} - found, err := repository.GetItem(key, profile) + found, err := repository.GetItemWithContext(context.Background(), key, profile) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) - Expect(profile.UUID).To(BeEqualTo(profileDBOutput["UUID"])) - Expect(profile.Email).To(BeEqualTo(profileDBOutput["Email"])) + Expect(profile.UUID).To(Equal(profileDBOutput["UUID"])) + Expect(profile.Email).To(Equal(profileDBOutput["Email"])) }) It("should return false and nil if item was not found", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info((djoemo.ErrNoItemFound).Error()) + dMock.Should(). Get( dMock.WithTable(key.TableName()), @@ -149,17 +184,20 @@ var _ = Describe("Repository", func() { ).Exec() user := &User{} - found, err := repository.GetItem(key, user) + found, err := repository.GetItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) Expect(found).To(BeFalse()) }) It("should return false and error in case of error", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") err := errors.New("invalid query") + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + dMock.Should(). Get( dMock.WithTable(key.TableName()), @@ -168,18 +206,23 @@ var _ = Describe("Repository", func() { ).Exec() user := &User{} - found, err := repository.GetItem(key, user) + found, err := repository.GetItemWithContext(context.Background(), key, user) Expect(err).To(BeEquivalentTo(err)) Expect(found).To(BeFalse()) }) }) - It("should return false and nil if dynamos ErrNotFound occured", func() { - key := Key().WithTableName(UserTableName). + It("should return false and nil if dynamos ErrNotFound occurred", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info((djoemo.ErrNoItemFound).Error()) + dMock.Should(). Get( dMock.WithTable(key.TableName()), @@ -189,7 +232,7 @@ var _ = Describe("Repository", func() { ).Exec() user := &User{} - found, err := repository.GetItem(key, user) + found, err := repository.GetItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) Expect(found).To(BeFalse()) @@ -197,7 +240,7 @@ var _ = Describe("Repository", func() { Describe("GetItems", func() { It("should get items with Hash", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -206,6 +249,8 @@ var _ = Describe("Repository", func() { {"UUID": "uuid", "UserName": "name2"}, } + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + dMock.Should(). Query( dMock.WithTable(key.TableName()), @@ -214,16 +259,16 @@ var _ = Describe("Repository", func() { ).Exec() users := &[]User{} - found, err := repository.GetItems(key, users) + found, err := repository.GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) result := *users - Expect(len(result)).To(BeEqualTo(2)) - Expect(result[0].UUID).To(BeEqualTo(userDBOutput[0]["UUID"])) + Expect(len(result)).To(Equal(2)) + Expect(result[0].UUID).To(Equal(userDBOutput[0]["UUID"])) }) It("should get items with Hash and ignore range", func() { - key := Key().WithTableName(ProfileTableName). + key := djoemo.Key().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -232,6 +277,8 @@ var _ = Describe("Repository", func() { {"UUID": "uuid", "Email": "email2"}, } + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + dMock.Should(). Query( dMock.WithTable(key.TableName()), @@ -240,20 +287,24 @@ var _ = Describe("Repository", func() { ).Exec() profiles := &[]Profile{} - found, err := repository.GetItems(key, profiles) + found, err := repository.GetItemsWithContext(context.Background(), key, profiles) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) result := *profiles - Expect(len(result)).To(BeEqualTo(2)) - Expect(result[0].UUID).To(BeEqualTo(profileDBOutput[0]["UUID"])) - + Expect(len(result)).To(Equal(2)) + Expect(result[0].UUID).To(Equal(profileDBOutput[0]["UUID"])) }) It("should return false and nil if item was not found", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info((djoemo.ErrNoItemFound).Error()) + dMock.Should(). Query( dMock.WithTable(key.TableName()), @@ -262,17 +313,22 @@ var _ = Describe("Repository", func() { ).Exec() users := &[]User{} - found, err := repository.GetItems(key, users) + found, err := repository.GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeNil()) Expect(found).To(BeFalse()) }) - It("should return false and nil if dynamos ErrNotFound occured", func() { - key := Key().WithTableName(UserTableName). + It("should return false and nil if dynamos ErrNotFound occurred", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info((djoemo.ErrNoItemFound).Error()) + dMock.Should(). Query( dMock.WithTable(key.TableName()), @@ -282,17 +338,20 @@ var _ = Describe("Repository", func() { ).Exec() users := &[]User{} - found, err := repository.GetItems(key, users) + found, err := repository.GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeNil()) Expect(found).To(BeFalse()) }) It("should return false and error in case of error", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") err := errors.New("invalid query") + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + dMock.Should(). Query( dMock.WithTable(key.TableName()), @@ -301,17 +360,16 @@ var _ = Describe("Repository", func() { ).Exec() users := &[]User{} - found, err := repository.GetItems(key, users) + found, err := repository.GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeEquivalentTo(err)) Expect(found).To(BeFalse()) }) - }) Describe("GetItems with Iterator", func() { It("should return items one-by-one when iterating via NextItem", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") scanLimit := int64(1) @@ -328,6 +386,8 @@ var _ = Describe("Repository", func() { }, } + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + dMock.Should().ScanAll( dMock.WithTable(UserTableName), dMock.WithScanAllOutput(scanOutput), @@ -342,11 +402,104 @@ var _ = Describe("Repository", func() { users = append(users, user) } - Expect(len(users)).To(BeEqualTo(2)) - Expect(users[0].UserName).To(BeEqualTo("user")) - Expect(users[1].UserName).To(BeEqualTo("userTwo")) + Expect(len(users)).To(Equal(2)) + Expect(users[0].UserName).To(Equal("user")) + Expect(users[1].UserName).To(Equal("userTwo")) }) }) + Describe("Log", func() { + It("should log with extra fields if log is supported for GetItemWithContext", func() { + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + err := errors.New("failed to get item") + dMock.Should(). + Get( + dMock.WithTable(key.TableName()), + dMock.WithHash(*key.HashKeyName(), key.HashKey()), + dMock.WithError(err), + ).Exec() + + user := &User{} + repository.WithLog(logMock) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, ret := repository.GetItemWithContext(context.Background(), key, user) + Expect(ret).To(BeEquivalentTo(err)) + Expect(found).To(BeFalse()) + }) + + It("should log with extra fields if log is supported for GetItemsWithContext", func() { + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + err := errors.New("failed to get items") + dMock.Should(). + Query( + dMock.WithTable(key.TableName()), + dMock.WithCondition(*key.HashKeyName(), key.HashKey(), "EQ"), + dMock.WithError(err), + ).Exec() + users := &[]User{} + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, ret := repository.GetItemsWithContext(context.Background(), key, users) + Expect(ret).To(BeEquivalentTo(err)) + Expect(found).To(BeFalse()) + }) + }) + + Describe("Metrics", func() { + It("should record metrics if metric is supported for GetItemWithContext", func() { + 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{} + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + + found, err := repository.GetItemWithContext(context.Background(), key, user) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + }) + + It("should record metrics if metric is supported for GetItemsWithContext", func() { + key := djoemo.Key().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + userDBOutput := []map[string]interface{}{ + {"UUID": "uuid", "UserName": "name1"}, + {"UUID": "uuid", "UserName": "name2"}, + } + + dMock.Should(). + Query( + dMock.WithTable(key.TableName()), + dMock.WithCondition(*key.HashKeyName(), key.HashKey(), "EQ"), + dMock.WithQueryOutput(userDBOutput), + ).Exec() + + users := &[]User{} + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + + found, err := repository.GetItemsWithContext(context.Background(), key, users) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + }) + }) }) }) diff --git a/repository_global_index_test.go b/repository_global_index_test.go index b1cf02a..ece9915 100644 --- a/repository_global_index_test.go +++ b/repository_global_index_test.go @@ -6,8 +6,10 @@ import ( "go.uber.org/mock/gomock" - . "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo" "github.com/adjoeio/djoemo/mock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("Global Index", func() { @@ -18,74 +20,98 @@ var _ = Describe("Global Index", func() { ) var ( - dMock mock.DynamoMock - repository RepositoryInterface + dMock mock.DynamoMock + repository djoemo.RepositoryInterface + metricsMock *mock.MockMetricsInterface + logMock *mock.MockLogInterface ) BeforeEach(func() { mockCtrl := gomock.NewController(GinkgoT()) dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) dMock = mock.NewDynamoMock(dAPIMock) - repository = NewRepository(dAPIMock) + logMock = mock.NewMockLogInterface(mockCtrl) + metricsMock = mock.NewMockMetricsInterface(mockCtrl) + repository = djoemo.NewRepository(dAPIMock) + repository.WithMetrics(metricsMock) + repository.WithLog(logMock) }) Describe("GetItem", func() { Describe("GetItem Invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") user := &User{} - found, err := repository.GIndex(IndexName).GetItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) Expect(found).To(BeFalse()) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") user := &User{} - found, err := repository.GIndex(IndexName).GetItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) Expect(found).To(BeFalse()) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") user := &User{} - found, err := repository.GIndex(IndexName).GetItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) Expect(found).To(BeFalse()) }) }) Describe("GetItems Invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") users := &[]User{} - found, err := repository.GIndex(IndexName).GetItem(key, users) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, users) + + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) Expect(found).To(BeFalse()) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") users := &[]User{} - found, err := repository.GIndex(IndexName).GetItem(key, users) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, users) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) Expect(found).To(BeFalse()) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") users := &[]User{} - found, err := repository.GIndex(IndexName).GetItem(key, users) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, users) + + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) Expect(found).To(BeFalse()) }) }) Describe("GetItem", func() { It("should get item with Hash", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -102,15 +128,18 @@ var _ = Describe("Global Index", func() { ).Exec() user := &User{} - found, err := repository.GIndex(IndexName).GetItem(key, user) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) - Expect(user.UUID).To(BeEqualTo(userDBOutput["UUID"])) + Expect(user.UUID).To(Equal(userDBOutput["UUID"])) }) It("should get item with Hash and range", func() { - key := Key().WithTableName(ProfileTableName). + key := djoemo.Key().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("Email"). @@ -131,16 +160,19 @@ var _ = Describe("Global Index", func() { ).Exec() profile := &Profile{} - found, err := repository.GIndex(IndexName).GetItem(key, profile) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, profile) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) - Expect(profile.UUID).To(BeEqualTo(profileDBOutput["UUID"])) - Expect(profile.Email).To(BeEqualTo(profileDBOutput["Email"])) + Expect(profile.UUID).To(Equal(profileDBOutput["UUID"])) + Expect(profile.Email).To(Equal(profileDBOutput["Email"])) }) It("should return false and nil if item was not found", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -153,14 +185,20 @@ var _ = Describe("Global Index", func() { ).Exec() user := &User{} - found, err := repository.GIndex(IndexName).GetItem(key, user) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info((djoemo.ErrNoItemFound).Error()) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) Expect(found).To(BeFalse()) }) It("should return false and error in case of error", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") err := errors.New("invalid query") @@ -174,7 +212,10 @@ var _ = Describe("Global Index", func() { ).Exec() user := &User{} - found, err := repository.GIndex(IndexName).GetItem(key, user) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemWithContext(context.Background(), key, user) Expect(err).To(BeEquivalentTo(err)) Expect(found).To(BeFalse()) @@ -182,7 +223,7 @@ var _ = Describe("Global Index", func() { }) Describe("GetItems", func() { It("should get items with Hash", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -200,16 +241,19 @@ var _ = Describe("Global Index", func() { ).Exec() users := &[]User{} - found, err := repository.GIndex(IndexName).GetItems(key, users) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + + found, err := repository.GIndex(IndexName).GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) result := *users - Expect(len(result)).To(BeEqualTo(2)) - Expect(result[0].UUID).To(BeEqualTo(userDBOutput[0]["UUID"])) + Expect(len(result)).To(Equal(2)) + Expect(result[0].UUID).To(Equal(userDBOutput[0]["UUID"])) }) It("should get items with Hash and ignore range", func() { - key := Key().WithTableName(ProfileTableName). + key := djoemo.Key().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -227,17 +271,19 @@ var _ = Describe("Global Index", func() { ).Exec() profiles := &[]Profile{} - found, err := repository.GIndex(IndexName).GetItems(key, profiles) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + + found, err := repository.GIndex(IndexName).GetItemsWithContext(context.Background(), key, profiles) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) result := *profiles - Expect(len(result)).To(BeEqualTo(2)) - Expect(result[0].UUID).To(BeEqualTo(profileDBOutput[0]["UUID"])) - + Expect(len(result)).To(Equal(2)) + Expect(result[0].UUID).To(Equal(profileDBOutput[0]["UUID"])) }) It("should return false and nil if item was not found", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("AppID"). @@ -252,14 +298,20 @@ var _ = Describe("Global Index", func() { ).Exec() users := &[]User{} - found, err := repository.GIndex(IndexName).GetItems(key, users) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + logMock.EXPECT().WithContext(gomock.Any()).Return(logMock) + logMock.EXPECT().WithField(djoemo.TableName, key.TableName()).Return(logMock) + logMock.EXPECT().Info((djoemo.ErrNoItemFound).Error()) + + found, err := repository.GIndex(IndexName).GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeNil()) Expect(found).To(BeFalse()) }) It("should return false and error in case of error", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") err := errors.New("invalid query") @@ -272,16 +324,18 @@ var _ = Describe("Global Index", func() { ).Exec() users := &[]User{} - found, err := repository.GIndex(IndexName).GetItems(key, users) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), false) + + found, err := repository.GIndex(IndexName).GetItemsWithContext(context.Background(), key, users) Expect(err).To(BeEquivalentTo(err)) Expect(found).To(BeFalse()) }) - }) Describe("GetItemsWithRangeWithContext", func() { It("should get items with Hash and Range", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("AppID"). @@ -301,13 +355,15 @@ var _ = Describe("Global Index", func() { dMock.WithQueryOutput(userDBOutput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, key, gomock.Any(), true) + users := &[]User{} found, err := repository.GIndex(IndexName).GetItemsWithRangeWithContext(context.Background(), key, users) Expect(err).To(BeNil()) Expect(found).To(BeTrue()) result := *users - Expect(len(result)).To(BeEqualTo(2)) - Expect(result[0].UUID).To(BeEqualTo(userDBOutput[0]["UUID"])) + Expect(len(result)).To(Equal(2)) + Expect(result[0].UUID).To(Equal(userDBOutput[0]["UUID"])) }) }) }) @@ -315,28 +371,31 @@ var _ = Describe("Global Index", func() { Describe("Query Items", func() { Describe("Query Items Invalid key ", func() { It("should fail with table name is nil", func() { - query := Query().WithHashKeyName("UUID").WithHashKey("uuid") + query := djoemo.Query().WithHashKeyName("UUID").WithHashKey("uuid") user := &[]User{} - err := repository.Query(query, user) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, query, gomock.Any(), false) + err := repository.QueryWithContext(context.Background(), query, user) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - query := Query().WithTableName(UserTableName).WithHashKey("uuid") + query := djoemo.Query().WithTableName(UserTableName).WithHashKey("uuid") user := &[]User{} - err := repository.GIndex(IndexName).Query(query, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, query, gomock.Any(), false) + err := repository.GIndex(IndexName).QueryWithContext(context.Background(), query, user) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - query := Query().WithTableName(UserTableName).WithHashKeyName("UUID") + query := djoemo.Query().WithTableName(UserTableName).WithHashKeyName("UUID") user := &[]User{} - err := repository.GIndex(IndexName).Query(query, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, query, gomock.Any(), false) + err := repository.GIndex(IndexName).QueryWithContext(context.Background(), query, user) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) Describe("Query Items", func() { It("should query items with Hash", func() { - q := Query().WithTableName(UserTableName). + q := djoemo.Query().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -348,23 +407,25 @@ var _ = Describe("Global Index", func() { Query( dMock.WithIndex(IndexName), dMock.WithTable(q.TableName()), - dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(Equal)), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), dMock.WithQueryOutput(userDBOutput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), true) + var users []User - err := repository.GIndex(IndexName).Query(q, &users) + err := repository.GIndex(IndexName).QueryWithContext(context.Background(), q, &users) Expect(err).To(BeNil()) - Expect(users[0].UUID).To(BeEqualTo(userDBOutput["UUID"])) + Expect(users[0].UUID).To(Equal(userDBOutput["UUID"])) }) It("should query items with hash and range", func() { - q := Query().WithTableName(ProfileTableName). + q := djoemo.Query().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("Email"). WithRangeKey("user"). - WithRangeOp(BeginsWith) + WithRangeOp(djoemo.BeginsWith) profileDBOutput := []map[string]interface{}{ { @@ -380,22 +441,24 @@ var _ = Describe("Global Index", func() { Query( dMock.WithIndex(IndexName), dMock.WithTable(q.TableName()), - dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(Equal)), - dMock.WithCondition(*q.RangeKeyName(), q.RangeKey(), string(BeginsWith)), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), + dMock.WithCondition(*q.RangeKeyName(), q.RangeKey(), string(djoemo.BeginsWith)), dMock.WithQueryOutput(profileDBOutput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), true) + var profiles []Profile - err := repository.GIndex(IndexName).Query(q, &profiles) + err := repository.GIndex(IndexName).QueryWithContext(context.Background(), q, &profiles) Expect(err).To(BeNil()) - Expect(len(profiles)).To(BeEqualTo(2)) - Expect(profiles[0].UUID).To(BeEqualTo("uuid1")) - Expect(profiles[1].UUID).To(BeEqualTo("uuid2")) + Expect(len(profiles)).To(Equal(2)) + Expect(profiles[0].UUID).To(Equal("uuid1")) + Expect(profiles[1].UUID).To(Equal("uuid2")) }) It("should query items with limit and order", func() { - q := Query().WithTableName(UserTableName). + q := djoemo.Query().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithLimit(2). @@ -409,30 +472,33 @@ var _ = Describe("Global Index", func() { Query( dMock.WithIndex(IndexName), dMock.WithTable(q.TableName()), - dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(Equal)), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), dMock.WithQueryOutput(userDBOutput), dMock.WithLimit(2), dMock.WithDesc(true), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), true) + var users []User - err := repository.GIndex(IndexName).Query(q, &users) + err := repository.GIndex(IndexName).QueryWithContext(context.Background(), q, &users) Expect(err).To(BeNil()) - Expect(users[0].UUID).To(BeEqualTo(userDBOutput["UUID"])) + Expect(users[0].UUID).To(Equal(userDBOutput["UUID"])) }) It("should return error if output is not pointer to slice ", func() { - q := Query().WithTableName(ProfileTableName). + q := djoemo.Query().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("Email"). WithRangeKey("user"). - WithRangeOp(BeginsWith) + WithRangeOp(djoemo.BeginsWith) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), false) var profile Profile - err := repository.GIndex(IndexName).Query(q, &profile) + err := repository.GIndex(IndexName).QueryWithContext(context.Background(), q, &profile) - Expect(err).To(BeEquivalentTo(ErrInvalidPointerSliceType)) + Expect(err).To(BeEquivalentTo(djoemo.ErrInvalidPointerSliceType)) }) }) }) diff --git a/repository_query_test.go b/repository_query_test.go index 0b26642..8b316ed 100644 --- a/repository_query_test.go +++ b/repository_query_test.go @@ -1,10 +1,14 @@ package djoemo_test import ( + "context" + "go.uber.org/mock/gomock" - . "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo" "github.com/adjoeio/djoemo/mock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("Repository", func() { @@ -14,42 +18,51 @@ var _ = Describe("Repository", func() { ) var ( - dMock mock.DynamoMock - repository RepositoryInterface + dMock mock.DynamoMock + repository djoemo.RepositoryInterface + logMock *mock.MockLogInterface + metricsMock *mock.MockMetricsInterface ) BeforeEach(func() { mockCtrl := gomock.NewController(GinkgoT()) dAPIMock := mock.NewMockDynamoDBAPI(mockCtrl) dMock = mock.NewDynamoMock(dAPIMock) - repository = NewRepository(dAPIMock) + logMock = mock.NewMockLogInterface(mockCtrl) + metricsMock = mock.NewMockMetricsInterface(mockCtrl) + repository = djoemo.NewRepository(dAPIMock) + repository.WithMetrics(metricsMock) + repository.WithLog(logMock) }) - Describe("Query Items", func() { - Describe("Query Items Invalid key ", func() { + Describe("djoemo.Query Items", func() { + Describe("djoemo.Query Items Invalid key ", func() { It("should fail with table name is nil", func() { - query := Query().WithHashKeyName("UUID").WithHashKey("uuid") + query := djoemo.Query().WithHashKeyName("UUID").WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, query, gomock.Any(), false) user := &[]User{} - err := repository.Query(query, user) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + err := repository.QueryWithContext(context.Background(), query, user) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - query := Query().WithTableName(UserTableName).WithHashKey("uuid") + query := djoemo.Query().WithTableName(UserTableName).WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, query, gomock.Any(), false) user := &[]User{} - err := repository.Query(query, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + err := repository.QueryWithContext(context.Background(), query, user) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - query := Query().WithTableName(UserTableName).WithHashKeyName("UUID") + query := djoemo.Query().WithTableName(UserTableName).WithHashKeyName("UUID") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, query, gomock.Any(), false) user := &[]User{} - err := repository.Query(query, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + err := repository.QueryWithContext(context.Background(), query, user) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) - Describe("Query Items", func() { + Describe("djoemo.Query Items", func() { It("should query items with Hash", func() { - q := Query().WithTableName(UserTableName). + q := djoemo.Query().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -60,18 +73,20 @@ var _ = Describe("Repository", func() { dMock.Should(). Query( dMock.WithTable(q.TableName()), - dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(Equal)), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), dMock.WithQueryOutput(userDBOutput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), true) + var users []User - err := repository.Query(q, &users) + err := repository.QueryWithContext(context.Background(), q, &users) Expect(err).To(BeNil()) - Expect(users[0].UUID).To(BeEqualTo(userDBOutput["UUID"])) + Expect(users[0].UUID).To(Equal(userDBOutput["UUID"])) }) It("should query items with Hash", func() { - q := Query().WithTableName(UserTableName). + q := djoemo.Query().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithLimit(2). @@ -84,25 +99,27 @@ var _ = Describe("Repository", func() { dMock.Should(). Query( dMock.WithTable(q.TableName()), - dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(Equal)), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), dMock.WithQueryOutput(userDBOutput), dMock.WithLimit(2), dMock.WithDesc(true), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), true) + var users []User - err := repository.Query(q, &users) + err := repository.QueryWithContext(context.Background(), q, &users) Expect(err).To(BeNil()) - Expect(users[0].UUID).To(BeEqualTo(userDBOutput["UUID"])) + Expect(users[0].UUID).To(Equal(userDBOutput["UUID"])) }) It("should query items with hash and range", func() { - q := Query().WithTableName(ProfileTableName). + q := djoemo.Query().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("Email"). WithRangeKey("user"). - WithRangeOp(BeginsWith) + WithRangeOp(djoemo.BeginsWith) profileDBOutput := []map[string]interface{}{ { @@ -117,32 +134,84 @@ var _ = Describe("Repository", func() { dMock.Should(). Query( dMock.WithTable(q.TableName()), - dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(Equal)), - dMock.WithCondition(*q.RangeKeyName(), q.RangeKey(), string(BeginsWith)), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), + dMock.WithCondition(*q.RangeKeyName(), q.RangeKey(), string(djoemo.BeginsWith)), dMock.WithQueryOutput(profileDBOutput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), true) + var profiles []Profile - err := repository.Query(q, &profiles) + err := repository.QueryWithContext(context.Background(), q, &profiles) Expect(err).To(BeNil()) - Expect(len(profiles)).To(BeEqualTo(2)) - Expect(profiles[0].UUID).To(BeEqualTo("uuid1")) - Expect(profiles[1].UUID).To(BeEqualTo("uuid2")) + Expect(len(profiles)).To(Equal(2)) + Expect(profiles[0].UUID).To(Equal("uuid1")) + Expect(profiles[1].UUID).To(Equal("uuid2")) }) It("should return error if output is not pointer to slice ", func() { - q := Query().WithTableName(ProfileTableName). + q := djoemo.Query().WithTableName(ProfileTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("Email"). WithRangeKey("user"). - WithRangeOp(BeginsWith) + WithRangeOp(djoemo.BeginsWith) + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), false) var profile Profile - err := repository.Query(q, &profile) + err := repository.QueryWithContext(context.Background(), q, &profile) + + Expect(err).To(BeEquivalentTo(djoemo.ErrInvalidPointerSliceType)) + }) + + Describe("Log", func() { + It("should log with extra fields if log is supported", func() { + q := djoemo.Query().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + err := context.DeadlineExceeded + dMock.Should(). + Query( + dMock.WithTable(q.TableName()), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), + dMock.WithError(err), + ).Exec() + + var users []User + + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpRead, q, gomock.Any(), false) + + ret := repository.QueryWithContext(context.Background(), q, &users) + Expect(ret).To(BeEquivalentTo(err)) + }) + }) + + Describe("Metrics", func() { + It("should record metrics if metric is supported", func() { + q := djoemo.Query().WithTableName(UserTableName). + WithHashKeyName("UUID"). + WithHashKey("uuid") + + userDBOutput := map[string]interface{}{ + "UUID": "uuid", + } + + dMock.Should(). + Query( + dMock.WithTable(q.TableName()), + dMock.WithCondition(*q.HashKeyName(), q.HashKey(), string(djoemo.Equal)), + dMock.WithQueryOutput(userDBOutput), + ).Exec() + + var users []User + + metricsMock.EXPECT().Record(gomock.Any(), "read", q, gomock.Any(), true) - Expect(err).To(BeEquivalentTo(ErrInvalidPointerSliceType)) + err := repository.QueryWithContext(context.Background(), q, &users) + Expect(err).To(BeNil()) + }) }) }) }) diff --git a/repository_save_test.go b/repository_save_test.go index abf338a..08044f7 100644 --- a/repository_save_test.go +++ b/repository_save_test.go @@ -4,13 +4,14 @@ import ( "context" "time" - "github.com/bouk/monkey" "github.com/pkg/errors" "go.uber.org/mock/gomock" + "github.com/adjoeio/djoemo" "github.com/adjoeio/djoemo/mock" - . "github.com/adjoeio/djoemo" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("Repository", func() { @@ -20,7 +21,7 @@ var _ = Describe("Repository", func() { var ( dMock mock.DynamoMock - repository RepositoryInterface + repository djoemo.RepositoryInterface logMock *mock.MockLogInterface metricsMock *mock.MockMetricsInterface ) @@ -31,41 +32,46 @@ var _ = Describe("Repository", func() { logMock = mock.NewMockLogInterface(mockCtrl) metricsMock = mock.NewMockMetricsInterface(mockCtrl) dMock = mock.NewDynamoMock(dAPIMock) - repository = NewRepository(dAPIMock) + repository = djoemo.NewRepository(dAPIMock) + repository.WithMetrics(metricsMock) + repository.WithLog(logMock) }) Describe("SaveItem", func() { Describe("SaveItem invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) user := &User{ UUID: "uuid", } - err := repository.SaveItem(key, user) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + err := repository.SaveItemWithContext(context.Background(), key, user) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) user := &User{ UUID: "uuid", } - err := repository.SaveItem(key, user) + err := repository.SaveItemWithContext(context.Background(), key, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) user := &User{ UUID: "uuid", } - err := repository.SaveItem(key, user) + err := repository.SaveItemWithContext(context.Background(), key, user) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) It("should save item", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -82,11 +88,13 @@ var _ = Describe("Repository", func() { dMock.WithInput(userDBInput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), true) + user := &User{ UUID: "uuid", UserName: "name1", } - err := repository.SaveItem(key, user) + err := repository.SaveItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) }) @@ -94,7 +102,8 @@ var _ = Describe("Repository", func() { Describe("SaveItems", func() { Describe("SaveItem invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) users := []User{ { UUID: "uuid1", @@ -103,11 +112,12 @@ var _ = Describe("Repository", func() { UUID: "uuid2", }, } - err := repository.SaveItems(key, users) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + err := repository.SaveItemsWithContext(context.Background(), key, users) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) users := []User{ { UUID: "uuid1", @@ -116,12 +126,13 @@ var _ = Describe("Repository", func() { UUID: "uuid2", }, } - err := repository.SaveItems(key, users) + err := repository.SaveItemsWithContext(context.Background(), key, users) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) users := []User{ { UUID: "uuid1", @@ -130,14 +141,14 @@ var _ = Describe("Repository", func() { UUID: "uuid2", }, } - err := repository.SaveItems(key, users) + err := repository.SaveItemsWithContext(context.Background(), key, users) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) It("should save items", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("UserName") @@ -163,6 +174,8 @@ var _ = Describe("Repository", func() { dMock.WithInputs(userDBInput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), true) + users := []User{ { UUID: "uuid1", @@ -173,26 +186,27 @@ var _ = Describe("Repository", func() { UserName: "name2", }, } - err := repository.SaveItems(key, users) + err := repository.SaveItemsWithContext(context.Background(), key, users) Expect(err).To(BeNil()) }) It("should fail when not pass slice", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("UserName") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) users := User{ UUID: "uuid1", UserName: "name1", } - err := repository.SaveItems(key, users) - Expect(err).To(BeEqualTo(ErrInvalidSliceType)) + err := repository.SaveItemsWithContext(context.Background(), key, users) + Expect(err).To(Equal(djoemo.ErrInvalidSliceType)) }) It("should return in err in case of db err", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("UserName") @@ -219,6 +233,8 @@ var _ = Describe("Repository", func() { dMock.WithError(err), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) + users := []User{ { UUID: "uuid1", @@ -229,24 +245,28 @@ var _ = Describe("Repository", func() { UserName: "name2", }, } - ret := repository.SaveItems(key, users) - Expect(ret).To(BeEqualTo(err)) + ret := repository.SaveItemsWithContext(context.Background(), key, users) + Expect(ret).To(Equal(err)) }) }) Describe("Optimistic Lock Save", func() { - It("should save an item with optimistic Locking", func() { + djoemoTimeNow := djoemo.Now + BeforeEach(func() { now := time.Date(2019, 1, 1, 12, 15, 0, 0, time.UTC) - - monkey.Patch(time.Now, func() time.Time { - return now - }) - + djoemo.Now = func() djoemo.DjoemoTime { + return djoemo.DjoemoTime{Time: now} + } + }) + AfterEach(func() { + djoemo.Now = djoemoTimeNow + }) + It("should save an item with optimistic Locking", func() { type DjoemoUser struct { - Model + djoemo.Model User } - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -265,6 +285,8 @@ var _ = Describe("Repository", func() { dMock.WithInput(userDBInput), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), true) + user := &DjoemoUser{ User: User{ UUID: "uuid", @@ -281,7 +303,7 @@ var _ = Describe("Repository", func() { Describe("Log", func() { It("should log with extra fields if log is supported", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -304,19 +326,17 @@ var _ = Describe("Repository", func() { UserName: "name1", } - logMock.EXPECT().WithContext(context.TODO()).Return(logMock) - repository.WithLog(logMock) - logMock.EXPECT().WithFields(map[string]interface{}{"TableName": key.TableName()}).Return(logMock) - logMock.EXPECT().Error(err.Error()) - ret := repository.SaveItem(key, user) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), false) + + ret := repository.SaveItemWithContext(context.Background(), key, user) Expect(ret).To(BeEquivalentTo(err)) }) }) Describe("Metrics", func() { Describe("SaveItem", func() { - It("should publish metrics if metric is supported", func() { - key := Key().WithTableName(UserTableName). + It("should record metrics if metric is supported", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -338,55 +358,16 @@ var _ = Describe("Repository", func() { UserName: "name1", } - repository.WithMetrics(metricsMock) - - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameSavedItemsCount, float64(1)).Return(nil) - err := repository.SaveItem(key, user) - Expect(err).To(BeNil()) - }) - - It("should not affect save and log error if publish failed", func() { - key := Key().WithTableName(UserTableName). - WithHashKeyName("UUID"). - WithHashKey("uuid") - - userDBInput := map[string]interface{}{ - "UUID": "uuid", - "UserName": "name1", - "UpdatedAt": "0001-01-01T00:00:00Z", - "CreatedAt": "0001-01-01T00:00:00Z", - } - - dMock.Should(). - Save( - dMock.WithTable(key.TableName()), - dMock.WithInput(userDBInput), - ).Exec() - - user := &User{ - UUID: "uuid", - UserName: "name1", - } - - repository.WithMetrics(metricsMock) - repository.WithLog(logMock) - - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameSavedItemsCount, float64(1)). - Return(errors.New("failed to publish")) - logMock.EXPECT().WithFields(map[string]interface{}{"TableName": key.TableName()}).Return(logMock) + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpCommit, key, gomock.Any(), true) - logMock.EXPECT().WithContext(context.TODO()).Return(logMock) - logMock.EXPECT().Error("failed to publish") - err := repository.SaveItem(key, user) + err := repository.SaveItemWithContext(context.Background(), key, user) Expect(err).To(BeNil()) }) }) Describe("SaveItems", func() { - It("should publish metrics if metric is supported", func() { - key := Key().WithTableName(UserTableName). + It("should record metrics with source label", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("UserName") @@ -423,62 +404,14 @@ var _ = Describe("Repository", func() { }, } - traceInfo := map[string]interface{}{"TraceID": "trace-id", "UUID": "uuid"} - repository.WithMetrics(metricsMock) - metricsMock.EXPECT().WithContext(WithFields(traceInfo)).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameSavedItemsCount, float64(2)).Return(nil) - err := repository.SaveItemsWithContext(WithFields(traceInfo), key, users) + ctx := djoemo.WithSourceLabel(context.Background(), "FooBarAPI") + metricsMock.EXPECT().Record(ctx, djoemo.OpCommit, key, gomock.Any(), true). + Do(func(ctx context.Context, caller string, key djoemo.KeyInterface, duration time.Duration, success bool) { + Expect(djoemo.GetLabelsFromContext(ctx)["source"]).To(Equal("FooBarAPI")) + }) - Expect(err).To(BeNil()) - }) - - It("should not affect save and log error if publish failed", func() { - key := Key().WithTableName(UserTableName). - WithHashKeyName("UUID"). - WithHashKey("uuid"). - WithRangeKeyName("UserName") - - userDBInput := []map[string]interface{}{ - { - "UUID": "uuid1", - "UserName": "name1", - "UpdatedAt": "0001-01-01T00:00:00Z", - "CreatedAt": "0001-01-01T00:00:00Z", - }, - { - "UUID": "uuid2", - "UserName": "name2", - "UpdatedAt": "0001-01-01T00:00:00Z", - "CreatedAt": "0001-01-01T00:00:00Z", - }, - } - - dMock.Should(). - SaveAll( - dMock.WithTable(key.TableName()), - dMock.WithInputs(userDBInput), - ).Exec() - - users := []User{ - { - UUID: "uuid1", - UserName: "name1", - }, - { - UUID: "uuid2", - UserName: "name2", - }, - } + err := repository.SaveItemsWithContext(ctx, key, users) - repository.WithMetrics(metricsMock) - repository.WithLog(logMock) - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameSavedItemsCount, float64(2)). - Return(errors.New("failed to publish")) - logMock.EXPECT().WithFields(map[string]interface{}{"TableName": key.TableName()}).Return(logMock) - logMock.EXPECT().WithContext(context.TODO()).Return(logMock) - logMock.EXPECT().Error("failed to publish") - err := repository.SaveItems(key, users) Expect(err).To(BeNil()) }) }) diff --git a/repository_update_test.go b/repository_update_test.go index 3579957..7d168e3 100644 --- a/repository_update_test.go +++ b/repository_update_test.go @@ -4,9 +4,11 @@ import ( "context" "errors" - . "github.com/adjoeio/djoemo" + "github.com/adjoeio/djoemo" "github.com/adjoeio/djoemo/mock" "github.com/aws/aws-sdk-go/service/dynamodb" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "go.uber.org/mock/gomock" ) @@ -17,7 +19,7 @@ var _ = Describe("Repository", func() { var ( dMock mock.DynamoMock - repository RepositoryInterface + repository djoemo.RepositoryInterface logMock *mock.MockLogInterface metricsMock *mock.MockMetricsInterface ) @@ -28,45 +30,50 @@ var _ = Describe("Repository", func() { logMock = mock.NewMockLogInterface(mockCtrl) metricsMock = mock.NewMockMetricsInterface(mockCtrl) dMock = mock.NewDynamoMock(dAPIMock) - repository = NewRepository(dAPIMock) + repository = djoemo.NewRepository(dAPIMock) + repository.WithMetrics(metricsMock) + repository.WithLog(logMock) }) Describe("Update", func() { Describe("Update Invalid key ", func() { It("should fail with table name is nil", func() { - key := Key().WithHashKeyName("UUID").WithHashKey("uuid") + key := djoemo.Key().WithHashKeyName("UUID").WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), false) updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - err := repository.Update(Set, key, updates) - Expect(err).To(BeEqualTo(ErrInvalidTableName)) + err := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) + Expect(err).To(Equal(djoemo.ErrInvalidTableName)) }) It("should fail with hash key name is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKey("uuid") + key := djoemo.Key().WithTableName(UserTableName).WithHashKey("uuid") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), false) updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - err := repository.Update(Set, key, updates) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyName)) + err := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyName)) }) It("should fail with hash key value is nil", func() { - key := Key().WithTableName(UserTableName).WithHashKeyName("UUID") + key := djoemo.Key().WithTableName(UserTableName).WithHashKeyName("UUID") + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), false) updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - err := repository.Update(Set, key, updates) - Expect(err).To(BeEqualTo(ErrInvalidHashKeyValue)) + err := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) + Expect(err).To(Equal(djoemo.ErrInvalidHashKeyValue)) }) }) It("should Update item with Set", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("email"). @@ -80,17 +87,19 @@ var _ = Describe("Repository", func() { ), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) + updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - err := repository.Update(Set, key, updates) + err := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) Expect(err).To(BeNil()) }) It("should Update item with SetSet", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -102,17 +111,19 @@ var _ = Describe("Repository", func() { ), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) + updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - err := repository.Update(SetSet, key, updates) + err := repository.UpdateWithContext(context.Background(), djoemo.SetSet, key, updates) Expect(err).To(BeNil()) }) It("should Update item with SetIfNotExists", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -124,17 +135,19 @@ var _ = Describe("Repository", func() { ), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) + updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - err := repository.Update(SetIfNotExists, key, updates) + err := repository.UpdateWithContext(context.Background(), djoemo.SetIfNotExists, key, updates) Expect(err).To(BeNil()) }) It("should Update item with SetExpr", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -146,15 +159,17 @@ var _ = Describe("Repository", func() { ), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) + updates := map[string]interface{}{ "Meta.$ = ?": []interface{}{"foo", "bar"}, } - err := repository.Update(SetExpr, key, updates) + err := repository.UpdateWithContext(context.Background(), djoemo.SetExpr, key, updates) Expect(err).To(BeNil()) }) It("should Update item with Add", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -166,15 +181,17 @@ var _ = Describe("Repository", func() { ), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) + updates := map[string]interface{}{ "ElemCount": 1, } - err := repository.Update(Add, key, updates) + err := repository.UpdateWithContext(context.Background(), djoemo.Add, key, updates) Expect(err).To(BeNil()) }) It("should return in err in case of db err", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("email"). @@ -186,19 +203,21 @@ var _ = Describe("Repository", func() { dMock.WithError(err), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), false) + updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - ret := repository.Update(Set, key, updates) - Expect(ret).To(BeEqualTo(err)) + ret := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) + Expect(ret).To(Equal(err)) }) }) Describe("UpdateItem with condition", func() { It("should save an item if the condition is met", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -212,16 +231,18 @@ var _ = Describe("Repository", func() { dMock.WithInput(updates), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) + expression := "UserName = ?" expressionArgs := "username" updated, err := repository.ConditionalUpdateWithContext(context.Background(), key, updates, expression, expressionArgs) Expect(err).To(BeNil()) - Expect(updated).To(BeEqualTo(true)) + Expect(updated).To(Equal(true)) }) It("should reject the update of an item if the condition is not met", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -236,18 +257,20 @@ var _ = Describe("Repository", func() { dMock.WithError(errors.New(dynamodb.ErrCodeConditionalCheckFailedException)), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), false) + expression := "UserName = ?" expressionArgs := "user" updated, err := repository.ConditionalUpdateWithContext(context.Background(), key, updates, expression, expressionArgs) Expect(err).To(HaveOccurred()) - Expect(updated).To(BeEqualTo(false)) + Expect(updated).To(Equal(false)) }) }) Describe("Log", func() { It("should log with extra fields if log is supported", func() { - key := Key().WithTableName(UserTableName). + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid"). WithRangeKeyName("email"). @@ -258,24 +281,21 @@ var _ = Describe("Repository", func() { dMock.WithError(err), ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), false) + updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - repository.WithLog(logMock) - logMock.EXPECT().WithFields(map[string]interface{}{"TableName": key.TableName()}).Return(logMock) - logMock.EXPECT().WithContext(context.TODO()).Return(logMock) - logMock.EXPECT().Error(err.Error()) - ret := repository.Update(Set, key, updates) + ret := repository.UpdateWithContext(context.Background(), djoemo.Set, key, updates) Expect(ret).To(BeEquivalentTo(err)) - }) }) Describe("Metrics", func() { - It("should publish metrics if metric is supported", func() { - key := Key().WithTableName(UserTableName). + It("should record metrics if metric is supported", func() { + key := djoemo.Key().WithTableName(UserTableName). WithHashKeyName("UUID"). WithHashKey("uuid") @@ -287,45 +307,14 @@ var _ = Describe("Repository", func() { ), ).Exec() - updates := map[string]interface{}{ - "UserName": "name2", - "TraceID": "name4", - } - - repository.WithMetrics(metricsMock) - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameUpdatedItemsCount, float64(1)).Return(nil) - err := repository.Update(SetSet, key, updates) - Expect(err).To(BeNil()) - }) - - It("should not affect update and log error if publish failed", func() { - key := Key().WithTableName(UserTableName). - WithHashKeyName("UUID"). - WithHashKey("uuid") - - dMock.Should().Update( - dMock.WithTable(key.TableName()), - dMock.WithMatch( - mock.InputExpect(). - FieldEq("UserName", "name2").FieldEq("TraceID", "name4"), - ), - ).Exec() + metricsMock.EXPECT().Record(gomock.Any(), djoemo.OpUpdate, key, gomock.Any(), true) updates := map[string]interface{}{ "UserName": "name2", "TraceID": "name4", } - repository.WithMetrics(metricsMock) - repository.WithLog(logMock) - metricsMock.EXPECT().WithContext(context.TODO()).Return(metricsMock) - metricsMock.EXPECT().Publish(key.TableName(), MetricNameUpdatedItemsCount, float64(1)). - Return(errors.New("failed to publish")) - logMock.EXPECT().WithFields(map[string]interface{}{"TableName": key.TableName()}).Return(logMock) - logMock.EXPECT().WithContext(context.TODO()).Return(logMock) - logMock.EXPECT().Error("failed to publish") - err := repository.Update(SetSet, key, updates) + err := repository.UpdateWithContext(context.Background(), djoemo.SetSet, key, updates) Expect(err).To(BeNil()) }) }) diff --git a/time.go b/time.go index ffc682e..67ac303 100644 --- a/time.go +++ b/time.go @@ -12,7 +12,7 @@ import ( // TimeFormatStandard is a mysql time.Time type with some helper functions const TimeFormatStandard = "2006-01-02T15:04:05.000Z07:00" -//TenYears ... +// TenYears ... const TenYears = time.Duration(time.Hour * 24 * 365 * 10) // RFC3339Milli with millisecond precision @@ -33,6 +33,7 @@ type DjoemoTime struct { } // Date returns the Time corresponding to +// // yyyy-mm-dd hh:mm:ss + nsec nanoseconds func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) DjoemoTime { return DjoemoTime{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} @@ -46,7 +47,6 @@ func (dt *DjoemoTime) UnmarshalJSON(p []byte) error { "", -1, )) - if err != nil { return err } @@ -61,7 +61,7 @@ func (dt DjoemoTime) MarshalJSON() ([]byte, error) { return json.Marshal(dt.Time.Format(TimeFormatStandard)) } -//MarshalDynamoDBAttributeValue ... +// MarshalDynamoDBAttributeValue ... func (dt *DjoemoTime) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error { unix := int64(0) if dt != nil { @@ -95,7 +95,6 @@ func (dt *DjoemoTime) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValu } n, err := strconv.ParseInt(*av.N, 10, 64) - if err != nil { return err } @@ -113,8 +112,7 @@ func (dt *DjoemoTime) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValu } // Now returns the current local time. -func Now() DjoemoTime { - +var Now = func() DjoemoTime { t := time.Now() // solves docker issue: // https://forum.golangbridge.org/t/nanosecond-timestamp-precision-not-playing-well-in-containers/6663/4