Skip to content
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

feat: Web metric provider #318

Merged
merged 29 commits into from
Jan 16, 2020
Merged

Conversation

tomsanbear
Copy link
Contributor

Today I ran into a situation where it'd be nice to link external metrics to Argo Rollouts, but unfortunately my platform is not supported. I notice the issue open for the generic WebMetric and created a basic implementation (Issue #177)

I would appreciate some feedback before I spend any more time on it. If there are glaring implementation issues, or schema changes you might suggest that would be great (I'm new-ish to Golang).

Some notes on my implementation:

  • I used JsonPath for specifying where to find the metric in the response body
  • I assume it's a float at all times (didn't investigate it for now)
  • Made timeout configurable for the http client, but did not restrict the context runtime

Cheers!

@claassistantio
Copy link

claassistantio commented Dec 3, 2019

CLA assistant check
All committers have signed the CLA.

Copy link
Member

@dthomson25 dthomson25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tomsanbear, this is awesome, and thank you for contributing! I like the overall approach, but I have some implementation questions and a couple of minor style comments.

Comment on lines 3 to 21
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"

"k8s.io/client-go/util/jsonpath"

"github.com/argoproj/argo-rollouts/utils/evaluate"
metricutil "github.com/argoproj/argo-rollouts/utils/metric"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
log "github.com/sirupsen/logrus"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style Nix: Golang convention for imports is to alphabetize the imports and group them by

import (
standard

external

internal
)

So, you can remove line 14 and remove line 20 to above 18 and run make lint to alphabetize them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem with this, will run the linter this time around.

pkg/apis/rollouts/v1alpha1/analysis_types.go Outdated Show resolved Hide resolved
metricproviders/webmetric/webmetric.go Outdated Show resolved Hide resolved
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not find JSONPath in body: %s", err)
}
out := buf.String()
outAsFloat, err := strconv.ParseFloat(buf.String(), 64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that only accepting float64 as a result does not provide enough flexibility for the web metrics. For example, if the endpoint pinged returned a struct like { "status": "success" }, the web provider couldn't check if the status field is equal to success. To achieve both goals, the jsonParser should parse the response and pass that parsed object into the evaluate condition method. That should achieve both outcomes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I had this in mind as well... I'll dig into the evaluation code a bit more to get a more elegant solution here.

metricproviders/webmetric/webmetric.go Outdated Show resolved Hide resolved
pkg/apis/rollouts/v1alpha1/analysis_types.go Outdated Show resolved Hide resolved
pkg/apis/rollouts/v1alpha1/analysis_types.go Outdated Show resolved Hide resolved
pkg/apis/rollouts/v1alpha1/analysis_types.go Outdated Show resolved Hide resolved
@tomsanbear
Copy link
Contributor Author

Thanks for the comments @dthomson25
I'll push some changes in with what I've commented, and what you've mentioned this week.

Cheers!

…ted, check for 2xx status code, minor naming changes for Go best practices
@tomsanbear
Copy link
Contributor Author

Alright, found some time today to fit this in. Got a smarter parser in there for interpreting between ints, floats, bools and strings.

Haven't bothered yet with adding in the alternate HTTP methods, I'll wait to hear back on the discussion.

Copy link
Member

@dthomson25 dthomson25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I have a couple more comments and I think we should be good to go after that!

I think your right that there is some additional work to get alternate HTTP methods working beyond just changing the method, and it can be done in another PR.

metricproviders/webmetric/webmetric.go Show resolved Hide resolved
Method: "GET", // TODO maybe make this configurable....also implies we will need body templates
}

request.URL, err = url.Parse(metric.Provider.Web.URL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style Nix: We can get rid of the err variable declaration at line 41 if we use the variable short assignment := here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one I actually did since we can't have a ':=' assignment combined with the field assignment I have there. I could swap that out with a new var that then gets assigned to request.URL, I'll push that in the next commit, and we can nitpick that if it's a problem also.

}

