Skip to content

feat(sdk/go): add baseline hand-written Go SDK for v3 meters#4644

Draft
gergely-kurucz-konghq wants to merge 16 commits into
mainfrom
feat/go-sdk-baseline
Draft

feat(sdk/go): add baseline hand-written Go SDK for v3 meters#4644
gergely-kurucz-konghq wants to merge 16 commits into
mainfrom
feat/go-sdk-baseline

Conversation

@gergely-kurucz-konghq

@gergely-kurucz-konghq gergely-kurucz-konghq commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

A hand-written, idiomatic Go SDK for the OpenMeter v3 API, added as a standalone module (github.com/openmeterio/openmeter/sdk/go/openmeter). It implements the meter endpoints — Get, List, Query, QueryCSV (+ streaming QueryCSVStream) — as a worked reference of the target SDK shape.

Why

The existing generated client (api/client/go) is a 1.7MB oapi-codegen monolith on the v1 cloud spec. We want an idiomatic, dependency-light SDK whose shape a TypeSpec emitter can later reproduce. This PR establishes that gold-standard shape and verifies it against real servers before automating it.

How

  • stdlib-only public surfaceClient exposes only net/http, context, typed request/response structs, and a typed APIError. No third-party type leaks; the emitter can mirror it verbatim.
  • Resource-grouped opsclient.Meters.Get/List/Query/QueryCSV/QueryCSVStream.
  • Query-string serialization — the three OpenAPI styles on list-meters: deepObject page, form sort, nested deepObject filter.
  • Transport is the single injection seam — default is an internal go-retryablehttp client (hidden behind *http.Client); WithHTTPClient swaps it wholesale. Retries are idempotent-only (no duplicate writes on 5xx), with a 30s per-attempt timeout.
  • Memory-safe reads — buffered paths capped at 10 MiB; QueryCSVStream returns an io.ReadCloser for large exports.
  • Robustness — empty-ID guard (ErrEmptyID) fails fast; shared resourcePath(base, id) centralizes ID validation + escaping for future entities.
  • Models cherry-picked from oapi-codegen output, then hand-curated (DateTimetime.Time, Numericstring, reference-type fields un-pointered).

Verification — unit tests via httptest (retry policy, query encoding, body cap, streaming, errors, auth, injection) + an env-gated TestLive. Verified end-to-end against a local server (unauthenticated), dev.openmeter.cloud (bearer token), and Kong Konnect Dev (kpat PAT) — including a non-empty Query (value 322 on the API Requests meter). README documents base URLs (note Konnect's /v3 vs cloud's /api/v3 prefix), HTTP-client/retry injection, and how third-party libs (go-retryablehttp, imroc/req, Heimdall, Resty, Sling) do or don't compose.

Note: standalone module, not in any go.work; main go build ./... doesn't cover it. CI wiring is a follow-up.

Summary by CodeRabbit

  • New Features

    • Added a Go SDK client with configurable base URL, token, user agent, and custom HTTP transport.
    • Added meter management and usage query support, including JSON, CSV, and streaming CSV responses.
    • Added helper types for pagination, filtering, query requests, and pointer-friendly value creation.
  • Bug Fixes

    • Improved client-side validation for empty resource IDs and invalid base URLs.
    • Added clearer API error handling for non-success responses.
  • Documentation

    • Added README and usage examples for the Go SDK.

Hand-written, idiomatic Go SDK as a reference shape for a future TypeSpec
emitter. Standalone module (github.com/openmeterio/openmeter/sdk/go).

- net/http public surface; go-retryablehttp hidden behind *http.Client
- resource-grouped ops: Meters.Get / List / Query / QueryCSV
- query-string serialization (deepObject page/filter, form sort)
- typed APIError; bearer auth set at request-build time (survives client injection)
- unit tests via httptest + env-gated TestLive (verified vs local and dev cloud)
@gergely-kurucz-konghq gergely-kurucz-konghq added kind/feature New feature or request release-note/ignore Ignore this change when generating release notes labels Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a brand-new, hand-written Go SDK package (sdk/go/openmeter) for the OpenMeter v3 API. It includes a client with functional options, retryable HTTP transport, meters resource methods (Get, List, Query, QueryCSV, QueryCSVStream), typed models, error handling, query-string serialization, README docs, and unit/example/live tests.

Changes

Go SDK for OpenMeter v3

