diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index ed6ce79a5..fb339a0fd 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -49,6 +49,7 @@ class ServiceType(StrEnum): firewall = "firewall" object_storage = "object_storage" aclb = "aclb" + net_load_balancer = "netloadbalancer" class MetricType(StrEnum): @@ -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): @@ -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): """ @@ -107,6 +123,34 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" + group_by: Optional[List[str]] = None + _filters: Optional[List[Filter]] = field( + default=None, metadata={"json_key": "filters"} + ) + + def __getattribute__(self, name): + """Override to handle the filters attribute specifically to avoid metaclass conflict.""" + if name == "filters": + return object.__getattribute__(self, "_filters") + return object.__getattribute__(self, name) + + def __setattr__(self, name, value): + """Override to handle setting the filters attribute.""" + if name == "filters": + object.__setattr__(self, "_filters", value) + else: + object.__setattr__(self, name, value) + + +@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 @@ -135,9 +179,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): @@ -154,7 +196,7 @@ class MonitorDashboard(Base): "label": Property(), "service_type": Property(ServiceType), "type": Property(DashboardType), - "widgets": Property(List[DashboardWidget]), + "widgets": Property(json_object=DashboardWidget), "updated": Property(is_datetime=True), } @@ -171,6 +213,7 @@ class MonitorService(Base): properties = { "service_type": Property(ServiceType), "label": Property(), + "alert": Property(json_object=ServiceAlert), } diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index 34577c336..c9dc05099 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -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. @@ -35,6 +47,7 @@ class Region(Base): "placement_group_limits": Property( json_object=RegionPlacementGroupLimits ), + "monitors": Property(json_object=RegionMonitors), } @property diff --git a/test/fixtures/monitor_dashboards.json b/test/fixtures/monitor_dashboards.json index 42de92b55..5e56923a1 100644 --- a/test/fixtures/monitor_dashboards.json +++ b/test/fixtures/monitor_dashboards.json @@ -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", @@ -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 } ] } diff --git a/test/fixtures/monitor_dashboards_1.json b/test/fixtures/monitor_dashboards_1.json index b78bf3447..afb5d71ee 100644 --- a/test/fixtures/monitor_dashboards_1.json +++ b/test/fixtures/monitor_dashboards_1.json @@ -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", @@ -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 } ] } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas_dashboards.json b/test/fixtures/monitor_services_dbaas_dashboards.json index 5fbb7e9db..e39a231b2 100644 --- a/test/fixtures/monitor_services_dbaas_dashboards.json +++ b/test/fixtures/monitor_services_dbaas_dashboards.json @@ -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", @@ -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" + } + ] + } ] } diff --git a/test/fixtures/regions.json b/test/fixtures/regions.json index b58db045d..1482def37 100644 --- a/test/fixtures/regions.json +++ b/test/fixtures/regions.json @@ -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", diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index 7c9249f42..bc996f7be 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -35,6 +35,57 @@ def test_get_all_dashboards(test_linode_client): assert dashboards_by_svc[0].service_type == get_service_type +def test_filter_and_group_by(test_linode_client): + client = test_linode_client + dashboards_by_svc = client.monitor.dashboards(service_type="linode") + assert isinstance(dashboards_by_svc[0], MonitorDashboard) + + # Get the first dashboard for linode service type + dashboard = dashboards_by_svc[0] + assert dashboard.service_type == "linode" + + # Ensure the dashboard has widgets + assert hasattr( + dashboard, "widgets" + ), "Dashboard should have widgets attribute" + assert dashboard.widgets is not None, "Dashboard widgets should not be None" + assert ( + len(dashboard.widgets) > 0 + ), "Dashboard should have at least one widget" + + # Test the first widget's group_by and filters fields + widget = dashboard.widgets[0] + + # Test group_by field type + group_by = widget.group_by + assert group_by is None or isinstance( + group_by, list + ), "group_by should be None or list type" + if group_by is not None: + for item in group_by: + assert isinstance(item, str), "group_by items should be strings" + + # Test filters field type + filters = widget.filters + assert filters is None or isinstance( + filters, list + ), "filters should be None or list type" + if filters is not None: + from linode_api4.objects.monitor import Filter + + for filter_item in filters: + assert isinstance( + filter_item, Filter + ), "filter items should be Filter objects" + assert hasattr( + filter_item, "dimension_label" + ), "Filter should have dimension_label" + assert hasattr( + filter_item, "operator" + ), "Filter should have operator" + assert hasattr(filter_item, "value"), "Filter should have value" + + # List supported services def test_get_supported_services(test_linode_client): client = test_linode_client diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index a010514c2..329a09063 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -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") @@ -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() @@ -83,12 +100,20 @@ 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") @@ -96,7 +121,7 @@ def test_metric_definitions(self): 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") diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index 6ae503098..73fdc8f5d 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -27,6 +27,11 @@ 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)