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 timeline_options to expose event_streams OPTIONS to the API #21552

Merged
merged 1 commit into from
Nov 16, 2021

Conversation

jrafanie
Copy link
Member

For issue: ManageIQ/manageiq-api#1090

Note, currently there are several methods in EventStream whose implementation is
for EmsEvent and should move there. We'll look at that when we need to
expose/test filtering on MiqEvent levels (success/failure) for the API.

@@ -1,5 +1,6 @@
class EmsEvent < EventStream
include_concern 'Automate'
supports :timeline
Copy link
Member Author

@jrafanie jrafanie Nov 10, 2021

Choose a reason for hiding this comment

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

I didn't know what to call this feature so I went with timeline. Basically, we need to ensure the other EventStream subclasses, such as CustomEvent or RequestEvent, don't show up in the payload until we support them for timelines.

Copy link
Member

Choose a reason for hiding this comment

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

timeline seems fine - @agrare opinions?

It's a little weird to be using supports for this, because it's not really a provider feature, but I think I'm ok with that.

Copy link
Member

@agrare agrare Nov 11, 2021

Choose a reason for hiding this comment

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

I'm not against using supports for non provider features. If this is just a single subclass check though could you just use EmsEvent for the query? (just a first take without reading how you're using this yet)

Copy link
Member

@Fryguy Fryguy Nov 11, 2021

Choose a reason for hiding this comment

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

yeah, supports is really about the outside callers (like UI and API) having an interface for asking if a feature is supported and being able to filter on it as well as getting why it's not supported for presentation purposes. In this case, that's not what it's being used for, as we aren't going to be presenting anything in the UI for the non-supported classes.

I'm thinking this is much cleaner with a simpler subclasses.select { |e| e.respond_to?(:group_levels_and_details) } construct.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Note, the motivation was something different than what the UI was doing, namely:

record.has_events? || record.has_events?(:policy_events) # EmsEvent || MiqEvent
joerafaniello@Joes-MBP-2 manageiq-ui-classic % git grep has_events
app/helpers/application_helper/button/availability_zone_timeline.rb:    @error_message = _("No Timeline data has been collected for this Availability Zone") unless @record.has_events?
app/helpers/application_helper/button/container_timeline.rb:    @record.has_events? || @record.has_events?(:policy_events)
app/helpers/application_helper/button/ems_cluster_timeline.rb:    @error_message = _("No Timeline data has been collected for this Cluster") unless @record.has_events? || @record.has_events?(:policy_events)
app/helpers/application_helper/button/ems_storage_timeline.rb:    unless @record.has_events? || @record.has_events?(:policy_events)
app/helpers/application_helper/button/ems_timeline.rb:    unless @record.has_events? || @record.has_events?(:policy_events)
app/helpers/application_helper/button/host_timeline.rb:    @error_message = _("No Timeline data has been collected for this Host") unless @record.has_events? || @record.has_events?(:policy_events)
app/helpers/application_helper/button/miq_template_timeline.rb:    unless @record.has_events? || @record.has_events?(:policy_events)
app/helpers/application_helper/button/physical_server_timeline.rb:    unless @record.has_events?
app/helpers/application_helper/button/vm_timeline.rb:    @record.has_events? || @record.has_events?(:policy_events)

Copy link
Member Author

Choose a reason for hiding this comment

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

Granted, the UI is just checking for events for display purposes but obviously, if they wanted to display CustomEvent, CustomButtonEvent, or RequestEvent, these conditionals would grow gnarly.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the backend code, we don't care about having events for display purposes. We only want to provide the details about options for classes that support timelines. 🤷

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'm thinking this is much cleaner with a simpler subclasses.select { |e| e.respond_to?(:group_levels_and_details) } construct.

I guess I can try doing this in EventStream

def self.timeline_classes
  subclasses.select { |e| e.respond_to?(:group_levels_and_details) }
end

I can worry about what the UI does with checking if there are events for record later.

Copy link
Member Author

Choose a reason for hiding this comment

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

This should be done, I dropped the private method and simplified things.


expect(options["MiqEvent"].keys.sort).to eq %w[description group_levels group_names]
expect(options["MiqEvent"]["description"]).to eq(MiqEvent.description)
expect(options["MiqEvent"]["group_levels"].keys.sort).to eq %w[failure success]
Copy link
Member Author

Choose a reason for hiding this comment

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