Layer / File(s) Summary
Client construction and options
sdk/go/openmeter/client.go, sdk/go/openmeter/option.go, sdk/go/openmeter/ptr.go, sdk/go/openmeter/errors.go
Adds Client struct, New constructor with base URL validation/resolution, resourcePath helper, Option functions (WithToken, WithHTTPClient, WithUserAgent), pointer helpers (String, Int, Bool, Time), and ErrEmptyID/APIError error types.
Transport and request execution
sdk/go/openmeter/transport.go, sdk/go/openmeter/transport_test.go
Implements a retryable default HTTP client, idempotent-only retry policy, newRequest, doJSON/doRaw/doStream, and capped body reading, with matching unit tests.
Models and query serialization
sdk/go/openmeter/models.go, sdk/go/openmeter/query.go, sdk/go/openmeter/query_test.go
Defines Meter, pagination, and query request/response types plus deepObject query-string serialization for filters and list params, verified by table-driven tests.
Meters resource and tests
sdk/go/openmeter/meters.go, sdk/go/openmeter/client_test.go
Implements MetersService methods (Get/List/Query/QueryCSV/QueryCSVStream) with ID validation, plus extensive tests covering requests, decoding, auth headers, error mapping, and transport injection.
Docs, example, and live tests
sdk/go/openmeter/README.md, sdk/go/openmeter/example_test.go, sdk/go/openmeter/live_test.go
Adds SDK usage documentation, a runnable Example, and an environment-gated TestLive integration test against a real server.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant MetersService
  participant Client
  participant HTTPClient as http.Client
  participant Server

  Caller->>Client: New(baseURL, opts...)
  Client-->>Caller: *Client (with Meters)
  Caller->>MetersService: Query(ctx, meterID, request)
  MetersService->>MetersService: validate meterID (ErrEmptyID)
  MetersService->>Client: newRequest(POST, /query, body)
  Client->>HTTPClient: doRaw(request)
  HTTPClient->>Server: POST /openmeter/meters/{id}/query
  Server-->>HTTPClient: JSON/CSV response or error
  alt non-2xx
    HTTPClient-->>Client: newAPIError(response)
    Client-->>MetersService: *APIError
  else 2xx
    HTTPClient-->>Client: response bytes
    Client-->>MetersService: MeterQueryResult
  end
  MetersService-->>Caller: result or error
Loading

Possibly related PRs

  • openmeterio/openmeter#4170: Adds the server-side filter[key]/filter[name] deepObject query parameters for ListMeters that this SDK's client-side query serialization targets.

Suggested labels: release-note/feature

Suggested reviewers: tothandras, chrisgacsal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: a baseline hand-written Go SDK for OpenMeter v3 meters.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/go-sdk-baseline

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a new standalone Go SDK (github.com/openmeterio/openmeter/sdk/go/openmeter) for the OpenMeter v3 API, hand-written as a reference shape for a future TypeSpec emitter. It implements meter Get, List, Query, QueryCSV, and QueryCSVStream with idiomatic Go conventions: standard-library public surface only, resource-grouped operations, typed APIError, and bearer-token auth set at request-build time.

  • Transport layer (transport.go): uses hashicorp/go-retryablehttp hidden behind the *http.Client seam, with a retryIdempotentOnly CheckRetry policy that restricts automatic server-response retries to GET/HEAD, and caps buffered reads at 10 MiB; QueryCSVStream returns an unbuffered io.ReadCloser for large exports.
  • Query-string serialization (query.go, meters.go): correctly handles three OpenAPI parameter styles on the list endpoint — deepObject for page, form (joined comma-separated) for sort, and nested deepObject for filter.
  • Testing (client_test.go, transport_test.go, query_test.go): comprehensive httptest-based unit tests covering all operations, body capping, auth, error decoding, the empty-ID guard, and the retry policy matrix.

Confidence Score: 5/5

Safe to merge — this is a net-new standalone module that modifies no existing files.

All new code is isolated in a standalone module not included in the main go build ./.... The implementation is well-tested with httptest-based unit tests covering every public method, the empty-ID guard, body capping, auth, error decoding, and the idempotent-only retry policy.

No files require special attention. The two observations are in transport.go (a latent edge case in the retry policy guard) and client_test.go (a missing QueryCSVStream entry in the empty-ID test).

