Skip to content

Commit 5142496

Browse files
maurogeorgebf4
authored andcommitted
ActiveSupport::Notifications render.active_model_serializers
Squashed commits: Add Logging Generates logging when renders a serializer. Tunning performance on notify_active_support - Use yield over block.call - Freeze the event name string Organize the logger architeture * Keep only the `ActiveModel::Serializer.logger` to follow the same public API we have for example to config, like `ActiveModel::Serializer.config.adapter` and remove the `ActiveModelSerializers.logger` API. * Define the logger on the load of the AMS, following the Rails convention on Railties [1], [2] and [3]. This way on non Rails apps we have a default logger and on Rails apps we will use the `Rails.logger` the same way that Active Job do [4]. [1]: https://github.com/rails/rails/blob/2ad9afe4ff2c12d1a9e4a00d9009d040e636c9b0/activejob/lib/active_job/railtie.rb#L9-L11 [2]: https://github.com/rails/rails/blob/2ad9afe4ff2c12d1a9e4a00d9009d040e636c9b0/activerecord/lib/active_record/railtie.rb#L75-L77 [3]: https://github.com/rails/rails/blob/2ad9afe4ff2c12d1a9e4a00d9009d040e636c9b0/actionview/lib/action_view/railtie.rb#L19-L21 [4]: https://github.com/rails/rails/blob/2ad9afe4ff2c12d1a9e4a00d9009d040e636c9b0/activejob/lib/active_job/logging.rb#L10-L11 Performance tunning on LogSubscriber#render Move the definition of locals to inside the `info` block this way the code is executed only when the logger is called. Remove not needed check on SerializableResource Use SerializableResource on ActionController integration On the ActionController was using a adapter, and since the instrumentation is made on the SerializableResource we need to use the SerializableResource over the adapter directly. Otherwise the logger is not called on a Rails app. Use SerializableResource on the ActionController, since this is the main interface to create and call a serializer. Using always the SerializableResource we can keep the adapter code more easy to mantain since no Adapter will need to call the instrumentation, only the SerializableResource care about this. Add docs about logging Add a CHANGELOG entry Keep the ActiveModelSerializers.logger Better wording on Logging docs [ci skip] Add doc about instrumentation [ci skip] Use ActiveModel::Callbacks on the SerializableResource
1 parent efe5128 commit 5142496

File tree

10 files changed

+183
-1
lines changed

10 files changed

+183
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Breaking changes:
1414

1515
Features:
1616

