diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index 7faa170155..11bc9102d1 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -653,6 +653,12 @@ type SandboxLogs struct { Logs []SandboxLog `json:"logs"` } +// SandboxLogsV2Response defines model for SandboxLogsV2Response. +type SandboxLogsV2Response struct { + // Logs Sandbox logs structured + Logs []SandboxLogEntry `json:"logs"` +} + // SandboxMetadata defines model for SandboxMetadata. type SandboxMetadata map[string]string @@ -1268,6 +1274,16 @@ type GetV2SandboxesParams struct { Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` } +// GetV2SandboxesSandboxIDLogsParams defines parameters for GetV2SandboxesSandboxIDLogs. +type GetV2SandboxesSandboxIDLogsParams struct { + // Cursor Starting timestamp of the logs that should be returned in milliseconds + Cursor *int64 `form:"cursor,omitempty" json:"cursor,omitempty"` + + // Limit Maximum number of logs that should be returned + Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + Direction *LogsDirection `form:"direction,omitempty" json:"direction,omitempty"` +} + // PostAccessTokensJSONRequestBody defines body for PostAccessTokens for application/json ContentType. type PostAccessTokensJSONRequestBody = NewAccessToken @@ -1662,6 +1678,9 @@ type ClientInterface interface { // GetV2Sandboxes request GetV2Sandboxes(ctx context.Context, params *GetV2SandboxesParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetV2SandboxesSandboxIDLogs request + GetV2SandboxesSandboxIDLogs(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostV2TemplatesWithBody request with any body PostV2TemplatesWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2331,6 +2350,18 @@ func (c *Client) GetV2Sandboxes(ctx context.Context, params *GetV2SandboxesParam return c.Client.Do(req) } +func (c *Client) GetV2SandboxesSandboxIDLogs(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV2SandboxesSandboxIDLogsRequest(c.Server, sandboxID, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PostV2TemplatesWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPostV2TemplatesRequestWithBody(c.Server, contentType, body) if err != nil { @@ -4395,6 +4426,94 @@ func NewGetV2SandboxesRequest(server string, params *GetV2SandboxesParams) (*htt return req, nil } +// NewGetV2SandboxesSandboxIDLogsRequest generates requests for GetV2SandboxesSandboxIDLogs +func NewGetV2SandboxesSandboxIDLogsRequest(server string, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "sandboxID", runtime.ParamLocationPath, sandboxID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v2/sandboxes/%s/logs", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Cursor != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "cursor", runtime.ParamLocationQuery, *params.Cursor); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Direction != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "direction", runtime.ParamLocationQuery, *params.Direction); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPostV2TemplatesRequest calls the generic PostV2Templates builder with application/json body func NewPostV2TemplatesRequest(server string, body PostV2TemplatesJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -4767,6 +4886,9 @@ type ClientWithResponsesInterface interface { // GetV2SandboxesWithResponse request GetV2SandboxesWithResponse(ctx context.Context, params *GetV2SandboxesParams, reqEditors ...RequestEditorFn) (*GetV2SandboxesResponse, error) + // GetV2SandboxesSandboxIDLogsWithResponse request + GetV2SandboxesSandboxIDLogsWithResponse(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*GetV2SandboxesSandboxIDLogsResponse, error) + // PostV2TemplatesWithBodyWithResponse request with any body PostV2TemplatesWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV2TemplatesResponse, error) @@ -5773,6 +5895,31 @@ func (r GetV2SandboxesResponse) StatusCode() int { return 0 } +type GetV2SandboxesSandboxIDLogsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *SandboxLogsV2Response + JSON401 *N401 + JSON404 *N404 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r GetV2SandboxesSandboxIDLogsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV2SandboxesSandboxIDLogsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PostV2TemplatesResponse struct { Body []byte HTTPResponse *http.Response @@ -6343,6 +6490,15 @@ func (c *ClientWithResponses) GetV2SandboxesWithResponse(ctx context.Context, pa return ParseGetV2SandboxesResponse(rsp) } +// GetV2SandboxesSandboxIDLogsWithResponse request returning *GetV2SandboxesSandboxIDLogsResponse +func (c *ClientWithResponses) GetV2SandboxesSandboxIDLogsWithResponse(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*GetV2SandboxesSandboxIDLogsResponse, error) { + rsp, err := c.GetV2SandboxesSandboxIDLogs(ctx, sandboxID, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV2SandboxesSandboxIDLogsResponse(rsp) +} + // PostV2TemplatesWithBodyWithResponse request with arbitrary body returning *PostV2TemplatesResponse func (c *ClientWithResponses) PostV2TemplatesWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV2TemplatesResponse, error) { rsp, err := c.PostV2TemplatesWithBody(ctx, contentType, body, reqEditors...) @@ -8186,6 +8342,53 @@ func ParseGetV2SandboxesResponse(rsp *http.Response) (*GetV2SandboxesResponse, e return response, nil } +// ParseGetV2SandboxesSandboxIDLogsResponse parses an HTTP response from a GetV2SandboxesSandboxIDLogsWithResponse call +func ParseGetV2SandboxesSandboxIDLogsResponse(rsp *http.Response) (*GetV2SandboxesSandboxIDLogsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV2SandboxesSandboxIDLogsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest SandboxLogsV2Response + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest N404 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePostV2TemplatesResponse parses an HTTP response from a PostV2TemplatesWithResponse call func ParsePostV2TemplatesResponse(rsp *http.Response) (*PostV2TemplatesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -8483,6 +8686,9 @@ type ServerInterface interface { // (GET /v2/sandboxes) GetV2Sandboxes(c *gin.Context, params GetV2SandboxesParams) + // (GET /v2/sandboxes/{sandboxID}/logs) + GetV2SandboxesSandboxIDLogs(c *gin.Context, sandboxID SandboxID, params GetV2SandboxesSandboxIDLogsParams) + // (POST /v2/templates) PostV2Templates(c *gin.Context) @@ -9833,6 +10039,63 @@ func (siw *ServerInterfaceWrapper) GetV2Sandboxes(c *gin.Context) { siw.Handler.GetV2Sandboxes(c, params) } +// GetV2SandboxesSandboxIDLogs operation middleware +func (siw *ServerInterfaceWrapper) GetV2SandboxesSandboxIDLogs(c *gin.Context) { + + var err error + + // ------------- Path parameter "sandboxID" ------------- + var sandboxID SandboxID + + err = runtime.BindStyledParameterWithOptions("simple", "sandboxID", c.Param("sandboxID"), &sandboxID, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sandboxID: %w", err), http.StatusBadRequest) + return + } + + c.Set(ApiKeyAuthScopes, []string{}) + + c.Set(Supabase1TokenAuthScopes, []string{}) + + c.Set(Supabase2TeamAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetV2SandboxesSandboxIDLogsParams + + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", c.Request.URL.Query(), ¶ms.Cursor) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter cursor: %w", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "direction" ------------- + + err = runtime.BindQueryParameter("form", true, false, "direction", c.Request.URL.Query(), ¶ms.Direction) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter direction: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetV2SandboxesSandboxIDLogs(c, sandboxID, params) +} + // PostV2Templates operation middleware func (siw *ServerInterfaceWrapper) PostV2Templates(c *gin.Context) { @@ -10009,6 +10272,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.GET(options.BaseURL+"/templates/:templateID/builds/:buildID/status", wrapper.GetTemplatesTemplateIDBuildsBuildIDStatus) router.GET(options.BaseURL+"/templates/:templateID/files/:hash", wrapper.GetTemplatesTemplateIDFilesHash) router.GET(options.BaseURL+"/v2/sandboxes", wrapper.GetV2Sandboxes) + router.GET(options.BaseURL+"/v2/sandboxes/:sandboxID/logs", wrapper.GetV2SandboxesSandboxIDLogs) router.POST(options.BaseURL+"/v2/templates", wrapper.PostV2Templates) router.PATCH(options.BaseURL+"/v2/templates/:templateID", wrapper.PatchV2TemplatesTemplateID) router.POST(options.BaseURL+"/v2/templates/:templateID/builds/:buildID", wrapper.PostV2TemplatesTemplateIDBuildsBuildID) @@ -10018,140 +10282,141 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "o/vB1uJxfsNp7z7f/V8AAAD//7QUZQDJ9wAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/api/internal/clusters/resources.go b/packages/api/internal/clusters/resources.go index c0cdb6ec10..c29bd56797 100644 --- a/packages/api/internal/clusters/resources.go +++ b/packages/api/internal/clusters/resources.go @@ -20,26 +20,26 @@ import ( type ClusterResource interface { GetSandboxMetrics(ctx context.Context, teamID string, sandboxID string, qStart *int64, qEnd *int64) ([]api.SandboxMetric, *api.APIError) GetSandboxesMetrics(ctx context.Context, teamID string, sandboxIDs []string) (map[string]api.SandboxMetric, *api.APIError) - GetSandboxLogs(ctx context.Context, teamID string, sandboxID string, start *int64, limit *int32) (api.SandboxLogs, *api.APIError) + GetSandboxLogs(ctx context.Context, teamID string, sandboxID string, start *int64, end *int64, limit *int32, direction *api.LogsDirection) (api.SandboxLogs, *api.APIError) GetBuildLogs(ctx context.Context, nodeID *string, templateID string, buildID string, offset int32, limit int32, level *logs.LogLevel, cursor *time.Time, direction api.LogsDirection, source *api.LogsSource) ([]logs.LogEntry, *api.APIError) } const ( - maxTimeRangeDuration = 7 * 24 * time.Hour + MaxTimeRangeDuration = 7 * 24 * time.Hour ) func logQueryWindow(cursor *time.Time, direction api.LogsDirection) (time.Time, time.Time) { - start, end := time.Now().Add(-maxTimeRangeDuration), time.Now() + start, end := time.Now().Add(-MaxTimeRangeDuration), time.Now() if cursor == nil { return start, end } if direction == api.LogsDirectionForward { start = *cursor - end = start.Add(maxTimeRangeDuration) + end = start.Add(MaxTimeRangeDuration) } else { end = *cursor - start = end.Add(-maxTimeRangeDuration) + start = end.Add(-MaxTimeRangeDuration) } return start, end diff --git a/packages/api/internal/clusters/resources_local.go b/packages/api/internal/clusters/resources_local.go index 06a28dd0e5..2041cd0558 100644 --- a/packages/api/internal/clusters/resources_local.go +++ b/packages/api/internal/clusters/resources_local.go @@ -22,12 +22,6 @@ type LocalClusterResourceProvider struct { instances *smap.Map[*Instance] } -const ( - sandboxLogsOldestLimit = 168 * time.Hour // 7 days - defaultLogsLimit = 1000 - defaultDirection = logproto.FORWARD -) - func newLocalClusterResourceProvider( querySandboxMetricsProvider clickhouse.SandboxQueriesProvider, queryLogsProvider *loki.LokiQueryProvider, @@ -115,22 +109,39 @@ func (l *LocalClusterResourceProvider) GetSandboxesMetrics(ctx context.Context, return metrics, nil } -func (l *LocalClusterResourceProvider) GetSandboxLogs(ctx context.Context, teamID string, sandboxID string, qStart *int64, qLimit *int32) (api.SandboxLogs, *api.APIError) { - end := time.Now() - var start time.Time - - if qStart != nil { - start = time.UnixMilli(*qStart) +func (l *LocalClusterResourceProvider) GetSandboxLogs( + ctx context.Context, + teamID string, + sandboxID string, + start *int64, + end *int64, + limit *int32, + direction *api.LogsDirection, +) (api.SandboxLogs, *api.APIError) { + endTime := time.Now() + var startTime time.Time + + if start != nil { + startTime = time.UnixMilli(*start) } else { - start = end.Add(-sandboxLogsOldestLimit) + startTime = endTime.Add(-MaxTimeRangeDuration) } - limit := defaultLogsLimit - if qLimit != nil { - limit = int(*qLimit) + if end != nil { + endTime = time.UnixMilli(*end) + } + + lokiLimit := loki.DefaultLogsLimit + if limit != nil { + lokiLimit = int(*limit) + } + + lokiDirection := loki.DefaultDirection + if direction != nil && *direction == api.LogsDirectionBackward { + lokiDirection = logproto.BACKWARD } - raw, err := l.queryLogsProvider.QuerySandboxLogs(ctx, teamID, sandboxID, start, end, limit) + raw, err := l.queryLogsProvider.QuerySandboxLogs(ctx, teamID, sandboxID, startTime, endTime, lokiLimit, lokiDirection) if err != nil { return api.SandboxLogs{}, &api.APIError{ Err: fmt.Errorf("error when fetching sandbox logs: %w", err), @@ -172,7 +183,7 @@ func (l *LocalClusterResourceProvider) GetBuildLogs( // Use shared implementation with Loki as the persistent log backend start, end := logQueryWindow(cursor, direction) - lokiDirection := defaultDirection + lokiDirection := loki.DefaultDirection if direction == api.LogsDirectionBackward { lokiDirection = logproto.BACKWARD } diff --git a/packages/api/internal/clusters/resources_remote.go b/packages/api/internal/clusters/resources_remote.go index d4dfc267b8..05b06a362e 100644 --- a/packages/api/internal/clusters/resources_remote.go +++ b/packages/api/internal/clusters/resources_remote.go @@ -25,7 +25,13 @@ func newRemoteClusterResourceProvider(instances *smap.Map[*Instance], client *ed } } -func (r *ClusterResourceProviderImpl) GetSandboxMetrics(ctx context.Context, teamID string, sandboxID string, qStart *int64, qEnd *int64) ([]api.SandboxMetric, *api.APIError) { +func (r *ClusterResourceProviderImpl) GetSandboxMetrics( + ctx context.Context, + teamID string, + sandboxID string, + qStart *int64, + qEnd *int64, +) ([]api.SandboxMetric, *api.APIError) { req := &edgeapi.V1SandboxMetricsParams{ TeamID: teamID, Start: qStart, @@ -63,7 +69,11 @@ func (r *ClusterResourceProviderImpl) GetSandboxMetrics(ctx context.Context, tea return items, nil } -func (r *ClusterResourceProviderImpl) GetSandboxesMetrics(ctx context.Context, teamID string, sandboxIDs []string) (map[string]api.SandboxMetric, *api.APIError) { +func (r *ClusterResourceProviderImpl) GetSandboxesMetrics( + ctx context.Context, + teamID string, + sandboxIDs []string, +) (map[string]api.SandboxMetric, *api.APIError) { res, err := r.client.V1SandboxesMetricsWithResponse(ctx, &edgeapi.V1SandboxesMetricsParams{TeamID: teamID, SandboxIds: sandboxIDs}) if err != nil { return nil, &api.APIError{ @@ -95,8 +105,27 @@ func (r *ClusterResourceProviderImpl) GetSandboxesMetrics(ctx context.Context, t return items, nil } -func (r *ClusterResourceProviderImpl) GetSandboxLogs(ctx context.Context, teamID string, sandboxID string, start *int64, limit *int32) (api.SandboxLogs, *api.APIError) { - res, err := r.client.V1SandboxLogsWithResponse(ctx, sandboxID, &edgeapi.V1SandboxLogsParams{TeamID: teamID, Start: start, Limit: limit}) +func (r *ClusterResourceProviderImpl) GetSandboxLogs( + ctx context.Context, + teamID string, + sandboxID string, + start *int64, + end *int64, + limit *int32, + direction *api.LogsDirection, +) (api.SandboxLogs, *api.APIError) { + edgeDirection := edgeapi.V1SandboxLogsParamsDirectionForward + if direction != nil && *direction == api.LogsDirectionBackward { + edgeDirection = edgeapi.V1SandboxLogsParamsDirectionBackward + } + + res, err := r.client.V1SandboxLogsWithResponse(ctx, sandboxID, &edgeapi.V1SandboxLogsParams{ + TeamID: teamID, + Start: start, + End: end, + Limit: limit, + Direction: utils.ToPtr(edgeDirection), + }) if err != nil { return api.SandboxLogs{}, &api.APIError{ Err: err, diff --git a/packages/api/internal/handlers/sandbox_logs.go b/packages/api/internal/handlers/sandbox_logs.go index d7f6ba1c8e..9ab89157ff 100644 --- a/packages/api/internal/handlers/sandbox_logs.go +++ b/packages/api/internal/handlers/sandbox_logs.go @@ -3,12 +3,14 @@ package handlers import ( "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "go.opentelemetry.io/otel/attribute" "github.com/e2b-dev/infra/packages/api/internal/api" "github.com/e2b-dev/infra/packages/api/internal/auth" + "github.com/e2b-dev/infra/packages/api/internal/clusters" "github.com/e2b-dev/infra/packages/api/internal/db/types" "github.com/e2b-dev/infra/packages/api/internal/utils" "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" @@ -34,7 +36,7 @@ func (a *APIStore) GetSandboxesSandboxIDLogs(c *gin.Context, sandboxID string, p return } - logs, apiErr := cluster.GetResources().GetSandboxLogs(ctx, team.ID.String(), sandboxID, params.Start, params.Limit) + logs, apiErr := cluster.GetResources().GetSandboxLogs(ctx, team.ID.String(), sandboxID, params.Start, nil, params.Limit, nil) if apiErr != nil { telemetry.ReportCriticalError(ctx, "error when returning logs for sandbox", apiErr.Err) a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) @@ -44,3 +46,57 @@ func (a *APIStore) GetSandboxesSandboxIDLogs(c *gin.Context, sandboxID string, p c.JSON(http.StatusOK, logs) } + +func (a *APIStore) GetV2SandboxesSandboxIDLogs(c *gin.Context, sandboxID api.SandboxID, params api.GetV2SandboxesSandboxIDLogsParams) { + ctx := c.Request.Context() + sandboxID = utils.ShortID(sandboxID) + + team := c.Value(auth.TeamContextKey).(*types.Team) + + telemetry.SetAttributes(ctx, + attribute.String("instance.id", sandboxID), + telemetry.WithTeamID(team.ID.String()), + ) + + clusterID := utils.WithClusterFallback(team.ClusterID) + cluster, ok := a.clusters.GetClusterById(clusterID) + if !ok { + telemetry.ReportCriticalError(ctx, "error getting cluster by ID", fmt.Errorf("cluster with ID '%s' not found", clusterID)) + a.sendAPIStoreError(c, http.StatusInternalServerError, fmt.Sprintf("Error getting cluster '%s'", clusterID)) + + return + } + + // Default to forward direction if not specified + direction := api.LogsDirectionForward + if params.Direction != nil { + direction = *params.Direction + } + + start, end := time.Now().Add(-clusters.MaxTimeRangeDuration), time.Now() + if params.Cursor != nil { + cursor := time.UnixMilli(*params.Cursor) + if direction == api.LogsDirectionForward { + start = cursor + end = cursor.Add(clusters.MaxTimeRangeDuration) + } else { + end = cursor + start = cursor.Add(-clusters.MaxTimeRangeDuration) + } + } + + startMs := start.UnixMilli() + endMs := end.UnixMilli() + + logs, apiErr := cluster.GetResources().GetSandboxLogs(ctx, team.ID.String(), sandboxID, &startMs, &endMs, params.Limit, params.Direction) + if apiErr != nil { + telemetry.ReportCriticalError(ctx, "error when returning logs for sandbox", apiErr.Err) + a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) + + return + } + + c.JSON(http.StatusOK, api.SandboxLogsV2Response{ + Logs: logs.LogEntries, + }) +} diff --git a/packages/shared/pkg/http/edge/generated.go b/packages/shared/pkg/http/edge/generated.go index 4dfaad3f4a..23d21ea3ca 100644 --- a/packages/shared/pkg/http/edge/generated.go +++ b/packages/shared/pkg/http/edge/generated.go @@ -41,10 +41,16 @@ const ( LogLevelWarn LogLevel = "warn" ) +// Defines values for V1SandboxLogsParamsDirection. +const ( + V1SandboxLogsParamsDirectionBackward V1SandboxLogsParamsDirection = "backward" + V1SandboxLogsParamsDirectionForward V1SandboxLogsParamsDirection = "forward" +) + // Defines values for V1TemplateBuildLogsParamsDirection. const ( - Backward V1TemplateBuildLogsParamsDirection = "backward" - Forward V1TemplateBuildLogsParamsDirection = "forward" + V1TemplateBuildLogsParamsDirectionBackward V1TemplateBuildLogsParamsDirection = "backward" + V1TemplateBuildLogsParamsDirectionForward V1TemplateBuildLogsParamsDirection = "forward" ) // BuildLogEntry defines model for BuildLogEntry. @@ -226,10 +232,19 @@ type V1SandboxLogsParams struct { // Start Starting timestamp of the logs that should be returned in milliseconds Start *int64 `form:"start,omitempty" json:"start,omitempty"` + // End Ending timestamp of the logs that should be returned in milliseconds + End *int64 `form:"end,omitempty" json:"end,omitempty"` + // Limit Maximum number of logs that should be returned Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + + // Direction Direction of the logs that should be returned. Defaults to forward + Direction *V1SandboxLogsParamsDirection `form:"direction,omitempty" json:"direction,omitempty"` } +// V1SandboxLogsParamsDirection defines parameters for V1SandboxLogs. +type V1SandboxLogsParamsDirection string + // V1SandboxMetricsParams defines parameters for V1SandboxMetrics. type V1SandboxMetricsParams struct { // TeamID Team ID that owns the sandbox @@ -669,6 +684,22 @@ func NewV1SandboxLogsRequest(server string, sandboxID string, params *V1SandboxL } + if params.End != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "end", runtime.ParamLocationQuery, *params.End); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + if params.Limit != nil { if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { @@ -685,6 +716,22 @@ func NewV1SandboxLogsRequest(server string, sandboxID string, params *V1SandboxL } + if params.Direction != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "direction", runtime.ParamLocationQuery, *params.Direction); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() } diff --git a/packages/shared/pkg/logs/loki/provider.go b/packages/shared/pkg/logs/loki/provider.go index 26b2c739bd..afd8c6f84d 100644 --- a/packages/shared/pkg/logs/loki/provider.go +++ b/packages/shared/pkg/logs/loki/provider.go @@ -19,6 +19,11 @@ type LokiQueryProvider struct { client *loki.DefaultClient } +const ( + DefaultLogsLimit = 1000 + DefaultDirection = logproto.FORWARD +) + func NewLokiQueryProvider(lokiURL string, lokiUser string, lokiPassword string) (*LokiQueryProvider, error) { lokiClient := &loki.DefaultClient{ Address: lokiURL, @@ -29,7 +34,17 @@ func NewLokiQueryProvider(lokiURL string, lokiUser string, lokiPassword string) return &LokiQueryProvider{client: lokiClient}, nil } -func (l *LokiQueryProvider) QueryBuildLogs(ctx context.Context, templateID string, buildID string, start time.Time, end time.Time, limit int, offset int32, level *logs.LogLevel, direction logproto.Direction) ([]logs.LogEntry, error) { +func (l *LokiQueryProvider) QueryBuildLogs( + ctx context.Context, + templateID string, + buildID string, + start time.Time, + end time.Time, + limit int, + offset int32, + level *logs.LogLevel, + direction logproto.Direction, +) ([]logs.LogEntry, error) { // https://grafana.com/blog/2021/01/05/how-to-escape-special-characters-with-lokis-logql/ templateIDSanitized := strings.ReplaceAll(templateID, "`", "") buildIDSanitized := strings.ReplaceAll(buildID, "`", "") @@ -56,14 +71,22 @@ func (l *LokiQueryProvider) QueryBuildLogs(ctx context.Context, templateID strin return lm, nil } -func (l *LokiQueryProvider) QuerySandboxLogs(ctx context.Context, teamID string, sandboxID string, start time.Time, end time.Time, limit int) ([]logs.LogEntry, error) { +func (l *LokiQueryProvider) QuerySandboxLogs( + ctx context.Context, + teamID string, + sandboxID string, + start time.Time, + end time.Time, + limit int, + direction logproto.Direction, +) ([]logs.LogEntry, error) { // https://grafana.com/blog/2021/01/05/how-to-escape-special-characters-with-lokis-logql/ sandboxIdSanitized := strings.ReplaceAll(sandboxID, "`", "") teamIdSanitized := strings.ReplaceAll(teamID, "`", "") query := fmt.Sprintf("{teamID=`%s`, sandboxID=`%s`, category!=\"metrics\"}", teamIdSanitized, sandboxIdSanitized) - res, err := l.client.QueryRange(query, limit, start, end, logproto.FORWARD, time.Duration(0), time.Duration(0), true) + res, err := l.client.QueryRange(query, limit, start, end, direction, time.Duration(0), time.Duration(0), true) if err != nil { telemetry.ReportError(ctx, "error when returning logs for sandbox", err) logger.L().Error(ctx, "error when returning logs for sandbox", zap.Error(err), logger.WithSandboxID(sandboxID)) diff --git a/spec/openapi-edge.yml b/spec/openapi-edge.yml index bc95f80c25..8f1235dcb0 100644 --- a/spec/openapi-edge.yml +++ b/spec/openapi-edge.yml @@ -403,6 +403,13 @@ paths: format: int64 minimum: 0 description: Starting timestamp of the logs that should be returned in milliseconds + - in: query + name: end + schema: + type: integer + format: int64 + minimum: 0 + description: Ending timestamp of the logs that should be returned in milliseconds - in: query name: limit schema: @@ -411,6 +418,14 @@ paths: minimum: 0 type: integer description: Maximum number of logs that should be returned + - in: query + name: direction + schema: + type: string + enum: + - forward + - backward + description: Direction of the logs that should be returned. Defaults to forward responses: "200": description: Successfully returned the sandbox logs diff --git a/spec/openapi.yml b/spec/openapi.yml index dab7c95535..6ce612858d 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -306,6 +306,17 @@ components: items: $ref: "#/components/schemas/SandboxLogEntry" + SandboxLogsV2Response: + required: + - logs + properties: + logs: + default: [] + description: Sandbox logs structured + type: array + items: + $ref: "#/components/schemas/SandboxLogEntry" + SandboxMetric: description: Metric entry with timestamp and line required: @@ -1877,7 +1888,8 @@ paths: /sandboxes/{sandboxID}/logs: get: - description: Get sandbox logs + description: Get sandbox logs. Use /v2/sandboxes/{sandboxID}/logs instead. + deprecated: true tags: [sandboxes] security: - ApiKeyAuth: [] @@ -1914,6 +1926,50 @@ paths: "500": $ref: "#/components/responses/500" + /v2/sandboxes/{sandboxID}/logs: + get: + description: Get sandbox logs (v2) + tags: [sandboxes] + security: + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + - in: query + name: cursor + schema: + type: integer + format: int64 + minimum: 0 + description: Starting timestamp of the logs that should be returned in milliseconds + - in: query + name: limit + schema: + default: 1000 + type: integer + format: int32 + minimum: 0 + maximum: 1000 + description: Maximum number of logs that should be returned + - in: query + name: direction + schema: + $ref: "#/components/schemas/LogsDirection" + responses: + "200": + description: Successfully returned the sandbox logs + content: + application/json: + schema: + $ref: "#/components/schemas/SandboxLogsV2Response" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + /sandboxes/{sandboxID}: get: description: Get a sandbox by id diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index cccc864d11..40ae1f765c 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -648,6 +648,12 @@ type SandboxLogs struct { Logs []SandboxLog `json:"logs"` } +// SandboxLogsV2Response defines model for SandboxLogsV2Response. +type SandboxLogsV2Response struct { + // Logs Sandbox logs structured + Logs []SandboxLogEntry `json:"logs"` +} + // SandboxMetadata defines model for SandboxMetadata. type SandboxMetadata map[string]string @@ -1263,6 +1269,16 @@ type GetV2SandboxesParams struct { Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` } +// GetV2SandboxesSandboxIDLogsParams defines parameters for GetV2SandboxesSandboxIDLogs. +type GetV2SandboxesSandboxIDLogsParams struct { + // Cursor Starting timestamp of the logs that should be returned in milliseconds + Cursor *int64 `form:"cursor,omitempty" json:"cursor,omitempty"` + + // Limit Maximum number of logs that should be returned + Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + Direction *LogsDirection `form:"direction,omitempty" json:"direction,omitempty"` +} + // PostAccessTokensJSONRequestBody defines body for PostAccessTokens for application/json ContentType. type PostAccessTokensJSONRequestBody = NewAccessToken @@ -1657,6 +1673,9 @@ type ClientInterface interface { // GetV2Sandboxes request GetV2Sandboxes(ctx context.Context, params *GetV2SandboxesParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetV2SandboxesSandboxIDLogs request + GetV2SandboxesSandboxIDLogs(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostV2TemplatesWithBody request with any body PostV2TemplatesWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2326,6 +2345,18 @@ func (c *Client) GetV2Sandboxes(ctx context.Context, params *GetV2SandboxesParam return c.Client.Do(req) } +func (c *Client) GetV2SandboxesSandboxIDLogs(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetV2SandboxesSandboxIDLogsRequest(c.Server, sandboxID, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PostV2TemplatesWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPostV2TemplatesRequestWithBody(c.Server, contentType, body) if err != nil { @@ -4390,6 +4421,94 @@ func NewGetV2SandboxesRequest(server string, params *GetV2SandboxesParams) (*htt return req, nil } +// NewGetV2SandboxesSandboxIDLogsRequest generates requests for GetV2SandboxesSandboxIDLogs +func NewGetV2SandboxesSandboxIDLogsRequest(server string, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "sandboxID", runtime.ParamLocationPath, sandboxID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v2/sandboxes/%s/logs", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Cursor != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "cursor", runtime.ParamLocationQuery, *params.Cursor); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Direction != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "direction", runtime.ParamLocationQuery, *params.Direction); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPostV2TemplatesRequest calls the generic PostV2Templates builder with application/json body func NewPostV2TemplatesRequest(server string, body PostV2TemplatesJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -4762,6 +4881,9 @@ type ClientWithResponsesInterface interface { // GetV2SandboxesWithResponse request GetV2SandboxesWithResponse(ctx context.Context, params *GetV2SandboxesParams, reqEditors ...RequestEditorFn) (*GetV2SandboxesResponse, error) + // GetV2SandboxesSandboxIDLogsWithResponse request + GetV2SandboxesSandboxIDLogsWithResponse(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*GetV2SandboxesSandboxIDLogsResponse, error) + // PostV2TemplatesWithBodyWithResponse request with any body PostV2TemplatesWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV2TemplatesResponse, error) @@ -5768,6 +5890,31 @@ func (r GetV2SandboxesResponse) StatusCode() int { return 0 } +type GetV2SandboxesSandboxIDLogsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *SandboxLogsV2Response + JSON401 *N401 + JSON404 *N404 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r GetV2SandboxesSandboxIDLogsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetV2SandboxesSandboxIDLogsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PostV2TemplatesResponse struct { Body []byte HTTPResponse *http.Response @@ -6338,6 +6485,15 @@ func (c *ClientWithResponses) GetV2SandboxesWithResponse(ctx context.Context, pa return ParseGetV2SandboxesResponse(rsp) } +// GetV2SandboxesSandboxIDLogsWithResponse request returning *GetV2SandboxesSandboxIDLogsResponse +func (c *ClientWithResponses) GetV2SandboxesSandboxIDLogsWithResponse(ctx context.Context, sandboxID SandboxID, params *GetV2SandboxesSandboxIDLogsParams, reqEditors ...RequestEditorFn) (*GetV2SandboxesSandboxIDLogsResponse, error) { + rsp, err := c.GetV2SandboxesSandboxIDLogs(ctx, sandboxID, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetV2SandboxesSandboxIDLogsResponse(rsp) +} + // PostV2TemplatesWithBodyWithResponse request with arbitrary body returning *PostV2TemplatesResponse func (c *ClientWithResponses) PostV2TemplatesWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostV2TemplatesResponse, error) { rsp, err := c.PostV2TemplatesWithBody(ctx, contentType, body, reqEditors...) @@ -8181,6 +8337,53 @@ func ParseGetV2SandboxesResponse(rsp *http.Response) (*GetV2SandboxesResponse, e return response, nil } +// ParseGetV2SandboxesSandboxIDLogsResponse parses an HTTP response from a GetV2SandboxesSandboxIDLogsWithResponse call +func ParseGetV2SandboxesSandboxIDLogsResponse(rsp *http.Response) (*GetV2SandboxesSandboxIDLogsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetV2SandboxesSandboxIDLogsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest SandboxLogsV2Response + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest N404 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePostV2TemplatesResponse parses an HTTP response from a PostV2TemplatesWithResponse call func ParsePostV2TemplatesResponse(rsp *http.Response) (*PostV2TemplatesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body)