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 filter conditions to release health rules #583

Merged
merged 11 commits into from
Dec 4, 2023
3 changes: 3 additions & 0 deletions app/assets/images/exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 18 additions & 13 deletions app/components/metric_card_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<%= link_to_external external_url, title: "Monitoring Dashboard" do %>
<div class="rounded-lg border-slate-200 border flex flex-col p-4">
<div class="flex flex-row justify-between">
<div><%= name %></div>
<div class="rounded-lg border-slate-200 border flex flex-col p-4">
<div class="flex flex-row justify-between">
<div><%= name %></div>
<%= link_to_external external_url, title: "Monitoring Dashboard" do %>
<div><%= image_tag("integrations/logo_#{provider.to_s}.png", width: 20, height: 20) %></div>
</div>
<div class="grid grid-cols-2 divide-x mt-2 place-content-between">
<% values.each do |key, val| %>
<div class="flex flex-col p-2">
<div class="text-xs text-gray-400 uppercase"><%= key %></div>
<div class="text-xl text-gray-800"><%= val %></div>
<% end %>
</div>
<div class="grid grid-cols-2 divide-x mt-2 place-content-between">
<% values.each do |key, val| %>
<div class="flex flex-col p-2">
<div class="text-xs text-gray-400 uppercase"><%= key %></div>
<div class="flex items-center gap-x-1 text-xl <%= metric_color(val[:is_healthy]) %>" title="<%= metric_title(val) %>">
<%= val[:value] %>
<% if current_user.release_monitoring? && val[:is_healthy] == false %>
<%= inline_svg "exclamation.svg", classname: "w-5 h-5 text-red-800" %>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
20 changes: 20 additions & 0 deletions app/components/metric_card_component.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
class MetricCardComponent < ViewComponent::Base
include LinkHelper
include AssetsHelper

attr_reader :name, :values, :provider, :external_url
delegate :current_user, to: :helpers

def initialize(name:, values:, provider:, external_url:)
@name = name
@values = values
@provider = provider
@external_url = external_url
end

def metric_color(is_healthy)
return "test-gray-800" unless current_user.release_monitoring?
case is_healthy
when true
"text-green-800 font-semibold"
when false
"text-red-800 font-semibold"
else
"test-gray-800"
end
end

def metric_title(metric)
return unless current_user.release_monitoring?
return "unhealthy" if metric[:is_healthy] == false
"healthy" if metric[:is_healthy] == true
end
end
15 changes: 13 additions & 2 deletions app/components/release_monitoring_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<% if release_data.present? %>
<article class="mt-4">
<div class="text-slate-500 text-md"><%= build_identifier %></div>
<% if current_user.release_monitoring? && release_health_rules.present? %>
<div class="text-slate-500 text-md mt-4">
Overall Release Health: <span class="<%= release_health_class %> font-semibold"><%= release_health %></span>
</div>
<% end %>
<div class="grid grid-cols-2 gap-5 mt-4">
<%= render ProgressCardComponent.new(name: "Staged Rollout",
current: staged_rollout_percentage,
Expand All @@ -15,12 +20,18 @@
provider: monitoring_provider,
external_url: monitoring_provider_url) %>
<%= render MetricCardComponent.new(name: "Errors",
values: { "total" => errors_count, "new" => new_errors_count },
values: { "total" => errors_count, "new" => new_errors_count},
provider: monitoring_provider,
external_url: monitoring_provider_url) %>
<% if helpers.current_user.release_monitoring? && adoption_chart_data.present? %>
<% if adoption_chart_data.present? %>
<div class="col-span-2"><%= render ChartComponent.new(adoption_chart_data, icon: "sparkles.svg") %></div>
<% end %>
</div>
<% if current_user.release_monitoring? && events.present? %>
<div class="text-slate-500 text-md mt-4">Health Trigger Events</div>
<div class="mt-4 ml-2">
<%= render TimelineComponent.new(events:) %>
</div>
<% end %>
</article>
<% end %>
62 changes: 56 additions & 6 deletions app/components/release_monitoring_component.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
class ReleaseMonitoringComponent < ViewComponent::Base
include AssetsHelper
include ApplicationHelper
attr_reader :deployment_run

delegate :adoption_rate, :errors_count, :new_errors_count, to: :release_data
delegate :app, to: :deployment_run
delegate :adoption_rate, to: :release_data
delegate :app, :release_health_rules, to: :deployment_run
delegate :monitoring_provider, to: :app
delegate :current_user, to: :helpers

def initialize(deployment_run:)
@deployment_run = deployment_run
Expand All @@ -29,6 +32,43 @@ def staged_rollout
@staged_rollout ||= deployment_run.staged_rollout
end

def events
@deployment_run.release_health_events.last(3).map do |event|
type = event.healthy? ? :success : :error
title = event.healthy? ? "Rule is healthy" : "Rule is unhealthy"
{
timestamp: time_format(event.event_timestamp, with_year: false),
title:,
description: event_description(event),
type:
}
end
end

def event_description(event)
metric = event.release_health_metric
triggers = event.release_health_rule.triggers
status = event.health_status
triggers.map do |expr|
value = metric.evaluate(expr.metric)
"#{expr.display_attr(:metric)} (#{value}) #{expr.describe_comparator(status)} the threshold value (#{expr.threshold_value})"
end.join(", ")
end

def release_healthy?
@is_healthy ||= @deployment_run.healthy?
end

def release_health
return "Healthy" if release_healthy?
"Unhealthy"
end

def release_health_class
return "text-green-800" if release_healthy?
"text-red-800"
end

def staged_rollout_percentage
staged_rollout&.last_rollout_percentage || Deployment::FULL_ROLLOUT_VALUE
end
Expand All @@ -40,13 +80,23 @@ def staged_rollout_text
end

def user_stability
return "-" if release_data.user_stability.blank?
"#{release_data.user_stability}%"
value = release_data.user_stability.blank? ? "-" : "#{release_data.user_stability}%"
{value:, is_healthy: release_data.metric_healthy?("user_stability")}
end

def session_stability
return "-" if release_data.session_stability.blank?
"#{release_data.session_stability}%"
value = release_data.session_stability.blank? ? "-" : "#{release_data.session_stability}%"
{value:, is_healthy: release_data.metric_healthy?("session_stability")}
end

def errors_count
value = release_data.errors_count
{value:, is_healthy: release_data.metric_healthy?("errors")}
end

def new_errors_count
value = release_data.new_errors_count
{value:, is_healthy: release_data.metric_healthy?("new_errors")}
end

def adoption_chart_data
Expand Down
10 changes: 10 additions & 0 deletions app/components/timeline_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<ol class="relative border-s <%= border_style %>">
<% events.each do |event| %>
<li class="mb-10 ms-4">
<div class="absolute w-3 h-3 <%= event_color(event) %> rounded-full mt-1.5 -start-1.5 border"></div>
<time class="mb-1 text-sm font-normal leading-none <%= time_style %>"><%= event[:timestamp] %></time>
<h3 class="text-md font-medium <%= title_style %>"><%= event[:title] %></h3>
<p class="mb-4 text-sm font-normal <%= description_style %>"><%= event[:description] %></p>
</li>
<% end %>
</ol>
45 changes: 45 additions & 0 deletions app/components/timeline_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

class TimelineComponent < ViewComponent::Base
include ApplicationHelper

EVENT_TYPE = {
success: "bg-green-600 border-green-100 dark:border-green-700 dark:bg-green-500",
error: "bg-red-600 border-red-100 dark:border-red-700 dark:bg-red-500",
neutral: "bg-gray-200 border-white dark:border-gray-900 dark:bg-gray-700"
}

def initialize(events:)
@events = events
end

attr_reader :events

def event_color(event)
EVENT_TYPE.fetch(event[:type] || :neutral)
end

def time_style(dark: false)
style = "text-gray-400"
style += " dark:text-gray-500" if dark
style
end

def title_style(dark: false)
style = "text-gray-900"
style += " dark:text-white" if dark
style
end

def description_style(dark: false)
style = "text-gray-500"
style += " dark:text-gray-400" if dark
style
end

def border_style(dark: false)
style = "border-gray-200"
style += " dark:border-gray-700" if dark
style
end
end
1 change: 1 addition & 0 deletions app/jobs/releases/fetch_health_metrics_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Releases::FetchHealthMetricsJob < ApplicationJob

def perform(deployment_run_id)
run = DeploymentRun.find(deployment_run_id)
return if run.release.stopped?
return if run.release.finished? && run.release.completed_at < RELEASE_MONITORING_PERIOD_IN_DAYS.days.ago

