@@ -10,6 +10,7 @@ import (
10
10
"net/url"
11
11
"reflect"
12
12
"strconv"
13
+ "strings"
13
14
"time"
14
15
15
16
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
@@ -184,7 +185,7 @@ func (p *Provider) createRequest(dd *v1alpha1.DatadogMetric, now int64, interval
184
185
dd .Queries = map [string ]string {"query" : dd .Query }
185
186
}
186
187
187
- return p .createRequestV2 (dd .Queries , dd .Formula , now , interval , dd .Aggregator , url )
188
+ return p .createRequestV2 (dd .Queries , dd .Formula , dd . Formulas , now , interval , dd .Aggregator , url )
188
189
}
189
190
190
191
func (p * Provider ) createRequestV1 (query string , now int64 , interval int64 , url * url.URL ) (* http.Request , error ) {
@@ -211,15 +212,31 @@ func buildQueriesPayload(queries map[string]string, aggregator string) []map[str
211
212
return qp
212
213
}
213
214
214
- func (p * Provider ) createRequestV2 (queries map [string ]string , formula string , now int64 , interval int64 , aggregator string , url * url.URL ) (* http.Request , error ) {
215
- formulas := []map [string ]string {}
216
- // ddAPI supports multiple formulas but doesn't make sense in our context
217
- // can't have a 'blank' formula, so have to guard
215
+ func (p * Provider ) createRequestV2 (queries map [string ]string , formula string , formulas []string , now int64 , interval int64 , aggregator string , url * url.URL ) (* http.Request , error ) {
216
+
217
+ var fp []map [string ]string
218
+
219
+ // We know either formula formulas are provided, but not both.
218
220
if formula != "" {
219
- formulas = []map [string ]string {{
221
+ fp = []map [string ]string {{
220
222
"formula" : formula ,
221
223
}}
224
+ } else if len (formulas ) != 0 {
225
+ fp = make ([]map [string ]string , len (formulas ))
226
+ for i , v := range formulas {
227
+ // can't have a 'blank' formula, so have to guard
228
+ // This won't happen though since we check in validateIncomingProps
229
+ if v != "" {
230
+ p := map [string ]string {
231
+ "formula" : v ,
232
+ }
233
+ fp [i ] = p
234
+ }
235
+ }
236
+ } else {
237
+ fp = []map [string ]string {}
222
238
}
239
+
223
240
// we cannot leave aggregator empty as it will be passed as such to datadog API and fail
224
241
if aggregator == "" {
225
242
aggregator = "last"
@@ -230,7 +247,7 @@ func (p *Provider) createRequestV2(queries map[string]string, formula string, no
230
247
From : (now - interval ) * 1000 ,
231
248
To : now * 1000 ,
232
249
Queries : buildQueriesPayload (queries , aggregator ),
233
- Formulas : formulas ,
250
+ Formulas : fp ,
234
251
}
235
252
236
253
queryBody , err := json .Marshal (datadogRequest {
@@ -296,6 +313,36 @@ func (p *Provider) parseResponseV1(metric v1alpha1.Metric, response *http.Respon
296
313
return strconv .FormatFloat (value , 'f' , - 1 , 64 ), status , err
297
314
}
298
315
316
+ func valuesToResultStr (value interface {}) string {
317
+
318
+ if v , ok := value .(float64 ); ok {
319
+ return strconv .FormatFloat (v , 'f' , - 1 , 64 )
320
+ }
321
+
322
+ if valueSlice , ok := value .([]interface {}); ok {
323
+
324
+ results := []string {}
325
+
326
+ for _ , v := range valueSlice {
327
+ // This never happens
328
+ if v == nil {
329
+ continue
330
+ }
331
+
332
+ // This is always true
333
+ if vFloat , ok := v .(float64 ); ok {
334
+ results = append (results , strconv .FormatFloat (vFloat , 'f' , - 1 , 64 ))
335
+ }
336
+ }
337
+
338
+ return fmt .Sprintf ("[%s]" , strings .Join (results , "," ))
339
+ }
340
+
341
+ // We should never reach here
342
+ return ""
343
+
344
+ }
345
+
299
346
func (p * Provider ) parseResponseV2 (metric v1alpha1.Metric , response * http.Response ) (string , v1alpha1.AnalysisPhase , error ) {
300
347
bodyBytes , err := io .ReadAll (response .Body )
301
348
if err != nil {
@@ -319,17 +366,65 @@ func (p *Provider) parseResponseV2(metric v1alpha1.Metric, response *http.Respon
319
366
return "" , v1alpha1 .AnalysisPhaseError , fmt .Errorf ("There were errors in your query: %v" , res .Data .Errors )
320
367
}
321
368
369
+ var somethingNil , allNil bool
370
+ var value interface {}
371
+ var nilFloat64 * float64
372
+
373
+ // formulasLength is the length of formulas array provided
374
+ formulasLength := len (metric .Provider .Datadog .Formulas )
375
+ // valuesLength is the length of returned values to access
376
+ // if no formulas array provided, that means it is only 1 value
377
+ // (the result of the signle formula or query)
378
+ valuesLength := formulasLength
379
+ if formulasLength == 0 {
380
+ valuesLength = 1
381
+ }
382
+
383
+ // Evalulate whether all formulas are nil
384
+ allNil = reflect .ValueOf (res .Data .Attributes ).IsZero () || len (res .Data .Attributes .Columns ) == 0
385
+
386
+ // Populate value and evalulate somethingNil
387
+ // value is a slice of interface, which is really a slice of float64 (and nils)
388
+ // somethingNil indicates whether there exists a formula with null response
389
+ value = make ([]interface {}, valuesLength )
390
+ valueAsSlice := value .([]interface {})
391
+
392
+ if allNil {
393
+ for i := range len (valueAsSlice ) {
394
+ valueAsSlice [i ] = nilFloat64
395
+ }
396
+ } else {
397
+ for i := range len (valueAsSlice ) {
398
+ if len (res .Data .Attributes .Columns [i ].Values ) == 0 || res .Data .Attributes .Columns [i ].Values [0 ] == nil {
399
+ valueAsSlice [i ] = nilFloat64
400
+ somethingNil = true
401
+ } else {
402
+ valueAsSlice [i ] = * res .Data .Attributes .Columns [i ].Values [0 ]
403
+ }
404
+ }
405
+ }
406
+
407
+ // To preserve backward conditions accessing `result` directly
408
+ // instead of `result[0]`. Cast value back to float64 instead of a slice
409
+ // when no `formulas` array is provided
410
+ if formulasLength == 0 {
411
+ value = valueAsSlice [0 ]
412
+ }
413
+
322
414
// Handle an empty query result
323
- if reflect .ValueOf (res .Data .Attributes ).IsZero () || len (res .Data .Attributes .Columns ) == 0 || len (res .Data .Attributes .Columns [0 ].Values ) == 0 || res .Data .Attributes .Columns [0 ].Values [0 ] == nil {
324
- var nilFloat64 * float64
325
- status , err := evaluate .EvaluateResult (nilFloat64 , metric , p .logCtx )
415
+ if allNil || somethingNil {
416
+ status , err := evaluate .EvaluateResult (value , metric , p .logCtx )
326
417
327
418
var attributesBytes []byte
328
419
var jsonErr error
329
420
// Should be impossible for this to not be true, based on dd openapi spec.
330
421
// But in this case, better safe than sorry
331
- if len (res .Data .Attributes .Columns ) == 1 {
332
- attributesBytes , jsonErr = json .Marshal (res .Data .Attributes .Columns [0 ].Values )
422
+ if len (res .Data .Attributes .Columns ) >= 1 {
423
+ allValues := []* float64 {}
424
+ for i := range len (res .Data .Attributes .Columns ) {
425
+ allValues = append (allValues , res .Data .Attributes .Columns [i ].Values ... )
426
+ }
427
+ attributesBytes , jsonErr = json .Marshal (allValues )
333
428
} else {
334
429
attributesBytes , jsonErr = json .Marshal (res .Data .Attributes )
335
430
}
@@ -342,10 +437,9 @@ func (p *Provider) parseResponseV2(metric v1alpha1.Metric, response *http.Respon
342
437
}
343
438
344
439
// Handle a populated query result
345
- column := res .Data .Attributes .Columns [0 ]
346
- value := * column .Values [0 ]
347
440
status , err := evaluate .EvaluateResult (value , metric , p .logCtx )
348
- return strconv .FormatFloat (value , 'f' , - 1 , 64 ), status , err
441
+ return valuesToResultStr (value ), status , err
442
+
349
443
}
350
444
351
445
// Resume should not be used the Datadog provider since all the work should occur in the Run method
@@ -381,21 +475,35 @@ func validateIncomingProps(dd *v1alpha1.DatadogMetric) error {
381
475
return errors .New ("Cannot have both a query and queries. Please review the Analysis Template." )
382
476
}
383
477
478
+ // check that we have ONE OF formula/formulas
479
+ if dd .Formula != "" && len (dd .Formulas ) > 0 {
480
+ return errors .New ("Cannot have both a formula and formulas. Please review the Analysis Template." )
481
+ }
482
+
384
483
// check that query is set for apiversion v1
385
484
if dd .ApiVersion == "v1" && dd .Query == "" {
386
485
return errors .New ("Query is empty. API Version v1 only supports using the query parameter in your Analysis Template." )
387
486
}
388
487
389
488
// formula <3 queries. won't go anywhere without them
390
- if dd .Formula != "" && len (dd .Queries ) == 0 {
391
- return errors .New ("Formula are only valid when queries are set. Please review the Analysis Template." )
489
+ if (dd .Formula != "" || len (dd .Formulas ) != 0 ) && len (dd .Queries ) == 0 {
490
+ return errors .New ("Formula/Formulas are only valid when queries are set. Please review the Analysis Template." )
491
+ }
492
+
493
+ // validate that if formulas are set, no one of them is an empty string
494
+ if len (dd .Formulas ) != 0 {
495
+ for _ , f := range dd .Formulas {
496
+ if f == "" {
497
+ return errors .New ("All formulas within Formulas field must be non-empty strings." )
498
+ }
499
+ }
392
500
}
393
501
394
- // Reject queries with more than 1 when NO formula provided. While this would technically work
502
+ // Reject queries with more than 1 when NO formula/formulas provided. While this would technically work
395
503
// DD will return 2 columns of data, and there is no guarantee what order they would be in, so
396
504
// there is no way to guess at intention of user. Since this is about metrics and monitoring, we should
397
505
// avoid ambiguity.
398
- if dd .Formula == "" && len (dd .Queries ) > 1 {
506
+ if ( dd .Formula == "" && len ( dd . Formulas ) == 0 ) && len (dd .Queries ) > 1 {
399
507
return errors .New ("When multiple queries are provided you must include a formula." )
400
508
}
401
509
0 commit comments