From 04de79e20c2af50998d141d64ad9529d9b387621 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:07:21 -0400 Subject: [PATCH 1/2] fix(logs): use project log search selectors --- internal/api/client_test.go | 37 ++++++++++----- internal/api/frontends.go | 42 +++++++--------- internal/api/frontends_test.go | 37 +++++++++++---- internal/api/functions.go | 38 +++++---------- internal/api/logs.go | 74 +++++++++++++++++++++++++++++ internal/apiclient/client_test.go | 37 ++++++--------- internal/cmd/frontends/logs.go | 4 +- internal/cmd/frontends/logs_test.go | 33 ++++++++++--- internal/cmd/functions/logs.go | 2 +- internal/cmd/functions/logs_test.go | 17 +++++-- internal/frontend/frontend.go | 8 ++-- internal/function/function.go | 4 +- internal/output/logs.go | 26 ---------- 13 files changed, 222 insertions(+), 137 deletions(-) create mode 100644 internal/api/logs.go diff --git a/internal/api/client_test.go b/internal/api/client_test.go index 354c751..bdcc155 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -290,7 +290,7 @@ func TestFunctionMethodsUseGeneratedRoutes(t *testing.T) { var batchFilenames []string var updateBody map[string]bool var invokeBody map[string]any - var logSearchBody map[string]any + var logSearchBodies []map[string]any var schedulerCreateBody map[string]any var schedulerUpdateBody map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -360,11 +360,13 @@ func TestFunctionMethodsUseGeneratedRoutes(t *testing.T) { "total": 1, }) case r.Method == http.MethodPost && r.URL.Path == "/projects/"+projectIDText+"/logs/search": - require.NoError(t, json.NewDecoder(r.Body).Decode(&logSearchBody)) - writeAPIJSON(t, w, http.StatusOK, logsResponse("function runtime")) - case r.Method == http.MethodGet && r.URL.Path == "/projects/"+projectIDText+"/functions/"+functionIDText+"/deployments/"+deploymentIDText+"/logs": - assert.Equal(t, "75", r.URL.Query().Get("limit")) - assert.Equal(t, "dep-next", r.URL.Query().Get("cursor")) + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + logSearchBodies = append(logSearchBodies, body) + if len(logSearchBodies) == 1 { + writeAPIJSON(t, w, http.StatusOK, logsResponse("function runtime")) + return + } writeAPIJSON(t, w, http.StatusOK, logsResponse("deployment build")) case r.Method == http.MethodGet && r.URL.Path == "/projects/"+projectIDText+"/functions/"+functionIDText+"/schedulers": writeAPIJSON(t, w, http.StatusOK, map[string]any{ @@ -463,14 +465,27 @@ 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", logSearchBody["resource_type"]) - assert.Equal(t, []any{functionIDText}, logSearchBody["resource_ids"]) - assert.InEpsilon(t, 50, logSearchBody["limit"], 0) - assert.Equal(t, "fn-next", logSearchBody["cursor"]) + require.Len(t, logSearchBodies, 1) + runtimeResource, ok := logSearchBodies[0]["resource"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "function", runtimeResource["type"]) + assert.Equal(t, []any{functionIDText}, runtimeResource["ids"]) + assert.InEpsilon(t, 50, logSearchBodies[0]["limit"], 0) + assert.Equal(t, "fn-next", logSearchBodies[0]["cursor"]) 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) + require.Len(t, logSearchBodies, 2) + buildResource, ok := logSearchBodies[1]["resource"].(map[string]any) + require.True(t, ok) + buildDeployments, ok := buildResource["deployments"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "function", buildResource["type"]) + assert.Equal(t, []any{functionIDText}, buildResource["ids"]) + assert.Equal(t, []any{deploymentIDText}, buildDeployments["ids"]) + assert.InEpsilon(t, 75, logSearchBodies[1]["limit"], 0) + assert.Equal(t, "dep-next", logSearchBodies[1]["cursor"]) schedulers, err := client.ListFunctionSchedulers(context.Background(), projectID, functionID) require.NoError(t, err) @@ -518,7 +533,7 @@ func TestFunctionMethodsUseGeneratedRoutes(t *testing.T) { "GET /functions/runtimes", "GET /projects/" + projectIDText + "/functions/" + functionIDText + "/deployments?page=3&limit=10", "POST /projects/" + projectIDText + "/logs/search", - "GET /projects/" + projectIDText + "/functions/" + functionIDText + "/deployments/" + deploymentIDText + "/logs?limit=75&cursor=dep-next", + "POST /projects/" + projectIDText + "/logs/search", "GET /projects/" + projectIDText + "/functions/" + functionIDText + "/schedulers", "POST /projects/" + projectIDText + "/functions/" + functionIDText + "/schedulers", "PATCH /projects/" + projectIDText + "/functions/" + functionIDText + "/schedulers/" + schedulerIDText, diff --git a/internal/api/frontends.go b/internal/api/frontends.go index c2e3c17..a14802f 100644 --- a/internal/api/frontends.go +++ b/internal/api/frontends.go @@ -117,38 +117,32 @@ func (c *Client) ListFrontendDeployments(ctx context.Context, projectID, fronten return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500) } -// GetFrontendLogs returns one runtime log page for a frontend. -func (c *Client) GetFrontendLogs(ctx context.Context, projectID, frontendID uuid.UUID, limit int, cursor string) (*apiclient.ListLogsResponse, error) { - params := &apiclient.GetFrontendLogsParams{} - if limit > 0 { - params.Limit = &limit +// GetFrontendLogs returns one runtime log search page for a frontend. +func (c *Client) GetFrontendLogs(ctx context.Context, projectID, frontendID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { + body := logSearchRequest{ + Resource: logResource(logResourceTypeFrontend, frontendID), } - if cursor = strings.TrimSpace(cursor); cursor != "" { - params.Cursor = &cursor + if limit > 0 { + body.Limit = &limit } - - resp, err := c.client.GetFrontendLogsWithResponse(ctx, projectID, frontendID, params) - if err != nil { - return nil, err + if cursor != "" { + body.Cursor = &cursor } - return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500, resp.JSON503) + return c.searchProjectLogs(ctx, projectID, body) } -// GetFrontendDeploymentLogs returns one build log page for a frontend deployment. -func (c *Client) GetFrontendDeploymentLogs(ctx context.Context, projectID, frontendID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.ListLogsResponse, error) { - params := &apiclient.GetFrontendDeploymentLogsParams{} - if limit > 0 { - params.Limit = &limit +// GetFrontendDeploymentLogs returns one build log search page for a frontend deployment. +func (c *Client) GetFrontendDeploymentLogs(ctx context.Context, projectID, frontendID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { + body := logSearchRequest{ + Resource: logDeploymentResource(logResourceTypeFrontend, frontendID, deploymentID), } - if cursor = strings.TrimSpace(cursor); cursor != "" { - params.Cursor = &cursor + if limit > 0 { + body.Limit = &limit } - - resp, err := c.client.GetFrontendDeploymentLogsWithResponse(ctx, projectID, frontendID, deploymentID, params) - if err != nil { - return nil, err + if cursor != "" { + body.Cursor = &cursor } - return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON500, resp.JSON503) + return c.searchProjectLogs(ctx, projectID, body) } // CreateFrontendCustomDomain attaches a BYOC custom domain to a frontend. diff --git a/internal/api/frontends_test.go b/internal/api/frontends_test.go index a42b36c..e835e9e 100644 --- a/internal/api/frontends_test.go +++ b/internal/api/frontends_test.go @@ -77,6 +77,7 @@ func TestFrontendDomainAndLogsMethodsUseGeneratedRoutes(t *testing.T) { deploymentID := uuid.MustParse(deploymentIDText) var requests []string var createBody map[string]any + var logSearchBodies []map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "Bearer token", r.Header.Get("Authorization")) requests = append(requests, r.Method+" "+r.URL.RequestURI()) @@ -91,13 +92,14 @@ func TestFrontendDomainAndLogsMethodsUseGeneratedRoutes(t *testing.T) { "limit": 10, "total": 1, }) - case r.Method == http.MethodGet && r.URL.Path == "/projects/"+projectIDText+"/frontends/"+frontendIDText+"/logs": - assert.Equal(t, "50", r.URL.Query().Get("limit")) - assert.Equal(t, "fe-next", r.URL.Query().Get("cursor")) - writeAPIJSON(t, w, http.StatusOK, logsResponse("frontend runtime")) - case r.Method == http.MethodGet && r.URL.Path == "/projects/"+projectIDText+"/frontends/"+frontendIDText+"/deployments/"+deploymentIDText+"/logs": - assert.Equal(t, "75", r.URL.Query().Get("limit")) - assert.Equal(t, "dep-next", r.URL.Query().Get("cursor")) + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+projectIDText+"/logs/search": + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + logSearchBodies = append(logSearchBodies, body) + if len(logSearchBodies) == 1 { + writeAPIJSON(t, w, http.StatusOK, logsResponse("frontend runtime")) + return + } writeAPIJSON(t, w, http.StatusOK, logsResponse("frontend build")) case r.Method == http.MethodPost && r.URL.Path == "/projects/"+projectIDText+"/frontends/"+frontendIDText+"/domain": require.NoError(t, json.NewDecoder(r.Body).Decode(&createBody)) @@ -123,10 +125,27 @@ 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) + require.Len(t, logSearchBodies, 1) + runtimeResource, ok := logSearchBodies[0]["resource"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "frontend", runtimeResource["type"]) + assert.Equal(t, []any{frontendIDText}, runtimeResource["ids"]) + assert.InEpsilon(t, 50, logSearchBodies[0]["limit"], 0) + assert.Equal(t, "fe-next", logSearchBodies[0]["cursor"]) 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) + require.Len(t, logSearchBodies, 2) + buildResource, ok := logSearchBodies[1]["resource"].(map[string]any) + require.True(t, ok) + buildDeployments, ok := buildResource["deployments"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "frontend", buildResource["type"]) + assert.Equal(t, []any{frontendIDText}, buildResource["ids"]) + assert.Equal(t, []any{deploymentIDText}, buildDeployments["ids"]) + assert.InEpsilon(t, 75, logSearchBodies[1]["limit"], 0) + assert.Equal(t, "dep-next", logSearchBodies[1]["cursor"]) createdDomain, err := client.CreateFrontendCustomDomain(context.Background(), projectID, frontendID, FrontendCustomDomainInput{ Domain: " app.example.com ", @@ -151,8 +170,8 @@ func TestFrontendDomainAndLogsMethodsUseGeneratedRoutes(t *testing.T) { require.NoError(t, client.DeleteFrontendCustomDomain(context.Background(), projectID, frontendID)) assert.Equal(t, []string{ "GET /projects/" + projectIDText + "/frontends/" + frontendIDText + "/deployments?page=3&limit=10", - "GET /projects/" + projectIDText + "/frontends/" + frontendIDText + "/logs?limit=50&cursor=fe-next", - "GET /projects/" + projectIDText + "/frontends/" + frontendIDText + "/deployments/" + deploymentIDText + "/logs?limit=75&cursor=dep-next", + "POST /projects/" + projectIDText + "/logs/search", + "POST /projects/" + projectIDText + "/logs/search", "POST /projects/" + projectIDText + "/frontends/" + frontendIDText + "/domain", "GET /projects/" + projectIDText + "/frontends/" + frontendIDText + "/domain", "DELETE /projects/" + projectIDText + "/frontends/" + frontendIDText + "/domain", diff --git a/internal/api/functions.go b/internal/api/functions.go index 57ae2bd..20846cd 100644 --- a/internal/api/functions.go +++ b/internal/api/functions.go @@ -6,12 +6,10 @@ import ( "encoding/json" "fmt" "mime/multipart" - "strings" "github.com/google/uuid" "github.com/Kong/volcano-cli/internal/apiclient" - apicommon "github.com/Kong/volcano-cli/internal/apiclient/common" "github.com/Kong/volcano-cli/internal/archive" ) @@ -158,23 +156,16 @@ func (c *Client) ListFunctionDeployments(ctx context.Context, projectID, functio // GetFunctionLogs returns one runtime log search page for a function. func (c *Client) GetFunctionLogs(ctx context.Context, projectID, functionID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { - resourceID := functionID.String() - body := apiclient.SearchProjectLogsJSONRequestBody{ - ResourceType: apicommon.LogSearchRequestResourceTypeFunction, - ResourceIds: &[]string{resourceID}, + body := logSearchRequest{ + Resource: logResource(logResourceTypeFunction, functionID), } if limit > 0 { body.Limit = &limit } - if cursor = strings.TrimSpace(cursor); cursor != "" { + if cursor != "" { body.Cursor = &cursor } - - resp, err := c.client.SearchProjectLogsWithResponse(ctx, projectID, body) - if err != nil { - return nil, err - } - return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON401, resp.JSON403, resp.JSON404) + return c.searchProjectLogs(ctx, projectID, body) } func buildFunctionDeployMultipart(fn FunctionDeployInput) (*bytes.Buffer, string, error) { @@ -305,19 +296,16 @@ func (c *Client) DeleteFunctionScheduler(ctx context.Context, projectID, functio return apiOK(resp.StatusCode(), resp.Body) } -// GetFunctionDeploymentLogs returns one build log page for a function deployment. -func (c *Client) GetFunctionDeploymentLogs(ctx context.Context, projectID, functionID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.ListLogsResponse, error) { - params := &apiclient.GetFunctionDeploymentLogsParams{} - if limit > 0 { - params.Limit = &limit +// GetFunctionDeploymentLogs returns one build log search page for a function deployment. +func (c *Client) GetFunctionDeploymentLogs(ctx context.Context, projectID, functionID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { + body := logSearchRequest{ + Resource: logDeploymentResource(logResourceTypeFunction, functionID, deploymentID), } - if cursor = strings.TrimSpace(cursor); cursor != "" { - params.Cursor = &cursor + if limit > 0 { + body.Limit = &limit } - - resp, err := c.client.GetFunctionDeploymentLogsWithResponse(ctx, projectID, functionID, deploymentID, params) - if err != nil { - return nil, err + if cursor != "" { + body.Cursor = &cursor } - return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON401, resp.JSON403, resp.JSON404) + return c.searchProjectLogs(ctx, projectID, body) } diff --git a/internal/api/logs.go b/internal/api/logs.go new file mode 100644 index 0000000..00ce077 --- /dev/null +++ b/internal/api/logs.go @@ -0,0 +1,74 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/google/uuid" + + "github.com/Kong/volcano-cli/internal/apiclient" +) + +const ( + logResourceTypeFrontend = "frontend" + logResourceTypeFunction = "function" +) + +type logSearchRequest struct { + Resource logRequestResource `json:"resource"` + Limit *int `json:"limit,omitempty"` + Cursor *string `json:"cursor,omitempty"` +} + +type logRequestResource struct { + Type string `json:"type"` + IDs []uuid.UUID `json:"ids,omitempty"` + Deployments *logDeploymentRequestSelector `json:"deployments,omitempty"` +} + +type logDeploymentRequestSelector struct { + IDs []uuid.UUID `json:"ids,omitempty"` +} + +func (c *Client) searchProjectLogs(ctx context.Context, projectID uuid.UUID, body logSearchRequest) (*apiclient.LogSearchResponse, error) { + if body.Limit != nil && *body.Limit <= 0 { + body.Limit = nil + } + if body.Cursor != nil { + cursor := strings.TrimSpace(*body.Cursor) + if cursor == "" { + body.Cursor = nil + } else { + body.Cursor = &cursor + } + } + + payload, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal log search request: %w", err) + } + + resp, err := c.client.SearchProjectLogsWithBodyWithResponse(ctx, projectID, "application/json", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON503) +} + +func logResource(resourceType string, resourceID uuid.UUID) logRequestResource { + return logRequestResource{ + Type: resourceType, + IDs: []uuid.UUID{resourceID}, + } +} + +func logDeploymentResource(resourceType string, resourceID, deploymentID uuid.UUID) logRequestResource { + resource := logResource(resourceType, resourceID) + resource.Deployments = &logDeploymentRequestSelector{ + IDs: []uuid.UUID{deploymentID}, + } + return resource +} diff --git a/internal/apiclient/client_test.go b/internal/apiclient/client_test.go index 5cce31b..ef37db2 100644 --- a/internal/apiclient/client_test.go +++ b/internal/apiclient/client_test.go @@ -2,33 +2,22 @@ package apiclient import ( "encoding/json" + "strings" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - apicommon "github.com/Kong/volcano-cli/internal/apiclient/common" ) func TestGetProjectLogActivityUsesPostBody(t *testing.T) { projectID := ProjectId(uuid.MustParse("11111111-1111-4111-8111-111111111111")) - resourceIDs := []string{"22222222-2222-4222-8222-222222222222"} - levels := []apicommon.LiveLogLevel{apicommon.LiveLogLevelWarn} - regions := []string{"us-east-1"} startTime := int64(1_700_000_000_000) endTime := int64(1_700_000_300_000) bucketCount := 24 + requestBody := `{"resource":{"type":"function","ids":["22222222-2222-4222-8222-222222222222"]},"levels":["warn"],"regions":["us-east-1"],"start_time":1700000000000,"end_time":1700000300000,"bucket_count":24}` - req, err := NewGetProjectLogActivityRequest("https://api.example.test", projectID, GetProjectLogActivityJSONRequestBody{ - ResourceType: apicommon.LogActivityRequestResourceTypeFunction, - ResourceIds: &resourceIDs, - Levels: &levels, - Regions: ®ions, - StartTime: &startTime, - EndTime: &endTime, - BucketCount: &bucketCount, - }) + req, err := NewGetProjectLogActivityRequestWithBody("https://api.example.test", projectID, "application/json", strings.NewReader(requestBody)) require.NoError(t, err) assert.Equal(t, "POST", req.Method) @@ -36,13 +25,15 @@ func TestGetProjectLogActivityUsesPostBody(t *testing.T) { assert.Empty(t, req.URL.RawQuery) assert.Equal(t, "application/json", req.Header.Get("Content-Type")) - var body map[string]any - require.NoError(t, json.NewDecoder(req.Body).Decode(&body)) - assert.Equal(t, "function", body["resource_type"]) - assert.Equal(t, []any{"22222222-2222-4222-8222-222222222222"}, body["resource_ids"]) - assert.Equal(t, []any{"warn"}, body["levels"]) - assert.Equal(t, []any{"us-east-1"}, body["regions"]) - assert.InEpsilon(t, startTime, body["start_time"], 0) - assert.InEpsilon(t, endTime, body["end_time"], 0) - assert.InEpsilon(t, bucketCount, body["bucket_count"], 0) + var decodedBody map[string]any + require.NoError(t, json.NewDecoder(req.Body).Decode(&decodedBody)) + resource, ok := decodedBody["resource"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "function", resource["type"]) + assert.Equal(t, []any{"22222222-2222-4222-8222-222222222222"}, resource["ids"]) + assert.Equal(t, []any{"warn"}, decodedBody["levels"]) + assert.Equal(t, []any{"us-east-1"}, decodedBody["regions"]) + assert.InEpsilon(t, startTime, decodedBody["start_time"], 0) + assert.InEpsilon(t, endTime, decodedBody["end_time"], 0) + assert.InEpsilon(t, bucketCount, decodedBody["bucket_count"], 0) } diff --git a/internal/cmd/frontends/logs.go b/internal/cmd/frontends/logs.go index 978a416..dc7b2ac 100644 --- a/internal/cmd/frontends/logs.go +++ b/internal/cmd/frontends/logs.go @@ -78,7 +78,7 @@ func runLogs(ctx context.Context, opts frontendLogsOptions) error { if logsType == frontendLogsTypeRuntime { fmt.Fprintf(opts.out, "Fetching runtime logs for frontend %s\n\n", frontend.Name) - return output.PrintLogs(opts.out, func(cursor string) (*apiclient.ListLogsResponse, error) { + return output.PrintSearchLogs(opts.out, func(cursor string) (*apiclient.LogSearchResponse, error) { return service.RuntimeLogs(ctx, frontend.Id, opts.limit, cursor) }) } @@ -110,7 +110,7 @@ func runLogs(ctx context.Context, opts frontendLogsOptions) error { } fmt.Fprintf(opts.out, "Fetching build logs for frontend %s deployment %s\n\n", frontend.Name, deploymentID.String()) - return output.PrintLogs(opts.out, func(cursor string) (*apiclient.ListLogsResponse, error) { + return output.PrintSearchLogs(opts.out, func(cursor string) (*apiclient.LogSearchResponse, error) { return service.DeploymentLogs(ctx, frontend.Id, *deploymentID, opts.limit, cursor) }) } diff --git a/internal/cmd/frontends/logs_test.go b/internal/cmd/frontends/logs_test.go index 4063102..605a236 100644 --- a/internal/cmd/frontends/logs_test.go +++ b/internal/cmd/frontends/logs_test.go @@ -1,6 +1,7 @@ package frontends import ( + "encoding/json" "net/http" "net/http/httptest" "testing" @@ -17,7 +18,7 @@ func TestFrontendsLogs(t *testing.T) { t.Run("runtime pages with next token", func(t *testing.T) { setFrontendCommandTestHome(t) saveFrontendCommandTestConfig(t) - var logQueries []string + var logBodies []map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/projects/"+frontendProjectID+"/frontends": @@ -28,13 +29,15 @@ func TestFrontendsLogs(t *testing.T) { "limit": 100, "total": 1, }) - case r.Method == http.MethodGet && r.URL.Path == "/projects/"+frontendProjectID+"/frontends/"+frontendID+"/logs": - logQueries = append(logQueries, r.URL.RawQuery) - if r.URL.Query().Get("cursor") == "" { + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+frontendProjectID+"/logs/search": + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + logBodies = append(logBodies, body) + if body["cursor"] == nil { writeFrontendCommandJSON(t, w, http.StatusOK, frontendLogCommandResponse("first runtime", true, "next token")) return } - assert.Equal(t, "next token", r.URL.Query().Get("cursor")) + assert.Equal(t, "next token", body["cursor"]) writeFrontendCommandJSON(t, w, http.StatusOK, frontendLogCommandResponse("second runtime", false, "")) default: http.NotFound(w, r) @@ -44,7 +47,14 @@ func TestFrontendsLogs(t *testing.T) { out, err := executeFrontendsCommand(t, New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "logs", "web", "--type", "runtime", "--limit", "2") require.NoError(t, err) - assert.Equal(t, []string{"limit=2", "limit=2&cursor=next+token"}, logQueries) + require.Len(t, logBodies, 2) + resource, ok := logBodies[0]["resource"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "frontend", resource["type"]) + assert.Equal(t, []any{frontendID}, resource["ids"]) + assert.InEpsilon(t, 2, logBodies[0]["limit"], 0) + assert.NotContains(t, logBodies[0], "cursor") + assert.Equal(t, "next token", logBodies[1]["cursor"]) assert.Contains(t, out, "Fetching runtime logs for frontend web") assert.Contains(t, out, "first runtime") assert.Contains(t, out, "second runtime") @@ -112,7 +122,16 @@ func frontendLogsBuildServer(t *testing.T, logDeploymentID string, includeCurren "limit": 100, "total": 1, }) - case r.Method == http.MethodGet && r.URL.Path == "/projects/"+frontendProjectID+"/frontends/"+frontendID+"/deployments/"+logDeploymentID+"/logs": + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+frontendProjectID+"/logs/search": + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + resource, ok := body["resource"].(map[string]any) + require.True(t, ok) + deployments, ok := resource["deployments"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "frontend", resource["type"]) + assert.Equal(t, []any{frontendID}, resource["ids"]) + assert.Equal(t, []any{logDeploymentID}, deployments["ids"]) writeFrontendCommandJSON(t, w, http.StatusOK, frontendLogCommandResponse("build log", false, "")) default: http.NotFound(w, r) diff --git a/internal/cmd/functions/logs.go b/internal/cmd/functions/logs.go index 85d4312..0409877 100644 --- a/internal/cmd/functions/logs.go +++ b/internal/cmd/functions/logs.go @@ -101,7 +101,7 @@ func runLogs(ctx context.Context, opts logsOptions) error { } fmt.Fprintf(opts.out, "Fetching build logs for function %s deployment %s\n\n", function.Name, deploymentID.String()) - return output.PrintLogs(opts.out, func(cursor string) (*apiclient.ListLogsResponse, error) { + return output.PrintSearchLogs(opts.out, func(cursor string) (*apiclient.LogSearchResponse, error) { return service.DeploymentLogs(ctx, function.Id, *deploymentID, opts.limit, cursor) }) } diff --git a/internal/cmd/functions/logs_test.go b/internal/cmd/functions/logs_test.go index ed74803..1a2b356 100644 --- a/internal/cmd/functions/logs_test.go +++ b/internal/cmd/functions/logs_test.go @@ -51,8 +51,10 @@ func TestFunctionsLogs(t *testing.T) { out, err := executeFunctionsCommand(t, New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "logs", "hello", "--type", "runtime", "--limit", "2") require.NoError(t, err) require.Len(t, logBodies, 2) - assert.Equal(t, "function", logBodies[0]["resource_type"]) - assert.Equal(t, []any{functionID}, logBodies[0]["resource_ids"]) + resource, ok := logBodies[0]["resource"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "function", resource["type"]) + assert.Equal(t, []any{functionID}, resource["ids"]) assert.InEpsilon(t, 2, logBodies[0]["limit"], 0) assert.NotContains(t, logBodies[0], "cursor") assert.Equal(t, "next token", logBodies[1]["cursor"]) @@ -133,7 +135,16 @@ func functionLogsBuildServer(t *testing.T, logDeploymentID string, includeCurren "limit": 100, "total": 1, }) - case r.Method == http.MethodGet && r.URL.Path == "/projects/"+functionProjectID+"/functions/"+functionID+"/deployments/"+logDeploymentID+"/logs": + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+functionProjectID+"/logs/search": + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + resource, ok := body["resource"].(map[string]any) + require.True(t, ok) + deployments, ok := resource["deployments"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "function", resource["type"]) + assert.Equal(t, []any{functionID}, resource["ids"]) + assert.Equal(t, []any{logDeploymentID}, deployments["ids"]) writeFunctionCommandJSON(t, w, http.StatusOK, logCommandResponse("build log", false, "")) default: http.NotFound(w, r) diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index 3413da8..923275e 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -183,8 +183,8 @@ func (s Service) LatestDeployment(ctx context.Context, frontendID uuid.UUID) (*a return &deployments.Data[0], nil } -// RuntimeLogs returns one runtime log page for a frontend. -func (s Service) RuntimeLogs(ctx context.Context, frontendID uuid.UUID, limit int, cursor string) (*apiclient.ListLogsResponse, error) { +// RuntimeLogs returns one runtime log search page for a frontend. +func (s Service) RuntimeLogs(ctx context.Context, frontendID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { authenticated, err := s.sessions.CurrentProject() if err != nil { return nil, err @@ -197,8 +197,8 @@ func (s Service) RuntimeLogs(ctx context.Context, frontendID uuid.UUID, limit in return logs, nil } -// DeploymentLogs returns one build log page for a frontend deployment. -func (s Service) DeploymentLogs(ctx context.Context, frontendID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.ListLogsResponse, error) { +// DeploymentLogs returns one build log search page for a frontend deployment. +func (s Service) DeploymentLogs(ctx context.Context, frontendID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { authenticated, err := s.sessions.CurrentProject() if err != nil { return nil, err diff --git a/internal/function/function.go b/internal/function/function.go index 5b6e51b..b22d723 100644 --- a/internal/function/function.go +++ b/internal/function/function.go @@ -444,8 +444,8 @@ func (s Service) RuntimeLogs(ctx context.Context, functionID uuid.UUID, limit in return logs, nil } -// DeploymentLogs returns one build log page for a function deployment. -func (s Service) DeploymentLogs(ctx context.Context, functionID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.ListLogsResponse, error) { +// DeploymentLogs returns one build log search page for a function deployment. +func (s Service) DeploymentLogs(ctx context.Context, functionID, deploymentID uuid.UUID, limit int, cursor string) (*apiclient.LogSearchResponse, error) { authenticated, err := s.sessions.CurrentProject() if err != nil { return nil, err diff --git a/internal/output/logs.go b/internal/output/logs.go index 61f3814..3c8d639 100644 --- a/internal/output/logs.go +++ b/internal/output/logs.go @@ -7,35 +7,9 @@ import ( "github.com/Kong/volcano-cli/internal/apiclient" ) -// LogsFetcher fetches one page of log events. An empty cursor requests the first page. -type LogsFetcher func(cursor string) (*apiclient.ListLogsResponse, error) - // SearchLogsFetcher fetches one page of searched log events. An empty cursor requests the first page. type SearchLogsFetcher func(cursor string) (*apiclient.LogSearchResponse, error) -// PrintLogs renders paginated log events from fetch until the response signals -// no more pages or the cursor cannot be advanced. -func PrintLogs(w io.Writer, fetch LogsFetcher) error { - cursor := "" - for { - resp, err := fetch(cursor) - if err != nil { - return err - } - if resp == nil { - return nil - } - LogEvents(w, resp.Data) - if !resp.HasMore || resp.NextCursor == nil { - return nil - } - cursor = strings.TrimSpace(*resp.NextCursor) - if cursor == "" { - return nil - } - } -} - // PrintSearchLogs renders paginated searched log events from fetch until the // response signals no more pages or the cursor cannot be advanced. func PrintSearchLogs(w io.Writer, fetch SearchLogsFetcher) error { From ae53bcd7fa66dcc1563c6ef6a9624908fc2adb42 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:48:01 -0400 Subject: [PATCH 2/2] test(logs): build activity request body from value --- internal/apiclient/client_test.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/internal/apiclient/client_test.go b/internal/apiclient/client_test.go index ef37db2..359246a 100644 --- a/internal/apiclient/client_test.go +++ b/internal/apiclient/client_test.go @@ -1,8 +1,8 @@ package apiclient import ( + "bytes" "encoding/json" - "strings" "testing" "github.com/google/uuid" @@ -12,12 +12,26 @@ import ( func TestGetProjectLogActivityUsesPostBody(t *testing.T) { projectID := ProjectId(uuid.MustParse("11111111-1111-4111-8111-111111111111")) + resourceIDs := []string{"22222222-2222-4222-8222-222222222222"} + levels := []string{"warn"} + regions := []string{"us-east-1"} startTime := int64(1_700_000_000_000) endTime := int64(1_700_000_300_000) bucketCount := 24 - requestBody := `{"resource":{"type":"function","ids":["22222222-2222-4222-8222-222222222222"]},"levels":["warn"],"regions":["us-east-1"],"start_time":1700000000000,"end_time":1700000300000,"bucket_count":24}` + requestBody, err := json.Marshal(map[string]any{ + "resource": map[string]any{ + "type": "function", + "ids": resourceIDs, + }, + "levels": levels, + "regions": regions, + "start_time": startTime, + "end_time": endTime, + "bucket_count": bucketCount, + }) + require.NoError(t, err) - req, err := NewGetProjectLogActivityRequestWithBody("https://api.example.test", projectID, "application/json", strings.NewReader(requestBody)) + req, err := NewGetProjectLogActivityRequestWithBody("https://api.example.test", projectID, "application/json", bytes.NewReader(requestBody)) require.NoError(t, err) assert.Equal(t, "POST", req.Method) @@ -30,9 +44,9 @@ func TestGetProjectLogActivityUsesPostBody(t *testing.T) { resource, ok := decodedBody["resource"].(map[string]any) require.True(t, ok) assert.Equal(t, "function", resource["type"]) - assert.Equal(t, []any{"22222222-2222-4222-8222-222222222222"}, resource["ids"]) - assert.Equal(t, []any{"warn"}, decodedBody["levels"]) - assert.Equal(t, []any{"us-east-1"}, decodedBody["regions"]) + assert.Equal(t, []any{resourceIDs[0]}, resource["ids"]) + assert.Equal(t, []any{levels[0]}, decodedBody["levels"]) + assert.Equal(t, []any{regions[0]}, decodedBody["regions"]) assert.InEpsilon(t, startTime, decodedBody["start_time"], 0) assert.InEpsilon(t, endTime, decodedBody["end_time"], 0) assert.InEpsilon(t, bucketCount, decodedBody["bucket_count"], 0)