Important Files Changed

Filename Overview
sdk/go/openmeter/transport.go HTTP request builder, retrying transport (idempotent-only retry policy), body capping, and stream support. One edge case: retryIdempotentOnly falls through to DefaultRetryPolicy when resp != nil && resp.Request == nil, potentially retrying non-idempotent requests.
sdk/go/openmeter/meters.go Get/List/Query/QueryCSV/QueryCSVStream operations with proper empty-ID guard and query-string serialization — well-structured.
sdk/go/openmeter/client.go Client constructor with URL validation, option application, and resourcePath helper with empty-ID guard — clean and correct.
sdk/go/openmeter/models.go Response/request model types using plain maps (not pointer-to-map) and time.Time — idiomatic.
sdk/go/openmeter/client_test.go Comprehensive unit tests covering Get, List, Query, QueryCSV, QueryCSVStream, body capping, auth, error decoding, empty-ID guard, and transport injection via httptest — good coverage, missing QueryCSVStream in empty-ID test.
sdk/go/openmeter/query.go deepObject and form-style query-string helpers for list-meters parameter serialization — correct and well-commented.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant Client
    participant MetersService
    participant transport.go
    participant retryablehttp
    participant Server

    Caller->>Client: New(baseURL, WithToken(...))
    Client-->>Caller: "*Client{Meters: *MetersService}"

    Caller->>MetersService: Meters.List(ctx, MeterListParams)
    MetersService->>transport.go: newRequest(ctx, GET, /openmeter/meters, query, nil, application/json)
    Note over transport.go: Bearer token + User-Agent attached at build time
    transport.go-->>MetersService: "*http.Request"

    MetersService->>transport.go: "doJSON(req, &out)"
    transport.go->>retryablehttp: httpClient.Do(req)
    Note over retryablehttp: retryIdempotentOnly CheckRetry
    retryablehttp->>Server: "HTTP GET /openmeter/meters?page[size]=..."
    Server-->>retryablehttp: 200 OK JSON
    retryablehttp-->>transport.go: "*http.Response"
    transport.go->>transport.go: readAllCapped(body, 10 MiB)
    transport.go-->>MetersService: []byte
    MetersService-->>Caller: "*MeterPagePaginatedResponse, nil"

    Caller->>MetersService: Meters.QueryCSVStream(ctx, id, req)
    MetersService->>transport.go: newRequest(ctx, POST, /meters/id/query, nil, body, text/csv)
    transport.go->>retryablehttp: doStream → httpClient.Do(req)
    retryablehttp->>Server: HTTP POST /openmeter/meters/id/query (Accept: text/csv)
    Server-->>retryablehttp: 200 OK CSV stream
    retryablehttp-->>transport.go: "*http.Response (unbuffered)"
    transport.go-->>MetersService: "*http.Response"
    MetersService-->>Caller: io.ReadCloser (caller must Close)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant Client
    participant MetersService
    participant transport.go
    participant retryablehttp
    participant Server

    Caller->>Client: New(baseURL, WithToken(...))
    Client-->>Caller: "*Client{Meters: *MetersService}"

    Caller->>MetersService: Meters.List(ctx, MeterListParams)
    MetersService->>transport.go: newRequest(ctx, GET, /openmeter/meters, query, nil, application/json)
    Note over transport.go: Bearer token + User-Agent attached at build time
    transport.go-->>MetersService: "*http.Request"

    MetersService->>transport.go: "doJSON(req, &out)"
    transport.go->>retryablehttp: httpClient.Do(req)
    Note over retryablehttp: retryIdempotentOnly CheckRetry
    retryablehttp->>Server: "HTTP GET /openmeter/meters?page[size]=..."
    Server-->>retryablehttp: 200 OK JSON
    retryablehttp-->>transport.go: "*http.Response"
    transport.go->>transport.go: readAllCapped(body, 10 MiB)
    transport.go-->>MetersService: []byte
    MetersService-->>Caller: "*MeterPagePaginatedResponse, nil"

    Caller->>MetersService: Meters.QueryCSVStream(ctx, id, req)
    MetersService->>transport.go: newRequest(ctx, POST, /meters/id/query, nil, body, text/csv)
    transport.go->>retryablehttp: doStream → httpClient.Do(req)
    retryablehttp->>Server: HTTP POST /openmeter/meters/id/query (Accept: text/csv)
    Server-->>retryablehttp: 200 OK CSV stream
    retryablehttp-->>transport.go: "*http.Response (unbuffered)"
    transport.go-->>MetersService: "*http.Response"
    MetersService-->>Caller: io.ReadCloser (caller must Close)
