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

Add usage report command #851

Merged
merged 35 commits into from
Jan 27, 2025
Merged

Add usage report command #851

merged 35 commits into from
Jan 27, 2025

Conversation

ares
Copy link
Member

@ares ares commented May 23, 2024

This is the PoC of generating yaml representing the usage of the instance. Test wit

foreman-maintain report generate

Currently, the command searches for all ReportChecks, executes them and collects the data from them. Then it prints out the resulting YAML to stdout.

lib/foreman_maintain/cli.rb Outdated Show resolved Hide resolved
scenario = run_scenario(Scenarios::Report::Generate.new({}, [:reports])).first

# description can be used too
report_data = scenario.steps.map(&:data).compact.reduce(&:merge).transform_keys(&:to_s)
Copy link
Member

Choose a reason for hiding this comment

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

does that take all reports, their .data and merge those hashes? what if two reports accidentally define the same hash key?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. I think that's very unlikely as the keys are very descriptive. Is it worth printing a warning to STDERR? Or perhaps better, having a some sort of a test that check for all keys in all checks? That may be a bit harder though as some of them are generated dynamically. Perhaps each check could have some explicit prefix that could that be verified, but that's likely schema v2 thing.

Copy link
Member

Choose a reason for hiding this comment

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

What if we add an add_data(key, value) method to Report, and it does something like

data[self.class.name+'_'+key] = value

I wouldn't block on this, but I think it's a potential source for hard to debug issues once a "not so descriptive" name leaks into the set.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

It would also save the transform_keys(to_s) here, which makes the code more readable?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd leave this for v2. While we're at it, we should maybe shuffle things around a bit, the report classes are currently named somewhat arbitrarily

Copy link
Member

Choose a reason for hiding this comment

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

If you keep something big like this I would find it more readable to split it across lines:

          report_data = scenario.
            steps.
            map(&:data).
            compact.
            reduce(&:merge).
            transform_keys(&:to_s)

Comment on lines 13 to 14
feature(:foreman_database).
query("select type, count(*) from hosts group by type").
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 Report should have a query itself, no need to call feature() all the time

Copy link
Member Author

Choose a reason for hiding this comment

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

nope, undefined method #query

Copy link
Member

Choose a reason for hiding this comment

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

I said "should", not "does" :)
As in: we should implement a Report.query and use that, instead of calling feature(:db).query all the time ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

@ares did you plan to address this, given Adam has a patch for it?

ares and others added 21 commits January 17, 2025 09:33
This extends the check so it can hold any arbitrary data. That is then
used in the generate report command, that finds all checks tagged with
report tag, executes them and gather their data for the final YAML
creation.

This commit also includes two examples of metrics that uses database as
the Data source.
With this new child class, report checks can share helpers easily. Also
the parent Check class, which is used elsewhere, does not need to be
extended by the data storage.
Earlier, all Reports were also considered Checks, so they showed up in
health checks. This change now fully isolates Reports and Checks while
all the code is shared.
@ares ares force-pushed the report branch 2 times, most recently from 68df50d to 88dde7d Compare January 17, 2025 09:17
@ares
Copy link
Member Author

ares commented Jan 17, 2025

I address most of the comments, rebased the PR and added one more report definition around RBAC

test/lib/cli_test.rb Outdated Show resolved Hide resolved
@evgeni
Copy link
Member

evgeni commented Jan 17, 2025

Can we get some basic tests for the new Report class you're introducing and the new scenario you're adding?
Tests for the individual reports are probably overkill (and would result in a lot of mocking, so doubtfully add any value)

# How many custom roles did you create and assigned to users?
# rubocop:disable Layout/LineLength
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
Copy link
Member

Choose a reason for hiding this comment

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

Break this method up instead of disabling rubocop. This could also help with the comments listed in these files. You can use ruby to help, e.g.

def number_of_users
def number_of_non_admin_users
def number_of_custom_roles

end

def run
raise NotImplementedError
Copy link
Member

Choose a reason for hiding this comment

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

Fun fact I learned and stuck with me -- https://kirillshevch.medium.com/misusing-of-notimplementederror-in-ruby-531dd866f461

Yes this code base already does it, but sharing is caring.

@adamruzicka adamruzicka mentioned this pull request Jan 20, 2025
@ares
Copy link
Member Author

ares commented Jan 21, 2025

All comments should be addressed atm, could you please re-review/merge?

@evgeni
Copy link
Member

evgeni commented Jan 23, 2025

Are the remaining open points (method length in rbac.rb, NotImplementedError in report.rb, Report#query method and tests) planned to be addressed?

@adamruzicka
Copy link
Contributor

Are the remaining open points (method length in rbac.rb, NotImplementedError in report.rb, Report#query method and tests) planned to be addressed?

I filed #976 to track all of those. Could we accept this PR as is and resolve those in additional PRs so that we can save the additional round-trip through @ares's fork?

Copy link
Member

@evgeni evgeni left a comment

Choose a reason for hiding this comment

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

I'm fine with this a a first round as suggested by Adam

Comment on lines +32 to +33
count = sql_count("cached_user_roles WHERE cached_user_roles.role_id IN (#{role_ids.join(',')})")
result["assigned_custom_roles_count"] = count
Copy link
Member

Choose a reason for hiding this comment

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

this returns an empty string and not 0 if there are no custom roles

Copy link
Contributor

Choose a reason for hiding this comment

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

Apparently, the query we generate isn't valid if there are no custom roles. Good catch

> query
=> " SELECT COUNT(*) AS COUNT FROM cached_user_roles WHERE cached_user_roles.role_id IN ()"

> feature(:foreman_database).query(query)
CSV::MalformedCSVError: Illegal quoting in line 1.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, IN () is not valid:

foreman=# SELECT COUNT(*) AS COUNT FROM cached_user_roles WHERE cached_user_roles.role_id IN ();
ERROR:  syntax error at or near ")"
LINE 1: ...ROM cached_user_roles WHERE cached_user_roles.role_id IN ();
                                                                     ^

end
end

def settings_fields
Copy link
Member

Choose a reason for hiding this comment

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

some settings (like db_pending_seed, instance_id and bcrypt_cost) are always modified -- is it worth logging them here or should they be filtered?

scenario = run_scenario(Scenarios::Report::Generate.new({}, [:reports])).first

# description can be used too
report_data = scenario.steps.map(&:data).compact.reduce(&:merge).transform_keys(&:to_s)
Copy link
Member

Choose a reason for hiding this comment

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

This produces quite a lot of output, which you probably don't want in the case where you run this via cron (as otherwise you'll get a mail from cron on every invocation. You could add --cron or --silent to suppress these?

@evgeni evgeni merged commit 3299952 into theforeman:master Jan 27, 2025
8 checks passed
@adamruzicka adamruzicka mentioned this pull request Jan 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants