-
Notifications
You must be signed in to change notification settings - Fork 12k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CloudWatch: Remove simplejson in favor of 'encoding/json' #51062
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To explain myself for all the newline nits: stylistically it's easier to read if code that works together (ex. creating a variable then setting it) is in the same "paragraph" of code.
The other big note is that we can use the json that already exist to make sure that we marshal to json the same way we used to.
dimensions := model.Get("dimensions").MustMap() | ||
statistic := model.Get("statistic").MustString() | ||
period := int64(model.Get("period").MustInt(0)) | ||
usePrefixMatch := model.PrefixMatching |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could the rest of the function just use the model.*
values instead of making copies?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, for sure.
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
Region string `json:",omitempty"` | ||
Namespace string `json:",omitempty"` | ||
MetricName string `json:",omitempty"` | ||
Dimensions map[string]interface{} `json:",omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there something more precise than interface{} we can parse to? From annotation_query.go
it looks like we expect these to be []string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, for legacy reasons dimensions values can be string | []string
. :( This is handled here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😞 unfortunate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I was going to chime in on this, thanks @sunker
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
@@ -188,7 +201,14 @@ func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx back | |||
if err != nil { | |||
return err | |||
} | |||
_, err = e.handleDescribeLogGroups(ctx, logsClient, simplejson.NewFromAny(map[string]interface{}{"limit": "1"})) | |||
|
|||
defaultLimit := int64(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default values should go at the top of the file with the other consts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know, thanks!
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
"region": model.Get("region").MustString(""), | ||
"queryId": *startQueryOutput.QueryId, | ||
}) | ||
requestParams := &LogQueryJson{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be created as a pointer
|
||
if isLogAlertQuery { | ||
return e.executeLogAlertQuery(ctx, req) | ||
} | ||
|
||
queryType := model.Get("type").MustString("") | ||
queryType := model.QueryType | ||
|
||
var result *backend.QueryDataResponse | ||
switch queryType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can model.QueryType just get plugged in here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, since QueryType
will just return ""
if it's not set. Which is what MustString
is doing.
@@ -406,33 +406,6 @@ func Test_executeStartQuery(t *testing.T) { | |||
}, cli.calls.startQueryWithContext) | |||
}) | |||
|
|||
t.Run("cannot parse limit as float", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little worried about why we had this test, because if getting a float limit is possible, we're going to error unmarshaling it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we ever figure out what was going on with this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was a test as documentation of some behavior or consequence of the behavior of the simplejson library but not sure. I vote for deletion!
} | ||
} | ||
|
||
statValue := "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could create the return object here and set values on the return object instead of creating these intermediate values.
@@ -21,15 +22,15 @@ func TestRequestParser(t *testing.T) { | |||
} | |||
oldQuery.RefID = "A" | |||
oldQuery.JSON = []byte(`{ | |||
"region": "us-east-1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These shouldn't have to change to parse successfully, I believe unmarshaling is case insensitive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll double check!
|
||
var query QueryJson | ||
err := json.Unmarshal(fixtureJSON, &query) | ||
query.Period = "900" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: setting period should go after the error check
migrateAliasToDynamicLabel(&query) | ||
|
||
matchedJson := []byte(fmt.Sprintf(`{ | ||
"Alias": "%s", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it used to marshal to camelCase, we should use the json struct tags to continue marshaling to camelCase.
16357cb
to
d46a047
Compare
@iwysiu can you explain this more? What already existing json? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great @asimpson! Added a few nits. Will do some more testing when changes are applied.
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
Region string `json:",omitempty"` | ||
Namespace string `json:",omitempty"` | ||
MetricName string `json:",omitempty"` | ||
Dimensions map[string]interface{} `json:",omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, for legacy reasons dimensions values can be string | []string
. :( This is handled here
@@ -324,18 +344,19 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa | |||
frontend, but because alerts are executed on the backend the logic needs to be reimplemented here. | |||
*/ | |||
q := req.Queries[0] | |||
model, err := simplejson.NewJson(q.JSON) | |||
var model DataQueryJson |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like DataQuery
is only used in annotation queries. I think it would be good to highlight the difference between the three different query types in a similar way to how it's done in the frontend. In the executeAnnotationQuery
you can unmarshal json into an AnnotationDataQuery
struct. In executeLogActions
, you can unmarshal into a LogsDataQuery and in executeTimeSeriesQuery
you can unmarshal into a MetricsDataQuery
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do use model
here to check the QueryType
. It is strange and inconsistent in that executeAnnotationQuery
is the only "execute" function we pass a model to. log_actions
unmarshal's from req.Queries
and timeSeriesQuery
doesn't do either, it deals with the data on req
directly. I'd propose we merge this as is and then create a new issue around unifying how the various query types handle data. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh and executeLogAlertQuery
is somehow in cloudwatch.go
🤷♂️ Which seems odd as well...
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
if err != nil { | ||
return nil, err | ||
} | ||
_, fromAlert := req.Headers["FromAlert"] | ||
isLogAlertQuery := fromAlert && model.Get("queryMode").MustString("") == "Logs" | ||
isLogAlertQuery := fromAlert && model.QueryMode == "Logs" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to introduce string constants for the query modes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
@@ -390,7 +415,7 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back | |||
|
|||
var frames []*data.Frame | |||
|
|||
statsGroups := model.Get("statsGroups").MustStringArray() | |||
statsGroups := model.StatsGroups |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to copy this value - should be fine to use it directly in the next lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice call, thanks!
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
const ( | ||
LimitExceededException = "LimitExceededException" | ||
defaultLimit = 10 | ||
defaultLimit = int64(10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if unexported constants should start with a capital letter or not (think it's lower case @iwysiu ?). Just as long as this constant and LimitExceededException use the same convention I'm happy. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, unexported values should be lower case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! 😎
TimezoneUTCOffset string `json:",omitempty"` | ||
QueryType string `json:",omitempty"` | ||
Hide *bool `json:",omitempty"` | ||
Alias *string `json:",omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the omitempty
like in my other comment:
A quick look at the tests suggests that we need omitempty
to pass some tests and not to pass other tests (for example Alias
).
To "dumbly" pass the tests, we can add omitempty
only on fields where it is necessary to pass the tests (it should be where there are no keys in the expected Json).
We ought to be able to make test assertions on the struct itself, not the encoded json. This will avoid the issue with the upper case in the expected json tags. But making these tests pass without any changes should be a good first step. So I think struct assertions would be better for a different commit. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, however for this specific PR I wanted to focus strictly on removing simplejson
. I think this would be a great future issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think my comment was very clear, sorry about that.
To "dumbly" pass the tests, we can add omitempty only on fields where it is necessary to pass the tests (it should be where there are no keys in the expected Json).
I think the above is where I was getting at: only adding omitempty
where strictly necessary. This is (I think) only relevant in the test Test_Test_migrateLegacyQuery
(yeah the title seems to have a typo...)
To clarify on the struct point:
Almost every test now has superfluous decoding from raw json into a struct. We should just build the struct directly as it very closely resembles the map[string]interface{} from the original code.
This looks great! |
This pull request was removed from the 9.0.1 milestone because 9.0.1 is currently being released. |
What I was trying to say there was that we shouldn't change the json in the marshalling/unmarshalling tests, but I managed to say that in the most confusing possible way |
d46a047
to
de2e765
Compare
Drone build failed: https://drone.grafana.net/grafana/grafana-enterprise/23794 |
Drone build failed: https://drone.grafana.net/grafana/grafana-enterprise/25525 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I included a comment about using the structs, but am happy to do that refactor in a different PR after this is merged.
Will let the others add another approval
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! A last few comments
|
||
var period int64 | ||
if model.Period != "" { | ||
p, err := strconv.Atoi(model.Period) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional: you could use strconv.ParseInt to convert directly to a int64
pkg/tsdb/cloudwatch/cloudwatch.go
Outdated
region := model.Get("region").MustString(defaultRegion) | ||
if region == defaultRegion { | ||
region := model.Region | ||
if model.Region != "" || region == defaultRegion { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be region == ""
?
pkg/tsdb/cloudwatch/log_actions.go
Outdated
) | ||
|
||
var logGroupDefaultLimit = int64(50) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value should be the const block and not edited in the functions.
pkg/tsdb/cloudwatch/log_actions.go
Outdated
logGroupNamePrefix := parameters.Get("logGroupNamePrefix").MustString("") | ||
logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters LogQueryJson) (*data.Frame, error) { | ||
if parameters.Limit != nil && *parameters.Limit != 0 { | ||
logGroupDefaultLimit = *parameters.Limit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A separate logGroupLimit
variable should be made here instead of changing the global
@@ -406,33 +406,6 @@ func Test_executeStartQuery(t *testing.T) { | |||
}, cli.calls.startQueryWithContext) | |||
}) | |||
|
|||
t.Run("cannot parse limit as float", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we ever figure out what was going on with this?
|
||
return &cloudWatchQuery, nil | ||
|
||
/* return &cloudWatchQuery{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this commented out block
@@ -257,9 +301,9 @@ func getRetainedPeriods(timeSince time.Duration) []int { | |||
} | |||
} | |||
|
|||
func parseDimensions(model *simplejson.Json) (map[string][]string, error) { | |||
func parseDimensions(model QueryJson) (map[string][]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this could be changed to just take the model.Dimensions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@iwysiu re: #51062 (comment) we don't need to test for float on Limit
since we now have a type (LogQueryJson
) that defines the data.
The backport to
To backport manually, run these commands in your terminal: # Fetch latest updates from GitHub
git fetch
# Create a new branch
git switch --create backport-51062-to-v9.0.x origin/v9.0.x
# Cherry-pick the merged commit of this pull request and resolve the conflicts
git cherry-pick -x 05cdef50040c06bf7e876a3b99a997ea4e826cc1
# Push it to GitHub
git push --set-upstream origin backport-51062-to-v9.0.x
git switch main
# Remove the local backport branch
git branch -D backport-51062-to-v9.0.x Then, create a pull request where the |
(cherry picked from commit 05cdef5)
Which issue(s) this PR fixes:
The
simplejson
library is no longer recommended by the backend team. Therefore the Cloud datasources team has decided to factor the library out of our plugins in favor of theencoding/json
package that's part of the Go standard library.Fixes #45417
Special notes for your reviewer:
simplejson
provides several helper methods to query data and to set defaults if the data returns "empty", e.g.parameters.Get("limit").MustInt64(50)
gets thelimit
property and if it'snull
sets the value to50
. Using theencoding/json
package for this results in a few lines of code instead of one, e.g.The second aspect of this is where
simplejson
will return anError
if the property isn't found, e.g._, err := queryJson.Get("statistic").String()
. Using theencoding/json
package for this means we need to modify our default struct definition to be a pointer to a type, e.g.*string
instead ofstring
. Here's a playground link showing the difference inUnmarshal
.Verification
go test
in thecloudwatch
package, all test should pass.