Loading

Reviews (4): Last reviewed commit: "test(sdk/go): add transport_test for ret..." | Re-trigger Greptile

Assert filter[key][eq] round-trips through a real server: exact-match
returns the meter, a bogus key returns none.
Comment thread sdk/go/openmeter/meters.go Outdated
Comment thread sdk/go/models.go Outdated
Comment thread sdk/go/openmeter/models.go
Comment thread sdk/go/go.mod Outdated
Comment thread sdk/go/openmeter/transport.go
Override retryablehttp CheckRetry so a non-idempotent request that gets a
response (e.g. 5xx on POST) is not retried, avoiding duplicate side effects;
transport errors with no response fall back to the default policy. Adds a
table test over method x status/err.

Document the single WithHTTPClient injection seam in the README, including
custom retry policies and how third-party libraries (go-retryablehttp,
imroc/req, Heimdall, Resty, Sling) do or do not compose at the transport layer.
Get/Query/QueryCSV now return ErrEmptyMeterID up front instead of building
/openmeter/meters/ (trailing slash), which could decode a list envelope into
a zero-value Meter or yield a generic 4xx. Exported sentinel for errors.Is;
covered by TestMeters_EmptyMeterID.
Maps and slices are already reference types: nil is the idiomatic "not set"
and omitempty omits it, so *map/*slice only forced double-dereference on
callers. Scalar optionals keep pointers to distinguish unset from zero.
Move the package to sdk/go/openmeter so the import base matches the package
name (github.com/openmeterio/openmeter/sdk/go/openmeter), letting callers use
openmeter.New without a goimports-inserted alias. Module root stays sdk/go.
@gergely-kurucz-konghq

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
sdk/go/openmeter/transport.go (2)

21-32: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No default request timeout; fully relies on caller context.

defaultHTTPClient doesn't set an overall timeout on the resulting *http.Client (nor does cleanhttp.DefaultPooledClient, used internally by retryablehttp.NewClient(), set one). Combined with up to 3 retries and up to 5s backoff each, a caller that forgets to pass a context deadline could block for a long time. Consider documenting this expectation prominently (e.g. in the README) or applying a sane default via context-wrapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/transport.go` around lines 21 - 32, The default HTTP client
created by defaultHTTPClient currently has no overall timeout and can retry for
a long time if callers do not set a context deadline. Update the SDK behavior by
either adding a sane default timeout around the retryablehttp client in
defaultHTTPClient/retryablehttp.NewClient usage, or make the no-timeout
expectation explicit in the SDK docs/README and any public client setup
guidance. Keep the change scoped to the default transport path in transport.go
and ensure the behavior is clearly documented where the SDK client is
introduced.

110-127: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Unbounded io.ReadAll on response bodies.

doRaw buffers the entire response body into memory with no size limit. This is shared by both JSON responses and (per the AI summary) QueryCSV, where usage exports could be large. Consider a size cap (io.LimitReader) or a streaming (io.ReadCloser) return path for the CSV case to avoid unbounded memory growth on large exports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/transport.go` around lines 110 - 127, The Client.doRaw
method currently reads the entire HTTP response body into memory without any
cap, which can cause unbounded growth for large payloads. Update doRaw to
enforce a maximum body size using a limited reader, or introduce a streaming
path for large exports such as QueryCSV so CSV responses can be consumed without
buffering everything. Keep the existing error handling in doRaw and adjust any
callers that rely on the raw byte slice to use the new streaming-safe path where
appropriate.
sdk/go/openmeter/live_test.go (1)

34-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a bounded context for the live calls.

ctx := context.Background() gets reused for every real network call in this test (List, Get, Query, QueryCSV). If the target server hangs, this test just blocks instead of failing fast with a clear message — a context.WithTimeout would make failures quicker and more diagnosable when running against a flaky/slow live endpoint.

💡 Suggested tweak
-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/live_test.go` around lines 34 - 104, The live test currently
reuses a background context for all network calls, so List, Get, Query, and
QueryCSV can hang indefinitely against a slow or stuck server. Update the test
to create a bounded context with timeout before the first client.Meters call,
use that context for the existing List/Get/Query/QueryCSV calls, and ensure it
is canceled when the test ends. Use the existing client.Meters methods and the
local ctx variable in live_test.go to keep the change localized.
sdk/go/openmeter/query_test.go (1)

5-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fill in the untested filter operators.

The table nicely covers Eq, Contains, and Exists, but addStringFilter also serializes Neq, Gt, Gte, Lt, and Lte — none of those have a test case, so a regression there would slip through silently.

As per path instructions, "Make sure the tests are comprehensive and cover the changes."

✅ Suggested additional case
 		{
 			name:   "filter nested deepObject",
 			params: MeterListParams{Filter: &MeterFilter{Key: &StringFilter{Eq: String("tokens")}}},
 			want:   "filter%5Bkey%5D%5Beq%5D=tokens",
 		},
+		{
+			name:   "filter range operators",
+			params: MeterListParams{Filter: &MeterFilter{Key: &StringFilter{Gte: String("a"), Lte: String("z")}}},
+			want:   "filter%5Bkey%5D%5Bgte%5D=a&filter%5Bkey%5D%5Blte%5D=z",
+		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/query_test.go` around lines 5 - 50, The
MeterListParams.values test table in TestMeterListParams_Values is missing
coverage for string filter operators handled by addStringFilter. Add cases for
Neq, Gt, Gte, Lt, and Lte on MeterFilter/StringFilter so their query encoding is
asserted alongside the existing Eq, Contains, and Exists coverage. Keep the new
cases in the same table-driven style and verify the encoded query string matches
the expected deepObject form.

Source: Path instructions

sdk/go/openmeter/meters.go (1)

88-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the meter-ID guard + path building.

Get, Query, and QueryCSV all repeat the same empty-ID check and metersBasePath+"/"+url.PathEscape(meterID) construction. Pulling this into one helper keeps future additions (e.g. a Delete) from forgetting the guard.

As per path instructions, "make readability and maintainability a priority, even potentially suggest restructuring the code to improve them."

♻️ Proposed helper extraction
+func meterPath(meterID string) (string, error) {
+	if meterID == "" {
+		return "", ErrEmptyMeterID
+	}
+	return metersBasePath + "/" + url.PathEscape(meterID), nil
+}
+
 func (s *MetersService) Get(ctx context.Context, meterID string) (*Meter, error) {
-	if meterID == "" {
-		return nil, ErrEmptyMeterID
-	}
-	req, err := s.client.newRequest(ctx, http.MethodGet, metersBasePath+"/"+url.PathEscape(meterID), nil, nil, contentTypeJSON)
+	path, err := meterPath(meterID)
+	if err != nil {
+		return nil, err
+	}
+	req, err := s.client.newRequest(ctx, http.MethodGet, path, nil, nil, contentTypeJSON)
 	if err != nil {
 		return nil, err
 	}

(similarly for Query/QueryCSV)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/meters.go` around lines 88 - 148, The meter ID validation
and request path construction are duplicated in MetersService.Get,
MetersService.Query, and MetersService.QueryCSV. Extract that shared logic into
a small helper on MetersService (or a private package helper) that returns the
escaped meter path or an error for empty IDs, then have Get, Query, QueryCSV,
and any future meter actions reuse it to keep the guard and path formatting
consistent.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sdk/go/openmeter/live_test.go`:
- Around line 34-104: The live test currently reuses a background context for
all network calls, so List, Get, Query, and QueryCSV can hang indefinitely
against a slow or stuck server. Update the test to create a bounded context with
timeout before the first client.Meters call, use that context for the existing
List/Get/Query/QueryCSV calls, and ensure it is canceled when the test ends. Use
the existing client.Meters methods and the local ctx variable in live_test.go to
keep the change localized.

In `@sdk/go/openmeter/meters.go`:
- Around line 88-148: The meter ID validation and request path construction are
duplicated in MetersService.Get, MetersService.Query, and
MetersService.QueryCSV. Extract that shared logic into a small helper on
MetersService (or a private package helper) that returns the escaped meter path
or an error for empty IDs, then have Get, Query, QueryCSV, and any future meter
actions reuse it to keep the guard and path formatting consistent.

In `@sdk/go/openmeter/query_test.go`:
- Around line 5-50: The MeterListParams.values test table in
TestMeterListParams_Values is missing coverage for string filter operators
handled by addStringFilter. Add cases for Neq, Gt, Gte, Lt, and Lte on
MeterFilter/StringFilter so their query encoding is asserted alongside the
existing Eq, Contains, and Exists coverage. Keep the new cases in the same
table-driven style and verify the encoded query string matches the expected
deepObject form.

In `@sdk/go/openmeter/transport.go`:
- Around line 21-32: The default HTTP client created by defaultHTTPClient
currently has no overall timeout and can retry for a long time if callers do not
set a context deadline. Update the SDK behavior by either adding a sane default
timeout around the retryablehttp client in
defaultHTTPClient/retryablehttp.NewClient usage, or make the no-timeout
expectation explicit in the SDK docs/README and any public client setup
guidance. Keep the change scoped to the default transport path in transport.go
and ensure the behavior is clearly documented where the SDK client is
introduced.
- Around line 110-127: The Client.doRaw method currently reads the entire HTTP
response body into memory without any cap, which can cause unbounded growth for
large payloads. Update doRaw to enforce a maximum body size using a limited
reader, or introduce a streaming path for large exports such as QueryCSV so CSV
responses can be consumed without buffering everything. Keep the existing error
handling in doRaw and adjust any callers that rely on the raw byte slice to use
the new streaming-safe path where appropriate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc2a4ddc-9e37-4dd1-9ee5-490ee427ebf8

📥 Commits

Reviewing files that changed from the base of the PR and between 5696db2 and a69457e.

⛔ Files ignored due to path filters (2)
  • sdk/go/go.mod is excluded by !**/*.mod
  • sdk/go/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (13)
  • sdk/go/README.md
  • sdk/go/openmeter/client.go
  • sdk/go/openmeter/client_test.go
  • sdk/go/openmeter/errors.go
  • sdk/go/openmeter/example_test.go
  • sdk/go/openmeter/live_test.go
  • sdk/go/openmeter/meters.go
  • sdk/go/openmeter/models.go
  • sdk/go/openmeter/option.go
  • sdk/go/openmeter/ptr.go
  • sdk/go/openmeter/query.go
  • sdk/go/openmeter/query_test.go
  • sdk/go/openmeter/transport.go

