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

API for fetching deployments with related incidents #36

Merged
merged 9 commits into from
Apr 10, 2024
65 changes: 61 additions & 4 deletions apiserver/dora/api/incidents.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
from typing import List
import json
from typing import Dict, List

from datetime import datetime

from flask import Blueprint
from voluptuous import Required, Schema, Coerce, All
from voluptuous import Required, Schema, Coerce, All, Optional
from dora.service.deployments.deployment_service import (
get_deployments_service,
)
from dora.service.deployments.models.models import Deployment
from dora.store.models.code.workflows.filter import WorkflowFilter
from dora.utils.time import Interval
from dora.service.incidents.incidents import get_incident_service
from dora.api.resources.incident_resources import adapt_incident
from dora.api.resources.incident_resources import (
adapt_deployments_with_related_incidents,
adapt_incident,
)
from dora.store.models.incidents import Incident

from dora.api.request_utils import queryschema
from dora.api.request_utils import coerce_workflow_filter, queryschema
from dora.service.query_validator import get_query_validator

app = Blueprint("incidents", __name__)
Expand Down Expand Up @@ -37,3 +48,49 @@ def get_resolved_incidents(team_id: str, from_time: datetime, to_time: datetime)
# ToDo: Generate a user map

return [adapt_incident(incident) for incident in resolved_incidents]


@app.route("/teams/<team_id>/deployments_with_related_incidents", methods=["GET"])
@queryschema(
Schema(
{
Required("from_time"): All(str, Coerce(datetime.fromisoformat)),
Required("to_time"): All(str, Coerce(datetime.fromisoformat)),
Optional("pr_filter"): All(str, Coerce(json.loads)),
Optional("workflow_filter"): All(str, Coerce(coerce_workflow_filter)),
}
),
)
def get_deployments_with_related_incidents(
team_id: str,
from_time: datetime,
to_time: datetime,
pr_filter: dict = None,
workflow_filter: WorkflowFilter = None,
):
query_validator = get_query_validator()
interval = Interval(from_time, to_time)
query_validator.team_validator(team_id)

deployments: List[
Deployment
] = get_deployments_service().get_team_all_deployments_in_interval(
team_id, interval, pr_filter, workflow_filter
)

incident_service = get_incident_service()

incidents: List[Incident] = incident_service.get_team_incidents(team_id, interval)

deployment_incidents_map: Dict[
Deployment, List[Incident]
] = incident_service.get_deployment_incidents_map(deployments, incidents)

return list(
map(
lambda deployment: adapt_deployments_with_related_incidents(
deployment, deployment_incidents_map
),
deployments,
)
)
17 changes: 17 additions & 0 deletions apiserver/dora/api/resources/incident_resources.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Dict, List
from dora.api.resources.deployment_resources import adapt_deployment
from dora.service.deployments.models.models import Deployment
from dora.store.models.incidents import Incident
from dora.api.resources.core_resources import adapt_user_info

Expand Down Expand Up @@ -31,3 +34,17 @@ def adapt_incident(
"summary": incident.meta.get("summary"),
"incident_type": incident.incident_type.value,
}


