Skip to content

Commit

Permalink
Implement Components Availability API Endpoint (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
bakhterets authored Dec 9, 2024
1 parent 13ce6ea commit 889c6a1
Show file tree
Hide file tree
Showing 6 changed files with 446 additions and 27 deletions.
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

0 comments on commit 889c6a1

Please sign in to comment.