Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GODRIVER-2586 Add log messages to CMAP spec #1165

Merged
merged 111 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 102 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
2c4a27f
GODRIVER-2570 initialize the logger package
prestonvasquez Dec 6, 2022
8d67dca
GODRIVER-2570 initialize command logging logic
prestonvasquez Dec 8, 2022
024ed89
GODRIVER-2570 unified spec test first draft
prestonvasquez Dec 14, 2022
20d3638
GODRIVER-2570 initialize unified spec test runner
prestonvasquez Dec 15, 2022
a281a74
GODRIVER-2570 remove panic
prestonvasquez Dec 15, 2022
9069935
GODRIVER-2570 first draft of logging verification proc
prestonvasquez Dec 19, 2022
f6c4903
GODRIVER-2570 clean up verification process
prestonvasquez Dec 21, 2022
e8e2171
GODRIVER-2570 clean up verification logic
prestonvasquez Dec 29, 2022
7e0f49a
GODRIVER-2570 more verification cleanup
prestonvasquez Dec 29, 2022
337b954
GODRIVER-2570 convert is to verify
prestonvasquez Dec 30, 2022
f5f3357
GODRIVER-2570 add waitForEvent to test runner
prestonvasquez Jan 4, 2023
e21f787
GODRIVER-2570 fix heartbeat tests
prestonvasquez Jan 4, 2023
e435f32
GODRIVER-2570 add rw locks to counter
prestonvasquez Jan 5, 2023
62998de
GODRIVER-2570 add redaction logic
prestonvasquez Jan 5, 2023
a4c5c20
GODRIVER-2570 complete adding CLAM spec tests
prestonvasquez Jan 5, 2023
3669a03
GODRIVER-2570 add logic to the standard sink
prestonvasquez Jan 5, 2023
5d5b937
GODRIVER-2570 finish up first prose test
prestonvasquez Jan 6, 2023
d96a313
GODRIVER-2570 add CLAM prose tests
prestonvasquez Jan 9, 2023
cbfc03a
GODRIVER-2570 decouple log test helpers
prestonvasquez Jan 9, 2023
7adf661
GODRIVER-2570 add logging examples
prestonvasquez Jan 10, 2023
e6018d1
GODRIVER-2570 clean up components
prestonvasquez Jan 11, 2023
f6c8117
GODRIVER-2570 remove 2
prestonvasquez Jan 11, 2023
78f11cb
GODRIVER-2570 cleanup comments and unecessary logic
prestonvasquez Jan 11, 2023
297686d
GODRIVER-2570 remove empty tests
prestonvasquez Jan 11, 2023
436fa9b
GODRIVER-2570 move duration to the info struct
prestonvasquez Jan 11, 2023
2840b5a
GODRIVER-2570 resolve merge conflict
prestonvasquez Jan 11, 2023
f5932fd
GODRIVER-2570 chore up prose tests
prestonvasquez Jan 12, 2023
8a09cd9
GODRIVER-2570 fix linting errors
prestonvasquez Jan 12, 2023
9d2b7ed
GODRIVER-2570 put a hold on prose test #3
prestonvasquez Jan 12, 2023
419c525
GODRIVER-2570 remove empty tests
prestonvasquez Jan 12, 2023
88e2668
GODRIVER-2570 fix < 4.0 failures
prestonvasquez Jan 12, 2023
9d5bf42
GODRIVER-2570 defer port parsing to logger
prestonvasquez Jan 12, 2023
c0a6684
GODRIVER-2570 add serverID to LB
prestonvasquez Jan 12, 2023
31ae8a2
GODRIVER-2570 add copyright and fix requests
prestonvasquez Jan 12, 2023
6c426f2
GODRIVER-2570 clean up logger examples
prestonvasquez Jan 12, 2023
2987e8e
GODRIVER-2570 PR revisions
prestonvasquez Jan 13, 2023
ed7b0db
GODRIVER-2570 removve functional logic in logger constructor
prestonvasquez Jan 13, 2023
50fe47f
GODRIVER-2570 reverse changes to err bufferSize
prestonvasquez Jan 13, 2023
d7e9309
GODRIVER-2570 PR revisions
prestonvasquez Jan 13, 2023
7853321
GODRIVER-2570 fix static analysis errors
prestonvasquez Jan 14, 2023
6bbc09e
GODRIVER-2570 synchronize logger print
prestonvasquez Jan 17, 2023
859df67
GODRIVER-2586 initial implementation
prestonvasquez Jan 19, 2023
ccc25e7
GODRIVER-2570 PR revisions
prestonvasquez Jan 19, 2023
5e6d187
GODRIVER-2570 fix typos in logger examples
prestonvasquez Jan 19, 2023
e4d152a
GODRIVER-2586 resolves merge conflicts
prestonvasquez Jan 19, 2023
a0d49c6
GODRIVER-2586 start adding wait queue
prestonvasquez Jan 20, 2023
7d51efa
GODRIVER-2570 fix failng tests; PR revisions
prestonvasquez Jan 20, 2023
dfcfad8
GODRIVER-2570 remove assertion test
prestonvasquez Jan 20, 2023
a2817bc
GODRIVER-2570 add a test for the truncate routine
prestonvasquez Jan 20, 2023
f0aff05
GODRIVER-2570 add third CLAM prose test
prestonvasquez Jan 20, 2023
fc2511e
GODRIVER-2570 clean up error handlingin prose test
prestonvasquez Jan 20, 2023
d80e7ee
GODRIVER-2570 clean up prose tests:
prestonvasquez Jan 20, 2023
16f3d3b
Merge branch 'GODRIVER-2570' into GODRIVER-2586
prestonvasquez Jan 20, 2023
5a82345
GODRIVER-2570 PR revisions
prestonvasquez Jan 23, 2023
c00f4b0
GODRIVER-2570 move Command to the component.go file
prestonvasquez Jan 23, 2023
5338b31
GODRIVER-2570 rename osSink to IOSink
prestonvasquez Jan 24, 2023
5c05825
Merge branch 'GODRIVER-2570' into GODRIVER-2586
prestonvasquez Jan 24, 2023
c2e5667
GODRIVER-2586 start formatting pool created message
prestonvasquez Jan 24, 2023
55d5e3a
GODRIVER-2570 fix logger tests
prestonvasquez Jan 24, 2023
edb7a07
Merge branch 'GODRIVER-2570' into GODRIVER-2586
prestonvasquez Jan 24, 2023
29b5561
GODRIVER-2586 work through pattern
prestonvasquez Jan 24, 2023
9c9c634
GODRIVER-2570 clean up operation logging
prestonvasquez Jan 24, 2023
8853676
GODRIVER-2586 resolve merge conflicts
prestonvasquez Jan 24, 2023
ac61a0f
GODRIVER-2586 refactor logging validator to subset exected for actual
prestonvasquez Jan 26, 2023
a622d7a
GODRIVER-2570 update PR for revision requests
prestonvasquez Jan 26, 2023
5892d06
GODRIVER-2570 update static analysis errors
prestonvasquez Jan 26, 2023
56e48df
GODRIVER-2570 revert loosened test conditions for logger verification
prestonvasquez Jan 27, 2023
b4156b2
Merge branch 'master' into GODRIVER-2570
prestonvasquez Jan 27, 2023
e97407a
GODRIVER-2586 start working on the io log messages
prestonvasquez Jan 27, 2023
7a5ecee
GODRIVER-2570 add licenses to internal logger files
prestonvasquez Jan 27, 2023
40bbb18
GODRIVER-2570 add a custom log sink example
prestonvasquez Jan 27, 2023
2752a6f
GODRIVER-2586 start working on second set of spec tests
prestonvasquez Jan 27, 2023
7e6bab6
GODRIVER-2570 clean up filepath logging
prestonvasquez Jan 27, 2023
532b815
GODRIVER-2586 continue with the async pipeline
prestonvasquez Jan 30, 2023
142a1fc
GODRIVER-2570 fix logger test
prestonvasquez Jan 30, 2023
67920bf
GODRIVER-2586 pipe the channel closures
prestonvasquez Jan 30, 2023
cd215a0
GODRIVER-2570 use pipelines for go channels
prestonvasquez Jan 30, 2023
0285a52
GODRIVER-2586 merge 2570 updates
prestonvasquez Jan 30, 2023
666d87a
GODRIVER-2586 add additional client entities
prestonvasquez Jan 31, 2023
af63e5c
GODRIVER-2586 remove the unecessary connection file
prestonvasquez Jan 31, 2023
5c4c3c5
GODRIVER-2589 remove magic strings
prestonvasquez Jan 31, 2023
511d0c1
GODRIVER-2586 update CMAP logging tests
prestonvasquez Feb 1, 2023
1f2cf92
GODRIVER-2586 resolve static analysis failures
prestonvasquez Feb 1, 2023
4395240
GODRIVER-2586 clean up pool errors
prestonvasquez Feb 2, 2023
4dc7657
add ready mutex to wantConn
prestonvasquez Feb 2, 2023
1df2828
GODRIVER-2570 merge ready guard
prestonvasquez Feb 3, 2023
f2c6824
GODRIVER-2688 Misc Updates to Logging
prestonvasquez Feb 3, 2023
7c7eddd
GODRIVER-2688 remove comment
prestonvasquez Feb 3, 2023
506588e
GODRIVER-2688 no service ID if nil
prestonvasquez Feb 3, 2023
283265d
GODRIVER-2586 merge GODRIVER-2688
prestonvasquez Feb 6, 2023
3c88560
GODRIVER-2586 clean up
prestonvasquez Feb 6, 2023
a2a430c
Merge branch 'master' into GODRIVER-2586
prestonvasquez Feb 6, 2023
ad334bf
GODRIVER-2586 clean up the operations
prestonvasquez Feb 7, 2023
466d36f
GODRIVER-2586 fix linting issues
prestonvasquez Feb 7, 2023
c8074b5
GODRIVER-2586 add CMAP log test back
prestonvasquez Feb 7, 2023
38d308f
GODRIVER-2586 account for unordered logs
prestonvasquez Feb 8, 2023
af58883
GODRIVER-2586 clean up logger verification
prestonvasquez Feb 8, 2023
153cece
GODRIVER-2586 fix linting errors
prestonvasquez Feb 8, 2023
2def83d
Delete lb-expansion.yml
prestonvasquez Feb 8, 2023
3589335
GODRIVER-2586 typo fixes
prestonvasquez Feb 9, 2023
c8befe5
Merge branch 'GODRIVER-2586' of github.com:prestonvasquez/mongo-go-dr…
prestonvasquez Feb 9, 2023
2680cae
GODRIVER-2586 resolve PR requests
prestonvasquez Feb 16, 2023
890ffb4
GODRIVER-2586 resolve PR requests
prestonvasquez Feb 17, 2023
9ffc6a7
GODRIVER-2586 add mutex to IOSink enc
prestonvasquez Feb 21, 2023
85a7a65
GODRIVER-2586 add io sink test
prestonvasquez Feb 23, 2023
5e7cbff
GODRIVER-2586 clean up tests
prestonvasquez Feb 24, 2023
e84767f
GODRIVER-2586 remove custom example
prestonvasquez Feb 24, 2023
586f368
GODRIVER-2586 update options example
prestonvasquez Feb 24, 2023
c6152c4
Update mongo/options/example_test.go
prestonvasquez Feb 24, 2023
2d6c406
GODRIVER-2586 add connection pool ready message
prestonvasquez Feb 25, 2023
4773954
Merge branch 'GODRIVER-2586' of github.com:prestonvasquez/mongo-go-dr…
prestonvasquez Feb 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/_logger/custom/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