run.fetch_health_data!
Expand Down
7 changes: 0 additions & 7 deletions app/models/concerns/health_awareness.rb

This file was deleted.

12 changes: 12 additions & 0 deletions app/models/deployment_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class DeploymentRun < ApplicationRecord
to: :deployment
delegate :release_metadata, :train, :app, to: :release
delegate :release_version, :platform, to: :release_platform_run
delegate :release_health_rules, to: :release_platform

STAMPABLE_REASONS = %w[
created
Expand Down Expand Up @@ -165,6 +166,17 @@ def self.reached_production
ready.includes(:step_run, :deployment).select(&:production_channel?)
end

def healthy?
return true if release_health_rules.blank?
return true if release_health_events.blank?

rule_health = release_health_rules.map do |rule|
release_health_events.where(release_health_rule: rule).last&.healthy?
end.compact

rule_health.all?
end

def fetch_health_data!
return if app.monitoring_provider.blank?
return unless production_channel?
Expand Down
16 changes: 16 additions & 0 deletions app/models/filter_rule_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# == Schema Information
#
# Table name: rule_expressions
#
# id :uuid not null, primary key
# comparator :string not null
# metric :string not null, indexed, indexed => [release_health_rule_id]
# threshold_value :float not null
# type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# release_health_rule_id :uuid not null, indexed, indexed => [metric]
#
class FilterRuleExpression < RuleExpression
enum metric: ReleaseHealthMetric::METRIC_VALUES.slice(:adoption_rate).transform_values(&:to_s)
end
4 changes: 3 additions & 1 deletion app/models/release_health_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
# release_health_rule_id :uuid not null, indexed => [deployment_run_id, release_health_metric_id], indexed
#
class ReleaseHealthEvent < ApplicationRecord
include HealthAwareness
include Displayable
self.implicit_order_column = :event_timestamp

enum health_status: {healthy: "healthy", unhealthy: "unhealthy"}

belongs_to :deployment_run
belongs_to :release_health_rule
belongs_to :release_health_metric
Expand Down
41 changes: 28 additions & 13 deletions app/models/release_health_metric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ class ReleaseHealthMetric < ApplicationRecord
belongs_to :deployment_run
has_one :release_health_event, dependent: :nullify

delegate :train, to: :deployment_run
delegate :release_health_rules, to: :deployment_run

after_create_commit :check_release_health

METRIC_VALUES = {
session_stability: :session_stability,
user_stability: :user_stability,
errors: :errors_count,
new_errors: :new_errors_count
errors_count: :errors_count,
new_errors_count: :new_errors_count,
adoption_rate: :adoption_rate
}.with_indifferent_access

def user_stability
Expand All @@ -47,20 +48,34 @@ def adoption_rate
end

def check_release_health
return if train.release_health_rules.blank?
train.release_health_rules.each do |rule|
value = send(METRIC_VALUES[rule.metric])
next unless value
create_health_event(rule, value)
return if release_health_rules.blank?
release_health_rules.each do |rule|
create_health_event(rule)
end
end

def create_health_event(rule, value)
last_event = deployment_run.release_health_events.where(release_health_rule: rule).last
def evaluate(metric_name)
METRIC_VALUES[metric_name].present? ? public_send(METRIC_VALUES[metric_name]) : nil
end

current_status = rule.evaluate(value)
return if last_event.blank? && current_status == ReleaseHealthRule.health_statuses[:healthy]
def create_health_event(release_health_rule)
last_event = deployment_run.release_health_events.where(release_health_rule:).last
is_healthy = release_health_rule.healthy?(self)
return if last_event.blank? && is_healthy
current_status = is_healthy ? ReleaseHealthEvent.health_statuses[:healthy] : ReleaseHealthEvent.health_statuses[:unhealthy]
return if last_event.present? && last_event.health_status == current_status
create_release_health_event(deployment_run:, release_health_rule: rule, health_status: current_status, event_timestamp: fetched_at)

create_release_health_event(deployment_run:, release_health_rule:, health_status: current_status, event_timestamp: fetched_at)
end

def metric_healthy?(metric_name)
raise ArgumentError "Invalid metric name" unless metric_name.in? METRIC_VALUES.keys

rule = release_health_rules.for_metric(metric_name).first
return unless rule

event = deployment_run.release_health_events.where(release_health_rule: rule).last
return true if event.blank?
event.healthy?
end
end
Loading