@@ -46,16 +46,11 @@ func (r *RootCmd) NewReplayCmd() *cobra.Command {
46
46
cmd := & cobra.Command {
47
47
Use : "replay" ,
48
48
Short : "Retrieve historical SLI data and recalculate their SLO error budgets." ,
49
- Long : "Replay pulls in the historical data while your SLO collects new data in real-time. " +
50
- "The historical and current data are merged, producing an error budget calculated for the entire period. " +
51
- "Refer to https://docs.nobl9.com/replay for more details on Replay.\n \n " +
52
- "The 'replay' command allows you to import data for multiple SLOs in bulk. " +
53
- "Before running the Replays it will verify if the SLOs you've provided are eligible for Replay. " +
54
- "It will only run a single Replay simultaneously (current limit for concurrent Replays). " +
55
- "When any Replay fails, it will attempt the import for the next SLO. " +
56
- "Importing data takes time: Replay for a single SLO may take several minutes up to an hour. " +
57
- "During that time, the command keeps on running, periodically checking the status of Replay. " +
58
- "If you cancel the program execution at any time, the current Replay in progress will not be revoked." ,
49
+ Long : "`sloctl replay` creates Replays to retrieve historical data for SLOs. " +
50
+ "Use it to replay SLOs one-by-one or in bulk. Historical data retrieval is time-consuming: " +
51
+ "replaying a single SLO can take up to an hour. Considering the number of ongoing Replays is limited, " +
52
+ "`sloctl` queues Replays if the limit is exceeded. Replay queues is an experimental feature, currently " +
53
+ "unavailable to all organizations. We're working on improving and expanding its availability." ,
59
54
Example : replayExample ,
60
55
Args : replay .arguments ,
61
56
PersistentPreRun : func (cmd * cobra.Command , args []string ) {
@@ -95,31 +90,73 @@ func (r *ReplayCmd) RunReplays(cmd *cobra.Command, replays []ReplayConfig) (fail
95
90
return 0 , err
96
91
}
97
92
93
+ arePlaylistEnabled := r .arePlaylistEnabled (cmd .Context ())
94
+
95
+ if arePlaylistEnabled {
96
+ cmd .Println (colorstring .Color ("[yellow]- Your organization has access to Replay queues!" ))
97
+ cmd .Println (colorstring .Color ("[yellow]- To learn more about Replay queues, follow this link: " +
98
+ "https://docs.nobl9.com/replay-canary/ [reset]" ))
99
+ }
100
+
98
101
failedIndexes := make ([]int , 0 )
99
102
for i , replay := range replays {
100
103
cmd .Println (colorstring .Color (fmt .Sprintf (
101
104
"[cyan][%d/%d][reset] SLO: %s, Project: %s, From: %s, To: %s" ,
102
105
i + 1 , len (replays ), replay .SLO , replay .Project ,
103
106
replay .From .Format (timeLayout ), time .Now ().In (replay .From .Location ()).Format (timeLayout ))))
104
107
105
- spinner := NewSpinner ("Importing data..." )
106
- spinner .Go ()
107
- err = r .runReplay (cmd .Context (), replay )
108
- spinner .Stop ()
108
+ if arePlaylistEnabled {
109
+ cmd .Println ("Replay is added to the queue..." )
110
+ err = r .runReplay (cmd .Context (), replay )
109
111
110
- if err != nil {
111
- cmd .Println (colorstring .Color ("[red]Import failed:[reset] " + err .Error ()))
112
- failedIndexes = append (failedIndexes , i )
113
- continue
112
+ if err != nil {
113
+ cmd .Println (colorstring .Color ("[red]Failed to add Replay to the queue:[reset] " + err .Error ()))
114
+ failedIndexes = append (failedIndexes , i )
115
+ continue
116
+ }
117
+ cmd .Println (colorstring .Color ("[green]Replay has been successfully added to the queue![reset]" ))
118
+ } else {
119
+ spinner := NewSpinner ("Importing data..." )
120
+ spinner .Go ()
121
+ err = r .runReplayWithStatusCheck (cmd .Context (), replay )
122
+ spinner .Stop ()
123
+
124
+ if err != nil {
125
+ cmd .Println (colorstring .Color ("[red]Import failed:[reset] " + err .Error ()))
126
+ failedIndexes = append (failedIndexes , i )
127
+ continue
128
+ }
129
+ cmd .Println (colorstring .Color ("[green]Import succeeded![reset]" ))
114
130
}
115
- cmd .Println (colorstring .Color ("[green]Import succeeded![reset]" ))
116
131
}
117
132
if len (replays ) > 0 {
118
133
r .printSummary (cmd , replays , failedIndexes )
119
134
}
120
135
return len (failedIndexes ), nil
121
136
}
122
137
138
+ func (r * ReplayCmd ) arePlaylistEnabled (ctx context.Context ) bool {
139
+ data , _ , err := r .doRequest (
140
+ ctx ,
141
+ http .MethodGet ,
142
+ endpointPlanInfo ,
143
+ "*" ,
144
+ nil ,
145
+ nil )
146
+ if err != nil {
147
+ return true
148
+ }
149
+ var pc PlaylistConfiguration
150
+ if err = json .Unmarshal (data , & pc ); err != nil {
151
+ return true
152
+ }
153
+ return pc .EnabledPlaylists
154
+ }
155
+
156
+ type PlaylistConfiguration struct {
157
+ EnabledPlaylists bool `json:"enabledPlaylists"`
158
+ }
159
+
123
160
type ReplayConfig struct {
124
161
Project string `json:"project" validate:"required"`
125
162
SLO string `json:"slo" validate:"required"`
@@ -264,7 +301,7 @@ func (r *ReplayCmd) verifySLOs(ctx context.Context, replays []ReplayConfig) erro
264
301
265
302
// Find non-existent or RBAC protected SLOs.
266
303
// We're also filling the Data Source spec here for ReplayConfig.
267
- data , err := r .doRequest (
304
+ data , _ , err := r .doRequest (
268
305
ctx ,
269
306
http .MethodGet ,
270
307
endpointGetSLO ,
@@ -352,10 +389,10 @@ outer:
352
389
353
390
const replayStatusCheckInterval = 30 * time .Second
354
391
355
- func (r * ReplayCmd ) runReplay (ctx context.Context , config ReplayConfig ) error {
356
- _ , err := r .doRequest (ctx , http . MethodPost , endpointReplayPost , config . Project , nil , config . ToReplay ( time . Now ()) )
392
+ func (r * ReplayCmd ) runReplayWithStatusCheck (ctx context.Context , config ReplayConfig ) error {
393
+ err := r .runReplay (ctx , config )
357
394
if err != nil {
358
- return errors . Wrap ( err , "failed to start new Replay" )
395
+ return err
359
396
}
360
397
ticker := time .NewTicker (replayStatusCheckInterval )
361
398
for {
@@ -379,6 +416,21 @@ func (r *ReplayCmd) runReplay(ctx context.Context, config ReplayConfig) error {
379
416
}
380
417
}
381
418
419
+ func (r * ReplayCmd ) runReplay (ctx context.Context , config ReplayConfig ) error {
420
+ _ , httpCode , err := r .doRequest (ctx , http .MethodPost , endpointReplayPost , config .Project ,
421
+ nil , config .ToReplay (time .Now ()),
422
+ )
423
+ if err != nil {
424
+ switch httpCode {
425
+ case 409 :
426
+ return errors .Errorf ("Replay for SLO: '%s' in project: '%s' already exist" , config .SLO , config .Project )
427
+ default :
428
+ return errors .Wrap (err , "failed to start new Replay" )
429
+ }
430
+ }
431
+ return nil
432
+ }
433
+
382
434
func (r * ReplayCmd ) getReplayAvailability (
383
435
ctx context.Context ,
384
436
config ReplayConfig ,
@@ -392,7 +444,7 @@ func (r *ReplayCmd) getReplayAvailability(
392
444
"durationUnit" : {durationUnit },
393
445
"durationValue" : {strconv .Itoa (durationValue )},
394
446
}
395
- data , err := r .doRequest (ctx , http .MethodGet , endpointReplayGetAvailability , config .Project , values , nil )
447
+ data , _ , err := r .doRequest (ctx , http .MethodGet , endpointReplayGetAvailability , config .Project , values , nil )
396
448
if err != nil {
397
449
return
398
450
}
@@ -406,7 +458,7 @@ func (r *ReplayCmd) getReplayStatus(
406
458
ctx context.Context ,
407
459
config ReplayConfig ,
408
460
) (string , error ) {
409
- data , err := r .doRequest (
461
+ data , _ , err := r .doRequest (
410
462
ctx ,
411
463
http .MethodGet ,
412
464
fmt .Sprintf (endpointReplayGetStatus , config .SLO ),
@@ -429,6 +481,7 @@ const (
429
481
endpointReplayList = "/timetravel/list"
430
482
endpointReplayGetStatus = "/timetravel/%s"
431
483
endpointReplayGetAvailability = "/internal/timemachine/availability"
484
+ endpointPlanInfo = "/internal/plan-info"
432
485
endpointGetSLO = "/get/slo"
433
486
)
434
487
@@ -437,30 +490,30 @@ func (r *ReplayCmd) doRequest(
437
490
method , endpoint , project string ,
438
491
values url.Values ,
439
492
payload interface {},
440
- ) ([]byte , error ) {
493
+ ) (data []byte , httpCode int , err error ) {
441
494
var body io.Reader
442
495
if payload != nil {
443
496
buf := new (bytes.Buffer )
444
497
if err := json .NewEncoder (buf ).Encode (payload ); err != nil {
445
- return nil , err
498
+ return nil , 0 , err
446
499
}
447
500
body = buf
448
501
}
449
502
header := http.Header {sdk .HeaderProject : []string {project }}
450
503
req , err := r .client .CreateRequest (ctx , method , endpoint , header , values , body )
451
504
if err != nil {
452
- return nil , err
505
+ return nil , 0 , err
453
506
}
454
507
resp , err := r .client .HTTP .Do (req )
455
508
if err != nil {
456
- return nil , err
509
+ return nil , 0 , err
457
510
}
458
511
defer func () { _ = resp .Body .Close () }()
512
+ data , err = io .ReadAll (resp .Body )
459
513
if resp .StatusCode >= 300 {
460
- data , _ := io .ReadAll (resp .Body )
461
- return nil , errors .Errorf ("bad response (status: %d): %s" , resp .StatusCode , string (data ))
514
+ return nil , resp .StatusCode , errors .Errorf ("bad response (status: %d): %s" , resp .StatusCode , string (data ))
462
515
}
463
- return io . ReadAll ( resp .Body )
516
+ return data , resp .StatusCode , err
464
517
}
465
518
466
519
func (r * ReplayCmd ) replayUnavailabilityReasonExplanation (
@@ -502,14 +555,14 @@ func (r *ReplayCmd) replayUnavailabilityReasonExplanation(
502
555
503
556
func (r * ReplayCmd ) printSummary (cmd * cobra.Command , replays []ReplayConfig , failedIndexes []int ) {
504
557
if len (failedIndexes ) == 0 {
505
- cmd .Printf ("\n Successfully imported data for all %d SLOs.\n " , len (replays ))
558
+ cmd .Printf ("\n Successfully finished operations for all %d SLOs.\n " , len (replays ))
506
559
} else {
507
560
failedDetails := make ([]string , 0 , len (failedIndexes ))
508
561
for _ , i := range failedIndexes {
509
562
fr , _ := json .Marshal (replays [i ])
510
563
failedDetails = append (failedDetails , string (fr ))
511
564
}
512
- cmd .Printf ("\n Successfully imported data for %d and failed for %d SLOs:\n - %s\n " ,
565
+ cmd .Printf ("\n Successfully finished operations for %d and failed for %d SLOs:\n - %s\n " ,
513
566
len (replays )- len (failedIndexes ), len (failedIndexes ), strings .Join (failedDetails , "\n - " ))
514
567
}
515
568
}
0 commit comments