Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions planetscale/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type Client struct {
Vtctld VtctldService
Webhooks WebhooksService
Workflows WorkflowsService
Logs LogsService
}

// ListOptions are options for listing responses.
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions planetscale/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -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))
}
152 changes: 152 additions & 0 deletions planetscale/logs.go
Original file line number Diff line number Diff line change
@@ -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
}

2 changes: 2 additions & 0 deletions planetscale/planetscale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package planetscale is PlanetScales GO SDK for interactive with planetscale services
package planetscale
Loading