Set the module path to github.com/openmeterio/openmeter/sdk/go/openmeter so
its last segment matches the package name. go get / import use the natural
path with no alias and no `go` leaf segment.
The Example and the New doc comment used a bare host, which resolves to the
wrong path. Both now include the required /api/v3 prefix.
Add a base-URL table covering OpenMeter Cloud, local, and Kong Konnect.
Note Konnect's /v3 prefix (vs /api/v3) and that a kpat_ PAT authenticates
through the same WithToken bearer mechanism.
The default transport had no timeout, so a stalled connection could block
indefinitely when no context deadline was set. Bound each attempt to 30s
(per attempt, not per call, so retries still run). Documented at the const
and in the README.
doRaw read the body unbounded via io.ReadAll. Cap buffered paths (JSON,
QueryCSV) at 10 MiB via io.LimitReader (1 MiB for error bodies) and add
doStream + Meters.QueryCSVStream returning an io.ReadCloser for large CSV
exports without buffering. Covered by new tests.
live_test.go used context.Background(), so calls could hang against a slow
server — derive ctx from context.WithTimeout(t.Context(), 30s) with defer
cancel(). Align client_test.go to t.Context() per repo convention.
Add a table case asserting deepObject encoding of Neq/Gt/Gte/Lt/Lte
alongside the existing Eq/Contains/Exists coverage.
meterPath/ErrEmptyMeterID were meter-specific, but the empty-ID guard and
URL escaping repeat identically for every resource. Replace with a resource-
agnostic resourcePath(base, id) in client.go and a single ErrEmptyID sentinel,
so future entities (and emitter-generated resources) reuse one primitive.
@gergely-kurucz-konghq

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
sdk/go/openmeter/client_test.go (1)

