Skip to content

Commit

Permalink
GOCBC-1040: Include error causes when doing serialization
Browse files Browse the repository at this point in the history
Motivation
----------
When we serialize our error types we omit the underlying "inner"
error from the serialized data. We should include this information
in the serialized data.

Changes
-------
Add custom MarshalJSON functions to our error types and convert
the errors to anonymous structs so that we can stringify the
"inner" error before marshalling the error.

Change-Id: I80ccf1dbb2b3abb902d5a1e040211b8122f98b9e
Reviewed-on: http://review.couchbase.org/c/gocb/+/144857
Reviewed-by: Brett Lawson <brett19@gmail.com>
Tested-by: Charles Dixon <chvckd@gmail.com>
  • Loading branch information
chvck committed Feb 11, 2021
1 parent 33094ff commit c59f1d8
Show file tree
Hide file tree
Showing 12 changed files with 527 additions and 10 deletions.
54 changes: 52 additions & 2 deletions error_analytics.go
@@ -1,6 +1,9 @@
package gocb

import gocbcore "github.com/couchbase/gocbcore/v9"
import (
"encoding/json"
gocbcore "github.com/couchbase/gocbcore/v9"
)

// AnalyticsErrorDesc represents a specific error returned from the analytics service.
type AnalyticsErrorDesc struct {
Expand Down Expand Up @@ -31,9 +34,56 @@ type AnalyticsError struct {
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
}

// MarshalJSON implements the Marshaler interface.

func (e AnalyticsError) MarshalJSON() ([]byte, error) {
var innerError string
if e.InnerError != nil {
innerError = e.InnerError.Error()
}
return json.Marshal(struct {
InnerError string `json:"msg,omitempty"`
Statement string `json:"statement,omitempty"`
ClientContextID string `json:"client_context_id,omitempty"`
Errors []AnalyticsErrorDesc `json:"errors,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
RetryReasons []RetryReason `json:"retry_reasons,omitempty"`
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
}{
InnerError: innerError,
Statement: e.Statement,
ClientContextID: e.ClientContextID,
Errors: e.Errors,
Endpoint: e.Endpoint,
RetryReasons: e.RetryReasons,
RetryAttempts: e.RetryAttempts,
})
}

// Error returns the string representation of this error.
func (e AnalyticsError) Error() string {
return e.InnerError.Error() + " | " + serializeWrappedError(e)
errBytes, serErr := json.Marshal(struct {
InnerError error `json:"-"`
Statement string `json:"statement,omitempty"`
ClientContextID string `json:"client_context_id,omitempty"`
Errors []AnalyticsErrorDesc `json:"errors,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
RetryReasons []RetryReason `json:"retry_reasons,omitempty"`
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
}{
InnerError: e.InnerError,
Statement: e.Statement,
ClientContextID: e.ClientContextID,
Errors: e.Errors,
Endpoint: e.Endpoint,
RetryReasons: e.RetryReasons,
RetryAttempts: e.RetryAttempts,
})
if serErr != nil {
logErrorf("failed to serialize error to json: %s", serErr.Error())
}

return e.InnerError.Error() + " | " + string(errBytes)
}

// Unwrap returns the underlying cause for this error.
Expand Down
32 changes: 32 additions & 0 deletions error_analytics_test.go
@@ -0,0 +1,32 @@
package gocb

import (
"encoding/json"
)

func (suite *UnitTestSuite) TestAnalyticsError() {
aErr := AnalyticsError{
InnerError: ErrDatasetNotFound,
Statement: "select * from dataset",
ClientContextID: "12345",
Errors: []AnalyticsErrorDesc{{
Code: 1000,
Message: "error 1000",
}},
Endpoint: "http://127.0.0.1:8095",
RetryReasons: []RetryReason{AnalyticsTemporaryFailureRetryReason},
RetryAttempts: 3,
}

b, err := json.Marshal(aErr)
suite.Require().Nil(err)

suite.Assert().Equal(
[]byte("{\"msg\":\"dataset not found\",\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"Code\":1000,\"Message\":\"error 1000\"}],\"endpoint\":\"http://127.0.0.1:8095\",\"retry_reasons\":[\"ANALYTICS_TEMPORARY_FAILURE\"],\"retry_attempts\":3}"),
b,
)
suite.Assert().Equal(
"dataset not found | {\"statement\":\"select * from dataset\",\"client_context_id\":\"12345\",\"errors\":[{\"Code\":1000,\"Message\":\"error 1000\"}],\"endpoint\":\"http://127.0.0.1:8095\",\"retry_reasons\":[\"ANALYTICS_TEMPORARY_FAILURE\"],\"retry_attempts\":3}",
aErr.Error(),
)
}
41 changes: 40 additions & 1 deletion error_http.go
@@ -1,6 +1,7 @@
package gocb

import (
"encoding/json"
gocbcore "github.com/couchbase/gocbcore/v9"
"github.com/pkg/errors"
)
Expand All @@ -15,9 +16,47 @@ type HTTPError struct {
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
}

// MarshalJSON implements the Marshaler interface.
func (e HTTPError) MarshalJSON() ([]byte, error) {
var innerError string
if e.InnerError != nil {
innerError = e.InnerError.Error()
}
return json.Marshal(struct {
InnerError string `json:"msg,omitempty"`
UniqueID string `json:"unique_id,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
RetryReasons []RetryReason `json:"retry_reasons,omitempty"`
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
}{
InnerError: innerError,
UniqueID: e.UniqueID,
Endpoint: e.Endpoint,
RetryReasons: e.RetryReasons,
RetryAttempts: e.RetryAttempts,
})
}

// Error returns the string representation of this error.
func (e HTTPError) Error() string {
return e.InnerError.Error() + " | " + serializeWrappedError(e)
errBytes, serErr := json.Marshal(struct {
InnerError error `json:"-"`
UniqueID string `json:"unique_id,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
RetryReasons []RetryReason `json:"retry_reasons,omitempty"`
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
}{
InnerError: e.InnerError,
UniqueID: e.UniqueID,
Endpoint: e.Endpoint,
RetryReasons: e.RetryReasons,
RetryAttempts: e.RetryAttempts,
})
if serErr != nil {
logErrorf("failed to serialize error to json: %s", serErr.Error())
}

return e.InnerError.Error() + " | " + string(errBytes)
}

// Unwrap returns the underlying cause for this error.
Expand Down
30 changes: 30 additions & 0 deletions error_http_test.go
@@ -0,0 +1,30 @@
package gocb

import (
"encoding/json"
"errors"
"fmt"
)

func (suite *UnitTestSuite) TestHTTPError() {
aErr := HTTPError{
InnerError: errors.New("uh oh"),
Endpoint: "http://127.0.0.1:8091",
UniqueID: "123445",
RetryReasons: nil,
RetryAttempts: 0,
}

b, err := json.Marshal(aErr)
suite.Require().Nil(err)

fmt.Println(string(b))
suite.Assert().Equal(
[]byte("{\"msg\":\"uh oh\",\"unique_id\":\"123445\",\"endpoint\":\"http://127.0.0.1:8091\"}"),
b,
)
suite.Assert().Equal(
"uh oh | {\"unique_id\":\"123445\",\"endpoint\":\"http://127.0.0.1:8091\"}",
aErr.Error(),
)
}
93 changes: 91 additions & 2 deletions error_keyvalue.go
@@ -1,6 +1,9 @@
package gocb

import "github.com/couchbase/gocbcore/v9/memd"
import (
"encoding/json"
"github.com/couchbase/gocbcore/v9/memd"
)

// KeyValueError wraps key-value errors that occur within the SDK.
// UNCOMMITTED: This API may change in the future.
Expand All @@ -24,9 +27,95 @@ type KeyValueError struct {
LastConnectionID string `json:"last_connection_id,omitempty"`
}

// MarshalJSON implements the Marshaler interface.
func (e KeyValueError) MarshalJSON() ([]byte, error) {
var innerError string
if e.InnerError != nil {
innerError = e.InnerError.Error()
}
return json.Marshal(struct {
InnerError string `json:"msg,omitempty"`
StatusCode memd.StatusCode `json:"status_code,omitempty"`
DocumentID string `json:"document_id,omitempty"`
BucketName string `json:"bucket,omitempty"`
ScopeName string `json:"scope,omitempty"`
CollectionName string `json:"collection,omitempty"`
CollectionID uint32 `json:"collection_id,omitempty"`
ErrorName string `json:"error_name,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
Opaque uint32 `json:"opaque,omitempty"`
Context string `json:"context,omitempty"`
Ref string `json:"ref,omitempty"`
RetryReasons []RetryReason `json:"retry_reasons,omitempty"`
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
LastDispatchedTo string `json:"last_dispatched_to,omitempty"`
LastDispatchedFrom string `json:"last_dispatched_from,omitempty"`
LastConnectionID string `json:"last_connection_id,omitempty"`
}{
InnerError: innerError,
StatusCode: e.StatusCode,
DocumentID: e.DocumentID,
BucketName: e.BucketName,
ScopeName: e.ScopeName,
CollectionName: e.CollectionName,
CollectionID: e.CollectionID,
ErrorName: e.ErrorName,
ErrorDescription: e.ErrorDescription,
Opaque: e.Opaque,
Context: e.Context,
Ref: e.Ref,
RetryReasons: e.RetryReasons,
RetryAttempts: e.RetryAttempts,
LastDispatchedTo: e.LastDispatchedTo,
LastDispatchedFrom: e.LastDispatchedFrom,
LastConnectionID: e.LastConnectionID,
})
}

// Error returns the string representation of a kv error.
func (e KeyValueError) Error() string {
return e.InnerError.Error() + " | " + serializeWrappedError(e)
errBytes, serErr := json.Marshal(struct {
InnerError error `json:"-"`
StatusCode memd.StatusCode `json:"status_code,omitempty"`
DocumentID string `json:"document_id,omitempty"`
BucketName string `json:"bucket,omitempty"`
ScopeName string `json:"scope,omitempty"`
CollectionName string `json:"collection,omitempty"`
CollectionID uint32 `json:"collection_id,omitempty"`
ErrorName string `json:"error_name,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
Opaque uint32 `json:"opaque,omitempty"`
Context string `json:"context,omitempty"`
Ref string `json:"ref,omitempty"`
RetryReasons []RetryReason `json:"retry_reasons,omitempty"`
RetryAttempts uint32 `json:"retry_attempts,omitempty"`
LastDispatchedTo string `json:"last_dispatched_to,omitempty"`
LastDispatchedFrom string `json:"last_dispatched_from,omitempty"`
LastConnectionID string `json:"last_connection_id,omitempty"`
}{
InnerError: e.InnerError,
StatusCode: e.StatusCode,
DocumentID: e.DocumentID,
BucketName: e.BucketName,
ScopeName: e.ScopeName,
CollectionName: e.CollectionName,
CollectionID: e.CollectionID,
ErrorName: e.ErrorName,
ErrorDescription: e.ErrorDescription,
Opaque: e.Opaque,
Context: e.Context,
Ref: e.Ref,
RetryReasons: e.RetryReasons,
RetryAttempts: e.RetryAttempts,
LastDispatchedTo: e.LastDispatchedTo,
LastDispatchedFrom: e.LastDispatchedFrom,
LastConnectionID: e.LastConnectionID,
})
if serErr != nil {
logErrorf("failed to serialize error to json: %s", serErr.Error())
}

return e.InnerError.Error() + " | " + string(errBytes)
}

// Unwrap returns the underlying reason for the error
Expand Down
38 changes: 38 additions & 0 deletions error_keyvalue_test.go
@@ -0,0 +1,38 @@
package gocb

import (
"encoding/json"
"github.com/couchbase/gocbcore/v9/memd"
)

func (suite *UnitTestSuite) TestKeyValueError() {
aErr := KeyValueError{
InnerError: ErrPathNotFound,
StatusCode: memd.StatusBusy,
DocumentID: "key",
BucketName: "bucket",
ScopeName: "scope",
CollectionName: "collection",
CollectionID: 9,
ErrorName: "barry",
ErrorDescription: "sheen",
Opaque: 0xa1,
RetryReasons: []RetryReason{CircuitBreakerOpenRetryReason},
RetryAttempts: 3,
LastDispatchedTo: "10.112.210.101",
LastDispatchedFrom: "10.112.210.1",
LastConnectionID: "123456",
}

b, err := json.Marshal(aErr)
suite.Require().Nil(err)

suite.Assert().Equal(
[]byte("{\"msg\":\"path not found\",\"status_code\":133,\"document_id\":\"key\",\"bucket\":\"bucket\",\"scope\":\"scope\",\"collection\":\"collection\",\"collection_id\":9,\"error_name\":\"barry\",\"error_description\":\"sheen\",\"opaque\":161,\"retry_reasons\":[\"CIRCUIT_BREAKER_OPEN\"],\"retry_attempts\":3,\"last_dispatched_to\":\"10.112.210.101\",\"last_dispatched_from\":\"10.112.210.1\",\"last_connection_id\":\"123456\"}"),
b,
)
suite.Assert().Equal(
"path not found | {\"status_code\":133,\"document_id\":\"key\",\"bucket\":\"bucket\",\"scope\":\"scope\",\"collection\":\"collection\",\"collection_id\":9,\"error_name\":\"barry\",\"error_description\":\"sheen\",\"opaque\":161,\"retry_reasons\":[\"CIRCUIT_BREAKER_OPEN\"],\"retry_attempts\":3,\"last_dispatched_to\":\"10.112.210.101\",\"last_dispatched_from\":\"10.112.210.1\",\"last_connection_id\":\"123456\"}",
aErr.Error(),
)
}

0 comments on commit c59f1d8

Please sign in to comment.