Skip to content

Commit

Permalink
Implement SLA functionality (#63)
Browse files Browse the repository at this point in the history
Implement SLA functionality

add calculate_sla() function to models.py to calculate component unavailability on monthly basis for 6 months. Formula: ([number of minutes in month] - [number of minutes all outages open])/[number of minutes in month]
adjust get_history_by_months() function for reusability
implement the component availability view

Reviewed-by: Artem Goncharov
  • Loading branch information
OlhaKashyrina authored Aug 23, 2023
1 parent bd97350 commit 54c94a6
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 9 deletions.
81 changes: 75 additions & 6 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from app import db

from dateutil.relativedelta import relativedelta

from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Index
Expand Down Expand Up @@ -110,6 +112,19 @@ def get_attributes_as_dict(self):
"""Return component attributes as dicionary"""
return {attr.name: attr.value for attr in self.attributes}

@staticmethod
def count_components_by_attributes(attr_dict):
"""Return the number of components that match specific attributes"""
counter = 0
for comp in Component.all():
comp_attrs = comp.get_attributes_as_dict()
if set(
comp_attrs.items()
).intersection(set(
attr_dict.items())) == set(attr_dict.items()):
counter += 1
return counter

@staticmethod
def find_by_name_and_attributes(name, attributes):
"""Find existing component by name and set of attributes
Expand Down Expand Up @@ -141,6 +156,54 @@ def find_by_name_and_attributes(name, attributes):
return comp
return None

def calculate_sla(self):
"""Calculate component availability on the month basis"""

time_now = datetime.datetime.now()
this_month_start = datetime.datetime(time_now.year, time_now.month, 1)

outages = [inc for inc in self.incidents
if inc.impact == 3 and inc.end_date is not None]
outages_dict = Incident.get_history_by_months(outages)
outages_dict_sorted = dict(sorted(outages_dict.items()))

prev_month_minutes = 0

months = [this_month_start + relativedelta(months=-mon)
for mon in range(6)]
sla_dict = {month: 1 for month in months}

for month_start, outage_group in outages_dict_sorted.items():
minutes_in_month = prev_month_minutes
outages_minutes = 0
prev_month_minutes = 0

if this_month_start.month == month_start.month:
minutes_in_month = (
time_now - month_start
).total_seconds() / 60
else:
next_month_start = month_start + relativedelta(months=1)
minutes_in_month = (
next_month_start - month_start
).total_seconds() / 60

for outage in outage_group:
outage_start = outage.start_date
if outage_start < month_start:
diff = month_start - outage_start
prev_month_minutes += diff.total_seconds() / 60
outage_start = month_start

diff = outage.end_date - outage_start
outages_minutes += diff.total_seconds() / 60

sla_dict[month_start] = (
minutes_in_month - outages_minutes
) / minutes_in_month

return sla_dict


class ComponentAttribute(Base):
"""Component Attribute model"""
Expand Down Expand Up @@ -219,13 +282,19 @@ def get_all_closed():
).all()

@staticmethod
def get_history_by_months():
def get_history_by_months(incident_list):
if incident_list is None:
incident_list = Incident.get_all_closed()
incident_dict = {}
for incident in Incident.get_all_closed():
incident_dict.setdefault(incident.end_date.month, []).append(
incident
)
return incident_dict.values()
for incident in incident_list:
incident_dict.setdefault(
datetime.datetime(
incident.end_date.year,
incident.end_date.month,
1),
[]
).append(incident)
return incident_dict

@staticmethod
def get_active_maintenance():
Expand Down
30 changes: 30 additions & 0 deletions app/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,36 @@ nav.navbar {
justify-content: center;
}

.table-header {
top:4;
background-color: var(--bs-blue);
color: var(--bs-white);
}

.table thead > tr > th {
border: none;
}

.table thead > tr {
border: none;
}

tbody:nth-child(odd) {
background: var(--bs-gray) !important;
}

.bg-0 {
background-color: var(--bs-green) !important;
}

.bg-1 {
background-color: var(--bs-yellow) !important;
}

.bg-2 {
background-color: var(--bs-red) !important;
}


/* Footer */
footer.footer {
Expand Down
17 changes: 17 additions & 0 deletions app/web/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from app.web.forms import IncidentForm
from app.web.forms import IncidentUpdateForm

from dateutil.relativedelta import relativedelta

from flask import abort
from flask import current_app
from flask import flash
Expand Down Expand Up @@ -127,6 +129,21 @@ def history():
)


@bp.route("/sla", methods=["GET"])
def sla():
time_now = datetime.now()
months = [time_now + relativedelta(months=-mon) for mon in range(6)]

return render_template(
"sla.html",
title="Component Availability",
components=Component,
component_attributes=ComponentAttribute,
incidents=Incident,
months=months,
)


@bp.route("/login/<name>")
def login(name):
"""Login
Expand Down
6 changes: 3 additions & 3 deletions app/web/templates/history.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ <h1>Incident History</h1>
</div>

<div class="mb-3">
{% set past_incidents = incidents.get_history_by_months() %}
{% if past_incidents | length == 0 %}
{% set past_incidents = incidents.get_history_by_months(None) %}
{% if past_incidents.values() | length == 0 %}
<div class="alert alert-success" role="alert">
No incidents yet.
</div>
{% else %}
{% for incident_group in past_incidents
{% for incident_group in past_incidents.values()
| sort(attribute='0.end_date', reverse = True) %}
<h3 class="mb-3">{{ incident_group[0].end_date.strftime('%B %Y') }}</h3>
<ul class="history">
Expand Down
105 changes: 105 additions & 0 deletions app/web/templates/sla.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{% extends "base.html" %}

{% block content %}

<div class="container">

<div class="mb-3">
<h1>Component Availability</h1>
</div>

{% set regions = component_attributes.get_unique_values('region') %}
{% set color_value = 1 %}
<!-- Nav tabs -->
<ul class="nav nav-tabs justify-content-center" id="myTab" role="tablist">
{% for region in regions %}
<li class="nav-item" role="presentation">
{% if loop.first %}
<button class="nav-link active"
{% else %}
<button class="nav-link"
{% endif %}
id="{{ region }}-tab"
data-bs-toggle="tab"
data-bs-target="#{{ region }}"
type="button"
role="tab"
aria-controls="{{region}}"
{% if loop.first %}
aria-selected="true"
{% else %}
aria-selected="false"
{% endif %}
>
{{ region | upper }}</button>
</li>
{% endfor %}
</ul>

<!-- Tab panes -->
<div class="tab-content mt-3">
{% for region in regions %}
{% if loop.first %}
<div class="tab-pane fade show active"
{% else %}
<div class="tab-pane fade"
{% endif %}
id="{{region}}"
tabindex="0"
role="tabpanel"
aria-labelledby="{{region}}-tab"
>
<div class="position-relative">
<table class="table table-bordered border-light table-hover text-center align-middle">
<thead class="align-middle table-header sticky-top top-0">
<tr>
<th scope="col" rowspan="2">Category</th>
<th scope="col" rowspan="2">Service</th>
<th scope="colgroup" colspan="6">Availability, %</th>
</tr>
<tr>
{% for month in months | reverse %}
<th scope="col">{{ month.strftime('%B %Y') }}</th>
{% endfor %}
</tr>
</thead>
{% set categories = component_attributes.get_unique_values("category") %}
{% for cat in categories|sort %}
<tbody>
{% set attr_dict = ({'region':region,'category':cat}) %}
{% set cat_length = components.count_components_by_attributes(attr_dict) %}
{% set ns = namespace(first=true) %}
{% for component in components.all() %}
{% with attrs = component.get_attributes_as_dict() %}
{% if 'region' in attrs and attrs['region'] == region and 'category' in attrs and attrs['category'] == cat %}
<tr>
{% if ns.first %}
<td rowspan="{{ cat_length }}">{{ cat }}</td>
{% set ns.first = false %}
{% endif %}
<td>{{ component.name }}</td>
{% with sla_dict = component.calculate_sla() %}
{% for (m, sla) in sla_dict | dictsort %}
{% if sla > 0.9995 %}
{% set color_value = 0 %}
{% elif sla < 0.9 %}
{% set color_value = 2 %}
{% endif %}
<td class="bg-{{color_value}}">{{ '%0.2f'| format(sla * 100|float) }}</td>
{% endfor %}
{% endwith %}
</tr>
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</div>
{% endfor %}
</div>
</div>


{% endblock %}

0 comments on commit 54c94a6

Please sign in to comment.