def adapt_deployments_with_related_incidents(
deployment: Deployment,
deployment_incidents_map: Dict[Deployment, List[Incident]],
username_user_map: dict = None,
):
deployment_response = adapt_deployment(deployment, username_user_map)
incidents = deployment_incidents_map.get(deployment, [])
incident_response = list(
map(lambda incident: adapt_incident(incident, username_user_map), incidents)
)
deployment_response["incidents"] = incident_response
return deployment_response
37 changes: 37 additions & 0 deletions apiserver/dora/service/deployments/deployment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,40 @@ def get_filtered_team_repos_with_workflow_configured_deployments(

return team_repos_with_workflow_deployments

def get_team_all_deployments_in_interval(
self,
team_id: str,
interval,
pr_filter: PRFilter = None,
workflow_filter: WorkflowFilter = None,
) -> List[Deployment]:

team_repos = self._get_team_repos_by_team_id(team_id)
(
team_repos_using_workflow_deployments,
team_repos_using_pr_deployments,
) = self.get_filtered_team_repos_by_deployment_config(team_repos)

deployments_using_workflow = self.workflow_based_deployments_service.get_repos_all_deployments_in_interval(
self._get_repo_ids_from_team_repos(team_repos_using_workflow_deployments),
interval,
workflow_filter,
)
deployments_using_pr = (
self.pr_based_deployments_service.get_repos_all_deployments_in_interval(
self._get_repo_ids_from_team_repos(team_repos_using_pr_deployments),
interval,
pr_filter,
)
)

deployments: List[Deployment] = (
deployments_using_workflow + deployments_using_pr
)
sorted_deployments = self._sort_deployments_by_date(deployments)

return sorted_deployments

def _get_team_repos_by_team_id(self, team_id: str) -> List[TeamRepos]:
return self.code_repo_service.get_active_team_repos_by_team_id(team_id)

Expand All @@ -99,6 +133,9 @@ def _get_repo_ids_from_team_repos(self, team_repos: List[TeamRepos]) -> List[str
def get_filtered_team_repos_by_deployment_config(
self, team_repos: List[TeamRepos]
) -> Tuple[List[TeamRepos], List[TeamRepos]]:
"""
Splits the input TeamRepos list into two TeamRepos List, TeamRepos using workflow and TeamRepos using pr deployments.
"""
return self._filter_team_repos_using_workflow_deployments(
team_repos
), self._filter_team_repos_using_pr_deployments(team_repos)
Expand Down
55 changes: 54 additions & 1 deletion apiserver/dora/service/incidents/incidents.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import List
from collections import defaultdict
from typing import List, Dict
from dora.service.deployments.models.models import Deployment
from dora.service.incidents.incident_filter import apply_incident_filter
from dora.store.models.incidents.filter import IncidentFilter
from dora.store.models.settings import EntityType, SettingType
Expand Down Expand Up @@ -36,6 +38,57 @@ def get_resolved_team_incidents(
team_id, interval, incident_filter
)

def get_team_incidents(self, team_id: str, interval: Interval) -> List[Incident]:
incident_filter: IncidentFilter = apply_incident_filter(
entity_type=EntityType.TEAM,
entity_id=team_id,
setting_types=[
SettingType.INCIDENT_SETTING,
SettingType.INCIDENT_TYPES_SETTING,
],
)
return self._incidents_repo_service.get_team_incidents(
team_id, interval, incident_filter
)

def get_deployment_incidents_map(
self, deployments: List[Deployment], incidents: List[Incident]
):
deployments = sorted(deployments, key=lambda x: x.conducted_at)
incidents = sorted(incidents, key=lambda x: x.creation_date)
incidents_pointer = 0

deployment_incidents_map: Dict[Deployment, List[Incident]] = defaultdict(list)

for current_deployment, next_deployment in zip(
deployments, deployments[1:] + [None]
):
current_deployment_incidents = []

if incidents_pointer >= len(incidents):
deployment_incidents_map[
current_deployment
] = current_deployment_incidents
continue

while incidents_pointer < len(incidents):
incident = incidents[incidents_pointer]

if incident.creation_date >= current_deployment.conducted_at and (
next_deployment is None
or incident.creation_date < next_deployment.conducted_at
):
current_deployment_incidents.append(incident)
incidents_pointer += 1
elif incident.creation_date < current_deployment.conducted_at:
incidents_pointer += 1
else:
break

deployment_incidents_map[current_deployment] = current_deployment_incidents

return deployment_incidents_map


def get_incident_service():
return IncidentService(IncidentsRepoService(), get_settings_service())
35 changes: 29 additions & 6 deletions apiserver/dora/store/repos/incidents.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import List
from dora.store.models.incidents.enums import IncidentType
from sqlalchemy import and_
from dora.store import rollback_on_exc, session
from dora.store.models.incidents import (
Incident,
IncidentFilter,
IncidentOrgIncidentServiceMap,
TeamIncidentService,
IncidentStatus,
)
from dora.utils.time import Interval

Expand All @@ -21,6 +22,32 @@ def _apply_incident_filter(self, query, incident_filter: IncidentFilter = None):
def get_resolved_team_incidents(
self, team_id: str, interval: Interval, incident_filter: IncidentFilter = None
) -> List[Incident]:
query = self._get_team_incidents_query(team_id, incident_filter)

query = query.filter(
and_(
Incident.status == IncidentStatus.RESOLVED.value,
Incident.resolved_date.between(interval.from_time, interval.to_time),
)
)

return query.all()

@rollback_on_exc
def get_team_incidents(
self, team_id: str, interval: Interval, incident_filter: IncidentFilter = None
) -> List[Incident]:
query = self._get_team_incidents_query(team_id, incident_filter)

query = query.filter(
Incident.creation_date.between(interval.from_time, interval.to_time),
)

return query.all()

def _get_team_incidents_query(
self, team_id: str, incident_filter: IncidentFilter = None
):
query = (
session.query(Incident)
.join(
Expand All @@ -38,9 +65,5 @@ def get_resolved_team_incidents(
)

query = self._apply_incident_filter(query, incident_filter)
query = query.filter(Incident.incident_type == IncidentType.ALERT)
query = query.filter(
Incident.resolved_date.between(interval.from_time, interval.to_time),
)

return query.order_by(Incident.creation_date.asc()).all()
return query.order_by(Incident.creation_date.asc())
5 changes: 5 additions & 0 deletions apiserver/dora/utils/string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from uuid import uuid4


def uuid4_str():
return str(uuid4())
2 changes: 2 additions & 0 deletions apiserver/tests/factories/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .code import get_repo_workflow_run
from .incidents import get_incident
40 changes: 40 additions & 0 deletions apiserver/tests/factories/models/incidents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import List
from datetime import datetime
from dora.store.models.incidents.incidents import Incident
from dora.utils.string import uuid4_str

from dora.utils.time import time_now


def get_incident(
id: str = uuid4_str(),
provider: str = "provider",
key: str = "key",
title: str = "title",
status: str = "status",
incident_number: int = 0,
creation_date: datetime = time_now(),
created_at: datetime = time_now(),
updated_at: datetime = time_now(),
resolved_date: datetime = time_now(),
acknowledged_date: datetime = time_now(),
assigned_to: str = "assigned_to",
assignees: List[str] = [],
meta: dict = {},
) -> Incident:
return Incident(
id=id,
provider=provider,
key=key,
title=title,
status=status,
incident_number=incident_number,
created_at=created_at,
updated_at=updated_at,
creation_date=creation_date,
resolved_date=resolved_date,
assigned_to=assigned_to,
assignees=assignees,
acknowledged_date=acknowledged_date,
meta=meta,
)
Loading
Loading