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 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/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/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/cmd/frontends/logs_test.go b/internal/cmd/frontends/logs_test.go index 657d5c5..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{ - "message": message, + "body": body, "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", }, @@ -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","message":"` + 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 78bf264..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{ - "message": message, + "body": body, "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", }, @@ -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","message":"` + 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 e0d19e6..6b1fe29 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() @@ -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 + `","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":"` + 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() } 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..c7e8bea --- /dev/null +++ b/internal/output/functions_test.go @@ -0,0 +1,79 @@ +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.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 == "" { + assert.NotContains(t, out.String(), "null") + return + } + assert.Contains(t, out.String(), tc.want) + }) + } +} 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: