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

Grape status codes #1238

Merged
merged 29 commits into from
Nov 12, 2020
Merged

Grape status codes #1238

merged 29 commits into from
Nov 12, 2020

Conversation

Kyle-Neale
Copy link
Contributor

No description provided.

Copy link
Contributor

@ericmustin ericmustin left a comment

Choose a reason for hiding this comment

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

Nice work here, sorry for the delay. I think this is pretty close all things considered. There are a few architecture decisions i have some feedback on that I think will help us leverage this feature for other frameworks, but the implementation of the logic itself looks good to me. The choice of a set is very clever, nice stuff.

I'll try to review the tests a bit more thoroughly as well but I wanted to get back this feedback so you could review the design feedback

lib/ddtrace/contrib/grape/endpoint.rb Outdated Show resolved Hide resolved
module Datadog
module Contrib
module Grape
class ErrorMatcher
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the idea of splitting out all the error matching logic into it's own re-usable chunk of code makes sense. We can abstract out some of the logic of set_range / handle_statuses / error_responses into a broad contrib helper function that other web frameworks could leverage in the future. One approach is to construct a module similar to Http Annotation Helper, that could live in the same directory, that exposes a method called something like compute_status_ranges which takes as an input the specific integration's raw error_responses value, and does the validation+parsing, and returns a Set. then in the grape/endpoint.rb method set_range all we'd have to do is include something like:
include Datadog::Contrib::HttpErrorResponseStatusHelper

call something like

def set_range
  @datadog_set ||= compute_status_ranges(datadog_configuration[:error_responses])`
end

And all the repetitive work would be re-usable in different web frameworks. basically let's encapsulate everything we can in a way that framework agnostic, so then all we have to do is include the helper module. This also makes testing easier

@@ -24,6 +24,7 @@ class Settings < Contrib::Configuration::Settings
end

option :service_name, default: Ext::SERVICE_NAME
option :error_responses, default: '500-599' # quite possible we may be able to use Datadog::Ext::HTTP::ERROR_RANGE
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, It makes sense to use the constant when possible, i think it fits here. It's also fine to make a new constant if you feel the use case is different (even if the value is the same) Datadog::Ext::HTTP::ERROR_RESPONSE_RANGE perhaps

Copy link
Member

@marcotc marcotc Nov 10, 2020

Choose a reason for hiding this comment

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

option :error_responses, default: '500-599' do |o|
  o.default { '500-599' }
  o.setter do |new_value, old_value|
    Datadog::Contrib::Helpers::StatusCodeMatcher.new(new_value)
  end
end

We should ensure that StatusCodeMatcher implements to_s, as we are going to print it during start up. to_s should return the configured value ('500-599' in this case).

lib/ddtrace/contrib/grape/endpoint.rb Outdated Show resolved Hide resolved
Comment on lines 208 to 209
return datadog_configuration[:error_responses] if datadog_configuration[:error_responses].is_a?(String) && !status.nil?
datadog_configuration[:error_responses].join(',') if status.is_a?(Array) && !status.empty?
Copy link
Contributor

Choose a reason for hiding this comment

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

what is status in this case? i don't see that method defined anywhere

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 was actually code that I was testing and forgot to update in the actual dd-trace-rb. I'll update it but its' supposed to be datadog_configuration[:error_responses].

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 sounds good

def handle_statuses
if error_responses
error_responses.gsub(/\s+/, '').split(',').select do |code|
if !code.to_s.match(/^\d{3}(?:-\d{3})?(?:,\d{3}(?:-\d{3})?)*$/)
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's make abstract as much of this regex stuff into constants as we can, it should help perf, see an example here

Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
def exception_is_error?(exception)
status = nil
return false unless exception
if exception.respond_to?('status') && set_range.include?(exception.status)
Copy link
Member

Choose a reason for hiding this comment

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

After https://github.com/DataDog/dd-trace-rb/pull/1238/files#r520721404 is implemented, datadog_configuration[:error_responses] will return the matcher object StatusCodeMatcher, and we'll be able to directly call it:

matcher = datadog_configuration[:error_responses]
if exception.respond_to?('status') && matcher && matcher.include?(exception.status)

@marcotc marcotc mentioned this pull request Nov 10, 2020
def exception_is_error?(exception)
matcher = datadog_configuration[:error_responses]
return false unless exception
if exception.respond_to?('status') && matcher && matcher.set_range.include?(exception.status)
Copy link
Member

Choose a reason for hiding this comment

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

I think the implementation looks more clear if we hide the fact the we have a set_range internally.

If we only expose the methods that we'd like the user to have access to, I believe the code looks more organized:

Suggested change
if exception.respond_to?('status') && matcher && matcher.set_range.include?(exception.status)
if exception.respond_to?('status') && matcher && matcher.include?(exception.status)

This requires implementing def include?(status) in the StatusCodeMatcher class.

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!

Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to @marcotc comments, and also just suggesting that && matcher is not necessary if we have a default

Copy link
Contributor

@ericmustin ericmustin left a comment

Choose a reason for hiding this comment

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

Some small points of feedback, but very close

One other thing not mentioned is that

  1. Let's move this out of draft (you can select "ready for review")
  2. Let's add a brief update to the GettingStarted.md markdown Grape section that explains the option briefly https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#grape. We just need to update the options table to include the new option
  • key (error_responses),
  • description (brief summary of what the option does. can be a few sentences if needed), and
  • default value('500-599')

@@ -24,6 +24,12 @@ class Settings < Contrib::Configuration::Settings
end

option :service_name, default: Ext::SERVICE_NAME
option :error_responses, default: '500-599' do |o|
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need default here and in the block, can remove here

require 'ddtrace/ext/http'
require 'ddtrace/ext/errors'
require 'ddtrace/contrib/analytics'
require 'ddtrace/contrib/rack/ext'
require 'ddtrace/contrib/status_code_matcher'
Copy link
Contributor

Choose a reason for hiding this comment

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

i dont think we need to include the status_code_matcher file here, but in the grape/settings.rb file instead

def exception_is_error?(exception)
matcher = datadog_configuration[:error_responses]
return false unless exception
if exception.respond_to?('status') && matcher && matcher.set_range.include?(exception.status)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to @marcotc comments, and also just suggesting that && matcher is not necessary if we have a default

@@ -24,6 +24,12 @@ class Settings < Contrib::Configuration::Settings
end

option :service_name, default: Ext::SERVICE_NAME
option :error_responses, default: '500-599' do |o|
o.default { '500-599' }
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the default should be Datadog::Contrib::StatusCodeMatcher.new(500-599), if i'm understanding things correctly.

also let's abstract out 500-599 into a constant within the ddtrace/ext/http module, that will help with keeping things DRY

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm wondering if it makes sense to just use the ERROR_RANGE that's already provided. With that, I can do something like this:

option :error_responses, default: '500-599' do |o|
    o.default { Datadog::Contrib::StatusCodeMatcher.new((Datadog::Ext::HTTP::ERROR_RANGE).to_a) }
    ...

This way, we get functionality from an existing variable rather than defining a new and similar constant

Copy link
Contributor

Choose a reason for hiding this comment

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

sure i think that works just fine

end

def to_s
@@error_response_range.to_s
Copy link
Contributor

Choose a reason for hiding this comment

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

why the @@ class variable here instead of the @ instance variable?

end
else
Datadog.logger.debug('No valid config was provided for :error_responses - falling back to default.')
['500-599'] # Rather than returning an empty array, we need to fallback to default config.
Copy link
Contributor

Choose a reason for hiding this comment

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

500-599 can be replaced with a sensibly named default constant within, `'ddtrace/ext/http', as discussed previously.

@Kyle-Neale Kyle-Neale marked this pull request as ready for review November 12, 2020 16:11
@Kyle-Neale Kyle-Neale requested a review from a team November 12, 2020 16:11
Copy link
Contributor

@ericmustin ericmustin left a comment

Choose a reason for hiding this comment

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

two extremely small changes then lgtm

@@ -816,6 +816,7 @@ Where `options` is an optional `Hash` that accepts the following parameters:
| `analytics_enabled` | Enable analytics for spans produced by this integration. `true` for on, `nil` to defer to global setting, `false` for off. | `nil` |
| `enabled` | Defines whether Grape should be traced. Useful for temporarily disabling tracing. `true` or `false` | `true` |
| `service_name` | Service name used for `grape` instrumentation | `'grape'` |
| `error_statuses`| Defines a status code or range of status codes which should be marked as errors. `'500-599'` or `['500-599']` | `500-599` |
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit, could we also include an example here that shows support multiple ranges, for clarity? It's unclear currently that there can be multiple ranges

lib/ddtrace/contrib/status_code_matcher.rb Show resolved Hide resolved
@ericmustin
Copy link
Contributor

ugh the linting gods

@ericmustin ericmustin added this to the 0.43.0 milestone Nov 12, 2020
@ericmustin ericmustin added the feature Involves a product feature label Nov 12, 2020
@ericmustin ericmustin added the integrations Involves tracing integrations label Nov 12, 2020
@Kyle-Neale Kyle-Neale dismissed ericmustin’s stale review November 12, 2020 16:48

Changes have been added

Comment on lines 30 to 33
o.default { Datadog::Contrib::StatusCodeMatcher.new(Datadog::Ext::HTTP::ERROR_RANGE.to_a) }
o.setter do |new_value, _old_value|
Datadog::Contrib::StatusCodeMatcher.new(new_value)
end
Copy link
Member

@marcotc marcotc Nov 12, 2020

Choose a reason for hiding this comment

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

Does the default case still work?

I think it will perform:

# from o.default { Datadog::Contrib::StatusCodeMatcher.new(Datadog::Ext::HTTP::ERROR_RANGE.to_a) }
default = Datadog::Contrib::StatusCodeMatcher.new(Datadog::Ext::HTTP::ERROR_RANGE.to_a)

