From 3e61dab246fbd03544ce54f421bab4ce96bf03ae Mon Sep 17 00:00:00 2001 From: Felix Breuer Date: Mon, 21 Sep 2026 16:28:40 +0200 Subject: [PATCH 1/2] add trace id to error logging output Signed-off-by: Felix Breuer --- go.mod | 1 + go.sum | 2 + pkg/client/helper.go | 37 ++++++++++++++++ pkg/client/helper_test.go | 88 +++++++++++++++++++++++++++++++++++++++ pkg/client/sdk.go | 37 +++++++++++----- pkg/client/sdk_test.go | 87 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 pkg/client/helper_test.go diff --git a/go.mod b/go.mod index cb8d7cee..54013348 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/spf13/cobra v1.10.2 // indirect + github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.26.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect diff --git a/go.sum b/go.sum index 0a0744ab..240575fb 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/stackitcloud/stackit-sdk-go/core v0.27.0 h1:7lc6qStcFDFf8zHP4ORPa9joy github.com/stackitcloud/stackit-sdk-go/core v0.27.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.4 h1:I+XePFdppgW3HRIj4emgWivm+9kWxc9KyEN6Ue4BnUk= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.4/go.mod h1:fxbh3BWSlLye/aGAT/cbp0r6YbHEcnwPoNjt8hp3SqU= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.26.0 h1:TzmBTja200jx6An39UPYBZPvZvLFgk3g1S0XZ1vDeBI= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.26.0/go.mod h1:Y5EM8b9qe/+AKI2LJoFvI0PzkyHO5RiW8nQuQaaNaFA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/pkg/client/helper.go b/pkg/client/helper.go index 4e080ba7..69d79743 100644 --- a/pkg/client/helper.go +++ b/pkg/client/helper.go @@ -1,5 +1,42 @@ package client +import ( + "context" + "fmt" + "net/http" + + "github.com/stackitcloud/stackit-sdk-go/core/runtime" + sdkWait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" +) + +// WrapError wraps the error with an identifier but only if the error is not nil. +func WrapError(err error, name, id string) error { + if err == nil { + return nil + } + if id == "" { + return err + } + return fmt.Errorf("[%s:%s]: %w", name, id, err) +} + +func execute[T any](ctx context.Context, call func(context.Context) (T, error)) (T, error) { + var httpResp *http.Response + ctx = runtime.WithCaptureHTTPResponse(ctx, &httpResp) + + resp, err := call(ctx) + if err != nil { + var zero T + err = WrapError(err, "X-Trace-Id", runtime.GetTraceId(ctx)) + if httpResp != nil { + reqID := httpResp.Header.Get(sdkWait.XRequestIDHeader) + err = WrapError(err, sdkWait.XRequestIDHeader, reqID) + } + return zero, err + } + return resp, nil +} + // convertLabelsToSDK converts map[string]string to *map[string]any for SDK func convertLabelsToSDK(labels map[string]string) map[string]any { if labels == nil { diff --git a/pkg/client/helper_test.go b/pkg/client/helper_test.go new file mode 100644 index 00000000..77787af6 --- /dev/null +++ b/pkg/client/helper_test.go @@ -0,0 +1,88 @@ +package client + +import ( + "context" + "errors" + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" +) + +var _ = Describe("WrapError", func() { + It("wraps the error with the provided identifier", func() { + err := errors.New("test error") + expected := fmt.Errorf("[X-Trace-Id:12345]: %w", err) + Expect(WrapError(err, "X-Trace-Id", "12345")).To(Equal(expected)) + }) + + It("returns the original error when the identifier is empty", func() { + err := errors.New("test error") + Expect(WrapError(err, "trace-id", "")).To(Equal(err)) + }) + + It("returns nil when the error is nil", func() { + Expect(WrapError(nil, "trace-id", "12345")).To(Succeed()) + }) +}) + +var _ = Describe("execute", func() { + It("wraps API errors with trace and request IDs", func() { + _, err := execute(context.Background(), func(ctx context.Context) (int, error) { + response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) + Expect(ok).To(BeTrue()) + *response = &http.Response{Header: http.Header{ + "X-Trace-Id": {"trace-123"}, + "X-Request-Id": {"request-456"}, + }} + return 0, errors.New("api error") + }) + + Expect(err).To(MatchError("[X-Request-Id:request-456]: [X-Trace-Id:trace-123]: api error")) + }) + + It("returns the result and nil error on success", func() { + res, err := execute(context.Background(), func(_ context.Context) (string, error) { + return "success", nil + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("success")) + }) + + It("wraps only trace ID if request ID is missing", func() { + _, err := execute(context.Background(), func(ctx context.Context) (int, error) { + response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) + Expect(ok).To(BeTrue()) + *response = &http.Response{Header: http.Header{ + "X-Trace-Id": {"trace-123"}, + }} + return 0, errors.New("api error") + }) + + Expect(err).To(MatchError("[X-Trace-Id:trace-123]: api error")) + }) + + It("wraps only request ID if trace ID is missing", func() { + _, err := execute(context.Background(), func(ctx context.Context) (int, error) { + response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) + Expect(ok).To(BeTrue()) + *response = &http.Response{Header: http.Header{ + "X-Request-Id": {"request-456"}, + }} + return 0, errors.New("api error") + }) + + Expect(err).To(MatchError("[X-Request-Id:request-456]: api error")) + }) + + It("returns the original error when neither trace ID nor response is present", func() { + _, err := execute(context.Background(), func(_ context.Context) (int, error) { + return 0, errors.New("api error") + }) + + Expect(err).To(MatchError("api error")) + }) +}) diff --git a/pkg/client/sdk.go b/pkg/client/sdk.go index dea59f27..0aa963e7 100644 --- a/pkg/client/sdk.go +++ b/pkg/client/sdk.go @@ -201,9 +201,11 @@ func (c *SdkStackitClient) CreateServer(ctx context.Context, projectID, region s } // Call SDK using the stored client - sdkServer, err := c.iaasClient.DefaultAPI.CreateServer(ctx, projectID, region). - CreateServerPayload(*payload). - Execute() + sdkServer, err := execute(ctx, func(ctx context.Context) (*iaas.Server, error) { + return c.iaasClient.DefaultAPI.CreateServer(ctx, projectID, region). + CreateServerPayload(*payload). + Execute() + }) if err != nil { return nil, fmt.Errorf("SDK CreateServer failed: %w", err) } @@ -216,7 +218,9 @@ func (c *SdkStackitClient) CreateServer(ctx context.Context, projectID, region s // GetServer retrieves a server by ID via STACKIT SDK func (c *SdkStackitClient) GetServer(ctx context.Context, projectID, region, serverID string) (*Server, error) { - sdkServer, err := c.iaasClient.DefaultAPI.GetServer(ctx, projectID, region, serverID).Execute() + sdkServer, err := execute(ctx, func(ctx context.Context) (*iaas.Server, error) { + return c.iaasClient.DefaultAPI.GetServer(ctx, projectID, region, serverID).Execute() + }) if err != nil { // Check if error is 404 Not Found if isNotFoundError(err) { @@ -233,7 +237,9 @@ func (c *SdkStackitClient) GetServer(ctx context.Context, projectID, region, ser // DeleteServer deletes a server by ID via STACKIT SDK func (c *SdkStackitClient) DeleteServer(ctx context.Context, projectID, region, serverID string) error { - err := c.iaasClient.DefaultAPI.DeleteServer(ctx, projectID, region, serverID).Execute() + _, err := execute(ctx, func(ctx context.Context) (any, error) { + return nil, c.iaasClient.DefaultAPI.DeleteServer(ctx, projectID, region, serverID).Execute() + }) if err != nil { // Check if error is 404 Not Found - this is OK (idempotent) if isNotFoundError(err) { @@ -247,8 +253,7 @@ func (c *SdkStackitClient) DeleteServer(ctx context.Context, projectID, region, // ListServers lists all servers in a project via STACKIT SDK func (c *SdkStackitClient) ListServers(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*Server, error) { - serverRequest := c.iaasClient.DefaultAPI.ListServers(ctx, projectID, region) - + var selector string if labelSelector != nil { sb := strings.Builder{} for k, v := range labelSelector { @@ -262,10 +267,16 @@ func (c *SdkStackitClient) ListServers(ctx context.Context, projectID, region st } } - serverRequest = serverRequest.LabelSelector(sb.String()) + selector = sb.String() } - sdkResponse, err := serverRequest.Execute() + sdkResponse, err := execute(ctx, func(ctx context.Context) (*iaas.ServerListResponse, error) { + serverRequest := c.iaasClient.DefaultAPI.ListServers(ctx, projectID, region) + if selector != "" { + serverRequest = serverRequest.LabelSelector(selector) + } + return serverRequest.Execute() + }) if err != nil { return nil, fmt.Errorf("SDK ListServers failed: %w", err) } @@ -283,7 +294,9 @@ func (c *SdkStackitClient) ListServers(ctx context.Context, projectID, region st } func (c *SdkStackitClient) GetNICsForServer(ctx context.Context, projectID, region, serverID string) ([]*NIC, error) { - res, err := c.iaasClient.DefaultAPI.ListServerNICs(ctx, projectID, region, serverID).Execute() + res, err := execute(ctx, func(ctx context.Context) (*iaas.NICListResponse, error) { + return c.iaasClient.DefaultAPI.ListServerNICs(ctx, projectID, region, serverID).Execute() + }) if err != nil { return nil, fmt.Errorf("SDK ListServerNICs failed: %w", err) } @@ -309,7 +322,9 @@ func (c *SdkStackitClient) UpdateNIC(ctx context.Context, projectID, region, net AllowedAddresses: addresses, } - sdkNic, err := c.iaasClient.DefaultAPI.UpdateNic(ctx, projectID, region, networkID, nicID).UpdateNicPayload(payload).Execute() + sdkNic, err := execute(ctx, func(ctx context.Context) (*iaas.NIC, error) { + return c.iaasClient.DefaultAPI.UpdateNic(ctx, projectID, region, networkID, nicID).UpdateNicPayload(payload).Execute() + }) if err != nil { return nil, fmt.Errorf("SDK UpdateNic failed: %w", err) } diff --git a/pkg/client/sdk_test.go b/pkg/client/sdk_test.go index cb74ff30..1ff9c0c4 100644 --- a/pkg/client/sdk_test.go +++ b/pkg/client/sdk_test.go @@ -1,8 +1,11 @@ package client import ( + "context" "errors" "fmt" + "net/http" + "net/http/httptest" "os" . "github.com/onsi/ginkgo/v2" @@ -432,4 +435,88 @@ var _ = Describe("SDK Type Conversion Helpers", func() { }) }) + Describe("SdkStackitClient error wrapping with trace and request IDs", func() { + var ( + server *httptest.Server + client *SdkStackitClient + projectID = "00000000-0000-0000-0000-000000000000" + serverID = "11111111-1111-1111-1111-111111111111" + networkID = "22222222-2222-2222-2222-222222222222" + nicID = "33333333-3333-3333-3333-333333333333" + ) + + BeforeEach(func() { + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("X-Trace-Id", "trace-test-123") + w.Header().Set("X-Request-Id", "req-test-456") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message": "internal server error"}`)) + })) + + origEndpoint := os.Getenv("STACKIT_IAAS_ENDPOINT") + origNoAuth := os.Getenv("STACKIT_NO_AUTH") + os.Setenv("STACKIT_IAAS_ENDPOINT", server.URL) + os.Setenv("STACKIT_NO_AUTH", "true") + DeferCleanup(func() { + server.Close() + if origEndpoint == "" { + os.Unsetenv("STACKIT_IAAS_ENDPOINT") + } else { + os.Setenv("STACKIT_IAAS_ENDPOINT", origEndpoint) + } + if origNoAuth == "" { + os.Unsetenv("STACKIT_NO_AUTH") + } else { + os.Setenv("STACKIT_NO_AUTH", origNoAuth) + } + }) + + var err error + client, err = NewStackitClient("") + Expect(err).NotTo(HaveOccurred()) + }) + + It("GetServer wraps error with trace ID and request ID", func() { + _, err := client.GetServer(context.Background(), projectID, "eu01-1", serverID) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("[X-Request-Id:req-test-456]: [X-Trace-Id:trace-test-123]")) + }) + + It("DeleteServer wraps error with trace ID and request ID", func() { + err := client.DeleteServer(context.Background(), projectID, "eu01-1", serverID) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("[X-Request-Id:req-test-456]: [X-Trace-Id:trace-test-123]")) + }) + + It("CreateServer wraps error with trace ID and request ID", func() { + _, err := client.CreateServer(context.Background(), projectID, "eu01-1", &CreateServerRequest{ + Name: "test-srv", + MachineType: "c1.2", + Networking: &ServerNetworkingRequest{ + NetworkID: networkID, + }, + }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("[X-Request-Id:req-test-456]: [X-Trace-Id:trace-test-123]")) + }) + + It("ListServers wraps error with trace ID and request ID", func() { + _, err := client.ListServers(context.Background(), projectID, "eu01-1", nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("[X-Request-Id:req-test-456]: [X-Trace-Id:trace-test-123]")) + }) + + It("GetNICsForServer wraps error with trace ID and request ID", func() { + _, err := client.GetNICsForServer(context.Background(), projectID, "eu01-1", serverID) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("[X-Request-Id:req-test-456]: [X-Trace-Id:trace-test-123]")) + }) + + It("UpdateNIC wraps error with trace ID and request ID", func() { + _, err := client.UpdateNIC(context.Background(), projectID, "eu01-1", networkID, nicID, []string{"10.0.0.1"}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("[X-Request-Id:req-test-456]: [X-Trace-Id:trace-test-123]")) + }) + }) + }) From bcfbfb40a99434363dfb910b652adfacc6e45b9a Mon Sep 17 00:00:00 2001 From: Felix Breuer Date: Tue, 22 Sep 2026 09:46:04 +0200 Subject: [PATCH 2/2] use constants Signed-off-by: Felix Breuer --- go.mod | 1 - go.sum | 2 -- pkg/client/helper.go | 12 ++++++++---- pkg/client/helper_test.go | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 54013348..cb8d7cee 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,6 @@ require ( github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/spf13/cobra v1.10.2 // indirect - github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.26.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect diff --git a/go.sum b/go.sum index 240575fb..0a0744ab 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,6 @@ github.com/stackitcloud/stackit-sdk-go/core v0.27.0 h1:7lc6qStcFDFf8zHP4ORPa9joy github.com/stackitcloud/stackit-sdk-go/core v0.27.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.4 h1:I+XePFdppgW3HRIj4emgWivm+9kWxc9KyEN6Ue4BnUk= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.4/go.mod h1:fxbh3BWSlLye/aGAT/cbp0r6YbHEcnwPoNjt8hp3SqU= -github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.26.0 h1:TzmBTja200jx6An39UPYBZPvZvLFgk3g1S0XZ1vDeBI= -github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.26.0/go.mod h1:Y5EM8b9qe/+AKI2LJoFvI0PzkyHO5RiW8nQuQaaNaFA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/pkg/client/helper.go b/pkg/client/helper.go index 69d79743..7c47b01b 100644 --- a/pkg/client/helper.go +++ b/pkg/client/helper.go @@ -6,7 +6,11 @@ import ( "net/http" "github.com/stackitcloud/stackit-sdk-go/core/runtime" - sdkWait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" +) + +const ( + XTraceIDHeader = "X-Trace-Id" + XRequestIDHeader = "X-Request-Id" ) // WrapError wraps the error with an identifier but only if the error is not nil. @@ -27,10 +31,10 @@ func execute[T any](ctx context.Context, call func(context.Context) (T, error)) resp, err := call(ctx) if err != nil { var zero T - err = WrapError(err, "X-Trace-Id", runtime.GetTraceId(ctx)) + err = WrapError(err, XTraceIDHeader, runtime.GetTraceId(ctx)) if httpResp != nil { - reqID := httpResp.Header.Get(sdkWait.XRequestIDHeader) - err = WrapError(err, sdkWait.XRequestIDHeader, reqID) + reqID := httpResp.Header.Get(XRequestIDHeader) + err = WrapError(err, XRequestIDHeader, reqID) } return zero, err } diff --git a/pkg/client/helper_test.go b/pkg/client/helper_test.go index 77787af6..46b38663 100644 --- a/pkg/client/helper_test.go +++ b/pkg/client/helper_test.go @@ -15,7 +15,7 @@ var _ = Describe("WrapError", func() { It("wraps the error with the provided identifier", func() { err := errors.New("test error") expected := fmt.Errorf("[X-Trace-Id:12345]: %w", err) - Expect(WrapError(err, "X-Trace-Id", "12345")).To(Equal(expected)) + Expect(WrapError(err, XTraceIDHeader, "12345")).To(Equal(expected)) }) It("returns the original error when the identifier is empty", func() { @@ -34,8 +34,8 @@ var _ = Describe("execute", func() { response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) Expect(ok).To(BeTrue()) *response = &http.Response{Header: http.Header{ - "X-Trace-Id": {"trace-123"}, - "X-Request-Id": {"request-456"}, + XTraceIDHeader: {"trace-123"}, + XRequestIDHeader: {"request-456"}, }} return 0, errors.New("api error") }) @@ -57,7 +57,7 @@ var _ = Describe("execute", func() { response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) Expect(ok).To(BeTrue()) *response = &http.Response{Header: http.Header{ - "X-Trace-Id": {"trace-123"}, + XTraceIDHeader: {"trace-123"}, }} return 0, errors.New("api error") }) @@ -70,7 +70,7 @@ var _ = Describe("execute", func() { response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) Expect(ok).To(BeTrue()) *response = &http.Response{Header: http.Header{ - "X-Request-Id": {"request-456"}, + XRequestIDHeader: {"request-456"}, }} return 0, errors.New("api error") })