-
Notifications
You must be signed in to change notification settings - Fork 369
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
Grape status codes #1238
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.
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
module Datadog | ||
module Contrib | ||
module Grape | ||
class ErrorMatcher |
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 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 |
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, 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
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.
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).
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? |
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.
what is status
in this case? i don't see that method defined anywhere
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 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]
.
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.
👍 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})?)*$/) |
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.
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) |
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.
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)
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) |
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 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:
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.
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.
Sounds good!
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.
Ditto to @marcotc comments, and also just suggesting that && matcher
is not necessary if we have a default
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.
Some small points of feedback, but very close
One other thing not mentioned is that
- Let's move this out of draft (you can select "ready for review")
- 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 theoptions
table to include the new option
key
(error_responses
),description
(brief summary of what the option does. can be a few sentences if needed
), anddefault 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| |
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 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' |
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 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) |
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.
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' } |
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 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
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.
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
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.
sure i think that works just fine
end | ||
|
||
def to_s | ||
@@error_response_range.to_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.
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. |
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.
500-599
can be replaced with a sensibly named default constant within, `'ddtrace/ext/http', as discussed previously.
…variable in alignment with RFC
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.
two extremely small changes then lgtm
docs/GettingStarted.md
Outdated
@@ -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` | |
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.
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
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
ugh the linting gods |
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 |
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.
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 🤔
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.
ah, woops, that's my bad i had suggested that
...yea let's use default as just Datadog::Ext::HTTP::ERROR_RANGE.to_a
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.
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.
…matchers Co-authored-by: Marco Costa <mmarcottulio@gmail.com>
Checks are failing from the changes using |
@Kyle-Neale I read your statement as the opposite: expect(spans[0].get_tag('error.stack')).to_not be_nil should become expect(spans[0]).to have_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.
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 |
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.
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 |
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 |
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.
Unfort we need to default to nil here to ensure we dont introduce a breaking 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 |
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 |
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.
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
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 |
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
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.
Some very small nits/typos then this is good to go, approving now, nice work!
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>
Co-authored-by: Eric Mustin <mustin.eric@gmail.com>
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 description provided.