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

Implement Components Availability API Endpoint #22

Merged
merged 10 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ func (a *API) InitRoutes() {
v2Api.POST("incidents", a.ValidateComponentsMW(), v2.PostIncidentHandler(a.db, a.log))
v2Api.GET("incidents/:id", v2.GetIncidentHandler(a.db, a.log))
v2Api.PATCH("incidents/:id", a.ValidateComponentsMW(), v2.PatchIncidentHandler(a.db, a.log))

v2Api.GET("availability", v2.GetComponentsAvailabilityHandler(a.db, a.log))
//nolint:gocritic
//v2Api.GET("rss")
//v2Api.GET("history")
//v2Api.GET("availability")
//v2Api.GET("/separate/<incident_id>/<component_id>") - > investigate it!!!
//
//v2Api.GET("/login/:name")
Expand Down
2 changes: 1 addition & 1 deletion internal/api/v1/v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestCustomTimeFormat(t *testing.T) {

data, err := json.Marshal(inc)
require.NoError(t, err)
assert.Equal(t, "{\"id\":0,\"text\":\"\",\"impact\":null,\"start_date\":\"2024-09-01 11:45\",\"end_date\":null,\"updates\":null}", string(data))
assert.JSONEq(t, "{\"id\":0,\"text\":\"\",\"impact\":null,\"start_date\":\"2024-09-01 11:45\",\"end_date\":null,\"updates\":null}", string(data))

inc = &Incident{}
err = json.Unmarshal(data, &inc)
Expand Down
205 changes: 205 additions & 0 deletions internal/api/v2/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package v2

import (
"errors"
"fmt"
"net/http"
"sort"
"time"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -245,6 +247,13 @@ type Component struct {
Name string `json:"name"`
}

type ComponentAvailability struct {
ComponentID
Name string `json:"name"`
Availability []MonthlyAvailability `json:"availability"`
Region string `json:"region"`
}

type ComponentID struct {
ID int `json:"id" uri:"id" binding:"required,gte=0"`
}
Expand All @@ -266,6 +275,12 @@ var availableAttrs = map[string]struct{}{ //nolint:gochecknoglobals
"category": {},
}

type MonthlyAvailability struct {
Year int `json:"year"`
Month int `json:"month"` // Number of the month (1 - 12)
Percentage float64 `json:"percentage"` // Percent (0 - 100 / example: 95.23478)
}

func GetComponentsHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
logger.Debug("retrieve components")
Expand Down Expand Up @@ -370,3 +385,193 @@ func checkComponentAttrs(attrs []ComponentAttribute) error {

return nil
}

func GetComponentsAvailabilityHandler(dbInst *db.DB, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
logger.Debug("retrieve availability of components")

components, err := dbInst.GetComponentsWithIncidents()
if err != nil {
apiErrors.RaiseInternalErr(c, err)
return
}

availability := make([]*ComponentAvailability, len(components))
for index, comp := range components {
attrs := make([]ComponentAttribute, len(comp.Attrs))
for i, attr := range comp.Attrs {
attrs[i] = ComponentAttribute{
Name: attr.Name,
Value: attr.Value,
}
}
regionValue := ""
for _, attr := range attrs {
if attr.Name == "region" {
regionValue = attr.Value
break
}
}

incidents := make([]*Incident, len(comp.Incidents))
for i, inc := range comp.Incidents {
newInc := &Incident{
IncidentID: IncidentID{int(inc.ID)},
IncidentData: IncidentData{
Title: *inc.Text,
Impact: inc.Impact,
StartDate: *inc.StartDate,
EndDate: inc.EndDate,
Updates: nil,
},
}
incidents[i] = newInc
}

compAvailability, calcErr := calculateAvailability(&comp)
if calcErr != nil {
apiErrors.RaiseInternalErr(c, calcErr)
return
}

sortComponentAvailability(compAvailability)
// sort.Slice(compAvailability, func(i, j int) bool {
// if compAvailability[i].Year == compAvailability[j].Year {
// return compAvailability[i].Month > compAvailability[j].Month
// }
// return compAvailability[i].Year > compAvailability[j].Year
// })

availability[index] = &ComponentAvailability{
ComponentID: ComponentID{int(comp.ID)},
Region: regionValue,
Name: comp.Name,
Availability: compAvailability,
}
}

c.JSON(http.StatusOK, gin.H{"data": availability})
}
}

func sortComponentAvailability(availabilities []MonthlyAvailability) {
sort.Slice(availabilities, func(i, j int) bool {
if availabilities[i].Year == availabilities[j].Year {
return availabilities[i].Month > availabilities[j].Month
}
return availabilities[i].Year > availabilities[j].Year
})
}

// TODO: add filters for GET request
func calculateAvailability(component *db.Component) ([]MonthlyAvailability, error) {
const (
monthsInYear = 12
precisionFactor = 100000
fullPercentage = 100
availabilityMonths = 11
roundFactor = 0.5
)

if component == nil {
return nil, fmt.Errorf("component is nil")
}

if len(component.Incidents) == 0 {
return nil, nil
}

periodEndDate := time.Now()
// Get the current date and starting point (12 months ago)
periodStartDate := periodEndDate.AddDate(0, -availabilityMonths, 0) // a year ago, including current the month
monthlyDowntime := make([]float64, monthsInYear) // 12 months

for _, inc := range component.Incidents {
if inc.EndDate == nil || *inc.Impact != 3 {
continue
}

// here we skip all incidents that are not correspond to our period
// if the incident started before availability period
// (as example the incident was started at 01:00 31/12 and finished at 02:00 01/01),
// we cut the beginning to the period start date, and do the same for the period ending

incidentStart, incidentEnd, valid := adjustIncidentPeriod(
*inc.StartDate,
*inc.EndDate,
periodStartDate,
periodEndDate,
)
if !valid {
continue
}

current := incidentStart
for current.Before(incidentEnd) {
monthStart := time.Date(current.Year(), current.Month(), 1, 0, 0, 0, 0, time.UTC)
monthEnd := monthStart.AddDate(0, 1, 0)

downtimeStart := maxTime(incidentStart, monthStart)
downtimeEnd := minTime(incidentEnd, monthEnd)
downtime := downtimeEnd.Sub(downtimeStart).Hours()

monthIndex := (downtimeStart.Year()-periodStartDate.Year())*monthsInYear +
int(downtimeStart.Month()-periodStartDate.Month())
if monthIndex >= 0 && monthIndex < len(monthlyDowntime) {
monthlyDowntime[monthIndex] += downtime
}

current = monthEnd
}
}

monthlyAvailability := make([]MonthlyAvailability, 0, monthsInYear)
for i := range [monthsInYear]int{} {
monthDate := periodStartDate.AddDate(0, i, 0)
totalHours := hoursInMonth(monthDate.Year(), int(monthDate.Month()))
availability := fullPercentage - (monthlyDowntime[i] / totalHours * fullPercentage)
availability = float64(int(availability*precisionFactor+roundFactor)) / precisionFactor

monthlyAvailability = append(monthlyAvailability, MonthlyAvailability{
Year: monthDate.Year(),
Month: int(monthDate.Month()),
Percentage: availability,
})
}

return monthlyAvailability, nil
}

func adjustIncidentPeriod(incidentStart, incidentEnd, periodStart, periodEnd time.Time) (time.Time, time.Time, bool) {
if incidentEnd.Before(periodStart) || incidentStart.After(periodEnd) {
return time.Time{}, time.Time{}, false
}
if incidentStart.Before(periodStart) {
incidentStart = periodStart
}
if incidentEnd.After(periodEnd) {
incidentEnd = periodEnd
}
return incidentStart, incidentEnd, true
}

func minTime(start, end time.Time) time.Time {
if start.Before(end) {
return start
}
return end
}

func maxTime(start, end time.Time) time.Time {
if start.After(end) {
return start
}
return end
}

func hoursInMonth(year int, month int) float64 {
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
nextMonth := firstDay.AddDate(0, 1, 0)

return float64(nextMonth.Sub(firstDay).Hours())
}
Loading
Loading