Skip to content
Open
12 changes: 12 additions & 0 deletions admin/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,14 @@ func (c *APIClient) prepareRequest(
return nil, err
}

// Handle request ID from context (OPTIONAL - only if present)
if requestID, ok := ctx.Value("X-Request-ID").(string); ok && requestID != "" {
if headerParams == nil {
headerParams = make(map[string]string)
}
headerParams["X-Request-ID"] = requestID
}

// add header parameters, if any
if len(headerParams) > 0 {
headers := http.Header{}
Expand Down Expand Up @@ -719,3 +727,7 @@ func (u *UntypedClient) CallAPI(request *http.Request) (*http.Response, error) {
func (u *UntypedClient) MakeApiError(res *http.Response, httpMethod, httpPath string) error {
return u.client.makeApiError(res, httpMethod, httpPath)
}

func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, "X-Request-ID", requestID)
}
43 changes: 43 additions & 0 deletions admin/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package admin

import (
"context"
"net/http"
"testing"
)

func TestPrepareRequest_WithRequestID(t *testing.T) {
client := NewAPIClient(NewConfiguration())

// Test with request ID in context
ctx := context.WithValue(context.Background(), "X-Request-ID", "test-request-123")
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the statement

Provide specific request identifiers to MongoDB support for faster resolution

Has there been a request from the support team to add this information? Do we know if they are capable of leveraging these values/find them useful?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair question. I made an assumption that support could leverage these values, but I don't have confirmation from the support team or know the right channel to ask them. Could you help me connect with the right people on the support team to get their opinion?

In the meantime, the primary value I see is actually for client-side debugging and tracing in distributed applications. The main benefits would be:

  • Correlating client-side errors with specific API requests in our own logs
  • Tracing requests across microservices in our architecture
  • Better debugging for complex client applications

Would you be able to help me either:

1- Connect with the support team to understand if they'd find client-provided request IDs valuable, or

2- Should we focus on the client-side benefits for now and consider the support angle separately?

I'm happy to adjust the PR based on whatever feedback we get from support.

req, err := client.prepareRequest(ctx, "/api/test", "GET", nil, nil, nil, nil, nil)

if err != nil {
t.Fatalf("PrepareRequest failed: %v", err)
}

// Check that request ID header was added
requestID := req.Header.Get("X-Request-ID")
if requestID != "test-request-123" {
t.Errorf("Expected X-Request-ID header 'test-request-123', got '%s'", requestID)
}
}

func TestPrepareRequest_WithoutRequestID(t *testing.T) {
client := NewAPIClient(NewConfiguration())

// Test without request ID in context (should work as before)
ctx := context.Background()
req, err := client.prepareRequest(ctx, "/api/test", "GET", nil, nil, nil, nil, nil)

if err != nil {
t.Fatalf("PrepareRequest failed: %v", err)
}

// Check that no request ID header was added
requestID := req.Header.Get("X-Request-ID")
if requestID != "" {
t.Errorf("Expected no X-Request-ID header, got '%s'", requestID)
}
}
11 changes: 11 additions & 0 deletions admin/model_api_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,14 @@ func (o *ApiError) HasReason() bool {
func (o *ApiError) SetReason(v string) {
o.Reason = &v
}

func (a *ApiError) GetContextualError(ctx context.Context) string {
baseError := a.Error()

// Check if context has request ID
if requestID, ok := ctx.Value("X-Request-ID").(string); ok && requestID != "" {
return fmt.Sprintf("%s (RequestID: %s)", baseError, requestID)
}

return baseError
}
Comment on lines +223 to +232
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we want to place this utility logic within ApiError model? Potentially this can be simplified to a utility function separate from ApiError which simply obtains the RequestID from the Go context.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great point! You're absolutely right that putting this logic in the ApiError model creates unnecessary coupling. A utility function approach would be much cleaner and more reusable.

I can simplify this by:

1- Removing the GetContextualError method from ApiError
2- Adding a simple GetRequestID(ctx) utility function
3- Letting users format the error message with request ID in their logging layer

This keeps the models clean and gives users more flexibility in how they format error messages.

Would you prefer I implement it as a simple GetRequestID(ctx) utility function?

36 changes: 36 additions & 0 deletions admin/model_api_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package admin

import (
"context"
"testing"
)

func TestApiError_GetContextualError(t *testing.T) {
// Create a sample API error
detail := "Project not found"
reason := "Resource not found"
apiError := &ApiError{
Detail: &detail,
Error: 404,
ErrorCode: "RESOURCE_NOT_FOUND",
Reason: &reason,
}

// Test with request ID in context
ctx := context.WithValue(context.Background(), "X-Request-ID", "debug-456")
contextualError := apiError.GetContextualError(ctx)

expected := "404 RESOURCE_NOT_FOUND Resource not found: Project not found (RequestID: debug-456)"
if contextualError != expected {
t.Errorf("Expected: %s\nGot: %s", expected, contextualError)
}

// Test without request ID in context
ctx = context.Background()
contextualError = apiError.GetContextualError(ctx)

expected = "404 RESOURCE_NOT_FOUND Resource not found: Project not found"
if contextualError != expected {
t.Errorf("Expected: %s\nGot: %s", expected, contextualError)
}
}
1 change: 1 addition & 0 deletions openapi/atlas-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,7 @@ components:
readOnly: true
type: object
ApiError:
x-is-api-error: true
properties:
badRequestDetail:
$ref: '#/components/schemas/BadRequestDetail'
Expand Down
12 changes: 12 additions & 0 deletions tools/config/go-templates/client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,14 @@ func (c *APIClient) prepareRequest(
return nil, err
}

// Handle request ID from context (OPTIONAL - only if present)
if requestID, ok := ctx.Value("X-Request-ID").(string); ok && requestID != "" {
if headerParams == nil {
headerParams = make(map[string]string)
}
headerParams["X-Request-ID"] = requestID
}

// add header parameters, if any
if len(headerParams) > 0 {
headers := http.Header{}
Expand Down Expand Up @@ -606,3 +614,7 @@ func (u *UntypedClient) CallAPI(request *http.Request) (*http.Response, error) {
func (u *UntypedClient) MakeApiError(res *http.Response, httpMethod, httpPath string) error {
return u.client.makeApiError(res, httpMethod, httpPath)
}

func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, "X-Request-ID", requestID)
}
20 changes: 20 additions & 0 deletions tools/config/go-templates/model_simple.mustache
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import (
"encoding/json"
{{#vendorExtensions.x-is-api-error}}
"context"
"fmt"
{{/vendorExtensions.x-is-api-error}}
{{#imports}}
"{{import}}"
{{/imports}}
)

// {{classname}} {{{description}}}{{^description}}struct for {{{classname}}}{{/description}}
type {{classname}} struct {
Expand Down Expand Up @@ -178,3 +188,13 @@ func (o *{{{classname}}}) UnmarshalJSON(bytes []byte) (err error) {
}

{{/isArray}}
{{#vendorExtensions.x-is-api-error}}
// GetContextualError returns the error message with request ID context if available
func (o *{{classname}}) GetContextualError(ctx context.Context) string {
baseError := o.Error()
if requestID, ok := ctx.Value("X-Request-ID").(string); ok && requestID != "" {
return fmt.Sprintf("%s (RequestID: %s)", baseError, requestID)
}
return baseError
}
{{/vendorExtensions.x-is-api-error}}