Skip to content
38 changes: 33 additions & 5 deletions linode_api4/objects/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"MonitorServiceToken",
"AggregateFunction",
]
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import List, Optional

from linode_api4.objects.base import Base, Property
Expand Down Expand Up @@ -49,6 +49,7 @@ class ServiceType(StrEnum):
firewall = "firewall"
object_storage = "object_storage"
aclb = "aclb"
net_load_balancer = "netloadbalancer"


class MetricType(StrEnum):
Expand Down Expand Up @@ -82,6 +83,10 @@ class MetricUnit(StrEnum):
RATIO = "ratio"
OPS_PER_SECOND = "ops_per_second"
IOPS = "iops"
KILO_BYTES_PER_SECOND = "kilo_bytes_per_second"
SESSIONS_PER_SECOND = "sessions_per_second"
PACKETS_PER_SECOND = "packets_per_second"
KILO_BITS_PER_SECOND = "kilo_bits_per_second"


class DashboardType(StrEnum):
Expand All @@ -93,6 +98,17 @@ class DashboardType(StrEnum):
custom = "custom"


@dataclass
class Filter(JSONObject):
"""
Represents a filter in the filters list of a dashboard widget.
"""

dimension_label: str = ""
operator: str = ""
value: str = ""


@dataclass
class DashboardWidget(JSONObject):
"""
Expand All @@ -107,6 +123,19 @@ class DashboardWidget(JSONObject):
chart_type: ChartType = ""
y_label: str = ""
aggregate_function: AggregateFunction = ""
group_by: Optional[List[str]] = None
filters: Optional[List[Filter]] = None

Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filters field should specify the Filter type properly in the DashboardWidget class. Consider adding a Property mapping in the api_spec similar to how other nested objects are handled, to ensure proper deserialization of Filter objects.

Suggested change
properties = {
"filters": Property(json_object=Filter),
}

Copilot uses AI. Check for mistakes.


@dataclass
class ServiceAlert(JSONObject):
"""
Represents alert configuration options for a monitor service.
"""

polling_interval_seconds: Optional[List[int]] = None
evaluation_period_seconds: Optional[List[int]] = None
scope: Optional[List[str]] = None


@dataclass
Expand Down Expand Up @@ -135,9 +164,7 @@ class MonitorMetricsDefinition(JSONObject):
scrape_interval: int = 0
is_alertable: bool = False
dimensions: Optional[List[Dimension]] = None
available_aggregate_functions: List[AggregateFunction] = field(
default_factory=list
)
available_aggregate_functions: Optional[List[AggregateFunction]] = None


class MonitorDashboard(Base):
Expand All @@ -154,7 +181,7 @@ class MonitorDashboard(Base):
"label": Property(),
"service_type": Property(ServiceType),
"type": Property(DashboardType),
"widgets": Property(List[DashboardWidget]),
"widgets": Property(json_object=DashboardWidget),
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widgets property should be a list of DashboardWidget objects, but the current implementation will only handle a single widget. This should be Property(List[DashboardWidget]) to maintain the original list functionality.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the codebase handles all lists in this way, its specified like this and client can handle widget as list.

Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widgets property should use a list type since widgets is expected to be a list of DashboardWidget objects. This should be Property(List[DashboardWidget]) or Property(json_object=DashboardWidget, is_list=True) depending on the Property class implementation.

Suggested change
"widgets": Property(json_object=DashboardWidget),
"widgets": Property(json_object=DashboardWidget, is_list=True),

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widgets property should handle a list of DashboardWidget objects, but the current implementation only handles a single object. This should be Property(List[DashboardWidget]) or use appropriate list handling for json_object.

Suggested change
"widgets": Property(json_object=DashboardWidget),
"widgets": Property(json_object=DashboardWidget, is_list=True),

Copilot uses AI. Check for mistakes.

"updated": Property(is_datetime=True),
}

Expand All @@ -171,6 +198,7 @@ class MonitorService(Base):
properties = {
"service_type": Property(ServiceType),
"label": Property(),
"alert": Property(json_object=ServiceAlert),
}


Expand Down
13 changes: 13 additions & 0 deletions linode_api4/objects/region.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ class RegionPlacementGroupLimits(JSONObject):
maximum_linodes_per_pg: int = 0


@dataclass
class RegionMonitors(JSONObject):
"""
Represents the monitor services available in a region.
Lists the services in this region that support metrics and alerts
use with Akamai Cloud Pulse (ACLP).
"""

alerts: Optional[list[str]] = None
metrics: Optional[list[str]] = None


class Region(Base):
"""
A Region. Regions correspond to individual data centers, each located in a different geographical area.
Expand All @@ -35,6 +47,7 @@ class Region(Base):
"placement_group_limits": Property(
json_object=RegionPlacementGroupLimits
),
"monitors": Property(json_object=RegionMonitors),
}

