Skip to content

Commit baae8aa

Browse files
authored
feat(rdb): add command to fetch logs from database instance (#5203)
1 parent 0e60089 commit baae8aa

File tree

5 files changed

+304
-0
lines changed

5 files changed

+304
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Prepare, wait for, and download logs from a Database Instance. This command automatically prepares the logs, waits for them to be ready, and downloads them to a file.
4+
5+
USAGE:
6+
scw rdb log download <instance-id ...> [arg=value ...]
7+
8+
EXAMPLES:
9+
Download logs from a database instance
10+
scw rdb log download 11111111-1111-1111-1111-111111111111
11+
12+
Download logs with a time range
13+
scw rdb log download 11111111-1111-1111-1111-111111111111 from=2023-01-01T00:00:00Z to=2023-01-02T00:00:00Z
14+
15+
Download logs to a specific file
16+
scw rdb log download 11111111-1111-1111-1111-111111111111 output=myLogs.txt
17+
18+
ARGS:
19+
instance-id UUID of the Database Instance you want logs of
20+
[from] Start datetime of your log. Supports absolute RFC3339 timestamps and relative times (see `scw help date`).
21+
[to] End datetime of your log. Supports absolute RFC3339 timestamps and relative times (see `scw help date`).
22+
[output] Filename to write to
23+
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par | nl-ams | pl-waw)
24+
25+
FLAGS:
26+
-h, --help help for download
27+
28+
GLOBAL FLAGS:
29+
-c, --config string The path to the config file
30+
-D, --debug Enable debug mode
31+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
32+
-p, --profile string The config profile to use

cmd/scw/testdata/test-all-usage-rdb-log-usage.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ USAGE:
66
scw rdb log <command>
77

88
AVAILABLE COMMANDS:
9+
download Download logs from a database instance
910
get Get given logs of a Database Instance
1011
list List available logs of a Database Instance
1112
list-details List remote Database Instance logs details

