From bdaa19610e542246869d1c4759272aa67e2cca52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Tue, 20 Jan 2026 16:40:48 +0100 Subject: [PATCH] feat: report runtime properties Propagate more information about the SDK's runtime including the API version, package version, etc. to get more observability into the SDKs usage. Also removes the generation of the client/client.go file as it doesn't depend on the OpenAPI specs and can be maintained manually. --- codegen/internal/generator/params.go | 19 ++++++++ codegen/internal/generator/resources.go | 24 ++++++++++ codegen/internal/generator/run.go | 8 ++++ src/build.gradle | 2 +- .../java/com/sumup/sdk/core/ApiClient.java | 1 + .../java/com/sumup/sdk/core/SdkMetadata.java | 44 +++++++++++++++++-- .../resources/com/sumup/sdk/api-version.txt | 1 + .../com/sumup/sdk/core/ApiClientTest.java | 21 +++++++++ 8 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 codegen/internal/generator/resources.go create mode 100644 src/main/resources/com/sumup/sdk/api-version.txt diff --git a/codegen/internal/generator/params.go b/codegen/internal/generator/params.go index 12a4c15..23401b5 100644 --- a/codegen/internal/generator/params.go +++ b/codegen/internal/generator/params.go @@ -12,6 +12,7 @@ import ( type Params struct { SpecPath string OutputDir string + ResourceDir string BasePackage string } @@ -34,6 +35,12 @@ func (p *Params) normalize() error { if p.OutputDir, err = filepath.Abs(p.OutputDir); err != nil { return fmt.Errorf("resolve output directory: %w", err) } + if p.ResourceDir == "" { + p.ResourceDir = filepath.Join(filepath.Dir(p.OutputDir), "resources") + } + if p.ResourceDir, err = filepath.Abs(p.ResourceDir); err != nil { + return fmt.Errorf("resolve resource directory: %w", err) + } return nil } @@ -55,6 +62,18 @@ func (p *Params) validate() error { } else if !fi.IsDir() { return fmt.Errorf("output directory %s is not a directory", p.OutputDir) } + fi, err = os.Stat(p.ResourceDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(p.ResourceDir, 0o755); err != nil { + return fmt.Errorf("create resource directory: %w", err) + } + } else { + return fmt.Errorf("resource directory: %w", err) + } + } else if !fi.IsDir() { + return fmt.Errorf("resource directory %s is not a directory", p.ResourceDir) + } return nil } diff --git a/codegen/internal/generator/resources.go b/codegen/internal/generator/resources.go new file mode 100644 index 0000000..4aa9df3 --- /dev/null +++ b/codegen/internal/generator/resources.go @@ -0,0 +1,24 @@ +package generator + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func renderApiVersionResource(apiVersion string, params Params) error { + apiVersion = strings.TrimSpace(apiVersion) + if apiVersion == "" { + return fmt.Errorf("missing api version in spec info") + } + targetDir := filepath.Join(params.ResourceDir, params.basePackagePath()) + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return fmt.Errorf("create api version resource directory: %w", err) + } + target := filepath.Join(targetDir, "api-version.txt") + if err := os.WriteFile(target, []byte(apiVersion), 0o644); err != nil { + return fmt.Errorf("write api version resource: %w", err) + } + return nil +} diff --git a/codegen/internal/generator/run.go b/codegen/internal/generator/run.go index 16445e6..7e66ecf 100644 --- a/codegen/internal/generator/run.go +++ b/codegen/internal/generator/run.go @@ -32,6 +32,11 @@ func Run(ctx context.Context, params Params) error { return err } + apiVersion := "" + if doc.Info != nil { + apiVersion = doc.Info.Version + } + slog.Info("Generating SDK", "spec", params.SpecPath) if err := renderClients(model, params); err != nil { return err @@ -42,6 +47,9 @@ func Run(ctx context.Context, params Params) error { if err := renderSumUpClient(model, params); err != nil { return err } + if err := renderApiVersionResource(apiVersion, params); err != nil { + return err + } return nil } diff --git a/src/build.gradle b/src/build.gradle index 269f448..2965614 100644 --- a/src/build.gradle +++ b/src/build.gradle @@ -21,7 +21,7 @@ sourceSets { def emptyDirs = [] main { java.srcDirs = ['main/java'] - resources.srcDirs = emptyDirs + resources.srcDirs = ['main/resources'] } test { java.srcDirs = ['test/java'] diff --git a/src/main/java/com/sumup/sdk/core/ApiClient.java b/src/main/java/com/sumup/sdk/core/ApiClient.java index 1c33bac..5756cf1 100644 --- a/src/main/java/com/sumup/sdk/core/ApiClient.java +++ b/src/main/java/com/sumup/sdk/core/ApiClient.java @@ -186,6 +186,7 @@ private void applyHeaders( RequestOptions requestOptions) { Map merged = new LinkedHashMap<>(); merged.put("User-Agent", SdkMetadata.userAgent()); + merged.putAll(SdkMetadata.runtimeHeaders()); if (headerParams != null) { headerParams.forEach( (name, value) -> { diff --git a/src/main/java/com/sumup/sdk/core/SdkMetadata.java b/src/main/java/com/sumup/sdk/core/SdkMetadata.java index d4eaa79..3b3141c 100644 --- a/src/main/java/com/sumup/sdk/core/SdkMetadata.java +++ b/src/main/java/com/sumup/sdk/core/SdkMetadata.java @@ -3,20 +3,38 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Map; /** * Provides metadata about the SDK that can be attached to outgoing requests, such as the current * version and the default {@code User-Agent} header value. */ public final class SdkMetadata { + private static final String API_VERSION_RESOURCE = "/com/sumup/sdk/api-version.txt"; private static final String VERSION_RESOURCE = "/com/sumup/sdk/sdk-version.txt"; private static final String USER_AGENT_PREFIX = "sumup-java"; + private static final String LANGUAGE = "java"; - private static final String VERSION = loadVersion(); + private static final String API_VERSION = loadResource(API_VERSION_RESOURCE); + private static final String VERSION = loadResource(VERSION_RESOURCE); private static final String USER_AGENT = USER_AGENT_PREFIX + "/v" + VERSION; + private static final Map RUNTIME_HEADERS = + Map.of( + "X-Sumup-Api-Version", API_VERSION, + "X-Sumup-Lang", LANGUAGE, + "X-Sumup-Package-Version", VERSION, + "X-Sumup-OS", System.getProperty("os.name", "unknown"), + "X-Sumup-Arch", runtimeArch(), + "X-Sumup-Runtime", runtimeIdentifier(), + "X-Sumup-Runtime-Version", Runtime.version().toString()); private SdkMetadata() {} + /** Returns the API version declared by the OpenAPI specification. */ + public static String apiVersion() { + return API_VERSION; + } + /** Returns the SDK version read from the generated version resource. */ public static String version() { return VERSION; @@ -27,8 +45,28 @@ public static String userAgent() { return USER_AGENT; } - private static String loadVersion() { - try (InputStream stream = SdkMetadata.class.getResourceAsStream(VERSION_RESOURCE)) { + /** Returns the runtime headers that should be sent with each request. */ + public static Map runtimeHeaders() { + return RUNTIME_HEADERS; + } + + static String runtimeArch() { + String arch = System.getProperty("os.arch", "unknown").toLowerCase(); + return switch (arch) { + case "amd64", "x86_64" -> "x86_64"; + case "x86", "i386", "i486", "i586", "i686" -> "x86"; + case "aarch64", "arm64" -> "arm64"; + case "arm", "armv7", "armv7l" -> "arm"; + default -> arch; + }; + } + + private static String runtimeIdentifier() { + return LANGUAGE + Runtime.version(); + } + + private static String loadResource(String path) { + try (InputStream stream = SdkMetadata.class.getResourceAsStream(path)) { if (stream == null) { return "unknown"; } diff --git a/src/main/resources/com/sumup/sdk/api-version.txt b/src/main/resources/com/sumup/sdk/api-version.txt new file mode 100644 index 0000000..afaf360 --- /dev/null +++ b/src/main/resources/com/sumup/sdk/api-version.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/src/test/java/com/sumup/sdk/core/ApiClientTest.java b/src/test/java/com/sumup/sdk/core/ApiClientTest.java index b9cb5d4..288e1ee 100644 --- a/src/test/java/com/sumup/sdk/core/ApiClientTest.java +++ b/src/test/java/com/sumup/sdk/core/ApiClientTest.java @@ -74,6 +74,27 @@ void requestOptionsCanOverrideUserAgent() { "custom/agent", httpClient.lastRequest().headers().firstValue("User-Agent").orElse(null)); } + @Test + void defaultRuntimeHeadersAreIncluded() { + CapturingHttpClient httpClient = new CapturingHttpClient(); + ApiClient client = ApiClient.builder().httpClient(httpClient).build(); + + client.send(HttpMethod.GET, "/v1/test", null, null, null, null, null); + + HttpHeaders headers = httpClient.lastRequest().headers(); + assertEquals(SdkMetadata.apiVersion(), headers.firstValue("X-Sumup-Api-Version").orElse(null)); + assertEquals("java", headers.firstValue("X-Sumup-Lang").orElse(null)); + assertEquals(SdkMetadata.version(), headers.firstValue("X-Sumup-Package-Version").orElse(null)); + assertEquals( + System.getProperty("os.name", "unknown"), headers.firstValue("X-Sumup-OS").orElse(null)); + assertEquals( + SdkMetadata.runtimeHeaders().get("X-Sumup-Arch"), + headers.firstValue("X-Sumup-Arch").orElse(null)); + assertEquals("java" + Runtime.version(), headers.firstValue("X-Sumup-Runtime").orElse(null)); + assertEquals( + Runtime.version().toString(), headers.firstValue("X-Sumup-Runtime-Version").orElse(null)); + } + @Test void requestOptionsCanOverrideTimeout() { CapturingHttpClient httpClient = new CapturingHttpClient();