value, status, err := p.parseResponse(metric, response)
if err != nil || response.StatusCode != http.StatusOK {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why we check the status code here? I think we can remove that check from the if statement because we check that a couple of lines before that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for that now, will remove and just check for error with the response parsing.

pkg/apis/rollouts/v1alpha1/analysis_types.go Outdated Show resolved Hide resolved
Comment on lines 205 to 228
func parsePrimitiveFromString(in string) interface{} {
// Chain ordering as follows:
// int -> float -> bool -> string

// 64 bit Int conversion
inAsInt, err := strconv.ParseInt(in, 10, 64)
if err == nil {
return inAsInt
}

// Float conversion
inAsFloat, err := strconv.ParseFloat(in, 64)
if err == nil {
return inAsFloat
}

// Bool conversion
inAsBool, err := strconv.ParseBool(in)
if err == nil {
return inAsBool
}

// Else
return in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned this approach maybe a little too implicit. For example, what if the user expects to evaluate a string, but the function converts the response into an int? In that case, they would have no options. I think we can make it a little more explicit by adding some functions to the condition language similar to https://github.com/antonmedv/expr/blob/master/docs/examples/demo.go#L23.

We would need to add these functions here: https://github.com/argoproj/argo-rollouts/blob/master/utils/evaluate/evaluate.go#L9-L13. This would allow us to have int, float, bool, and string functions that users can use to convert their results as they please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll take a look into this, thanks for the feedback on this portion!

@dthomson25
Copy link
Member

Also, can you rebase on master? We made a CI change in another commit that this PR can't use until rebasing with master. The change will make sure the Circle has enough resources to run the tests, and there shouldn't be any conflicts

@codecov
Copy link

codecov bot commented Dec 17, 2019

Codecov Report

Merging #318 into master will increase coverage by 0.16%.
The diff coverage is 84.71%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #318      +/-   ##
==========================================
+ Coverage   83.59%   83.76%   +0.16%     
==========================================
  Files          67       70       +3     
  Lines        6146     6456     +310     
==========================================
+ Hits         5138     5408     +270     
- Misses        727      751      +24     
- Partials      281      297      +16
Impacted Files Coverage Δ
utils/log/log.go 100% <ø> (ø) ⬆️
utils/replicaset/replicaset.go 88.33% <0%> (-0.5%) ⬇️
experiments/experiment.go 93.3% <100%> (+0.19%) ⬆️
utils/analysis/helpers.go 93.1% <100%> (+0.58%) ⬆️
utils/controller/controller.go 95.29% <100%> (+0.55%) ⬆️
rollout/controller.go 71.63% <100%> (-2.29%) ⬇️
utils/analysis/factory.go 97.29% <100%> (+0.32%) ⬆️
utils/experiment/experiment.go 93.04% <100%> (+0.81%) ⬆️
utils/service/service.go 100% <100%> (ø) ⬆️
rollout/canary.go 86.34% <100%> (+0.11%) ⬆️
... and 16 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6bd5577...e476ea7. Read the comment docs.

Web: &v1alpha1.WebMetric{
// URL: server.URL,
JSONPath: "{$.key[0].key2.value}",
Headers: []v1alpha1.WebMetricHeader{v1alpha1.WebMetricHeader{Key: "key", Value: "value"}},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File is not gofmt-ed with -s (from gofmt)

Suggested change
Headers: []v1alpha1.WebMetricHeader{v1alpha1.WebMetricHeader{Key: "key", Value: "value"}},
Headers: []v1alpha1.WebMetricHeader{{Key: "key", Value: "value"}},

@tomsanbear
Copy link
Contributor Author

I made the easy changes and added some additional testing. But will have to revisit the evaluation logic as discussed above when I have some time this week. 👍

@dthomson25
Copy link
Member

Hey @tomsanbear, I wanted to check in and see if you had a chance to get those last changes in. I totally understand if not with the holidays and everything the past couple of weeks. Please let me know if there's anything I can do to help!

@tomsanbear
Copy link
Contributor Author

Hey @tomsanbear, I wanted to check in and see if you had a chance to get those last changes in. I totally understand if not with the holidays and everything the past couple of weeks. Please let me know if there's anything I can do to help!

Hey @dthomson25 it's as you said holidays have been particularly busy for me. I think this weekend at the latest I should be able to get this last bit finished up. I took a look at the code for the evaluation mechanism during some downtime and it seems fairly straight forward to add for this PR. Thanks for reaching out!

@dthomson25 dthomson25 changed the title Web metric provider feat: Web metric provider Jan 7, 2020
@tomsanbear
Copy link
Contributor Author

@dthomson25 So I did a quick code of what we discussed... not sure if it's along the same idea as what you were thinking. Just a few of my thoughts:

  1. Is this a little too verbose? The way jsonpath handles this logic is mostly the problem here 🤔
  2. Any thoughts on the error handling for the eval engine?

If you had a different approach in mind I'll be interested to hear it.

@jessesuen
Copy link
Member

jessesuen commented Jan 11, 2020

We were trying this out, and utils/analysis/factory.go needs to be updated with this:

	if metric.Provider.Web != nil {
		numProviders++
	}

But once we got this change, things seemed to be working.

@tomsanbear tomsanbear marked this pull request as ready for review January 12, 2020 06:30
Comment on lines 13 to 26
"asInt": func(in string) int64 {
inAsInt, err := strconv.ParseInt(in, 10, 64)
if err == nil {
return inAsInt
}
panic(err)
},
"asFloat": func(in string) float64 {
inAsFloat, err := strconv.ParseFloat(in, 64)
if err == nil {
return inAsFloat
}
panic(err)
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely in line with what I was thinking! For the error handling, we should add a recover at line 28 to catch these panics. From there, we can return an error saying Unable to convert to Float and capture that in the status. Otherwise, the panic will break the rest of the analysis run. I think it will make the comparisons more explicit for the users and gives them flexibility if needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be a good candidate for some unit tests using the assert.Panic function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I'll add that in later today 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I added tests, and I did also add the recover clause. But it may be unecessary after playing around with the evaluation library. From the looks of it, it already wraps panics from internal functions as errors with nice pretty print debug messages.

got expected error: strconv.ParseFloat: parsing "NotANum": invalid syntax (1:9)
        	            	 | asFloat(result) == 1.1
        	            	 | ........^

Copy link
Member

@dthomson25 dthomson25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed a merge conflict

mkdocs.yml Outdated Show resolved Hide resolved
… a recover clause to the eval logic. NOTE: this clause might not be necessary.
Copy link
Member

@dthomson25 dthomson25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Unless you have any other changes you want to get in, I'll merge it in!

@tomsanbear
Copy link
Contributor Author

Nope! I am happy with this for a first implementation as this achieves what I was looking to have for pulling other metrics and stats.

@dthomson25 dthomson25 merged commit 002944a into argoproj:master Jan 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants