Skip to content

Commit

Permalink
[Integration][Datadog] Datadog Teams and Users (#1256)
Browse files Browse the repository at this point in the history
  • Loading branch information
shariff-6 authored Dec 30, 2024
1 parent b66841f commit 504e3a6
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 19 deletions.
120 changes: 105 additions & 15 deletions integrations/datadog/.port/resources/blueprints.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,100 @@
[
{
"identifier": "datadogUser",
"description": "This blueprint represents a Datadog user account. Users can be assigned to teams, granted specific permissions, and can interact with various Datadog features based on their access levels.",
"title": "Datadog User",
"icon": "Datadog",
"schema": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email",
"description": "The email address associated with the user account"
},
"handle": {
"type": "string",
"title": "Handle",
"description": "The unique handle identifier for the user within Datadog"
},
"status": {
"type": "string",
"title": "Status",
"description": "The current status of the user account (e.g., active, pending, disabled)"
},
"disabled": {
"type": "boolean",
"title": "Disabled",
"description": "Indicates whether the user account is currently disabled"
},
"verified": {
"type": "boolean",
"title": "Verified",
"description": "Indicates whether the user's email address has been verified"
},
"createdAt": {
"type": "string",
"format": "date-time",
"title": "Created At",
"description": "The timestamp when the user account was created"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
},
{
"identifier": "datadogTeam",
"description": "This blueprint represents a Datadog team",
"title": "Datadog Team",
"icon": "Datadog",
"schema": {
"properties": {
"description": {
"type": "string",
"title": "Description",
"description": "A description of the team's purpose and responsibilities"
},
"handle": {
"type": "string",
"title": "Handle",
"description": "The unique handle identifier for the team within Datadog"
},
"userCount": {
"type": "number",
"title": "User Count",
"description": "The total number of users that are members of this team"
},
"summary": {
"type": "string",
"title": "Summary",
"description": "A brief summary of the team's purpose or main responsibilities"
},
"createdAt": {
"type": "string",
"format": "date-time",
"title": "Created At",
"description": "The timestamp when the team was created"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {
"members": {
"target": "datadogUser",
"title": "Members",
"description": "Users who are members of this team",
"many": true,
"required": false
}
}
},
{
"identifier": "datadogHost",
"description": "This blueprint represents a datadog host",
Expand Down Expand Up @@ -202,7 +298,14 @@
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
"relations": {
"team": {
"target": "datadogTeam",
"title": "Team",
"many": false,
"required": false
}
}
},
{
"identifier": "datadogSlo",
Expand Down Expand Up @@ -252,20 +355,7 @@
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {
"sli_average": {
"title": "SLI Average",
"type": "number",
"target": "datadogSloHistory",
"calculationSpec": {
"func": "average",
"averageOf": "total",
"property": "sliValue",
"measureTimeBy": "$createdAt",
"calculationBy": "property"
}
}
},
"aggregationProperties": {},
"relations": {
"monitors": {
"title": "SLO Monitors",
Expand Down
42 changes: 42 additions & 0 deletions integrations/datadog/.port/resources/port-app-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
deleteDependentEntities: true
createMissingRelatedEntities: true
resources:
- kind: user
selector:
query: 'true'
port:
entity:
mappings:
identifier: .id | tostring
title: .attributes.name
blueprint: '"datadogUser"'
properties:
email: .attributes.email
handle: .attributes.handle
status: .attributes.status
disabled: .attributes.disabled
verified: .attributes.verified
createdAt: .attributes.created_at | todate
- kind: team
selector:
query: 'true'
includeMembers: 'true'
port:
entity:
mappings:
identifier: .id | tostring
title: .attributes.name
blueprint: '"datadogTeam"'
properties:
description: .attributes.description
handle: .attributes.handle
userCount: .attributes.user_count
summary: .attributes.summary
createdAt: .attributes.created_at | todate
relations:
members: if .__members then .__members[].id else [] end

- kind: host
selector:
query: "true"
Expand Down Expand Up @@ -58,6 +93,13 @@ resources:
owners: >-
[.attributes.schema.contacts[] | select(.type == "email") |
.contact]
relations:
team:
combinator: '"and"'
rules:
- property: '"handle"'
operator: '"="'
value: .attributes.schema.team
- kind: slo
selector:
query: "true"
Expand Down
4 changes: 4 additions & 0 deletions integrations/datadog/.port/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ features:
- type: exporter
section: APM & Alerting
resources:
- kind: user
- kind: team
- kind: host
- kind: monitor
- kind: service
- kind: slo
- kind: sloHistory
- kind: serviceMetric


configurations:
- name: datadogBaseUrl
description: Datadog Base URL (e.g., <a target="_blank" href="https://api.datadoghq.com">https://api.datadoghq.com</a> or <a target="_blank" href= "https://api.datadoghq.eu")>https://api.datadoghq.eu</a>. To identify your base URL, see the <a target="_blank" href="https://docs.datadoghq.com/getting_started/site/#:~:text=within%20their%20environments.-,Access%20the%20Datadog%20site,-You%20can%20identify">Datadog documentation</a>.
Expand Down
8 changes: 8 additions & 0 deletions integrations/datadog/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- towncrier release notes start -->

## 0.1.71 (2024-12-30)


### Improvements

- Added Datadog Users and Teams


## 0.1.70 (2024-12-30)


Expand Down
77 changes: 77 additions & 0 deletions integrations/datadog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,83 @@ async def _fetch_with_rate_limit_handling(
raise
return response.json()

async def get_team_members(
self, team_id: str, page_size: int = MAX_PAGE_SIZE
) -> AsyncGenerator[list[dict[str, Any]], None]:
"""Get teams members from DataDog
Docs: https://docs.datadoghq.com/api/latest/teams/#get-team-memberships
"""

logger.info(f"Enriching team {team_id} with members information")

page = 0

while True:
url = f"{self.api_url}/api/v2/team/{team_id}/memberships"
result = await self._send_api_request(
url,
params={
"page[size]": page_size,
"page[number]": page,
},
)

users = result.get("included", [])

if not users:
break

yield users
page += 1

async def get_teams(self) -> AsyncGenerator[list[dict[str, Any]], None]:
"""Get teams from DataDog
Docs: https://docs.datadoghq.com/api/latest/teams/#get-all-teams
"""
page = 0
page_size = MAX_PAGE_SIZE

while True:
url = f"{self.api_url}/api/v2/team"
result = await self._send_api_request(
url,
params={
"page[size]": page_size,
"page[number]": page,
},
)

teams = result.get("data", [])
if not teams:
break

yield teams
page += 1

async def get_users(self) -> AsyncGenerator[list[dict[str, Any]], None]:
"""Get users from DataDog
Docs: https://docs.datadoghq.com/api/latest/users/#list-all-users
"""
page = 0
page_size = MAX_PAGE_SIZE

while True:
url = f"{self.api_url}/api/v2/users"
result = await self._send_api_request(
url,
params={
"page[number]": page,
"page[size]": page_size,
},
)

users = result.get("data", [])
if not users:
break

yield users
page += 1

async def get_hosts(self) -> AsyncGenerator[list[dict[str, Any]], None]:
start = 0
count = MAX_PAGE_SIZE
Expand Down
37 changes: 35 additions & 2 deletions integrations/datadog/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import typing
from enum import StrEnum
from typing import Any
from typing import Any, cast

from loguru import logger

from client import DatadogClient
from overrides import SLOHistoryResourceConfig, DatadogResourceConfig, DatadogSelector
from overrides import (
SLOHistoryResourceConfig,
DatadogResourceConfig,
DatadogSelector,
TeamResourceConfig,
)
from port_ocean.context.event import event
from port_ocean.context.ocean import ocean
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE
Expand All @@ -18,6 +23,8 @@ class ObjectKind(StrEnum):
SERVICE = "service"
SLO_HISTORY = "sloHistory"
SERVICE_METRIC = "serviceMetric"
TEAM = "team"
USER = "user"


def init_client() -> DatadogClient:
Expand All @@ -28,6 +35,32 @@ def init_client() -> DatadogClient:
)


@ocean.on_resync(ObjectKind.TEAM)
async def on_resync_teams(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
dd_client = init_client()

selector = cast(TeamResourceConfig, event.resource_config).selector

async for teams in dd_client.get_teams():
logger.info(f"Received teams batch with {len(teams)} teams")
if selector.include_members:
for team in teams:
members = []
async for member_batch in dd_client.get_team_members(team["id"]):
members.extend(member_batch)
team["__members"] = members
yield teams


@ocean.on_resync(ObjectKind.USER)
async def on_resync_users(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
dd_client = init_client()

async for users in dd_client.get_users():
logger.info(f"Received batch with {len(users)} users")
yield users


@ocean.on_resync(ObjectKind.HOST)
async def on_resync_hosts(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
dd_client = init_client()
Expand Down
18 changes: 17 additions & 1 deletion integrations/datadog/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,25 @@ class DatadogResourceConfig(ResourceConfig):
selector: DatadogResourceSelector


class TeamSelector(Selector):
include_members: bool = Field(
alias="includeMembers",
default=False,
description="Whether to include the members of the team, defaults to false",
)


class TeamResourceConfig(ResourceConfig):
kind: typing.Literal["team"]
selector: TeamSelector


class DataDogPortAppConfig(PortAppConfig):
resources: list[
SLOHistoryResourceConfig | DatadogResourceConfig | ResourceConfig
TeamResourceConfig
| SLOHistoryResourceConfig
| DatadogResourceConfig
| ResourceConfig
] = Field(default_factory=list)


Expand Down
Loading

0 comments on commit 504e3a6

Please sign in to comment.