@property
Expand Down
8 changes: 6 additions & 2 deletions test/fixtures/monitor_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"metric": "cpu_usage",
"size": 12,
"unit": "%",
"y_label": "cpu_usage"
"y_label": "cpu_usage",
"group_by": ["entity_id"],
"filters": null
},
{
"aggregate_function": "sum",
Expand All @@ -26,7 +28,9 @@
"metric": "write_iops",
"size": 6,
"unit": "IOPS",
"y_label": "write_iops"
"y_label": "write_iops",
"group_by": ["entity_id"],
"filters": null
}
]
}
Expand Down
8 changes: 6 additions & 2 deletions test/fixtures/monitor_dashboards_1.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"metric": "cpu_usage",
"size": 12,
"unit": "%",
"y_label": "cpu_usage"
"y_label": "cpu_usage",
"group_by": ["entity_id"],
"filters": null
},
{
"aggregate_function": "sum",
Expand All @@ -24,7 +26,9 @@
"metric": "available_memory",
"size": 6,
"unit": "GB",
"y_label": "available_memory"
"y_label": "available_memory",
"group_by": ["entity_id"],
"filters": null
}
]
}
15 changes: 13 additions & 2 deletions test/fixtures/monitor_services_dbaas_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"metric": "cpu_usage",
"size": 12,
"unit": "%",
"y_label": "cpu_usage"
"y_label": "cpu_usage",
"group_by": ["entity_id"],
"filters": null
},
{
"aggregate_function": "sum",
Expand All @@ -26,7 +28,16 @@
"metric": "memory_usage",
"size": 6,
"unit": "%",
"y_label": "memory_usage"
"y_label": "memory_usage",
"group_by": ["entity_id"],
"filters": [
{
"dimension_label": "pattern",
"operator": "in",
"value": "publicout,privateout"
}
]

}
]
}
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/regions.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@
"Object Storage",
"Linode Interfaces"
],
"monitors": {
"alerts": [
"Managed Databases"
],
"metrics": [
"Managed Databases"
]
},
"status": "ok",
"resolvers": {
"ipv4": "66.228.42.5,96.126.106.5,50.116.53.5,50.116.58.5,50.116.61.5,50.116.62.5,66.175.211.5,97.107.133.4,207.192.69.4,207.192.69.5",
Expand Down
27 changes: 26 additions & 1 deletion test/unit/objects/monitor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def test_dashboard_by_ID(self):
self.assertEqual(dashboard.widgets[0].size, 12)
self.assertEqual(dashboard.widgets[0].unit, "%")
self.assertEqual(dashboard.widgets[0].y_label, "cpu_usage")
self.assertEqual(dashboard.widgets[0].group_by, ["entity_id"])
self.assertIsNone(dashboard.widgets[0].filters)

def test_dashboard_by_service_type(self):
dashboards = self.client.monitor.dashboards(service_type="dbaas")
Expand All @@ -62,6 +64,21 @@ def test_dashboard_by_service_type(self):
self.assertEqual(dashboards[0].widgets[0].size, 12)
self.assertEqual(dashboards[0].widgets[0].unit, "%")
self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage")
self.assertEqual(dashboards[0].widgets[0].group_by, ["entity_id"])
self.assertIsNone(dashboards[0].widgets[0].filters)

# Test the second widget which has filters
self.assertEqual(dashboards[0].widgets[1].label, "Memory Usage")
self.assertEqual(dashboards[0].widgets[1].group_by, ["entity_id"])
self.assertIsNotNone(dashboards[0].widgets[1].filters)
self.assertEqual(len(dashboards[0].widgets[1].filters), 1)
self.assertEqual(
dashboards[0].widgets[1].filters[0].dimension_label, "pattern"
)
self.assertEqual(dashboards[0].widgets[1].filters[0].operator, "in")
self.assertEqual(
dashboards[0].widgets[1].filters[0].value, "publicout,privateout"
)

def test_get_all_dashboards(self):
dashboards = self.client.monitor.dashboards()
Expand All @@ -83,20 +100,28 @@ def test_get_all_dashboards(self):
self.assertEqual(dashboards[0].widgets[0].size, 12)
self.assertEqual(dashboards[0].widgets[0].unit, "%")
self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage")
self.assertEqual(dashboards[0].widgets[0].group_by, ["entity_id"])
self.assertIsNone(dashboards[0].widgets[0].filters)

def test_specific_service_details(self):
data = self.client.load(MonitorService, "dbaas")
self.assertEqual(data.label, "Databases")
self.assertEqual(data.service_type, "dbaas")

# Test alert configuration
self.assertIsNotNone(data.alert)
self.assertEqual(data.alert.polling_interval_seconds, [300])
self.assertEqual(data.alert.evaluation_period_seconds, [300])
self.assertEqual(data.alert.scope, ["entity"])

def test_metric_definitions(self):

metrics = self.client.monitor.metric_definitions(service_type="dbaas")
self.assertEqual(
metrics[0].available_aggregate_functions,
["max", "avg", "min", "sum"],
)
self.assertEqual(metrics[0].is_alertable, True)
self.assertTrue(metrics[0].is_alertable)
self.assertEqual(metrics[0].label, "CPU Usage")
self.assertEqual(metrics[0].metric, "cpu_usage")
self.assertEqual(metrics[0].metric_type, "gauge")
Expand Down
7 changes: 7 additions & 0 deletions test/unit/objects/region_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@ def test_get_region(self):
region.placement_group_limits.maximum_linodes_per_pg, 5
)


# Test monitors section
self.assertIsNotNone(region.monitors)
self.assertEqual(region.monitors.alerts, ["Managed Databases"])
self.assertEqual(region.monitors.metrics, ["Managed Databases"])

self.assertIsNotNone(region.capabilities)
self.assertIn("Linode Interfaces", region.capabilities)


def test_region_availability(self):
"""
Tests that availability for a specific region can be listed and filtered on.
Expand Down
Loading