diff --git a/planetscale/client.go b/planetscale/client.go index 4adc76d..42e5b96 100644 --- a/planetscale/client.go +++ b/planetscale/client.go @@ -72,6 +72,7 @@ type Client struct { Vtctld VtctldService Webhooks WebhooksService Workflows WorkflowsService + Logs LogsService } // ListOptions are options for listing responses. @@ -309,6 +310,7 @@ func NewClient(opts ...ClientOption) (*Client, error) { c.Vtctld = &vtctldService{client: c} c.Webhooks = &webhooksService{client: c} c.Workflows = &workflowsService{client: c} + c.Logs = &logsService{client: c} return c, nil } diff --git a/planetscale/integration_test.go b/planetscale/integration_test.go index 750970c..7845150 100644 --- a/planetscale/integration_test.go +++ b/planetscale/integration_test.go @@ -65,7 +65,7 @@ func TestIntegration_Databases_List(t *testing.T) { fmt.Printf("Notes: %q\n", db.Notes) } - err = client.Databases.Delete(ctx, &DeleteDatabaseRequest{ + _, err = client.Databases.Delete(ctx, &DeleteDatabaseRequest{ Organization: org, Database: dbName, }) @@ -105,8 +105,8 @@ func TestIntegration_AuditLogs_List(t *testing.T) { t.Fatalf("get audit logs failed: %s", err) } - for _, l := range auditLogs { + for _, l := range auditLogs.Data { fmt.Printf("l. = %+v\n", l.AuditAction) } - fmt.Printf("len(auditLogs) = %+v\n", len(auditLogs)) + fmt.Printf("len(auditLogs) = %+v\n", len(auditLogs.Data)) } diff --git a/planetscale/logs.go b/planetscale/logs.go new file mode 100644 index 0000000..7b073c8 --- /dev/null +++ b/planetscale/logs.go @@ -0,0 +1,152 @@ +package planetscale + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" +) + +const ( + logsBaseURL = "logs.psdb.cloud" + logsSignaturesAPIPath = "/logs/signatures" +) + +type LogsService interface { + Get(ctx context.Context, getReq *GetLogsRequest) (string, error) + GetSignature(ctx context.Context, getReq *GetLogsSignatureRequest) (*Signature, error) +} + +type logsService struct { + client *Client +} + +// GetLogsRequest encapsulates the request for resetting the default role of a Postgres database branch. +type GetLogsRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + + Replica bool `json:"-"` + Start time.Time `json:"-"` + End time.Time `json:"-"` + Level []string `json:"-"` + Limit int `json:"-"` +} + +type GetLogsSignatureRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` +} + +type Signature struct { + Exp string `json:"exp"` + Sig string `json:"sig"` +} + +var _ LogsService = &logsService{} + +func (l *logsService) GetSignature(ctx context.Context, getSigReq *GetLogsSignatureRequest) (*Signature, error) { + path := path.Join(databaseBranchAPIPath(getSigReq.Organization, getSigReq.Database, getSigReq.Branch), logsSignaturesAPIPath) + req, err := l.client.newRequest(http.MethodPost, path, nil) + if err != nil { + return nil, fmt.Errorf("error creating request for list regions: %w", err) + } + + var signature Signature + if err := l.client.do(ctx, req, &signature); err != nil { + return nil, err + } + return &signature, nil +} + +func (l *logsService) Get(ctx context.Context, getReq *GetLogsRequest) (string, error) { + if getReq.End.Compare(getReq.Start) < 0 { + return "", errors.New("end time cannot be before start time") + } + getReq.Limit = min(max(getReq.Limit, 0), 1000) // TODO reasonable? + + gbr := GetLogsSignatureRequest{Organization: getReq.Organization, Database: getReq.Database, Branch: getReq.Branch} + sig, err := l.GetSignature(ctx, &gbr) + if err != nil { + fmt.Println("Signature error") + return "", err + } + + query := "* " + + // Time range + query = fmt.Sprintf(query+"_time:[%s, %s] ", getReq.Start.Format(time.RFC3339), getReq.End.Format(time.RFC3339)) + + // Log levels + logLevels := map[string]struct{}{} + for _, v := range getReq.Level { + V := strings.ToUpper(v) + switch V { + case "DEBUG", "INFO", "WARNING", "ERROR": + logLevels[V] = struct{}{} + default: + continue + } + } + keys := make([]string, 0, len(logLevels)) + for k := range logLevels { + keys = append(keys, fmt.Sprintf("planetscale.level:%s", k)) + } + if len(keys) >= 1 { + query = fmt.Sprintf(query+"(%s) ", strings.Join(keys, " OR ")) + } + + // Replica / Primary + target := "primary" + if getReq.Replica { + target = "replica" + } + query = fmt.Sprintf(query+"(planetscale.role:%s) ", target) + + // Sort // TODO should this be more flexible? + query = query + "| sort by (_time desc) | offset 0" + + u := &url.URL{ + Scheme: "https", + Host: logsBaseURL, + Path: fmt.Sprintf("/logs/branch/%s/query", getReq.Branch), + } + + // Manage query parameters + q := u.Query() + q.Set("sig", sig.Sig) + q.Set("exp", sig.Exp) + q.Set("limit", fmt.Sprintf("%d", getReq.Limit)) + q.Set("query", query) + u.RawQuery = q.Encode() + fmt.Println(u.String()) + + req, err := l.client.newRequest(http.MethodGet, u.String(), nil) + if err != nil { + return "", fmt.Errorf("error creating request for list regions: %w", err) + } + + req = req.WithContext(ctx) + res, err := l.client.client.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + out, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + if res.StatusCode != 200 { + return "", fmt.Errorf("server responded with [%d]: %s", res.StatusCode, out) + } + + return string(out), nil +} + diff --git a/planetscale/planetscale.go b/planetscale/planetscale.go new file mode 100644 index 0000000..2fe961f --- /dev/null +++ b/planetscale/planetscale.go @@ -0,0 +1,2 @@ +// Package planetscale is PlanetScales GO SDK for interactive with planetscale services +package planetscale