Skip to content

Make the output of ActiveRecord::Core#inspect configurable. #49765

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

Merged
merged 1 commit into from
Nov 8, 2023

Conversation

andrewn617
Copy link
Member

@andrewn617 andrewn617 commented Oct 24, 2023

Motivation / Background

Fixes #49707

This Pull Request has been created because we noticed that ActiveRecord::Core#inspect was taking >9s to run for some large objects in production. This is because the parameter filter is be executed for every attribute.

Detail

This Pull Request introduces configuration for ActiveRecord::Core#inspect. By default, inspect just returns the object with its id (eg #<Post id: 1>. The attributes to include can be configured with Post.attributes_to_inspect=. If you want to full output with all the attributes you can call Post.full_inspect.

Additional information

In my first draft I had a config to disable this, if you just always want to use full_inspect. But it seems easy enough just to do alias_method :inspect, :full_inspect on the model, or in ApplicationRecord if you want to disable it globally. So the config didn't seem worth it to me.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

@andrewn617 andrewn617 force-pushed the configurable-inspect branch 4 times, most recently from a419cda to 6c384a2 Compare October 24, 2023 14:40
@@ -132,6 +132,8 @@ class SymbolIgnoredDeveloper < ActiveRecord::Base
class AuditLog < ActiveRecord::Base
belongs_to :developer, validate: true
belongs_to :unvalidated_developer, class_name: "Developer"

self.attributes_to_inspect = [:id, :message]
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixes AssociationsTest#test_inspect_does_not_reload_a_not_yet_loaded_target

@@ -51,9 +65,9 @@ def test_inspect_class_without_table
assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
end

def test_inspect_relation_with_virtual_field
relation = Topic.limit(1).select("1 as virtual_field")
assert_match(/virtual_field: 1/, relation.inspect)
Copy link
Member Author

Choose a reason for hiding this comment

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

I decided to remove this test. Relation#inspect calls inspect on each of the records. It seems unnatural that virtual columns would be included in attributes_to_expect so I didn't want to add that as a test case.

I think we could add a full_inspect method to Relation if we want to preserve this behaviour.

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 you can rewrite this test in a way that makes sure virtual attributes are inside the loaded record and can be inspected. You could call full_inspect in relation.first

Copy link
Contributor

@adrianna-chang-shopify adrianna-chang-shopify left a comment

Choose a reason for hiding this comment

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

Would there be value in adding a global config, e.g. config.active_record.attributes_to_inspect? If we opted for an :all option, might be useful to be able to set this across all Active Records in order to maintain the existing behaviour.

I think the argument is less strong if we stick with #full_inspect instead of an :all option, although you could make the argument that users may always want inspect to include certain fields (e.g. timestamps).