docs/commands/rdb.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ This API allows you to manage your Managed Databases for PostgreSQL and MySQL.
4747
- [Upgrade a Database Instance](#upgrade-a-database-instance)
4848
- [Wait for an instance to reach a stable state](#wait-for-an-instance-to-reach-a-stable-state)
4949
- [Instance logs management commands](#instance-logs-management-commands)
50+
- [Download logs from a database instance](#download-logs-from-a-database-instance)
5051
- [Get given logs of a Database Instance](#get-given-logs-of-a-database-instance)
5152
- [List available logs of a Database Instance](#list-available-logs-of-a-database-instance)
5253
- [List remote Database Instance logs details](#list-remote-database-instance-logs-details)
@@ -1039,6 +1040,49 @@ scw rdb instance wait 11111111-1111-1111-1111-111111111111
10391040
Instance logs management commands.
10401041

10411042

1043+
### Download logs from a database instance
1044+
1045+
Prepare, wait for, and download logs from a Database Instance. This command automatically prepares the logs, waits for them to be ready, and downloads them to a file.
1046+
1047+
**Usage:**
1048+
1049+
```
1050+
scw rdb log download <instance-id ...> [arg=value ...]
1051+
```
1052+
1053+
1054+
**Args:**
1055+
1056+
| Name | | Description |
1057+
|------|---|-------------|
1058+
| instance-id | Required | UUID of the Database Instance you want logs of |
1059+
| from | | Start datetime of your log. Supports absolute RFC3339 timestamps and relative times (see `scw help date`). |
1060+
| to | | End datetime of your log. Supports absolute RFC3339 timestamps and relative times (see `scw help date`). |
1061+
| output | | Filename to write to |
1062+
| region | Default: `fr-par`<br />One of: `fr-par`, `nl-ams`, `pl-waw` | Region to target. If none is passed will use default region from the config |
1063+
1064+
1065+
**Examples:**
1066+
1067+
1068+
Download logs from a database instance
1069+
```
1070+
scw rdb log download 11111111-1111-1111-1111-111111111111
1071+
```
1072+
1073+
Download logs with a time range
1074+
```
1075+
scw rdb log download 11111111-1111-1111-1111-111111111111 from=2023-01-01T00:00:00Z to=2023-01-02T00:00:00Z
1076+
```
1077+
1078+
Download logs to a specific file
1079+
```
1080+
scw rdb log download 11111111-1111-1111-1111-111111111111 output=myLogs.txt
1081+
```
1082+
1083+
1084+
1085+
10421086
### Get given logs of a Database Instance
10431087

10441088
Retrieve information about the logs of a Database Instance. Specify the `instance_log_id` and `region` in your request to get information such as `download_url`, `status`, `expires_at` and `created_at` about your logs in the response.

internal/namespaces/rdb/v1/custom.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func GetCommands() *core.Commands {
1717
human.RegisterMarshalerFunc(CreateInstanceResult{}, createInstanceResultMarshalerFunc)
1818
human.RegisterMarshalerFunc(CustomACLResult{}, rdbACLCustomResultMarshalerFunc)
1919
human.RegisterMarshalerFunc(rdbEndpointCustomResult{}, rdbEndpointCustomResultMarshalerFunc)
20+
human.RegisterMarshalerFunc(logDownloadResult{}, logDownloadResultMarshalerFunc)
2021

2122
human.RegisterMarshalerFunc(
2223
rdb.InstanceStatus(""),
@@ -81,6 +82,9 @@ func GetCommands() *core.Commands {
8182
cmds.MustFind("rdb", "user", "update").Override(userUpdateBuilder)
8283

8384
cmds.MustFind("rdb", "log", "prepare").Override(logPrepareBuilder)
85+
cmds.Merge(core.NewCommands(
86+
logDownloadCommand(),
87+
))
8488

8589
return cmds
8690
}

internal/namespaces/rdb/v1/custom_log.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ package rdb
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"net/url"
9+
"os"
10+
"path"
11+
"reflect"
12+
"strings"
13+
"time"
514

615
"github.com/fatih/color"
716
"github.com/scaleway/scaleway-cli/v2/core"
817
"github.com/scaleway/scaleway-cli/v2/core/human"
18+
"github.com/scaleway/scaleway-cli/v2/internal/interactive"
919
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
1020
"github.com/scaleway/scaleway-sdk-go/scw"
1121
)
@@ -44,3 +54,216 @@ func logPrepareBuilder(c *core.Command) *core.Command {
4454

4555
return c
4656
}
57+
58+
type logDownloadArgs struct {
59+
InstanceID string
60+
From *time.Time
61+
To *time.Time
62+
Output string
63+
Region scw.Region
64+
}
65+
66+
type logDownloadResult struct {
67+
Size scw.Size `json:"size"`
68+
FileName string `json:"file_name"`
69+
}
70+
71+
func logDownloadResultMarshalerFunc(i any, _ *human.MarshalOpt) (string, error) {
72+
result := i.(logDownloadResult)
73+
sizeStr, err := human.Marshal(result.Size, nil)
74+
if err != nil {
75+
return "", err
76+
}
77+
78+
return fmt.Sprintf(
79+
"Log downloaded to %s successfully (%s written)",
80+
result.FileName,
81+
sizeStr,
82+
), nil
83+
}
84+
85+
func getLogDefaultFileName(rawURL string) (string, error) {
86+
u, err := url.Parse(rawURL)
87+
if err != nil {
88+
return "", err
89+
}
90+
splitURL := strings.Split(u.Path, "/")
91+
filename := splitURL[len(splitURL)-1]
92+
93+
return filename, nil
94+
}
95+
96+
func logDownloadCommand() *core.Command {
97+
return &core.Command{
98+
Short: `Download logs from a database instance`,
99+
Long: `Prepare, wait for, and download logs from a Database Instance. This command automatically prepares the logs, waits for them to be ready, and downloads them to a file.`,
100+
Namespace: "rdb",
101+
Resource: "log",
102+
Verb: "download",
103+
ArgsType: reflect.TypeOf(logDownloadArgs{}),
104+
ArgSpecs: core.ArgSpecs{
105+
{
106+
Name: "instance-id",
107+
Short: `UUID of the Database Instance you want logs of`,
108+
Required: true,
109+
Positional: true,
110+
},
111+
{
112+
Name: "from",
113+
Short: `Start datetime of your log. Supports absolute RFC3339 timestamps and relative times (see ` + "`scw help date`" + `).`,
114+
Required: false,
115+
},
116+
{
117+
Name: "to",
118+
Short: `End datetime of your log. Supports absolute RFC3339 timestamps and relative times (see ` + "`scw help date`" + `).`,
119+
Required: false,
120+
},
121+
{
122+
Name: "output",
123+
Short: "Filename to write to",
124+
},
125+
core.RegionArgSpec(
126+
scw.RegionFrPar,
127+
scw.RegionNlAms,
128+
scw.RegionPlWaw,
129+
),
130+
},
131+
Run: func(ctx context.Context, argsI any) (i any, err error) {
132+
args := argsI.(*logDownloadArgs)
133+
api := rdb.NewAPI(core.ExtractClient(ctx))
134+
135+
prepareRequest := &rdb.PrepareInstanceLogsRequest{
136+
InstanceID: args.InstanceID,
137+
Region: args.Region,
138+
}
139+
if args.From != nil {
140+
prepareRequest.StartDate = args.From
141+
}
142+
if args.To != nil {
143+
prepareRequest.EndDate = args.To
144+
}
145+
146+
_, err = interactive.Print("Preparing logs... ")
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
prepareResp, err := api.PrepareInstanceLogs(prepareRequest)
152+
if err != nil {
153+
return nil, err
154+
}
155+
156+
if len(prepareResp.InstanceLogs) == 0 {
157+
return nil, errors.New("no logs found for the specified time range")
158+
}
159+
160+
_, err = interactive.Println("OK")
161+
if err != nil {
162+
return nil, err
163+
}
164+
165+
_, err = interactive.Print("Waiting for logs to be ready... ")
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
readyLogs := make([]*rdb.InstanceLog, len(prepareResp.InstanceLogs))
171+
for i := range prepareResp.InstanceLogs {
172+
logs, err := api.WaitForInstanceLog(&rdb.WaitForInstanceLogRequest{
173+
InstanceLogID: prepareResp.InstanceLogs[i].ID,
174+
Region: prepareResp.InstanceLogs[i].Region,
175+
Timeout: scw.TimeDurationPtr(instanceActionTimeout),
176+
RetryInterval: core.DefaultRetryInterval,
177+
})
178+
if err != nil {
179+
return nil, err
180+
}
181+
readyLogs[i] = logs
182+
}
183+
184+
_, err = interactive.Println("OK")
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
if len(readyLogs) == 0 {
190+
return nil, errors.New("no logs ready after waiting")
191+
}
192+
193+
logToDownload := readyLogs[0]
194+
if logToDownload.DownloadURL == nil {
195+
return nil, errors.New("download URL is not available")
196+
}
197+
198+
httpClient := core.ExtractHTTPClient(ctx)
199+
200+
_, err = interactive.Print("Downloading logs... ")
201+
if err != nil {
202+
return nil, err
203+
}
204+
205+
res, err := httpClient.Get(*logToDownload.DownloadURL)
206+
if err != nil {
207+
return nil, err
208+
}
209+
defer res.Body.Close()
210+
211+
defaultFilename, err := getLogDefaultFileName(*logToDownload.DownloadURL)
212+
if err != nil {
213+
return nil, err
214+
}
215+
filename := defaultFilename
216+
if args.Output != "" {
217+
fi, err := os.Stat(args.Output)
218+
if err != nil {
219+
if !os.IsNotExist(err) {
220+
return nil, err
221+
}
222+
filename = args.Output
223+
} else {
224+
switch mode := fi.Mode(); {
225+
case mode.IsDir():
226+
filename = path.Join(args.Output, defaultFilename)
227+
case mode.IsRegular():
228+
filename = args.Output
229+
}
230+
}
231+
}
232+
233+
out, err := os.Create(filename)
234+
if err != nil {
235+
return nil, err
236+
}
237+
defer out.Close()
238+
239+
size, err := io.Copy(out, res.Body)
240+
if err != nil {
241+
return nil, err
242+
}
243+
244+
_, err = interactive.Println("OK")
245+
if err != nil {
246+
return nil, err
247+
}
248+
249+
return logDownloadResult{
250+
Size: scw.Size(size),
251+
FileName: filename,
252+
}, nil
253+
},
254+
Examples: []*core.Example{
255+
{
256+
Short: "Download logs from a database instance",
257+
ArgsJSON: `{"instance_id": "11111111-1111-1111-1111-111111111111"}`,
258+
},
259+
{
260+
Short: "Download logs with a time range",
261+
ArgsJSON: `{"instance_id": "11111111-1111-1111-1111-111111111111", "from": "2023-01-01T00:00:00Z", "to": "2023-01-02T00:00:00Z"}`,
262+
},
263+
{
264+
Short: "Download logs to a specific file",
265+
ArgsJSON: `{"instance_id": "11111111-1111-1111-1111-111111111111", "output": "myLogs.txt"}`,
266+
},
267+
},
268+
}
269+
}

0 commit comments

Comments
 (0)