226-245: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing empty-ID coverage for QueryCSVStream.

TestMeters_EmptyMeterID covers Get, Query, and QueryCSV but not QueryCSVStream, which shares the same resourcePath/ErrEmptyID guard. Since it's a distinct exported method with its own regression risk, worth adding for completeness.

✅ Suggested addition
 	if _, err := c.Meters.QueryCSV(ctx, "", MeterQueryRequest{}); !errors.Is(err, ErrEmptyID) {
 		t.Errorf("QueryCSV(\"\") error = %v, want ErrEmptyID", err)
 	}
+
+	if _, err := c.Meters.QueryCSVStream(ctx, "", MeterQueryRequest{}); !errors.Is(err, ErrEmptyID) {
+		t.Errorf("QueryCSVStream(\"\") error = %v, want ErrEmptyID", err)
+	}
 }

As per path instructions, **/*_test.go: "Make sure the tests are comprehensive and cover the changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/client_test.go` around lines 226 - 245,
`TestMeters_EmptyMeterID` is missing coverage for the `Meters.QueryCSVStream`
guard, even though it should fail fast with `ErrEmptyID` like `Get`, `Query`,
and `QueryCSV`. Extend this test to call `c.Meters.QueryCSVStream(ctx, "",
MeterQueryRequest{})` and assert `errors.Is(err, ErrEmptyID)`, reusing the
existing `newTestClient` setup so no request is sent for an empty meter ID.

Source: Path instructions

sdk/go/openmeter/transport.go (1)

179-192: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Oversized/failed error-body reads silently produce an empty APIError.

readAllCapped discards the body entirely (nil, err) whenever the read fails or exceeds max. Both doRaw and doStream call this for the non-2xx path and swallow the error (body, _ := readAllCapped(...)), so any error response that's truncated by a slow/broken connection or that exceeds maxErrorBody results in an *APIError with empty Title/Detail/etc. — exactly when you most need diagnostic info (e.g., an HTML error page from a misconfigured proxy).

Consider having readAllCapped return the partial bytes it did manage to read alongside the error, so newAPIError can still be built with whatever was captured up to the cap.

♻️ Suggested fix
 func readAllCapped(r io.Reader, max int64) ([]byte, error) {
 	body, err := io.ReadAll(io.LimitReader(r, max+1))
 	if err != nil {
-		return nil, fmt.Errorf("openmeter: reading response body: %w", err)
+		return body, fmt.Errorf("openmeter: reading response body: %w", err)
 	}
 
 	if int64(len(body)) > max {
-		return nil, fmt.Errorf("openmeter: response body exceeds %d-byte limit; use a streaming method for large payloads", max)
+		return body[:max], fmt.Errorf("openmeter: response body exceeds %d-byte limit; use a streaming method for large payloads", max)
 	}
 
 	return body, nil
 }

Also applies to: 145-148, 168-174

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/transport.go` around lines 179 - 192, The error-body
handling in readAllCapped currently drops all captured bytes whenever io.ReadAll
fails or the payload exceeds the cap, and doRaw/doStream then ignore that error
and build an empty APIError. Update readAllCapped to return any partial body it
managed to read along with the error, and adjust the non-2xx paths in doRaw and
doStream to preserve and pass those bytes into newAPIError instead of discarding
them. Use the existing readAllCapped, doRaw, doStream, and newAPIError symbols
to keep the failure response populated with whatever diagnostic content was
available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/go/openmeter/transport.go`:
- Around line 40-51: The defaultHTTPClient setup in transport.go is applying a
global http.Client timeout that can still expire during QueryCSVStream body
reads. Update defaultHTTPClient (or introduce a separate streaming client used
by QueryCSVStream) so streamed CSV responses do not inherit the overall timeout,
while preserving the existing retry and attempt-level behavior for non-streaming
requests.

---

Nitpick comments:
In `@sdk/go/openmeter/client_test.go`:
- Around line 226-245: `TestMeters_EmptyMeterID` is missing coverage for the
`Meters.QueryCSVStream` guard, even though it should fail fast with `ErrEmptyID`
like `Get`, `Query`, and `QueryCSV`. Extend this test to call
`c.Meters.QueryCSVStream(ctx, "", MeterQueryRequest{})` and assert
`errors.Is(err, ErrEmptyID)`, reusing the existing `newTestClient` setup so no
request is sent for an empty meter ID.

In `@sdk/go/openmeter/transport.go`:
- Around line 179-192: The error-body handling in readAllCapped currently drops
all captured bytes whenever io.ReadAll fails or the payload exceeds the cap, and
doRaw/doStream then ignore that error and build an empty APIError. Update
readAllCapped to return any partial body it managed to read along with the
error, and adjust the non-2xx paths in doRaw and doStream to preserve and pass
those bytes into newAPIError instead of discarding them. Use the existing
readAllCapped, doRaw, doStream, and newAPIError symbols to keep the failure
response populated with whatever diagnostic content was available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 711f4dc0-166f-4b8a-9ca6-b5d7118fabf9

📥 Commits

Reviewing files that changed from the base of the PR and between a69457e and 86b31b0.

⛔ Files ignored due to path filters (2)
  • sdk/go/openmeter/go.mod is excluded by !**/*.mod
  • sdk/go/openmeter/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (11)
  • sdk/go/openmeter/README.md
  • sdk/go/openmeter/client.go
  • sdk/go/openmeter/client_test.go
  • sdk/go/openmeter/errors.go
  • sdk/go/openmeter/example_test.go
  • sdk/go/openmeter/live_test.go
  • sdk/go/openmeter/meters.go
  • sdk/go/openmeter/query.go
  • sdk/go/openmeter/query_test.go
  • sdk/go/openmeter/transport.go
  • sdk/go/openmeter/transport_test.go