Does it make sense to to_i all nested keys to have consistent keys since the event_handling part of the settings.yml has symbols keys, or should the nested keys be strings to match how they'd be accessed over json?

Copy link
Member

Choose a reason for hiding this comment

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

Do you mean to_s all of the keys? If so, I'm not sure.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I think I'll just use the existing settings.yml event_handling format and do symbols keys even though there's no such concept on the other side of json.

Copy link
Member Author

Choose a reason for hiding this comment

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

Either way, I don't like that there's currently both string and symbol keys based on what level of depth.

Copy link
Member Author

Choose a reason for hiding this comment

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

FYI, this is what I meant... I changed it in a commit I just pushed:

Change mixed string / symbol keys to all symbol keys

Before:

```
{"EmsEvent"=>
  {"description"=>"Management Events",
   "group_names"=>
    {:addition=>"Creation/Addition",
...
     :security=>"Security"},
   "group_levels"=>
    {"critical"=>"Critical", "detail"=>"Detail", "warning"=>"Warning"}},
 "MiqEvent"=>
  {"description"=>"Policy Events",
   "group_names"=>
    {"vm_process"=>"VM Lifecycle",
...
     "auth_validation"=>"Authentication Validation (Provider)"},
   "group_levels"=>{"success"=>"Success", "failure"=>"Failure"}}}
```

After:

```
{:EmsEvent=>
  {:description=>"Management Events",
   :group_names=>
    {:addition=>"Creation/Addition",
...
     :security=>"Security"},
   :group_levels=>
    {:critical=>"Critical", :detail=>"Detail", :warning=>"Warning"}},
 :MiqEvent=>
  {:description=>"Policy Events",
   :group_names=>
    {:vm_process=>"VM Lifecycle",
...
     :auth_validation=>"Authentication Validation (Provider)"},
   :group_levels=>{:success=>"Success", :failure=>"Failure"}}}
```

app/models/miq_event.rb Outdated Show resolved Hide resolved

def self.group_names_and_levels
hsh = {}
hsh["group_names"] = MiqEventDefinitionSet.all.map {|s| [s.name, s.description]}.to_h
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
hsh["group_names"] = MiqEventDefinitionSet.all.map {|s| [s.name, s.description]}.to_h
hash["group_names"] = MiqEventDefinitionSet.all.pluck(:name, :description).to_h

Comment on lines 14 to 15
CONDITION_ALLOW = "Success"
CONDITION_DENY = "Failure"
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
CONDITION_ALLOW = "Success"
CONDITION_DENY = "Failure"
CONDITION_ALLOW = N_("Success")
CONDITION_DENY = N_("Failure")

@@ -10,6 +11,24 @@ class EmsEvent < EventStream
'Rename_Task',
]

def self.description
"Management Events"
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
"Management Events"
_("Management Events")

end

def self.group_names_and_levels
event_groups.each_with_object({}) do |arr, hsh|
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure what the array is here, but can we deconstruct it here and give the parameters names?

Copy link
Member

Choose a reason for hiding this comment

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

Something like:

Suggested change
event_groups.each_with_object({}) do |arr, hsh|
event_groups.each_with_object({}) do |(group_name, group_details), hsh|

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea.

@@ -35,14 +36,22 @@ class EventStream < ApplicationRecord

after_commit :emit_notifications, :on => :create

supports_not :timeline, :reason => "Only subclasses can be on a timeline"
Copy link
Member

Choose a reason for hiding this comment

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

For example, this reason will never be presented in the UI nor API, which goes against why you'd need it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's more for developers. I know supports is a provider concept but it describes what I wanted to do.

I originally had self.timeline_supported? => false in the EventStream class and only set self.timeline_supported? => true for MiqEvent and EmsEvent classes.

I'm not against using something else, but saw it and thought I was reinventing the wheel.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to respond_to? in the timeline_options method.

GROUP_LEVELS = %i(critical detail warning).freeze

def self.description
raise NotImplementedError, _("description must be implemented in a subclass")
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to translate this Exception because it should never be presented to a user (i.e. getting a NotImplementedError raise in production is a bug that needs to be fixed). Mainly thinking about translators here, as they won't know what this means in order to translate it.

end

private_class_method def self.group_names_and_levels
raise NotImplementedError, _("group_names_and_levels must be implemented in a subclass")
Copy link
Member

Choose a reason for hiding this comment

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

