Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* [#2603](https://github.com/ruby-grape/grape/pull/2603): Remove `namespace_stackable_with_hash` from public interface and move to internal InheritableSetting - [@ericproulx](https://github.com/ericproulx).
* [#2604](https://github.com/ruby-grape/grape/pull/2604): Enable branch coverage - [@ericproulx](https://github.com/ericproulx).
* [#2605](https://github.com/ruby-grape/grape/pull/2605): Add Rack 3.2 support with new gemfile and CI integration - [@ericproulx](https://github.com/ericproulx).
* [#2608](https://github.com/ruby-grape/grape/pull/2608): Add body metadata to `ActiveSupport::Notifications` instrumentation - [@braktar](https://github.com/braktar).
* Your contribution here.

#### Fixes
Expand Down
40 changes: 40 additions & 0 deletions lib/grape/dsl/body_metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Grape
module DSL
# Module for extracting body metadata for endpoint notifications
# This provides information about the response body without reading its content
module BodyMetadata
# Extract body metadata for endpoint notifications
# This provides information about the response body without reading its content
def extract_endpoint_body_metadata
metadata = {
has_body: instance_variable_defined?(:@body) && [email protected]?,
has_stream: instance_variable_defined?(:@stream) && [email protected]?,
status: instance_variable_defined?(:@status) ? @status : nil
}

if metadata[:has_body]
metadata[:body_type] = @body.class.name
metadata[:body_responds_to_size] = @body.respond_to?(:size)
metadata[:body_size] = @body.respond_to?(:size) ? @body.size : nil
end

if metadata[:has_stream]
metadata[:stream_type] = @stream.class.name
if @stream.respond_to?(:stream)
metadata[:stream_inner_type] = @stream.stream.class.name
metadata[:stream_file_path] = @stream.stream.to_path if @stream.stream.respond_to?(:to_path)
end
end

if env
metadata[:api_format] = env[Grape::Env::API_FORMAT]
metadata[:content_type] = env['CONTENT_TYPE']
end

metadata
end
end
end
end
9 changes: 5 additions & 4 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Grape
class Endpoint
extend Forwardable
include Grape::DSL::Settings
include Grape::DSL::BodyMetadata
include Grape::DSL::Headers
include Grape::DSL::InsideRoute

Expand Down Expand Up @@ -80,7 +81,7 @@ def initialize(new_settings, options = {}, &block)
if block
@source = block
@block = lambda do |endpoint_instance|
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance, body_metadata: endpoint_instance.extract_endpoint_body_metadata) do
endpoint_instance.instance_exec(&block)
rescue LocalJumpError => e
Grape.deprecator.warn 'Using `return` in an endpoint has been deprecated.'
Expand Down Expand Up @@ -218,7 +219,7 @@ def inspect
protected

def run
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env, body_metadata: extract_endpoint_body_metadata) do
@request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
begin
self.class.run_before_each(self)
Expand Down Expand Up @@ -272,7 +273,7 @@ def lazy_initialize!
def run_validators(validators, request)
validation_errors = []

ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request, body_metadata: extract_endpoint_body_metadata) do
validators.each do |validator|
validator.validate(request)
rescue Grape::Exceptions::Validation => e
Expand All @@ -288,7 +289,7 @@ def run_validators(validators, request)
end

def run_filters(filters, type = :other)
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type, body_metadata: extract_endpoint_body_metadata) do
filters&.each { |filter| instance_eval(&filter) }
end
post_extension = DSL::InsideRoute.post_filter_methods(type)
Expand Down
27 changes: 26 additions & 1 deletion lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ def build_formatted_response(status, headers, bodies)
else
# Allow content-type to be explicitly overwritten
formatter = fetch_formatter(headers, options)
bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do
bodymap = ActiveSupport::Notifications.instrument('format_response.grape',
formatter: formatter,
env: env,
body_metadata: extract_body_metadata(bodies, status, headers)) do
bodies.collect { |body| formatter.call(body, env) }
end
Rack::Response.new(bodymap, status, headers)
Expand Down Expand Up @@ -145,6 +148,28 @@ def format_from_header
media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
mime_types[media_type] if media_type
end

def extract_body_metadata(bodies, status, headers)
metadata = {
is_stream: bodies.is_a?(Grape::ServeStream::StreamResponse),
status: status,
content_type: headers[Rack::CONTENT_TYPE] || content_type_for(env[Grape::Env::API_FORMAT]),
format: env[Grape::Env::API_FORMAT],
has_entity_body: !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
}

if bodies.is_a?(Grape::ServeStream::StreamResponse)
metadata[:stream_type] = bodies.stream.class.name
# For file streams, we can get the path without reading content
metadata[:file_path] = bodies.stream.to_path if bodies.stream.respond_to?(:to_path)
else
# For regular bodies (Array), provide count and types without reading content
metadata[:body_count] = bodies.respond_to?(:size) ? bodies.size : 1
metadata[:body_types] = bodies.respond_to?(:map) ? bodies.map { |x| x.class.name } : [bodies.class.name]
end

metadata
end
end
end
end
Loading