//go:build logrus
//go:build customlog

package main

Expand Down Expand Up @@ -46,6 +46,7 @@ func main() {
SetLoggerOptions(loggerOptions)

client, err := mongo.Connect(context.TODO(), clientOptions)

if err != nil {
log.Fatalf("error connecting to MongoDB: %v", err)
}
Expand Down
100 changes: 86 additions & 14 deletions internal/logger/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,59 @@ import (
)

const (
CommandFailed = "Command failed"
CommandStarted = "Command started"
CommandSucceeded = "Command succeeded"
CommandFailed = "Command failed"
CommandStarted = "Command started"
CommandSucceeded = "Command succeeded"
ConnectionPoolCreated = "Connection pool created"
ConnectionPoolReady = "Connection pool ready"
ConnectionPoolCleared = "Connection pool cleared"
ConnectionPoolClosed = "Connection pool closed"
ConnectionCreated = "Connection created"
ConnectionReady = "Connection ready"
ConnectionClosed = "Connection closed"
ConnectionCheckoutStarted = "Connection checkout started"
ConnectionCheckoutFailed = "Connection checkout failed"
ConnectionCheckedOut = "Connection checked out"
ConnectionCheckedIn = "Connection checked in"
)

const (
KeyCommand = "command"
KeyCommandName = "commandName"
KeyDatabaseName = "databaseName"
KeyDriverConnectionID = "driverConnectionId"
KeyDurationMS = "durationMS"
KeyError = "error"
KeyFailure = "failure"
KeyMaxConnecting = "maxConnecting"
KeyMaxIdleTimeMS = "maxIdleTimeMS"
KeyMaxPoolSize = "maxPoolSize"
KeyMessage = "message"
KeyMinPoolSize = "minPoolSize"
KeyOperationID = "operationId"
KeyReason = "reason"
KeyReply = "reply"
KeyRequestID = "requestId"
KeyServerConnectionID = "serverConnectionId"
KeyServerHost = "serverHost"
KeyServerPort = "serverPort"
KeyServiceID = "serviceId"
)

type KeyValues []interface{}

func (kvs *KeyValues) Add(key string, value interface{}) {
*kvs = append(*kvs, key, value)
}

const (
ReasonConnClosedStale = "Connection became stale because the pool was cleared"
ReasonConnClosedIdle = "Connection has been available but unused for longer than the configured max idle time"
ReasonConnClosedError = "An error occurred while using the connection"
ReasonConnClosedPoolClosed = "Connection pool was closed"
ReasonConnCheckoutFailedTimout = "Wait queue timeout elapsed without a connection becoming available"
ReasonConnCheckoutFailedError = "An error occurred while trying to establish a new connection"
ReasonConnCheckoutFailedPoolClosed = "Connection pool was closed"
)

// Component is an enumeration representing the "components" which can be
Expand Down Expand Up @@ -88,30 +138,52 @@ type Command struct {
func SerializeCommand(cmd Command, extraKeysAndValues ...interface{}) []interface{} {
// Initialize the boilerplate keys and values.
keysAndValues := append([]interface{}{
"commandName", cmd.Name,
"driverConnectionId", cmd.DriverConnectionID,
"message", cmd.Message,
"operationId", cmd.OperationID,
"requestId", cmd.RequestID,
"serverHost", cmd.ServerHost,
KeyCommandName, cmd.Name,
KeyDriverConnectionID, cmd.DriverConnectionID,
KeyMessage, cmd.Message,
KeyOperationID, cmd.OperationID,
KeyRequestID, cmd.RequestID,
KeyServerHost, cmd.ServerHost,
}, extraKeysAndValues...)

// Add the optional keys and values.
port, err := strconv.ParseInt(cmd.ServerPort, 0, 32)
if err == nil {
keysAndValues = append(keysAndValues, "serverPort", port)
keysAndValues = append(keysAndValues, KeyServerPort, port)
}

// Add the "serverConnectionId" if it is not nil.
if cmd.ServerConnectionID != nil {
keysAndValues = append(keysAndValues,
"serverConnectionId", *cmd.ServerConnectionID)
keysAndValues = append(keysAndValues, KeyServerConnectionID, *cmd.ServerConnectionID)
}

// Add the "serviceId" if it is not nil.
if cmd.ServiceID != nil {
keysAndValues = append(keysAndValues,
"serviceId", cmd.ServiceID.Hex())
keysAndValues = append(keysAndValues, KeyServiceID, cmd.ServiceID.Hex())
}

return keysAndValues
}

// Connection contains data that all connection log messages MUST contain.
type Connection struct {
Message string // Message associated with the connection
ServerHost string // Hostname or IP address for the server
ServerPort string // Port for the server
}

// SerializeConnection serializes a ConnectionMessage into a slice of keys
// and values that can be passed to a logger.
func SerializeConnection(conn Connection, extraKeysAndValues ...interface{}) []interface{} {
keysAndValues := append([]interface{}{
KeyMessage, conn.Message,
KeyServerHost, conn.ServerHost,
}, extraKeysAndValues...)

// Convert the ServerPort into an integer.
port, err := strconv.ParseInt(conn.ServerPort, 0, 32)
if err == nil {
keysAndValues = append(keysAndValues, KeyServerPort, port)
}

return keysAndValues
Expand Down
69 changes: 13 additions & 56 deletions internal/logger/io_sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package logger
import (
"io"
"log"

"go.mongodb.org/mongo-driver/bson"
)

// IOSink writes to an io.Writer using the standard library logging solution and
Expand All @@ -27,71 +29,26 @@ func NewIOSink(out io.Writer) *IOSink {
}
}

func logCommandMessageStarted(log *log.Logger, kvMap map[string]interface{}) {
format := "Command %q started on database %q using a connection with " +
"server-generated ID %d to %s:%d. The requestID is %d and " +
"the operation ID is %d. Command: %s"

log.Printf(format,
kvMap["commandName"],
kvMap["databaseName"],
kvMap["serverConnectionId"],
kvMap["serverHost"],
kvMap["serverPort"],
kvMap["requestId"],
kvMap["operationId"],
kvMap["command"])

}

func logCommandMessageSucceeded(log *log.Logger, kvMap map[string]interface{}) {
format := "Command %q succeeded in %d ms using server-generated ID " +
"%d to %s:%d. The requestID is %d and the operation ID is " +
"%d. Command reply: %s"

log.Printf(format,
kvMap["commandName"],
kvMap["duration"],
kvMap["serverConnectionId"],
kvMap["serverHost"],
kvMap["serverPort"],
kvMap["requestId"],
kvMap["operationId"],
kvMap["reply"])
}

func logCommandMessageFailed(log *log.Logger, kvMap map[string]interface{}) {
format := "Command %q failed in %d ms using a connection with " +
"server-generated ID %d to %s:%d. The requestID is %d and " +
"the operation ID is %d. Error: %s"

log.Printf(format,
kvMap["commandName"],
kvMap["duration"],
kvMap["serverConnectionID"],
kvMap["serverHost"],
kvMap["serverPort"],
kvMap["requestId"],
kvMap["operationId"],
kvMap["failure"])
}

// Info will write the provided message and key-value pairs to the io.Writer
// as extended JSON.
func (osSink *IOSink) Info(_ int, msg string, keysAndValues ...interface{}) {
kvMap := make(map[string]interface{})
kvMap[KeyMessage] = msg

for i := 0; i < len(keysAndValues); i += 2 {
kvMap[keysAndValues[i].(string)] = keysAndValues[i+1]
}

switch msg {
case CommandStarted:
logCommandMessageStarted(osSink.log, kvMap)
case CommandSucceeded:
logCommandMessageSucceeded(osSink.log, kvMap)
case CommandFailed:
logCommandMessageFailed(osSink.log, kvMap)
kvBytes, err := bson.MarshalExtJSON(kvMap, false, false)
if err != nil {
panic(err)
}

osSink.log.Println(string(kvBytes))
}

// Error will write the provided error and key-value pairs to the io.Writer
// as extended JSON.
func (osSink *IOSink) Error(err error, msg string, kv ...interface{}) {
osSink.Info(0, msg, kv...)
}
28 changes: 28 additions & 0 deletions mongo/integration/unified/client_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var securitySensitiveCommands = []string{"authenticate", "saslStart", "saslConti
// execution.
type clientEntity struct {
*mongo.Client
disconnected bool

recordEvents atomic.Value
started []*event.CommandStartedEvent
Expand Down Expand Up @@ -199,6 +200,25 @@ func getURIForClient(opts *entityOptions) string {
}
}

// Disconnect disconnects the client associated with this entity. It is an
// idempotent operation, unlike the mongo client's Disconnect method. This
// property will help avoid unnecessary errors when calling Disconnect on a
// client that has already been disconnected, such as the case when the test
// runner is required to run the closure as part of an operation.
func (c *clientEntity) Disconnect(ctx context.Context) error {
if c.disconnected {
return nil
}

if err := c.Client.Disconnect(ctx); err != nil {
return err
}

c.disconnected = true

return nil
}

func (c *clientEntity) stopListeningForEvents() {
c.setRecordEvents(false)
}
Expand Down Expand Up @@ -458,8 +478,14 @@ func setClientOptionsFromURIOptions(clientOpts *options.ClientOptions, uriOpts b
clientOpts.SetHeartbeatInterval(time.Duration(value.(int32)) * time.Millisecond)
case "loadBalanced":
clientOpts.SetLoadBalanced(value.(bool))
case "maxIdleTimeMS":
clientOpts.SetMaxConnIdleTime(time.Duration(value.(int32)) * time.Millisecond)
case "minPoolSize":
clientOpts.SetMinPoolSize(uint64(value.(int32)))
case "maxPoolSize":
clientOpts.SetMaxPoolSize(uint64(value.(int32)))
case "maxConnecting":
clientOpts.SetMaxConnecting(uint64(value.(int32)))
case "readConcernLevel":
clientOpts.SetReadConcern(readconcern.New(readconcern.Level(value.(string))))
case "retryReads":
Expand All @@ -473,6 +499,8 @@ func setClientOptionsFromURIOptions(clientOpts *options.ClientOptions, uriOpts b
wcSet = true
case "waitQueueTimeoutMS":
return newSkipTestError("the waitQueueTimeoutMS client option is not supported")
case "waitQueueSize":
return newSkipTestError("the waitQueueSize client option is not supported")
case "timeoutMS":
clientOpts.SetTimeout(time.Duration(value.(int32)) * time.Millisecond)
default:
Expand Down
11 changes: 0 additions & 11 deletions mongo/integration/unified/cursor_operation_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,6 @@ import (
"go.mongodb.org/mongo-driver/bson"
)

func executeClose(ctx context.Context, operation *operation) error {
cursor, err := entities(ctx).cursor(operation.Object)
if err != nil {
return err
}

// Per the spec, we ignore all errors from Close.
_ = cursor.Close(ctx)
return nil
}

func executeIterateOnce(ctx context.Context, operation *operation) (*operationResult, error) {
cursor, err := entities(ctx).cursor(operation.Object)
if err != nil {
Expand Down
13 changes: 10 additions & 3 deletions mongo/integration/unified/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ import (
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)

// ErrEntityMapOpen is returned when a slice entity is accessed while the EntityMap is open
var ErrEntityMapOpen = errors.New("slices cannot be accessed while EntityMap is open")
var (
// ErrEntityMapOpen is returned when a slice entity is accessed while the EntityMap is open
ErrEntityMapOpen = errors.New("slices cannot be accessed while EntityMap is open")

// ErrEntityNotFound is returned when an entity is not found in an
// EntityMap hash.
ErrEntityNotFound = errors.New("entity not found")
)

var (
tlsCAFile = os.Getenv("CSFLE_TLS_CA_FILE")
Expand Down Expand Up @@ -403,6 +409,7 @@ func (em *EntityMap) close(ctx context.Context) []error {
// Client will be closed in clientEncryption.Close()
continue
}

if err := client.Disconnect(ctx); err != nil {
errs = append(errs, fmt.Errorf("error closing client with ID %q: %v", id, err))
}
Expand Down Expand Up @@ -714,5 +721,5 @@ func (em *EntityMap) verifyEntityDoesNotExist(id string) error {
}

func newEntityNotFoundError(entityType, entityID string) error {
return fmt.Errorf("no %s entity found with ID %q", entityType, entityID)
return fmt.Errorf("%w for type %q and ID %q", ErrEntityNotFound, entityType, entityID)
}
Loading