Same here on the translation thing.

raise NotImplementedError, _("group_names_and_levels must be implemented in a subclass")
end

def self.event_stream_options
Copy link
Member

Choose a reason for hiding this comment

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

wonder if this should be named timeline_options, so it is clear what the purpose is for?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I guess that's the UI usage of it. I suppose various events could be presented in any format, not just a timeline. For example, you could just tableize the events, level, group/category, etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea. I can't come up with a better name so I'm fine with using the only client of this.

Comment on lines 23 to 28
group_details.each_key do |level|
level = level.to_s
next if level == 'name'

hash[:group_levels] ||= {}
hash[:group_levels][level.to_sym] ||= level.capitalize
end
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
group_details.each_key do |level|
level = level.to_s
next if level == 'name'
hash[:group_levels] ||= {}
hash[:group_levels][level.to_sym] ||= level.capitalize
end
group_details.each_key do |level|
next if level == :name
hash[:group_levels] ||= {}
hash[:group_levels][level.to_sym] ||= level.to_s.capitalize
end

def group_name
@group_name ||= self.class.group_name(group)
end

def self.timeline_classes
@timeline_classes ||= EventStream.subclasses.select { |e| e.respond_to?(:group_names_and_levels) }
Copy link
Member

Choose a reason for hiding this comment

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

Could caching this cause issues with code reload since we are caching classes?

Copy link
Member

Choose a reason for hiding this comment

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

It's a relatively cheap operation that really should only create 1 new Array object, so I think we don't need caching here.

Copy link
Member Author

@jrafanie jrafanie Nov 16, 2021

Choose a reason for hiding this comment

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

Sure, it was premature. The caller shouldn't be hitting this constantly and it's not a problem once classes are loaded.

end

def self.timeline_options
timeline_classes.map { |c| [c.name.to_sym, c.group_names_and_levels] }.to_h
Copy link
Member

Choose a reason for hiding this comment

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

Is this the only caller of timeline_classes? If so, then a) timeline_classes should be private and b) it might be cheaper to consolidate into something like:

Suggested change
timeline_classes.map { |c| [c.name.to_sym, c.group_names_and_levels] }.to_h
EventStream.subclasses.compact_map { |c| [c.name.to_sym, c.group_names_and_levels] if c.respond_to?(:group_names_and_levels) }.to_h

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I can make it private.

I made it a method because timeline_classes describes what a class needs to do to implement timelines, namely, define group_names_and_levels. In the example b), it feels like we're mixing the concerns of "which classes support timelines" and "how to get the options for it".

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, no, I think it makes sense to be able to expose what classes support timelines.

Comment on lines 14 to 15
CONDITION_ALLOW = N_("Success")
CONDITION_DENY = N_("Failure")
Copy link
Member

Choose a reason for hiding this comment

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

it's weird to me that the constant name is one thing but the text is something else. maybe:

Suggested change
CONDITION_ALLOW = N_("Success")
CONDITION_DENY = N_("Failure")
CONDITION_SUCCESS = N_("Success")
CONDITION_FAILURE = N_("Failure")

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'm mixing policy terminology that prefers allow/deny over success/failure but I agree, it doesn't make sense at all here.

@jrafanie jrafanie force-pushed the event_stream_options_for_api branch 2 times, most recently from d7a753a to 4c1dd49 Compare November 16, 2021 20:03
@jrafanie jrafanie changed the title Add event_stream_options to expose OPTIONS to the API Add timeline_options to expose event_streams OPTIONS to the API Nov 16, 2021
For issue: ManageIQ/manageiq-api#1090

Note, currently there are several methods in EventStream whose implementation is
for EmsEvent and should move there.  We'll look at that when we need to
expose/test filtering on MiqEvent levels (success/failure) for the API.
@jrafanie
Copy link
Member Author

Ok, applied the suggested changes, other than making timeline_classes private.

@miq-bot
Copy link
Member

miq-bot commented Nov 16, 2021

Checked commit jrafanie@97324f8 with ruby 2.6.3, rubocop 1.13.0, haml-lint 0.35.0, and yamllint
5 files checked, 0 offenses detected
Everything looks fine. 🍪

@Fryguy Fryguy merged commit 7077112 into ManageIQ:master Nov 16, 2021
@jrafanie jrafanie deleted the event_stream_options_for_api branch November 16, 2021 21:46
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.

4 participants