# from o.setter do |new_value, _old_value|
Datadog::Contrib::StatusCodeMatcher.new(default)

Which will likely break when StatusCodeMatcher.new receives an instance of StatusCodeMatcher instead of a string 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, woops, that's my bad i had suggested that

...yea let's use default as just Datadog::Ext::HTTP::ERROR_RANGE.to_a

Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't break bc @Kyle-Neale wrote good error handling / checking in StatusCodeMatcher but, yea, we should be passing in the raw value, my fault.

@Kyle-Neale
Copy link
Contributor Author

Kyle-Neale commented Nov 12, 2020

Checks are failing from the changes using custom handles that @marcotc suggested. Why should we be expecting the exception to not throw an error?

@marcotc
Copy link
Member

marcotc commented Nov 12, 2020

@Kyle-Neale I read your statement as the opposite: 86c8c32 (#1238)

expect(spans[0].get_tag('error.stack')).to_not be_nil

should become

expect(spans[0]).to have_error

Copy link
Contributor

@ericmustin ericmustin left a comment

Choose a reason for hiding this comment

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

Left some last mile changes here to ensure we dont introduce breaking changes. should be good to go after this

expect(spans[0].get_tag('error.type')).to_not be_nil
expect(spans[0].get_tag('error.msg')).to_not be_nil
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
end
end
context 'and error_responses with arrays that dont contain exception status' do
subject(:response) { post '/base/hard_failure' }
let(:configuration_options) { { error_statuses: ['300-399', 'xxx-xxx', 1111, 406] } }
it 'should handle exceptions' do
expect(response.body).to eq('405 Not Allowed')
expect(spans.length).to eq(1)
expect(spans[0].name).to eq('grape.endpoint_run')
expect(spans[0]).to_not have_error
expect(spans[0].get_tag('error.stack')).to be_nil
expect(spans[0].get_tag('error.type')).to be_nil
expect(spans[0].get_tag('error.msg')).to be_nil
end
end

docs/GettingStarted.md Outdated Show resolved Hide resolved
Comment on lines 29 to 33
option :error_statuses do |o|
o.default { Datadog::Ext::HTTP::ERROR_RANGE.to_a }
o.setter do |new_value, _old_value|
Datadog::Contrib::StatusCodeMatcher.new(new_value)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfort we need to default to nil here to ensure we dont introduce a breaking change

Suggested change
option :error_statuses do |o|
o.default { Datadog::Ext::HTTP::ERROR_RANGE.to_a }
o.setter do |new_value, _old_value|
Datadog::Contrib::StatusCodeMatcher.new(new_value)
end
option :error_statuses do |o|
o.default { nil }
o.setter do |new_value, _old_value|
return if new_value.nil?
Datadog::Contrib::StatusCodeMatcher.new(new_value)
end

Comment on lines 206 to 215
def exception_is_error?(exception)
matcher = datadog_configuration[:error_statuses]
return false unless exception
if exception.respond_to?('status') && matcher.include?(exception.status)
status = exception.status
else
return true
end
!status.nil?
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that datadog_configuration[:error_statuses] willl now default to nil, we can clean up this logic a bit. currently, by having if exception.respond_to?('status') && matcher.include?(exception.status) in the same if statment, we aren't properly returning false when status exists but matcher.include?(exception.status) returns false. The below updates to account for this

Suggested change
def exception_is_error?(exception)
matcher = datadog_configuration[:error_statuses]
return false unless exception
if exception.respond_to?('status') && matcher.include?(exception.status)
status = exception.status
else
return true
end
!status.nil?
end
def exception_is_error?(exception)
matcher = datadog_configuration[:error_statuses]
return false unless exception
return true unless matcher
return true unless exception.respond_to?('status')
matcher.include?(exception.status)
end

ericmustin
ericmustin previously approved these changes Nov 12, 2020
Copy link
Contributor

@ericmustin ericmustin left a comment

Choose a reason for hiding this comment

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

Some very small nits/typos then this is good to go, approving now, nice work!

lib/ddtrace/ext/http.rb Outdated Show resolved Hide resolved
docs/GettingStarted.md Outdated Show resolved Hide resolved
lib/ddtrace/contrib/status_code_matcher.rb Outdated Show resolved Hide resolved
lib/ddtrace/contrib/status_code_matcher.rb Outdated Show resolved Hide resolved
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
Kyle-Neale and others added 3 commits November 12, 2020 15:58
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
ericmustin
ericmustin previously approved these changes Nov 12, 2020
Copy link
Contributor

@ericmustin ericmustin left a comment

Choose a reason for hiding this comment

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

🚀

@ericmustin ericmustin merged commit f4346a6 into DataDog:master Nov 12, 2020
@Kyle-Neale Kyle-Neale deleted the grape_status_codes branch November 12, 2020 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Involves a product feature integrations Involves tracing integrations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants