Add URL option for sampling strategies file#2519
Add URL option for sampling strategies file#2519yurishkuro merged 9 commits intojaegertracing:masterfrom
Conversation
| go 1.14 | ||
|
|
||
| require ( | ||
| github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect |
There was a problem hiding this comment.
I do not believe this PR requires changes to dependencies. Please exclude the go.mod/go.sum files.
There was a problem hiding this comment.
I've synced my fork with the upstream master. Do I still need to exclude go.mod/go.sum files?
There was a problem hiding this comment.
if this PR does not require new dependencies, it should not be updating dependency file. Please do checkout master <file> to get the originals.
|
|
||
| const ( | ||
| // SamplingStrategiesFile contains the name of CLI opions for config file. | ||
| // SamplingStrategiesFile contains the name of CLI options for config file. |
There was a problem hiding this comment.
| // SamplingStrategiesFile contains the name of CLI options for config file. | |
| // SamplingStrategiesFile contains the name of CLI option for config file. |
| SamplingStrategiesFile = "sampling.strategies-file" | ||
| samplingStrategiesReloadInterval = "sampling.strategies-reload-interval" | ||
| // SamplingStrategiesURL contains the name of CLI option for config file URL. | ||
| SamplingStrategiesURL = "sampling.strategies-url" |
There was a problem hiding this comment.
I think the intention of the ticket was to allow sampling.strategies-file to contain a URL. A new flag seems redundant.
| if err != nil { | ||
| h.logger.Error("failed to load sampling strategies", zap.String("file", filePath), zap.Error(err)) | ||
| return lastValue | ||
| func (h *strategyStore) reloadSamplingStrategy(reloadFrom string, path string, lastValue string) string { |
There was a problem hiding this comment.
The caller of this function already does checks for file vs. url. I would pass a loader function here that abstracts the source of the new config, e.g. loader func () (string, error), and only deal with processing the new value.
There was a problem hiding this comment.
Yes, that's better. Was going to do the same but changed my mind later.
| } | ||
|
|
||
| func downloadStrategies(strategiesURL string) (*strategies, error) { | ||
| resp, err := http.Get(strategiesURL) |
There was a problem hiding this comment.
this duplicates code from L112
|
@yurishkuro Thanks for the suggestions. I've made changes to the pull request |
yurishkuro
left a comment
There was a problem hiding this comment.
top-level question: should there be some local file fallback option if the URL is temporarily unavailable? Because if whichever service provides the strategy for download is down the collector would not be able to start.
| } | ||
| if err = h.updateSamplingStrategy(currBytes); err != nil { | ||
| h.logger.Error("failed to update sampling strategies from file", zap.Error(err)) | ||
| if err := h.updateSamplingStrategy([]byte(newValue)); err != nil { |
There was a problem hiding this comment.
Consider if ([]byte, error) would be more appropriate signature for the loader(), to avoid casting back and forth.
| if strategiesFile == "" { | ||
| return nil, nil | ||
| // Check if it's a URL. | ||
| if ok := isURL(strategiesFile); ok { |
There was a problem hiding this comment.
there is an existing redundancy in the original source, please consolidate it. You should not need to check isURL twice in this source file.
How about falling back to default strategies similar to when |
|
Default strategy is fine. The difference with StrategiesFile not found is in that case it's ok to fail because it likely indicates a mis-configuration, whereas in case of HTTP server not available then failing is counterproductive. |
| } | ||
|
|
||
| func samplingStrategyLoader(strategiesFile string) strategyLoader { | ||
| return func() ([]byte, error) { |
There was a problem hiding this comment.
nit: you only need to test strategiesFile once, and return different functors. Otherwise strategyLoader is pointless, you could've just call the function to read stuff
if strategiesFile == "" {
return func() ([]byte, error) {
return []byte("null"), nil
}
}
if isURL(strategiesFile) {
return func() ([]byte, error) {
return downloadSamplingStrategies(strategiesFile)
}
}
There was a problem hiding this comment.
Oops, I was being silly. I get this, thanks.
| return func() ([]byte, error) { | ||
| if strategiesFile == "" { | ||
| // Using "null" so that it un-marshals to nil pointer. | ||
| return []byte("null"), nil |
There was a problem hiding this comment.
may want to declare this as a constant
|
|
||
| func (h *strategyStore) reloadSamplingStrategyFile(filePath string, lastValue string) string { | ||
| currBytes, err := ioutil.ReadFile(filepath.Clean(filePath)) | ||
| func (h *strategyStore) reloadSamplingStrategy(loader strategyLoader, lastValue string) string { |
There was a problem hiding this comment.
| func (h *strategyStore) reloadSamplingStrategy(loader strategyLoader, lastValue string) string { | |
| func (h *strategyStore) reloadSamplingStrategy(loadFn strategyLoader, lastValue string) string { |
|
please check why the DCO test is failing: You need to ensure full name in git matches full name in github |
This change will let user to provide a URL to download sampling strategies. Default strategy is the fallback option if the URL is temporarily unavailable. Signed-off-by: Deepak <sah.sslpu@gmail.com>
| return func() ([]byte, error) { | ||
| currBytes, err := ioutil.ReadFile(strategiesFile) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to open strategies file: %w", err) |
There was a problem hiding this comment.
| return nil, fmt.Errorf("failed to open strategies file: %w", err) | |
| return nil, fmt.Errorf("failed to read strategies file %s: %w", strategiesFile , err) |
| ) | ||
|
|
||
| // null represents "null" JSON value. | ||
| const null = "null" |
There was a problem hiding this comment.
I was rather thinking this:
| const null = "null" | |
| var nullJSON = []byte("null") |
| defer resp.Body.Close() | ||
| if resp.StatusCode != http.StatusOK { | ||
| if resp.StatusCode == http.StatusServiceUnavailable { | ||
| return []byte("null"), nil |
There was a problem hiding this comment.
| return []byte("null"), nil | |
| return nullJSON, nil |
|
|
||
| defer resp.Body.Close() | ||
| if resp.StatusCode != http.StatusOK { | ||
| if resp.StatusCode == http.StatusServiceUnavailable { |
There was a problem hiding this comment.
this does not need to be nested inside if != ok, you can do this check first, then if != OK.
|
|
||
| buf := new(bytes.Buffer) | ||
| if _, err = buf.ReadFrom(resp.Body); err != nil { | ||
| return nil, fmt.Errorf("failed to read sampling strategies from downloaded JSON: %w", err) |
There was a problem hiding this comment.
| return nil, fmt.Errorf("failed to read sampling strategies from downloaded JSON: %w", err) | |
| return nil, fmt.Errorf("failed to read sampling strategies HTTP response body: %w", err) |
| // Using null so that it un-marshals to nil pointer. | ||
| return []byte(null), nil |
There was a problem hiding this comment.
this comment should be on the constant
| // Using null so that it un-marshals to nil pointer. | |
| return []byte(null), nil | |
| return nullJSON, nil |
| } | ||
|
|
||
| func samplingStrategyLoader(strategiesFile string) strategyLoader { | ||
| if strategiesFile == "" { |
There was a problem hiding this comment.
Shouldn't this be an error upon instantiation of strategyStore?
There was a problem hiding this comment.
Didn't quite understand this one. Can you elaborate it further? 😅
There was a problem hiding this comment.
There is a constructor function NewStrategyStore. This struct cannot do anything useful if the strategiesFile is empty, correct? So we could just return an error from there if the param is empty. If that's not sufficient (e.g. because it still needs to return the default strategy), then we can still do that in the constructor and just not execute all the loading/reloading functions if the file name is blank. This way we don't have to keep checking if the file path is empty or not.
There was a problem hiding this comment.
I get it. Thanks for the explanation.
| } | ||
|
|
||
| return func() ([]byte, error) { | ||
| currBytes, err := ioutil.ReadFile(strategiesFile) |
There was a problem hiding this comment.
you may get go-sec linter error here, wrap the file name:
| currBytes, err := ioutil.ReadFile(strategiesFile) | |
| currBytes, err := ioutil.ReadFile(filepath.Clean(strategiesFile)) |
There was a problem hiding this comment.
While you're at it, can you fix this line too? I.e. remove nolint comment and use filepath.Clean
./cmd/query/app/static_handler.go:210: bytes, err := ioutil.ReadFile(uiConfig) /* nolint #nosec , this comes from an admin, not user */
This updates the logic for NewStrategyStore constructor to skip any loading/reloading logic to execute when StrategiesFile is empty. Signed-off-by: Deepak <sah.sslpu@gmail.com>
yurishkuro
left a comment
There was a problem hiding this comment.
Looks great! I left a couple comments around the tests, otherwise LGTM.
| } | ||
|
|
||
| func (h *strategyStore) autoUpdateStrategies(interval time.Duration, loader strategyLoader) { | ||
| lastValue := string(nullJSON) |
There was a problem hiding this comment.
Technically, we could have already loaded something before calling this function, so lastValue would be different. But since it's a periodic check anyway, I think it's fine.
|
|
||
| // update original strategies file with new probability of 0.9 | ||
| newStr = strings.Replace(string(srcBytes), "0.8", "0.9", 1) | ||
| require.NoError(t, ioutil.WriteFile(srcFile, []byte(newStr), 0644)) |
There was a problem hiding this comment.
I don't think it's a good idea for the test to override the fixture file. In L316 it is already copied to a temp file that can be overwritten.
Also, if you're testing with a mock HTTP server, why use file in the first place? You can simply replace the content returned by the mock server since you can control it.
| } | ||
| assert.EqualValues(t, makeResponse(sampling.SamplingStrategyType_PROBABILISTIC, 0.9), *s) | ||
|
|
||
| // Test auto update strategy with URL option. |
There was a problem hiding this comment.
This seems like a pretty separate testing scenario, any reason why it cannot be moved into a new test function with appropriate name?
There was a problem hiding this comment.
Yes, it makes sense to move it to a separate test function.
| func TestStrategyStore(t *testing.T) { | ||
| _, err := NewStrategyStore(Options{StrategiesFile: "fileNotFound.json"}, zap.NewNop()) | ||
| assert.EqualError(t, err, "failed to open strategies file: open fileNotFound.json: no such file or directory") | ||
| assert.EqualError(t, err, "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") |
There was a problem hiding this comment.
I recommend using assert.Contains (as in L79) to check only for the text produced by our code, not the parts produced by stdlib functions (since they can change the text between Go versions).
This refactors the strategy store tests, moving tests with URL to separate test functions. Signed-off-by: Deepak <sah.sslpu@gmail.com>
Signed-off-by: Deepak <sah.sslpu@gmail.com>
| func TestStrategyStoreWithFile(t *testing.T) { | ||
| _, err := NewStrategyStore(Options{StrategiesFile: "fileNotFound.json"}, zap.NewNop()) | ||
| assert.EqualError(t, err, "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") | ||
| assert.Contains(t, err.Error(), "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") |
There was a problem hiding this comment.
| assert.Contains(t, err.Error(), "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") | |
| assert.Contains(t, err.Error(), "failed to read strategies file fileNotFound.json") |
only include the string that we generate, not the stdlib portion
| assert.Equal(t, string(strategiesJSON), value) | ||
|
|
||
| // update original strategies with new probability of 0.9 | ||
| strategiesJSON = []byte(` |
There was a problem hiding this comment.
This is not a good approach to change package variable in-place. Even though it's a var, the general assumption is that package-level vars are constants (in fact I would change its type to string to make a real constant).
An even better approach is to convert it into a function that takes the probability value and returns the full JSON string via Sprintf. Then I would also change makeMockServer to return (strategy *atomic.String, server *httptest.Server) (using uber-go/atomic), so that you can manipulate the strategy externally, e.g.:
mockStrategy, server := mockStrategyServer()
...
mockStrategy.Store(strategiesJSON(0.9))
...This updates the mockStrategyServer to return a mock strategy along with a mock server. The returned strategy can be used to update it externally. Also, changed package-level var strategiesJSON to a function to return strategies JSON with a given probability. Signed-off-by: Deepak <sah.sslpu@gmail.com>
|
please check the linter failures |
Signed-off-by: Deepak <sah.sslpu@gmail.com>
Fixed the linter issues. |
Codecov Report
@@ Coverage Diff @@
## master #2519 +/- ##
=========================================
Coverage ? 95.31%
=========================================
Files ? 208
Lines ? 9281
Branches ? 0
=========================================
Hits ? 8846
Misses ? 356
Partials ? 79
Continue to review full report at Codecov.
|
|
Thank you @goku321 ! |
Which problem is this PR solving?
Short description of the changes