Post.first.inspect #=> "#<(Post id: 1)>"
```

The attributes to be included in the out of `inspect` can be configured with
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
The attributes to be included in the out of `inspect` can be configured with
The attributes to be included in the output of `inspect` can be configured with

assert_match(/virtual_field: 1/, relation.inspect)
def test_full_inspect_instance
topic = topics(:first)
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_fs(:inspect)}", bonus_time: "#{topic.bonus_time.to_fs(:inspect)}", last_read: "#{topic.last_read.to_fs(:inspect)}", content: "Have a nice day", important: nil, binary_content: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_fs(:inspect)}", updated_at: "#{topic.updated_at.to_fs(:inspect)}">), topic.full_inspect
Copy link
Contributor

Choose a reason for hiding this comment

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

We could pull this out into a HEREDOC to make it easier to read?

@andrewn617 andrewn617 force-pushed the configurable-inspect branch 4 times, most recently from 3053798 to e22000f Compare October 25, 2023 20:38
@@ -680,21 +683,18 @@ def connection_handler
self.class.connection_handler
end

# Returns the contents of the record as a nicely formatted string.
# Returns the attributes specified by #attributes_for_inspect as a nicely formatted string.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Returns the attributes specified by #attributes_for_inspect as a nicely formatted string.
# Returns the attributes specified by <tt>.attributes_for_inspect</tt> as a nicely formatted string.

@@ -51,9 +65,9 @@ def test_inspect_class_without_table
assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
end

def test_inspect_relation_with_virtual_field
relation = Topic.limit(1).select("1 as virtual_field")
assert_match(/virtual_field: 1/, relation.inspect)
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 you can rewrite this test in a way that makes sure virtual attributes are inside the loaded record and can be inspected. You could call full_inspect in relation.first

@andrewn617 andrewn617 force-pushed the configurable-inspect branch from e22000f to 9289e5b Compare October 26, 2023 19:44
end
end.join(", ")
if attributes_for_inspect == :all
inspect_with_attributes(attribute_names)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
inspect_with_attributes(attribute_names)
full_inspect

Copy link
Member

Choose a reason for hiding this comment

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

Agree

Copy link
Member Author

Choose a reason for hiding this comment

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

Argh, thanks. I think I left it like this when I was playing with a version that had inspect(:all) instead of full_inspect

@p8
Copy link
Member

p8 commented Oct 26, 2023

We also already have a filter_attributes and filter_attributes= method.
It seems these have a lot of overlap with this feature:
https://github.com/rails/rails/blob/9289e5bbc3b5b5d43e477f00f5ce2d8446d657fc/activerecord/lib/active_record/core.rb#L308-L321

@rafaelfranca
Copy link
Member

It doesn't overlap. filter_attributes tell active Record to show FILTERED for each attribute. We don't want to show anything, we don't even want to run the parameter filtering, or reading the value, etc.

BTW, maybe we should implement another improvement on this entire pipeline because it doesn't make sense to:

Just to show [FILTERED] and don't use the value for anything.

@p8
Copy link
Member

p8 commented Oct 26, 2023

Ah sorry, my mistake. I thought it would bypass the parameter filter, but instead it creates a customer filter if filter_attributes is set.

@andrewn617 andrewn617 force-pushed the configurable-inspect branch from 9289e5b to add9776 Compare October 26, 2023 21:33
@Earlopain
Copy link
Contributor

Currently there doesn't seem to be a distinction between local/remote here, which the original issue mentions.

I have accidentally printed much more than I intended in production and because of slow connections that took quite a while. IRB had its autocomplete disabled in production because of similar reasons.

In development however I want to see everything. I know I can set the required config to do that, but it really should be the default in my opinion.

@rafaelfranca
Copy link
Member

rafaelfranca commented Oct 27, 2023

I do agree with that. Let's automatically set it to :all in active_record/railtie.rb when consider_all_requests_local is true, of course respecting any explicit config the user might set.

Add a test to make sure it is set to :all in test and development in the railties test as well.

@andrewn617 andrewn617 force-pushed the configurable-inspect branch 4 times, most recently from 848bfe7 to a80564a Compare October 30, 2023 14:21
@rails-bot rails-bot bot added the railties label Oct 30, 2023
@andrewn617 andrewn617 force-pushed the configurable-inspect branch 3 times, most recently from c3fbfc4 to 62e9693 Compare October 30, 2023 14:34
@levicole
Copy link
Contributor

levicole commented Aug 30, 2024

why is this not opt in? I imagine this is a massive annoyance to most people who have upgraded to 7.2. If I opened a PR to make this opt in instead of opt out, would it be accepted, or is rails core taking a hard stance on this?

@rafaelfranca
Copy link
Member

This is opt-out, not opt-in.

@levicole
Copy link
Contributor

Sorry, maybe I got that reversed...why did this not default to:all? I can't imagine why this would be the desired behavior.

@rafaelfranca
Copy link
Member

Because :all is slow in production, and rails in production mode, by default is configured to be fast. You can still configure your production mode to be easier to debug, that is why we have an option, but easy to debug should not be the default in production.

@rafaelfranca
Copy link
Member

It is explained in the issue #49707 (comment).

@levicole
Copy link
Contributor

levicole commented Aug 30, 2024

This seems like a very specific problem in a very specific rails app that most rails apps will not experience. Thanks for the explanation.

@rafaelfranca
Copy link
Member

rafaelfranca commented Aug 30, 2024

Wait a sec, I just now realized this that changed for existing applications without given them any warning. I do think :id is a better default, but you app should not change to that unless you chose to. Do you want to open a PR to keep the default for < 7.2 apps :all?

@levicole
Copy link
Contributor

The problem came up for us when we upgraded to 7.2, so it really caught us off guard even when upgrading. I can see what I can do in the next week or so.

@rafaelfranca
Copy link
Member

Yeah, that wasn't not the intention. New apps having that new default is fair, but for existing apps this can break a lot of behavior. I'll try to fix today.

@levicole
Copy link
Contributor

I appreciate that! Thanks for talking this out with me!!

@levicole
Copy link
Contributor

Where might this live? I can try to fix it, but I also know you probably know exactly where to do this.

@rafaelfranca
Copy link
Member

rafaelfranca commented Aug 30, 2024

There are a few things that I was planning to do:

I think if we do those 3 things, all apps, even new ones will be able to see all attribute when inspecting on the Rails console. New apps will not suffer the performance problem that can take down apps when calling a Hash#to_s in production, and if people want that, they can just change the default in their apps.

@levicole
Copy link
Contributor

Got it! I'll give this a shot, and put up a PR.

@andrewn617
Copy link
Member Author

andrewn617 commented Aug 30, 2024

@levicole Thanks for the report and sorry this is causing you issues. I am happy to help if you run into any trouble with the PR.

I agree with Rafael's suggestions except for one thing. Altho it is not the original motivation for the feature, something I like about this feature is that it allows the console output for models to be less noisey when you using the development console or debugging a test. So, I would not like to have an IRB inspector calling full_inspect on models in development or test mode, since it takes that capability away from the user. Instead, I think it would be better to just add that in production. After all it is :all by default in dev and test so the user still has to opt in to the less noisey output.

@levicole
Copy link
Contributor

levicole commented Aug 30, 2024

@andrewn617 I wondered about the IRB inspector as well. I'm definitely tackling the defaults, and adding it to the framework defaults. I will probably skip the IRB inspector. It's super interesting though!

This would be my first PR to rails. Would this PR need to be based on the v7.2 tag? I saw that this file doesn't exist on main.

@levicole
Copy link
Contributor

Got it. Not sure how to bring that old file back since it's not present on main.

@andrewn617
Copy link
Member Author

@levicole Yeah, its a bit of an uncommon situation to add a framework default after the release. I believe @rafaelfranca will have to add it himself when he merges your PR. So it's ok just to open a PR on main for the other changes.

@levicole
Copy link
Contributor

Got it! Ok, almost done here. I just need to find where there might be tests for the configuration and update/add.

@jmkoni
Copy link

jmkoni commented Aug 30, 2024

Just FYI that I see this behavior in development as well.

@levicole
Copy link
Contributor

@andrewn617 / @rafaelfranca this is probably going to take me longer than I expected due to me getting my the dev environment set up. But I do want to do this. If you need to get the change in sooner than I'm able to, then I understand!

@levicole
Copy link
Contributor

PR is opened. We can move the discussion there.

@jmkoni
Copy link

jmkoni commented Sep 3, 2024

Just FYI that I see this behavior in development as well.

Just want to be clear that I did make a mistake with environments (ugh long week), but I am seeing this behavior in my test environment despite setting this to :all in my primary app config.

@MatheusRich
Copy link
Contributor

@andrewn617 hey, thanks for this PR! Is there a good way to exclude a column from this config? I basically want :all - some columns. I can't just use self.attributes_for_inspect = attribute_names - ['undesired'] because that will try to load the schema upfront

@zzak
Copy link
Member

zzak commented May 27, 2025

@MatheusRich I think you can just define that method on your model:

class Foo < AR::Base
  def attributes_for_inspect
    super - ['undesired']
  end
end

Something to that affect, maybe.

@MatheusRich
Copy link
Contributor

@zzak I totally missed that overriding the instance method was possible. TY!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ActiveRecord::Core#inspect should have an option to not show the attributes in production