-
Notifications
You must be signed in to change notification settings - Fork 880
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
Changes from 4 commits
296169f
eb42130
56074a1
94cd16b
ff388e4
9eac5a3
3fa6f71
084b1d7
1046115
a24a733
5feb382
53ee15b
81f59fe
bdc1ec8
867a0f6
c61a43b
f870efe
88e59d6
53b806e
4496241
5dd2fce
e3321fe
0feb20b
81b91e1
eb30b36
6bd5577
99c8430
79d8718
e476ea7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
package webmetric | ||
|
||
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" | ||
) | ||
|
||
const ( | ||
//ProviderType indicates the provider is prometheus | ||
ProviderType = "WebMetric" | ||
) | ||
|
||
// Provider contains all the required components to run a WebMetric query | ||
// Implements the Provider Interface | ||
type Provider struct { | ||
logCtx log.Entry | ||
client *http.Client | ||
jsonParser *jsonpath.JSONPath | ||
} | ||
|
||
// Type incidates provider is a WebMetric provider | ||
func (p *Provider) Type() string { | ||
return ProviderType | ||
} | ||
|
||
func (p *Provider) Run(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric) v1alpha1.Measurement { | ||
var ( | ||
err error | ||
) | ||
|
||
startTime := metav1.Now() | ||
|
||
// Measurement to pass back | ||
measurement := v1alpha1.Measurement{ | ||
StartedAt: &startTime, | ||
} | ||
|
||
// Create request | ||
request := &http.Request{ | ||
Method: "GET", | ||
dthomson25 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
request.URL, err = url.Parse(metric.Provider.WebMetric.Url) | ||
if err != nil { | ||
return metricutil.MarkMeasurementError(measurement, err) | ||
} | ||
|
||
for _, header := range metric.Provider.WebMetric.Headers { | ||
request.Header.Set(header.Key, header.Value) | ||
} | ||
|
||
// Send Request | ||
response, err := p.client.Do(request) | ||
if err != nil || response.StatusCode != http.StatusOK { | ||
dthomson25 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return metricutil.MarkMeasurementError(measurement, err) | ||
dthomson25 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
value, status, err := p.parseResponse(metric, response) | ||
if err != nil || response.StatusCode != http.StatusOK { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
return metricutil.MarkMeasurementError(measurement, err) | ||
} | ||
|
||
measurement.Value = value | ||
measurement.Phase = status | ||
finishedTime := metav1.Now() | ||
measurement.FinishedAt = &finishedTime | ||
|
||
return measurement | ||
} | ||
|
||
func (p *Provider) parseResponse(metric v1alpha1.Metric, response *http.Response) (string, v1alpha1.AnalysisPhase, error) { | ||
var data interface{} | ||
|
||
bodyBytes, err := ioutil.ReadAll(response.Body) | ||
if err != nil { | ||
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Received no bytes in response: %v", err) | ||
} | ||
|
||
err = json.Unmarshal(bodyBytes, &data) | ||
if err != nil { | ||
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not parse JSON body: %v", err) | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
err = p.jsonParser.Execute(buf, data) | ||
if err != nil { | ||
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not find JSONPath in body: %s", err) | ||
} | ||
out := buf.String() | ||
outAsFloat, err := strconv.ParseFloat(buf.String(), 64) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
if err != nil { | ||
return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not convert response to a number: %s", err) | ||
} | ||
status := p.evaluateResponse(metric, outAsFloat) | ||
return out, status, nil | ||
} | ||
|
||
func (p *Provider) evaluateResponse(metric v1alpha1.Metric, result float64) v1alpha1.AnalysisPhase { | ||
successCondition := false | ||
failCondition := false | ||
var err error | ||
|
||
if metric.SuccessCondition != "" { | ||
successCondition, err = evaluate.EvalCondition(result, metric.SuccessCondition) | ||
if err != nil { | ||
p.logCtx.Warning(err.Error()) | ||
return v1alpha1.AnalysisPhaseError | ||
} | ||
} | ||
if metric.FailureCondition != "" { | ||
failCondition, err = evaluate.EvalCondition(result, metric.FailureCondition) | ||
if err != nil { | ||
return v1alpha1.AnalysisPhaseError | ||
} | ||
} | ||
|
||
switch { | ||
case metric.SuccessCondition == "" && metric.FailureCondition == "": | ||
//Always return success unless there is an error | ||
return v1alpha1.AnalysisPhaseSuccessful | ||
case metric.SuccessCondition != "" && metric.FailureCondition == "": | ||
// Without a failure condition, a measurement is considered a failure if the measurement's success condition is not true | ||
failCondition = !successCondition | ||
case metric.SuccessCondition == "" && metric.FailureCondition != "": | ||
// Without a success condition, a measurement is considered a successful if the measurement's failure condition is not true | ||
successCondition = !failCondition | ||
} | ||
|
||
if failCondition { | ||
return v1alpha1.AnalysisPhaseFailed | ||
} | ||
|
||
if !failCondition && !successCondition { | ||
return v1alpha1.AnalysisPhaseInconclusive | ||
} | ||
|
||
// If we reach this code path, failCondition is false and successCondition is true | ||
return v1alpha1.AnalysisPhaseSuccessful | ||
} | ||
|
||
// Resume should not be used the WebMetric provider since all the work should occur in the Run method | ||
func (p *Provider) Resume(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, measurement v1alpha1.Measurement) v1alpha1.Measurement { | ||
p.logCtx.Warn("WebMetric provider should not execute the Resume method") | ||
return measurement | ||
} | ||
|
||
// Terminate should not be used the WebMetric provider since all the work should occur in the Run method | ||
func (p *Provider) Terminate(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, measurement v1alpha1.Measurement) v1alpha1.Measurement { | ||
p.logCtx.Warn("WebMetric provider should not execute the Terminate method") | ||
return measurement | ||
} | ||
|
||
// GarbageCollect is a no-op for the WebMetric provider | ||
func (p *Provider) GarbageCollect(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, limit int) error { | ||
return nil | ||
} | ||
|
||
func NewWebMetricHttpClient(metric v1alpha1.Metric) *http.Client { | ||
var ( | ||
timeout time.Duration | ||
) | ||
|
||
if metric.Provider.WebMetric.Timeout <= 0 { | ||
timeout = time.Duration(10) * time.Second | ||
} else { | ||
timeout = time.Duration(metric.Provider.WebMetric.Timeout) * time.Second | ||
} | ||
c := &http.Client{ | ||
Timeout: timeout, | ||
} | ||
return c | ||
} | ||
|
||
func NewWebMetricJsonParser(metric v1alpha1.Metric) (*jsonpath.JSONPath, error) { | ||
jsonParser := jsonpath.New("metrics") | ||
|
||
err := jsonParser.Parse(metric.Provider.WebMetric.JsonPath) | ||
|
||
return jsonParser, err | ||
} | ||
|
||
func NewWebMetricProvider(logCtx log.Entry, client *http.Client, jsonParser *jsonpath.JSONPath) *Provider { | ||
return &Provider{ | ||
logCtx: logCtx, | ||
client: client, | ||
jsonParser: jsonParser, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package webmetric | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" | ||
log "github.com/sirupsen/logrus" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestRunSuccess(t *testing.T) { | ||
input := ` | ||
{ | ||
"key": [ | ||
{ | ||
"key2": { | ||
"value": 1 | ||
} | ||
} | ||
] | ||
}` | ||
|
||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | ||
rw.Header().Set("Content-Type", "application/json") | ||
io.WriteString(rw, input) | ||
})) | ||
// Close the server when test finishes | ||
defer server.Close() | ||
|
||
e := log.Entry{} | ||
|
||
metric := v1alpha1.Metric{ | ||
Name: "foo", | ||
SuccessCondition: "result > 0", | ||
FailureCondition: "result <= 0", | ||
Provider: v1alpha1.MetricProvider{ | ||
WebMetric: &v1alpha1.WebMetricMetric{ | ||
Url: server.URL, | ||
JsonPath: "{$.key[0].key2.value}", | ||
}, | ||
}, | ||
} | ||
|
||
jp, err := NewWebMetricJsonParser(metric) | ||
assert.NoError(t, err) | ||
|
||
p := NewWebMetricProvider(e, server.Client(), jp) | ||
|
||
measurement := p.Run(newAnalysisRun(), metric) | ||
assert.NotNil(t, measurement.StartedAt) | ||
assert.Equal(t, "1", measurement.Value) | ||
assert.NotNil(t, measurement.FinishedAt) | ||
assert.Equal(t, v1alpha1.AnalysisPhaseSuccessful, measurement.Phase) | ||
} | ||
|
||
func newAnalysisRun() *v1alpha1.AnalysisRun { | ||
return &v1alpha1.AnalysisRun{} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
API rule violation: names_match,github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1,MetricProvider,WebMetric | ||
API rule violation: names_match,github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1,RolloutStatus,HPAReplicas |
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.
Style Nix: Golang convention for imports is to alphabetize the imports and group them by
So, you can remove line 14 and remove line 20 to above 18 and run
make lint
to alphabetize them.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 problem with this, will run the linter this time around.