17+
- [#1291](https://github.com/rails-api/active_model_serializers/pull/1291) Add logging (@maurogeorge)
1718
- [#1225](https://github.com/rails-api/active_model_serializers/pull/1125) Better serializer lookup, use nested serializer when it exists (@beauby)
1819
- [#1172](https://github.com/rails-api/active_model_serializers/pull/1172) Better serializer registration, get more than just the first module (@bf4)
1920
- [#1158](https://github.com/rails-api/active_model_serializers/pull/1158) Add support for wildcards in `include` option (@beauby)

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ This is the documentation of AMS, it's focused on the **0.10.x version.**
99
- [Getting Started](general/getting_started.md)
1010
- [Adapters](general/adapters.md)
1111
- [Configuration Options](general/configuration_options.md)
12+
- [Logging](general/logging.md)
13+
- [Instrumentation](general/instrumentation.md)
1214

1315
## How to
1416

docs/general/instrumentation.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Instrumentation
2+
3+
AMS uses the instrumentation API provided by Active Support this way we
4+
can choose to be notified when AMS events occur inside our application.
5+
6+
## render.active_model_serializers
7+
8+
|key | value |
9+
|-------------|----------------------|
10+
|:serializer | The serializer class |
11+
|:adapter | The adapter instance |
12+
13+
```ruby
14+
{
15+
serializer: PostSerializer,
16+
adapter: #<ActiveModel::Serializer::Adapter::Attributes:0x007f96e81eb730>
17+
}
18+
```

docs/general/logging.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Logging
2+
3+
If we are using AMS on Rails app by default the `Rails.logger` will be used.
4+
5+
On a non Rails enviroment by default the `ActiveSupport::TaggedLogging` will be
6+
used.
7+
8+
If we need to customize the logger we can define this in an initializer:
9+
10+
```ruby
11+
ActiveModel::Serializer.logger = Logger.new(STDOUT)
12+
```

lib/action_controller/serialization.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_serializer(resource, options = {})
3232
serializable_resource.serialization_scope_name = _serialization_scope
3333
begin
3434
serializable_resource.adapter
35+
serializable_resource
3536
rescue ActiveModel::Serializer::CollectionSerializer::NoSerializerError
3637
resource
3738
end

lib/active_model/serializable_resource.rb

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
module ActiveModel
33
class SerializableResource
44
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :meta, :meta_key, :links])
5+
extend ActiveModel::Callbacks
6+
7+
define_model_callbacks :render
8+
9+
around_render do |_, block, _|
10+
notify_active_support do
11+
block.call
12+
end
13+
end
514

615
# Primary interface to composing a resource with a serializer and adapter.
716
# @return the serializable_resource, ready for #as_json/#to_json/#serializable_hash.
@@ -11,7 +20,23 @@ def initialize(resource, options = {})
1120
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
1221
end
1322

14-
delegate :serializable_hash, :as_json, :to_json, to: :adapter
23+
def serializable_hash(*args)
24+
run_callbacks :render do
25+
adapter.serializable_hash(*args)
26+
end
27+
end
28+
29+
def as_json(*args)
30+
run_callbacks :render do
31+
adapter.as_json(*args)
32+
end
33+
end
34+
35+
def to_json(*args)
36+
run_callbacks :render do
37+
adapter.to_json(*args)
38+
end
39+
end
1540

1641
def serialization_scope=(scope)
1742
serializer_opts[:scope] = scope
@@ -64,5 +89,13 @@ def serializer?
6489
protected
6590

6691
attr_reader :resource, :adapter_opts, :serializer_opts
92+
93+
def notify_active_support
94+
event_name = 'render.active_model_serializers'.freeze
95+
payload = { serializer: serializer, adapter: adapter }
96+
ActiveSupport::Notifications.instrument(event_name, payload) do
97+
yield
98+
end
99+
end
67100
end
68101
end

lib/active_model/serializer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
require 'active_model/serializer/configuration'
77
require 'active_model/serializer/fieldset'
88
require 'active_model/serializer/lint'
9+
require 'active_model/serializer/logging'
910

1011
# ActiveModel::Serializer is an abstract class that is
1112
# reified when subclassed to decorate a resource.
1213
module ActiveModel
1314
class Serializer
1415
include Configuration
1516
include Associations
17+
include Logging
1618
require 'active_model/serializer/adapter'
1719

1820
# Matches
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module ActiveModel
2+
class Serializer
3+
module Logging
4+
extend ActiveSupport::Concern
5+
6+
class LogSubscriber < ActiveSupport::LogSubscriber
7+
def render(event)
8+
logger.tagged('AMS') do
9+
info do
10+
serializer = event.payload[:serializer]
11+
adapter = event.payload[:adapter]
12+
duration = event.duration.round(2)
13+
"Rendered #{serializer.name} with #{adapter.class} (#{duration}ms)"
14+
end
15+
end
16+
end
17+
18+
def logger
19+
ActiveModelSerializers.logger
20+
end
21+
end
22+
end
23+
end
24+
end
25+
26+
ActiveModel::Serializer::Logging::LogSubscriber.attach_to :active_model_serializers

test/action_controller/serialization_test.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,16 @@ def use_adapter?
420420
controller.get_serializer(Profile.new)
421421
end)
422422
end
423+
424+
def test_render_event_is_emmited
425+
ActiveSupport::Notifications.subscribe('render.active_model_serializers') do |name|
426+
@name = name
427+
end
428+
429+
get :render_using_implicit_serializer
430+
431+
assert_equal 'render.active_model_serializers', @name
432+
end
423433
end
424434
end
425435
end

test/logging_test.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
require 'test_helper'
2+
3+
module ActiveModel
4+
class Serializer
5+
class LoggingTest < Minitest::Test
6+
class TestLogger < ActiveSupport::Logger
7+
def initialize
8+
@file = StringIO.new
9+
super(@file)
10+
end
11+
12+
def messages
13+
@file.rewind
14+
@file.read
15+
end
16+
end
17+
18+
def setup
19+
@author = Author.new(name: 'Steve K.')
20+
@post = Post.new(title: 'New Post', body: 'Body')
21+
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
22+
@post.comments = [@comment]
23+
@comment.post = @post
24+
@post.author = @author
25+
@author.posts = [@post]
26+
@post_serializer = PostSerializer.new(@post, custom_options: true)
27+
28+
@old_logger = ActiveModelSerializers.logger
29+
@logger = ActiveSupport::TaggedLogging.new(TestLogger.new)
30+
logger @logger
31+
end
32+
33+
def teardown
34+
logger @old_logger
35+
end
36+
37+
def logger(logger)
38+
ActiveModelSerializers.logger = logger
39+
end
40+
41+
def test_uses_ams_as_tag
42+
ActiveModel::SerializableResource.new(@post).serializable_hash
43+
assert_match(/\[AMS\]/, @logger.messages)
44+
end
45+
46+
def test_logs_when_call_serializable_hash
47+
ActiveModel::SerializableResource.new(@post).serializable_hash
48+
assert_match(/Rendered/, @logger.messages)
49+
end
50+
51+
def test_logs_when_call_as_json
52+
ActiveModel::SerializableResource.new(@post).as_json
53+
assert_match(/Rendered/, @logger.messages)
54+
end
55+
56+
def test_logs_when_call_to_json
57+
ActiveModel::SerializableResource.new(@post).to_json
58+
assert_match(/Rendered/, @logger.messages)
59+
end
60+
61+
def test_logs_correct_serializer
62+
ActiveModel::SerializableResource.new(@post).serializable_hash
63+
assert_match(/PostSerializer/, @logger.messages)
64+
end
65+
66+
def test_logs_correct_adapter
67+
ActiveModel::SerializableResource.new(@post).serializable_hash
68+
assert_match(/ActiveModel::Serializer::Adapter::Attributes/, @logger.messages)
69+
end
70+
71+
def test_logs_the_duration
72+
ActiveModel::SerializableResource.new(@post).serializable_hash
73+
assert_match(/\(\d+\.\d+ms\)/, @logger.messages)
74+
end
75+
end
76+
end
77+
end

0 commit comments

Comments
 (0)