From 6b3cb14981900cfa7b84e707447d1c76c0c89ae7 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:13:39 -0400 Subject: [PATCH 1/7] fix(logs): match LogEvent body field after server-side message rename volcano-hosting renamed LogEvent's content field from message (string) to body (oneOf string/object/array/number/boolean), but the vendored OpenAPI spec and generated client were never updated. Build/publish log streaming rendered blank lines for every event since event.Message no longer existed in the API response. Regenerate the client from the corrected spec and render the union body type for both function log commands. VOL-636 --- internal/apiclient/client.gen.go | 344 ++++++++++++++++++++++++++++-- internal/output/functions.go | 35 ++- internal/output/functions_test.go | 51 +++++ openapi/openapi.yaml | 25 +-- 4 files changed, 423 insertions(+), 32 deletions(-) create mode 100644 internal/output/functions_test.go diff --git a/internal/apiclient/client.gen.go b/internal/apiclient/client.gen.go index 3ee194c..e96a6ef 100644 --- a/internal/apiclient/client.gen.go +++ b/internal/apiclient/client.gen.go @@ -3430,6 +3430,9 @@ type LogDeploymentRequestSelector struct { // LogEvent Normalized historical log event returned by paginated log APIs. type LogEvent struct { + // Body Application log value. JSON arguments retain their JSON type. Strings containing a serialized JSON object or array are normalized to that object or array; all other strings remain strings. + Body *LogEvent_Body `json:"body"` + // Deployment Deployment context associated with a historical deployment log event. Deployment *LogDeployment `json:"deployment,omitempty"` @@ -3442,15 +3445,6 @@ type LogEvent struct { // Level Canonical lowercase function runtime log level. Level *LiveLogLevel `json:"level,omitempty"` - // Message Display log message. - Message string `json:"message"` - - // Metadata Parsed JSON fields and other indexed metadata, when available. - Metadata *map[string]interface{} `json:"metadata,omitempty"` - - // RawMessage Original log line/message after platform sanitization. - RawMessage *string `json:"raw_message,omitempty"` - // Region Region where this log event originated. Region *string `json:"region,omitempty"` @@ -3461,6 +3455,26 @@ type LogEvent struct { Timestamp time.Time `json:"timestamp"` } +// LogEventBody0 defines model for . +type LogEventBody0 = string + +// LogEventBody1 defines model for . +type LogEventBody1 map[string]interface{} + +// LogEventBody2 defines model for . +type LogEventBody2 = []interface{} + +// LogEventBody3 defines model for . +type LogEventBody3 = float64 + +// LogEventBody4 defines model for . +type LogEventBody4 = bool + +// LogEvent_Body Application log value. JSON arguments retain their JSON type. Strings containing a serialized JSON object or array are normalized to that object or array; all other strings remain strings. +type LogEvent_Body struct { + union json.RawMessage +} + // LogFrontendRequestResource Frontend log resource selector. type LogFrontendRequestResource struct { // Deployments Deployment log selector for deployable resources. @@ -3513,6 +3527,9 @@ type LogResourceType string // LogSearchEvent defines model for LogSearchEvent. type LogSearchEvent struct { + // Body Application log value. JSON arguments retain their JSON type. Strings containing a serialized JSON object or array are normalized to that object or array; all other strings remain strings. + Body *LogSearchEvent_Body `json:"body"` + // Deployment Deployment context associated with a historical deployment log event. Deployment *LogDeployment `json:"deployment,omitempty"` @@ -3525,15 +3542,6 @@ type LogSearchEvent struct { // Level Canonical lowercase function runtime log level. Level *LiveLogLevel `json:"level,omitempty"` - // Message Display log message. - Message string `json:"message"` - - // Metadata Parsed JSON fields and other indexed metadata, when available. - Metadata *map[string]interface{} `json:"metadata,omitempty"` - - // RawMessage Original log line/message after platform sanitization. - RawMessage *string `json:"raw_message,omitempty"` - // Region Region where this log event originated. Region *string `json:"region,omitempty"` @@ -3544,6 +3552,26 @@ type LogSearchEvent struct { Timestamp time.Time `json:"timestamp"` } +// LogSearchEventBody0 defines model for . +type LogSearchEventBody0 = string + +// LogSearchEventBody1 defines model for . +type LogSearchEventBody1 map[string]interface{} + +// LogSearchEventBody2 defines model for . +type LogSearchEventBody2 = []interface{} + +// LogSearchEventBody3 defines model for . +type LogSearchEventBody3 = float64 + +// LogSearchEventBody4 defines model for . +type LogSearchEventBody4 = bool + +// LogSearchEvent_Body Application log value. JSON arguments retain their JSON type. Strings containing a serialized JSON object or array are normalized to that object or array; all other strings remain strings. +type LogSearchEvent_Body struct { + union json.RawMessage +} + // LogSearchRequest Search request for project logs. type LogSearchRequest struct { // Cursor Opaque pagination cursor from the previous response's `next_cursor`. @@ -6108,6 +6136,146 @@ type UploadStorageObjectMultipartRequestBody UploadStorageObjectMultipartBody // UpdateStorageObjectVisibilityJSONRequestBody defines body for UpdateStorageObjectVisibility for application/json ContentType. type UpdateStorageObjectVisibilityJSONRequestBody = StorageVisibilityRequest +// AsLogEventBody0 returns the union data inside the LogEvent_Body as a LogEventBody0 +func (t LogEvent_Body) AsLogEventBody0() (LogEventBody0, error) { + var body LogEventBody0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogEventBody0 overwrites any union data inside the LogEvent_Body as the provided LogEventBody0 +func (t *LogEvent_Body) FromLogEventBody0(v LogEventBody0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogEventBody0 performs a merge with any union data inside the LogEvent_Body, using the provided LogEventBody0 +func (t *LogEvent_Body) MergeLogEventBody0(v LogEventBody0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogEventBody1 returns the union data inside the LogEvent_Body as a LogEventBody1 +func (t LogEvent_Body) AsLogEventBody1() (LogEventBody1, error) { + var body LogEventBody1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogEventBody1 overwrites any union data inside the LogEvent_Body as the provided LogEventBody1 +func (t *LogEvent_Body) FromLogEventBody1(v LogEventBody1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogEventBody1 performs a merge with any union data inside the LogEvent_Body, using the provided LogEventBody1 +func (t *LogEvent_Body) MergeLogEventBody1(v LogEventBody1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogEventBody2 returns the union data inside the LogEvent_Body as a LogEventBody2 +func (t LogEvent_Body) AsLogEventBody2() (LogEventBody2, error) { + var body LogEventBody2 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogEventBody2 overwrites any union data inside the LogEvent_Body as the provided LogEventBody2 +func (t *LogEvent_Body) FromLogEventBody2(v LogEventBody2) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogEventBody2 performs a merge with any union data inside the LogEvent_Body, using the provided LogEventBody2 +func (t *LogEvent_Body) MergeLogEventBody2(v LogEventBody2) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogEventBody3 returns the union data inside the LogEvent_Body as a LogEventBody3 +func (t LogEvent_Body) AsLogEventBody3() (LogEventBody3, error) { + var body LogEventBody3 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogEventBody3 overwrites any union data inside the LogEvent_Body as the provided LogEventBody3 +func (t *LogEvent_Body) FromLogEventBody3(v LogEventBody3) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogEventBody3 performs a merge with any union data inside the LogEvent_Body, using the provided LogEventBody3 +func (t *LogEvent_Body) MergeLogEventBody3(v LogEventBody3) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogEventBody4 returns the union data inside the LogEvent_Body as a LogEventBody4 +func (t LogEvent_Body) AsLogEventBody4() (LogEventBody4, error) { + var body LogEventBody4 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogEventBody4 overwrites any union data inside the LogEvent_Body as the provided LogEventBody4 +func (t *LogEvent_Body) FromLogEventBody4(v LogEventBody4) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogEventBody4 performs a merge with any union data inside the LogEvent_Body, using the provided LogEventBody4 +func (t *LogEvent_Body) MergeLogEventBody4(v LogEventBody4) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t LogEvent_Body) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *LogEvent_Body) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsLogFunctionRequestResource returns the union data inside the LogRequestResource as a LogFunctionRequestResource func (t LogRequestResource) AsLogFunctionRequestResource() (LogFunctionRequestResource, error) { var body LogFunctionRequestResource @@ -6227,6 +6395,146 @@ func (t *LogRequestResource) UnmarshalJSON(b []byte) error { return err } +// AsLogSearchEventBody0 returns the union data inside the LogSearchEvent_Body as a LogSearchEventBody0 +func (t LogSearchEvent_Body) AsLogSearchEventBody0() (LogSearchEventBody0, error) { + var body LogSearchEventBody0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogSearchEventBody0 overwrites any union data inside the LogSearchEvent_Body as the provided LogSearchEventBody0 +func (t *LogSearchEvent_Body) FromLogSearchEventBody0(v LogSearchEventBody0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogSearchEventBody0 performs a merge with any union data inside the LogSearchEvent_Body, using the provided LogSearchEventBody0 +func (t *LogSearchEvent_Body) MergeLogSearchEventBody0(v LogSearchEventBody0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogSearchEventBody1 returns the union data inside the LogSearchEvent_Body as a LogSearchEventBody1 +func (t LogSearchEvent_Body) AsLogSearchEventBody1() (LogSearchEventBody1, error) { + var body LogSearchEventBody1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogSearchEventBody1 overwrites any union data inside the LogSearchEvent_Body as the provided LogSearchEventBody1 +func (t *LogSearchEvent_Body) FromLogSearchEventBody1(v LogSearchEventBody1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogSearchEventBody1 performs a merge with any union data inside the LogSearchEvent_Body, using the provided LogSearchEventBody1 +func (t *LogSearchEvent_Body) MergeLogSearchEventBody1(v LogSearchEventBody1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogSearchEventBody2 returns the union data inside the LogSearchEvent_Body as a LogSearchEventBody2 +func (t LogSearchEvent_Body) AsLogSearchEventBody2() (LogSearchEventBody2, error) { + var body LogSearchEventBody2 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogSearchEventBody2 overwrites any union data inside the LogSearchEvent_Body as the provided LogSearchEventBody2 +func (t *LogSearchEvent_Body) FromLogSearchEventBody2(v LogSearchEventBody2) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogSearchEventBody2 performs a merge with any union data inside the LogSearchEvent_Body, using the provided LogSearchEventBody2 +func (t *LogSearchEvent_Body) MergeLogSearchEventBody2(v LogSearchEventBody2) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogSearchEventBody3 returns the union data inside the LogSearchEvent_Body as a LogSearchEventBody3 +func (t LogSearchEvent_Body) AsLogSearchEventBody3() (LogSearchEventBody3, error) { + var body LogSearchEventBody3 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogSearchEventBody3 overwrites any union data inside the LogSearchEvent_Body as the provided LogSearchEventBody3 +func (t *LogSearchEvent_Body) FromLogSearchEventBody3(v LogSearchEventBody3) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogSearchEventBody3 performs a merge with any union data inside the LogSearchEvent_Body, using the provided LogSearchEventBody3 +func (t *LogSearchEvent_Body) MergeLogSearchEventBody3(v LogSearchEventBody3) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLogSearchEventBody4 returns the union data inside the LogSearchEvent_Body as a LogSearchEventBody4 +func (t LogSearchEvent_Body) AsLogSearchEventBody4() (LogSearchEventBody4, error) { + var body LogSearchEventBody4 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLogSearchEventBody4 overwrites any union data inside the LogSearchEvent_Body as the provided LogSearchEventBody4 +func (t *LogSearchEvent_Body) FromLogSearchEventBody4(v LogSearchEventBody4) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLogSearchEventBody4 performs a merge with any union data inside the LogSearchEvent_Body, using the provided LogSearchEventBody4 +func (t *LogSearchEvent_Body) MergeLogSearchEventBody4(v LogSearchEventBody4) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t LogSearchEvent_Body) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *LogSearchEvent_Body) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsQueryDatabaseDeleteJSONBodyFiltersValue0 returns the union data inside the QueryDatabaseDeleteJSONBody_Filters_Value as a QueryDatabaseDeleteJSONBodyFiltersValue0 func (t QueryDatabaseDeleteJSONBody_Filters_Value) AsQueryDatabaseDeleteJSONBodyFiltersValue0() (QueryDatabaseDeleteJSONBodyFiltersValue0, error) { var body QueryDatabaseDeleteJSONBodyFiltersValue0 diff --git a/internal/output/functions.go b/internal/output/functions.go index 2d71c3e..28b1b69 100644 --- a/internal/output/functions.go +++ b/internal/output/functions.go @@ -1,6 +1,7 @@ package output import ( + "encoding/json" "fmt" "io" "strings" @@ -103,7 +104,7 @@ func FunctionRuntimes(w io.Writer, runtimes []apiclient.FunctionRuntimeOption) { func LogEvents(w io.Writer, events []apiclient.LogEvent) { on := theme.On(w) for _, event := range events { - printLogEvent(w, on, event.Timestamp, event.Region, event.Message) + printLogEvent(w, on, event.Timestamp, event.Region, logEventBodyText(event.Body)) } } @@ -111,10 +112,40 @@ func LogEvents(w io.Writer, events []apiclient.LogEvent) { func LogSearchEvents(w io.Writer, events []apiclient.LogSearchEvent) { on := theme.On(w) for _, event := range events { - printLogEvent(w, on, event.Timestamp, event.Region, event.Message) + printLogEvent(w, on, event.Timestamp, event.Region, logSearchEventBodyText(event.Body)) } } +// logEventBodyText renders a LogEvent's body for display. The API normalizes +// string bodies as plain strings and everything else (objects, arrays, +// numbers, booleans) as their JSON encoding, per the body field's contract. +func logEventBodyText(body *apiclient.LogEvent_Body) string { + if body == nil { + return "" + } + return bodyText(body) +} + +// logSearchEventBodyText is the LogSearchEvent_Body equivalent of logEventBodyText. +func logSearchEventBodyText(body *apiclient.LogSearchEvent_Body) string { + if body == nil { + return "" + } + return bodyText(body) +} + +func bodyText(body json.Marshaler) string { + raw, err := body.MarshalJSON() + if err != nil || len(raw) == 0 { + return "" + } + var s string + if err := json.Unmarshal(raw, &s); err == nil { + return s + } + return string(raw) +} + // printLogEvent renders one event. on is computed once by the batch/stream // renderer and passed in, so a high-volume log stream doesn't do a getenv+stat // per line. diff --git a/internal/output/functions_test.go b/internal/output/functions_test.go new file mode 100644 index 0000000..e1eab39 --- /dev/null +++ b/internal/output/functions_test.go @@ -0,0 +1,51 @@ +package output + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Kong/volcano-cli/internal/apiclient" +) + +// Regression coverage for the body/message field rename: the API's LogEvent +// body is untyped JSON (string, object, array, number, or bool), not a plain +// "message" string, so these decode the same wire shape the server sends. + +func TestLogEventsRendersStringBody(t *testing.T) { + var events []apiclient.LogEvent + raw := `[{"timestamp":"2026-07-30T10:11:20-04:00","body":"next build failed: some error"}]` + require.NoError(t, json.Unmarshal([]byte(raw), &events)) + + var out bytes.Buffer + LogEvents(&out, events) + assert.Contains(t, out.String(), "next build failed: some error") +} + +func TestLogEventsRendersNonStringBodyAsJSON(t *testing.T) { + var events []apiclient.LogEvent + raw := `[{"timestamp":"2026-07-30T10:11:20-04:00","body":{"phase":"BUILD","status":"FAILED"}}]` + require.NoError(t, json.Unmarshal([]byte(raw), &events)) + + var out bytes.Buffer + LogEvents(&out, events) + assert.Contains(t, out.String(), `{"phase":"BUILD","status":"FAILED"}`) +} + +func TestLogSearchEventsRendersStringBody(t *testing.T) { + var events []apiclient.LogSearchEvent + raw := `[{"id":"evt-1","timestamp":"2026-07-30T10:11:20-04:00","resource":{"type":"frontend","id":"` + outputProjectID + `"},"body":"hello from build"}]` + require.NoError(t, json.Unmarshal([]byte(raw), &events)) + + var out bytes.Buffer + LogSearchEvents(&out, events) + assert.Contains(t, out.String(), "hello from build") +} + +func TestLogEventBodyTextHandlesNil(t *testing.T) { + assert.Equal(t, "", logEventBodyText(nil)) + assert.Equal(t, "", logSearchEventBodyText(nil)) +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 7db9185..0ac292f 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -9045,9 +9045,18 @@ components: description: Event timestamp. level: $ref: '#/components/schemas/LiveLogLevel' - message: - type: string - description: Display log message. + body: + description: Application log value. JSON arguments retain their JSON type. Strings containing a serialized JSON object or array are normalized to that object or array; all other strings remain strings. + nullable: true + oneOf: + - type: string + - type: object + additionalProperties: true + - type: array + items: {} + - type: number + format: double + - type: boolean region: type: string description: Region where this log event originated. @@ -9058,17 +9067,9 @@ components: invocation_id: type: string description: Function invocation ID associated with this log event, when available. - raw_message: - type: string - description: Original log line/message after platform sanitization. - metadata: - type: object - nullable: true - additionalProperties: true - description: Parsed JSON fields and other indexed metadata, when available. required: - timestamp - - message + - body LiveLogLevel: type: string enum: From 452f9e23d33f1d74ff45a99eae60cc7d37a79fed Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:13:39 -0400 Subject: [PATCH 2/7] docs: fix region format example, surface projects keys command Database regions are Neon-style aws- identifiers (e.g. aws-us-east-1); the example used a plain AWS region and would 400. volcano projects keys already returns a project's anon/publishable keys for browser SDK use, but wasn't mentioned in the CLI overview. VOL-645, VOL-646 --- docs/README.md | 3 ++- docs/databases.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index 05227ba..533f42a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,7 +69,7 @@ NO_COLOR=1 volcano databases list | Element | CLI can… | Commands | Details | |---|---|---|---| | Account / auth | sign up, log in/out | `signup`, `login`, `logout` | [authentication.md](authentication.md) | -| Project | create, list, get, delete, select | `projects …`, `use` | below | +| Project | create, list, get, delete, select, get anon keys | `projects …`, `use` | below | | Functions | deploy, invoke, inspect, schedule, alias | `functions …` | [functions.md](functions.md) | | Databases | create, inspect, delete, migrate | `databases …`, `migrations …` | [databases.md](databases.md) | | Storage | manage buckets, objects, policies | `storage …` | [storage.md](storage.md) | @@ -90,6 +90,7 @@ volcano projects create my-app # create a project volcano projects list # list your projects volcano use my-app # set the active project volcano projects get # details for the active project +volcano projects keys # anon (publishable) API keys for the browser/SDK volcano projects delete my-app # delete ``` diff --git a/docs/databases.md b/docs/databases.md index 4be1468..55c1e4d 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -22,7 +22,7 @@ it using its connection string. | Operation | Command | |---|---| -| Create | `volcano databases create [--type …] [--region …] [--pg-version …]` | +| Create | `volcano databases create [--type …] [--region aws-] [--pg-version …]` | | List | `volcano databases list` | | Get | `volcano databases get [--show-connection-string]` | | Delete | `volcano databases delete ` | @@ -36,7 +36,7 @@ Prefix with `cloud` to force the cloud target. ```bash # Create a database (defaults to type volcano-db-xs) volcano databases create app -volcano databases create app --type volcano-db-s --region us-east-1 +volcano databases create app --type volcano-db-s --region aws-us-east-1 # Show the connection string volcano databases get app --show-connection-string From c1a5436d9f4589b5f2f47fe362869f21a01805f2 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:50:00 -0400 Subject: [PATCH 3/7] fix(logs): update remaining test fixtures for body field rename internal/api's own test fixtures and assertions still referenced the old LogSearchEvent.Message field/JSON key, missed in the initial client.gen.go regeneration for the message->body rename. --- internal/api/client_test.go | 17 +++++++++++++---- internal/api/frontends_test.go | 4 ++-- internal/api/log_stream_test.go | 4 ++-- internal/output/functions_test.go | 4 ++-- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/internal/api/client_test.go b/internal/api/client_test.go index aee7fe1..20983c5 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/Kong/volcano-cli/internal/apiclient" "github.com/Kong/volcano-cli/internal/version" ) @@ -528,7 +529,7 @@ func TestFunctionMethodsUseGeneratedRoutes(t *testing.T) { runtimeLogs, err := client.GetFunctionLogs(context.Background(), projectID, functionID, 50, "fn-next") require.NoError(t, err) - assert.Equal(t, "function runtime", runtimeLogs.Data[0].Message) + assert.Equal(t, "function runtime", logSearchEventBody(t, runtimeLogs.Data[0])) require.Len(t, logSearchBodies, 1) runtimeResource, ok := logSearchBodies[0]["resource"].(map[string]any) require.True(t, ok) @@ -539,7 +540,7 @@ func TestFunctionMethodsUseGeneratedRoutes(t *testing.T) { deploymentLogs, err := client.GetFunctionDeploymentLogs(context.Background(), projectID, functionID, deploymentID, 75, "dep-next") require.NoError(t, err) - assert.Equal(t, "deployment build", deploymentLogs.Data[0].Message) + assert.Equal(t, "deployment build", logSearchEventBody(t, deploymentLogs.Data[0])) require.Len(t, logSearchBodies, 2) buildResource, ok := logSearchBodies[1]["resource"].(map[string]any) require.True(t, ok) @@ -738,11 +739,11 @@ func logSearchIsBuild(body map[string]any) bool { return hasDeployments } -func logsResponse(message string) map[string]any { +func logsResponse(body string) map[string]any { return map[string]any{ "data": []any{ map[string]any{ - "message": message, + "body": body, "timestamp": "2025-10-09T08:53:20Z", }, }, @@ -753,6 +754,14 @@ func logsResponse(message string) map[string]any { } } +func logSearchEventBody(t *testing.T, event apiclient.LogSearchEvent) string { + t.Helper() + require.NotNil(t, event.Body) + body, err := event.Body.AsLogSearchEventBody0() + require.NoError(t, err) + return body +} + func writeAPIJSON(t *testing.T, w http.ResponseWriter, status int, value any) { t.Helper() w.Header().Set("Content-Type", "application/json") diff --git a/internal/api/frontends_test.go b/internal/api/frontends_test.go index 6b383e7..9b40857 100644 --- a/internal/api/frontends_test.go +++ b/internal/api/frontends_test.go @@ -124,7 +124,7 @@ func TestFrontendDomainAndLogsMethodsUseGeneratedRoutes(t *testing.T) { runtimeLogs, err := client.GetFrontendLogs(context.Background(), projectID, frontendID, 50, "fe-next") require.NoError(t, err) - assert.Equal(t, "frontend runtime", runtimeLogs.Data[0].Message) + assert.Equal(t, "frontend runtime", logSearchEventBody(t, runtimeLogs.Data[0])) require.Len(t, logSearchBodies, 1) runtimeResource, ok := logSearchBodies[0]["resource"].(map[string]any) require.True(t, ok) @@ -135,7 +135,7 @@ func TestFrontendDomainAndLogsMethodsUseGeneratedRoutes(t *testing.T) { deploymentLogs, err := client.GetFrontendDeploymentLogs(context.Background(), projectID, frontendID, deploymentID, 75, "dep-next") require.NoError(t, err) - assert.Equal(t, "frontend build", deploymentLogs.Data[0].Message) + assert.Equal(t, "frontend build", logSearchEventBody(t, deploymentLogs.Data[0])) require.Len(t, logSearchBodies, 2) buildResource, ok := logSearchBodies[1]["resource"].(map[string]any) require.True(t, ok) diff --git a/internal/api/log_stream_test.go b/internal/api/log_stream_test.go index 2fa1db4..b504b96 100644 --- a/internal/api/log_stream_test.go +++ b/internal/api/log_stream_test.go @@ -39,7 +39,7 @@ func TestStreamProjectLogsRequestAndEvents(t *testing.T) { _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: next-id\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"log-1","message":"build finished","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"},"deployment":{"id":"` + deploymentID.String() + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"log-1","body":"build finished","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"},"deployment":{"id":"` + deploymentID.String() + `"}}` + "\n\n")) _, _ = w.Write([]byte("event: warning\n")) _, _ = w.Write([]byte(`data: {"error":"temporary read failure"}` + "\n\n")) })) @@ -72,7 +72,7 @@ func TestStreamProjectLogsRequestAndEvents(t *testing.T) { require.NotNil(t, event.Log) assert.Equal(t, "next-id", event.ID) assert.Equal(t, "log-1", event.Log.Id) - assert.Equal(t, "build finished", event.Log.Message) + assert.Equal(t, "build finished", logSearchEventBody(t, *event.Log)) assert.Equal(t, time.Date(2025, 10, 9, 8, 53, 20, 0, time.UTC), event.Log.Timestamp) event, err = stream.Next() diff --git a/internal/output/functions_test.go b/internal/output/functions_test.go index e1eab39..fd07c21 100644 --- a/internal/output/functions_test.go +++ b/internal/output/functions_test.go @@ -46,6 +46,6 @@ func TestLogSearchEventsRendersStringBody(t *testing.T) { } func TestLogEventBodyTextHandlesNil(t *testing.T) { - assert.Equal(t, "", logEventBodyText(nil)) - assert.Equal(t, "", logSearchEventBodyText(nil)) + assert.Empty(t, logEventBodyText(nil)) + assert.Empty(t, logSearchEventBodyText(nil)) } From 924030714de1460fce9127de1d67178689b48a4b Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:56:48 -0400 Subject: [PATCH 4/7] docs(databases): hint region format in --help, add body variant tests --region's usage string gave no hint of the aws- format the API expects, so a --help user could still hit the same 400 the docs fix addresses. Also add array/number/boolean/null coverage for LogEvent body rendering alongside the existing string/object cases. --- internal/cmd/databases/create.go | 2 +- internal/output/functions_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/internal/cmd/databases/create.go b/internal/cmd/databases/create.go index 2166876..8ce743a 100644 --- a/internal/cmd/databases/create.go +++ b/internal/cmd/databases/create.go @@ -80,7 +80,7 @@ func newCreateWithOptions(deps cliruntime.Deps, commandOpts createCommandOptions }) }, } - cmd.Flags().StringVar(®ion, "region", "", "Database region") + cmd.Flags().StringVar(®ion, "region", "", "Database region (aws-, e.g. aws-us-east-1)") cmd.Flags().StringVar(&pgVersion, "pg-version", "", "PostgreSQL version") cmd.Flags().StringVar(&databaseType, "type", "", "Database type (default: volcano-db-xs)") cmd.Flags().BoolVar(&showConnectionString, "show-connection-string", false, "Show database connection string") diff --git a/internal/output/functions_test.go b/internal/output/functions_test.go index fd07c21..3f8333b 100644 --- a/internal/output/functions_test.go +++ b/internal/output/functions_test.go @@ -49,3 +49,30 @@ func TestLogEventBodyTextHandlesNil(t *testing.T) { assert.Empty(t, logEventBodyText(nil)) assert.Empty(t, logSearchEventBodyText(nil)) } + +func TestLogEventsRendersEveryBodyVariant(t *testing.T) { + cases := []struct { + name string + bodyJSON string + want string + }{ + {name: "array", bodyJSON: `["a","b"]`, want: `["a","b"]`}, + {name: "number", bodyJSON: `42`, want: "42"}, + {name: "boolean", bodyJSON: `true`, want: "true"}, + {name: "null", bodyJSON: `null`, want: ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var events []apiclient.LogEvent + raw := `[{"timestamp":"2026-07-30T10:11:20-04:00","body":` + tc.bodyJSON + `}]` + require.NoError(t, json.Unmarshal([]byte(raw), &events)) + + var out bytes.Buffer + LogEvents(&out, events) + if tc.want == "" { + return + } + assert.Contains(t, out.String(), tc.want) + }) + } +} From f10f602e75ed9b66346bbcdb63e81c1ab5be10b0 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:23:32 -0400 Subject: [PATCH 5/7] test(output): assert null log body renders no literal "null" The null-body case in TestLogEventsRendersEveryBodyVariant only proved no panic; assert the rendered line actually stays empty so a regression that prints the literal "null" would be caught. --- internal/output/functions_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/output/functions_test.go b/internal/output/functions_test.go index 3f8333b..c7e8bea 100644 --- a/internal/output/functions_test.go +++ b/internal/output/functions_test.go @@ -70,6 +70,7 @@ func TestLogEventsRendersEveryBodyVariant(t *testing.T) { var out bytes.Buffer LogEvents(&out, events) if tc.want == "" { + assert.NotContains(t, out.String(), "null") return } assert.Contains(t, out.String(), tc.want) From 9b9133d763482b677ac2fd4f11fe0afe0f34355a Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:27:01 -0400 Subject: [PATCH 6/7] fix(logs): update remaining stale message fixtures after body rename internal/cmd/frontends, internal/cmd/functions, and internal/logfollow still built test log events with the old "message" field, so their fixtures silently decoded to an empty body under the regenerated client and left TestFrontendsLogs/TestFunctionsLogs asserting on blank output. internal/logfollow's stream tests additionally hung: the follow loop never observed a non-empty body to detect stream progress, so it blocked forever on the next read instead of failing fast. --- internal/cmd/frontends/logs_test.go | 8 ++++---- internal/cmd/functions/logs_test.go | 8 ++++---- internal/logfollow/follow_test.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/cmd/frontends/logs_test.go b/internal/cmd/frontends/logs_test.go index 657d5c5..c5dd4f5 100644 --- a/internal/cmd/frontends/logs_test.go +++ b/internal/cmd/frontends/logs_test.go @@ -282,7 +282,7 @@ func frontendLogCommandResponse(message string, hasMore bool, next string) map[s response := map[string]any{ "data": []any{ map[string]any{ - "message": message, + "body": message, "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20Z", }, @@ -303,13 +303,13 @@ func catchUpLogResponse() map[string]any { "data": []any{ map[string]any{ "id": "stream-log", - "message": "build follow", + "body": "build follow", "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20Z", }, map[string]any{ "id": "catch-up-log", - "message": "catch up log", + "body": "catch up log", "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20.001Z", }, @@ -327,7 +327,7 @@ func writeFrontendLogStream(t *testing.T, w http.ResponseWriter, message string) _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"stream-log","message":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"frontend","id":"` + frontendID + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"stream-log","body":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"frontend","id":"` + frontendID + `"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/cmd/functions/logs_test.go b/internal/cmd/functions/logs_test.go index 78bf264..c38765b 100644 --- a/internal/cmd/functions/logs_test.go +++ b/internal/cmd/functions/logs_test.go @@ -293,7 +293,7 @@ func logCommandResponse(message string, hasMore bool, next string) map[string]an response := map[string]any{ "data": []any{ map[string]any{ - "message": message, + "body": message, "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20Z", }, @@ -314,13 +314,13 @@ func catchUpLogResponse() map[string]any { "data": []any{ map[string]any{ "id": "stream-log", - "message": "build follow", + "body": "build follow", "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20Z", }, map[string]any{ "id": "catch-up-log", - "message": "catch up log", + "body": "catch up log", "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20.001Z", }, @@ -338,7 +338,7 @@ func writeFunctionLogStream(t *testing.T, w http.ResponseWriter, message string) _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"stream-log","message":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"stream-log","body":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID + `"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/logfollow/follow_test.go b/internal/logfollow/follow_test.go index e0d19e6..e4e4dfb 100644 --- a/internal/logfollow/follow_test.go +++ b/internal/logfollow/follow_test.go @@ -29,7 +29,7 @@ func TestDeploymentStopsAtTerminalStatusAndRunsCatchUp(t *testing.T) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"streamed-id","message":"streamed log","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"streamed-id","body":"streamed log","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) flusher.Flush() <-r.Context().Done() })) @@ -77,7 +77,7 @@ func TestDeploymentRunsCatchUpWhenStreamEndsBeforeTerminal(t *testing.T) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"streamed-id","message":"streamed log","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"streamed-id","body":"streamed log","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID.String() + `"}}` + "\n\n")) })) defer server.Close() @@ -210,7 +210,7 @@ func TestRuntimeSurfacesOpenError(t *testing.T) { func writeStreamLog(w http.ResponseWriter, cursor, logID, message string) { _, _ = w.Write([]byte("id: " + cursor + "\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"` + logID + `","message":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"22222222-2222-4222-8222-222222222222"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"` + logID + `","body":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"22222222-2222-4222-8222-222222222222"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } From 3f5ff65eadb32f4a29188e6e14f8c51aa13d01d1 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:31:57 -0400 Subject: [PATCH 7/7] test(logs): rename message params/vars to body for consistency These test helpers build the "body" wire field but still named the parameter "message" from before the server-side rename, same as client_test.go's logsResponse already does. --- internal/cmd/frontends/logs_test.go | 8 ++++---- internal/cmd/functions/logs_test.go | 8 ++++---- internal/logfollow/follow_test.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/cmd/frontends/logs_test.go b/internal/cmd/frontends/logs_test.go index c5dd4f5..d4412fa 100644 --- a/internal/cmd/frontends/logs_test.go +++ b/internal/cmd/frontends/logs_test.go @@ -278,11 +278,11 @@ func frontendDeploymentCommandPayload(id string) map[string]any { } } -func frontendLogCommandResponse(message string, hasMore bool, next string) map[string]any { +func frontendLogCommandResponse(body string, hasMore bool, next string) map[string]any { response := map[string]any{ "data": []any{ map[string]any{ - "body": message, + "body": body, "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20Z", }, @@ -321,13 +321,13 @@ func catchUpLogResponse() map[string]any { } } -func writeFrontendLogStream(t *testing.T, w http.ResponseWriter, message string) { +func writeFrontendLogStream(t *testing.T, w http.ResponseWriter, body string) { t.Helper() w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"stream-log","body":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"frontend","id":"` + frontendID + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"stream-log","body":"` + body + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"frontend","id":"` + frontendID + `"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/cmd/functions/logs_test.go b/internal/cmd/functions/logs_test.go index c38765b..d226c87 100644 --- a/internal/cmd/functions/logs_test.go +++ b/internal/cmd/functions/logs_test.go @@ -289,11 +289,11 @@ func deploymentCommandPayload(id string) map[string]any { } } -func logCommandResponse(message string, hasMore bool, next string) map[string]any { +func logCommandResponse(body string, hasMore bool, next string) map[string]any { response := map[string]any{ "data": []any{ map[string]any{ - "body": message, + "body": body, "region": "aws-us-east-1", "timestamp": "2025-10-09T08:53:20Z", }, @@ -332,13 +332,13 @@ func catchUpLogResponse() map[string]any { } } -func writeFunctionLogStream(t *testing.T, w http.ResponseWriter, message string) { +func writeFunctionLogStream(t *testing.T, w http.ResponseWriter, body string) { t.Helper() w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(": connected\n\n")) _, _ = w.Write([]byte("id: stream-cursor\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"stream-log","body":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID + `"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"stream-log","body":"` + body + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"` + functionID + `"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/internal/logfollow/follow_test.go b/internal/logfollow/follow_test.go index e4e4dfb..6b1fe29 100644 --- a/internal/logfollow/follow_test.go +++ b/internal/logfollow/follow_test.go @@ -207,10 +207,10 @@ func TestRuntimeSurfacesOpenError(t *testing.T) { require.ErrorIs(t, err, wantErr) } -func writeStreamLog(w http.ResponseWriter, cursor, logID, message string) { +func writeStreamLog(w http.ResponseWriter, cursor, logID, body string) { _, _ = w.Write([]byte("id: " + cursor + "\n")) _, _ = w.Write([]byte("event: log\n")) - _, _ = w.Write([]byte(`data: {"id":"` + logID + `","body":"` + message + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"22222222-2222-4222-8222-222222222222"}}` + "\n\n")) + _, _ = w.Write([]byte(`data: {"id":"` + logID + `","body":"` + body + `","timestamp":"2025-10-09T08:53:20Z","resource":{"type":"function","id":"22222222-2222-4222-8222-222222222222"}}` + "\n\n")) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() }