✅ Files skipped from review due to trivial changes (2)
  • sdk/go/openmeter/query.go
  • sdk/go/openmeter/README.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • sdk/go/openmeter/example_test.go
  • sdk/go/openmeter/query_test.go
  • sdk/go/openmeter/errors.go
  • sdk/go/openmeter/client.go
  • sdk/go/openmeter/live_test.go

Comment on lines +40 to +51
func defaultHTTPClient() *http.Client {
rc := retryablehttp.NewClient()

rc.RetryMax = 3
rc.RetryWaitMin = 500 * time.Millisecond
rc.RetryWaitMax = 5 * time.Second
rc.Logger = nil // silence the default stdout logger
rc.CheckRetry = retryIdempotentOnly
rc.HTTPClient.Timeout = defaultAttemptTimeout

return rc.StandardClient()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== transport.go ==\n'
sed -n '1,240p' sdk/go/openmeter/transport.go

printf '\n== search timeout/stream symbols ==\n'
rg -n "defaultAttemptTimeout|QueryCSVStream|doStream|WithHTTPClient|HTTPClient.Timeout|retryablehttp.NewClient" sdk/go/openmeter -S

Repository: openmeterio/openmeter

Length of output: 2231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' sdk/go/openmeter/transport.go
printf '\n---\n'
rg -n "defaultAttemptTimeout|QueryCSVStream|doStream|WithHTTPClient|HTTPClient.Timeout|retryablehttp.NewClient" sdk/go/openmeter -S

Repository: openmeterio/openmeter

Length of output: 2231


🌐 Web query:

Go http.Client Timeout includes reading the response body and cancels in-flight body reads documentation

💡 Result:

Yes, the Timeout field in a Go http.Client includes the time spent reading the response body, and it does cancel in-flight body reads [1][2][3]. According to the official Go documentation, the Timeout specifies a time limit for requests made by the client [1][4]. This duration encompasses: - Connection time [1][2] - Any redirects [1][2] - Reading the response body [1][2] The timer associated with the Timeout remains active even after the client's methods (such as Get, Head, Post, or Do) have returned [1][2]. If the timeout duration is reached while the application is still reading the Response.Body, the timer will interrupt the read operation [1][2]. Internally, the client uses a wrapper (often referred to as cancelTimerBody) around the response body that checks if the timeout has been exceeded during read operations [2][3]. If a timeout occurs, the read operation will return an error marked as a network timeout [2][3]. A Timeout value of zero means there is no timeout [1][3].

Citations:


Drop the overall timeout for streamed CSV responses. http.Client.Timeout keeps running while resp.Body is being read, so QueryCSVStream can still hit the 30s limit mid-download and fail large exports after the request has already returned. A separate streaming client or transport-level timeouts would avoid that.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/go/openmeter/transport.go` around lines 40 - 51, The defaultHTTPClient
setup in transport.go is applying a global http.Client timeout that can still
expire during QueryCSVStream body reads. Update defaultHTTPClient (or introduce
a separate streaming client used by QueryCSVStream) so streamed CSV responses do
not inherit the overall timeout, while preserving the existing retry and
attempt-level behavior for non-streaming requests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature New feature or request release-note/ignore Ignore this change when generating release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant