Skip to content

Commit 44fd924

Browse files
authored
[gcp][feat] Add metrics collection to GcpPubSub and GcpBucket resources (#2296)
1 parent 68832c0 commit 44fd924

File tree

7 files changed

+163
-51
lines changed

7 files changed

+163
-51
lines changed

plugins/gcp/fix_plugin_gcp/collector.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def rm_leaf_nodes(clazz: Any, ignore_kinds: Optional[Type[Any]] = None) -> None:
198198
rm_leaf_nodes(compute.GcpAcceleratorType)
199199
rm_leaf_nodes(billing.GcpSku)
200200
rm_leaf_nodes(billing.GcpService)
201+
rm_leaf_nodes(compute.GcpInterconnectLocation)
201202
# remove regions that are not in use
202203
self.graph.remove_recursively(builder.nodes(GcpRegion, lambda r: r.compute_region_in_use(builder) is False))
203204

plugins/gcp/fix_plugin_gcp/gcp_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
InternalZoneProp = "_zone"
1515
ZoneProp = "zone"
1616
RegionProp = "region"
17+
LocationProp = "location"
1718

1819
# Store the discovery function as separate variable.
1920
# This is used in tests to change the builder function.

plugins/gcp/fix_plugin_gcp/resources/base.py

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from googleapiclient.errors import HttpError
1616

1717
from fix_plugin_gcp.config import GcpConfig
18-
from fix_plugin_gcp.gcp_client import GcpClient, GcpApiSpec, InternalZoneProp, ZoneProp, RegionProp
18+
from fix_plugin_gcp.gcp_client import GcpClient, GcpApiSpec, LocationProp, InternalZoneProp, ZoneProp, RegionProp
1919
from fix_plugin_gcp.utils import Credentials
2020
from fixlib.baseresources import (
2121
BaseResource,
@@ -210,60 +210,61 @@ def add_region_to_node(self, node: GcpResourceType, source: Optional[Json] = Non
210210
self.add_edge(node, node=node._region, reverse=True)
211211
return
212212

213-
parts = node.id.split("/", maxsplit=4)
214-
if len(parts) > 3 and parts[0] == "projects":
215-
if parts[2] in ["locations", "zones", "regions"]:
216-
location_name = parts[3]
217-
# Check for zone first
218-
if zone := self.zone_by_name.get(location_name):
219-
node._zone = zone
220-
node._region = self.region_by_zone_name.get(zone.id)
221-
self.add_edge(zone, node=node)
222-
return
213+
def set_zone_or_region(location_name: str) -> bool:
214+
return set_zone(location_name) or set_region(location_name)
223215

224-
# Then check for region
225-
if region := self.region_by_name.get(location_name):
226-
node._region = region
227-
self.add_edge(region, node=node)
228-
return
216+
def set_zone(zone_name: str) -> bool:
217+
if zone := self.zone_by_name.get(zone_name):
218+
node._zone = zone
219+
node._region = self.region_by_zone_name.get(zone.id)
220+
self.add_edge(zone, node=node)
221+
return True
222+
else:
223+
log.debug(
224+
"Zone property '%s' found in the source but no corresponding region object is available to associate with the node.",
225+
zone_name,
226+
)
227+
return False
228+
229+
def set_region(region_name: str) -> bool:
230+
if region := self.region_by_name.get(region_name):
231+
node._region = region
232+
self.add_edge(node, node=region, reverse=True)
233+
return True
234+
else:
235+
log.debug(
236+
"Region property '%s' found in the source but no corresponding region object is available to associate with the node.",
237+
region_name,
238+
)
239+
return False
229240

230241
if source is not None:
231242
if ZoneProp in source:
232-
zone_name = source[ZoneProp].rsplit("/", 1)[-1]
233-
if zone := self.zone_by_name.get(zone_name):
234-
node._zone = zone
235-
node._region = self.region_by_zone_name[zone_name]
236-
self.add_edge(node, node=zone, reverse=True)
243+
zone_name = source[ZoneProp].lower().rsplit("/", 1)[-1]
244+
if set_zone(zone_name):
237245
return
238-
else:
239-
log.debug(
240-
"Zone property '%s' found in the source but no corresponding zone object is available to associate with the node.",
241-
zone_name,
242-
)
243246

244247
if InternalZoneProp in source:
245-
if zone := self.zone_by_name.get(source[InternalZoneProp]):
246-
node._zone = zone
247-
node._region = self.region_by_zone_name[source[InternalZoneProp]]
248-
self.add_edge(node, node=zone, reverse=True)
248+
zone_name = source[InternalZoneProp].lower().rsplit("/", 1)[-1]
249+
if set_zone(zone_name):
249250
return
250-
else:
251-
log.debug(
252-
"Internal zone property '%s' exists in the source but no corresponding zone object is available to associate with the node.",
253-
source[InternalZoneProp],
254-
)
255251

256252
if RegionProp in source:
257-
region_name = source[RegionProp].rsplit("/", 1)[-1]
258-
if region := self.region_by_name.get(region_name):
259-
node._region = region
260-
self.add_edge(node, node=region, reverse=True)
253+
region_name = source[RegionProp].lower().rsplit("/", 1)[-1]
254+
if set_region(region_name):
255+
return
256+
# location property can be a zone or region
257+
if LocationProp in source:
258+
location_name = source[LocationProp].lower().rsplit("/", 1)[-1]
259+
if set_zone_or_region(location_name):
260+
return
261+
262+
parts = node.id.split("/", maxsplit=4)
263+
if len(parts) > 3 and parts[0] == "projects":
264+
if parts[2] in ["locations", "zones", "regions"]:
265+
location_name = parts[3].lower()
266+
if set_zone_or_region(location_name):
261267
return
262-
else:
263-
log.debug(
264-
"Region property '%s' found in the source but no corresponding region object is available to associate with the node.",
265-
region_name,
266-
)
267268

268269
# Fallback to GraphBuilder region, i.e. regional collection
269270
if self.region is not None:

plugins/gcp/fix_plugin_gcp/resources/compute.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4429,7 +4429,7 @@ class GcpMachineType(GcpResource, BaseInstanceType):
44294429
"maximum_persistent_disks_size_gb": S("maximumPersistentDisksSizeGb"),
44304430
"scratch_disks": S("scratchDisks", default=[]) >> ForallBend(S("diskGb")),
44314431
"instance_type": S("name"),
4432-
"instance_cores": S("guestCpus") >> F(lambda x: float(x)),
4432+
"instance_cores": S("guestCpus") >> F(float),
44334433
"instance_memory": S("memoryMb") >> F(lambda x: float(x) / 1024),
44344434
}
44354435
accelerators: Optional[List[GcpAccelerators]] = field(default=None)
@@ -5419,7 +5419,7 @@ class GcpNotificationEndpointGrpcSettings:
54195419

54205420

54215421
@define(eq=False, slots=False)
5422-
class GcpNotificationEndpoint(GcpResource, PhantomBaseResource):
5422+
class GcpNotificationEndpoint(GcpResource):
54235423
kind: ClassVar[str] = "gcp_notification_endpoint"
54245424
_kind_display: ClassVar[str] = "GCP Notification Endpoint"
54255425
_kind_description: ClassVar[str] = "GCP Notification Endpoint is a Google Cloud Platform service that receives and processes notifications from various GCP resources. It acts as a central point for collecting and routing alerts, updates, and event data. Users can configure endpoints to direct notifications to specific destinations like email, SMS, or third-party applications for monitoring and response purposes." # fmt: skip

plugins/gcp/fix_plugin_gcp/resources/monitoring.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def _query_for_chunk(
117117
# Base filter
118118
filters = [
119119
f'metric.type = "{query.query_name}"',
120-
f'resource.labels.project_id="{query.project_id}"',
120+
f'resource.labels.project_id = "{query.project_id}"',
121121
]
122122

123123
# Add additional filters

plugins/gcp/fix_plugin_gcp/resources/pubsub.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from attr import define, field
55

66
from fix_plugin_gcp.gcp_client import GcpApiSpec
7-
from fix_plugin_gcp.resources.base import GcpResource, GcpDeprecationStatus, GraphBuilder
8-
from fixlib.baseresources import BaseQueue, ModelReference, QueueType
7+
from fix_plugin_gcp.resources.base import GcpMonitoringQuery, GcpResource, GcpDeprecationStatus, GraphBuilder
8+
from fix_plugin_gcp.resources.monitoring import STANDART_STAT_MAP, normalizer_factory
9+
from fixlib.baseresources import BaseQueue, MetricName, ModelReference, QueueType
910
from fixlib.json_bender import Bender, S, Bend, K, F
1011
from fixlib.types import Json
1112

@@ -268,6 +269,65 @@ def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None:
268269
if topic := self.subscription_topic:
269270
builder.add_edge(self, clazz=GcpPubSubTopic, reverse=True, name=topic)
270271

272+
def collect_usage_metrics(self, builder: GraphBuilder) -> List[GcpMonitoringQuery]:
273+
queries: List[GcpMonitoringQuery] = []
274+
delta = builder.metrics_delta
275+
276+
queries.extend(
277+
[
278+
GcpMonitoringQuery.create(
279+
query_name="pubsub.googleapis.com/subscription/push_request_count",
280+
period=delta,
281+
ref_id=f"{self.kind}/{self.id}/{self.region().id}",
282+
metric_name=MetricName.NumberOfMessagesReceived,
283+
normalization=normalizer_factory.count,
284+
stat=stat,
285+
project_id=builder.project.id,
286+
metric_filters={
287+
"resource.labels.subscription_id": self.resource_raw_name,
288+
},
289+
)
290+
for stat in STANDART_STAT_MAP
291+
]
292+
)
293+
if self.subscription_topic:
294+
topic_id = self.subscription_topic.rsplit("/", maxsplit=1)[-1]
295+
queries.extend(
296+
[
297+
GcpMonitoringQuery.create(
298+
query_name="pubsub.googleapis.com/topic/send_message_operation_count",
299+
period=delta,
300+
ref_id=f"{self.kind}/{self.id}/{self.region().id}",
301+
metric_name=MetricName.NumberOfMessagesSent,
302+
normalization=normalizer_factory.count,
303+
stat=stat,
304+
project_id=builder.project.id,
305+
metric_filters={
306+
"resource.labels.topic_id": topic_id,
307+
},
308+
)
309+
for stat in STANDART_STAT_MAP
310+
]
311+
)
312+
queries.extend(
313+
[
314+
GcpMonitoringQuery.create(
315+
query_name="pubsub.googleapis.com/subscription/oldest_unacked_message_age",
316+
period=delta,
317+
ref_id=f"{self.kind}/{self.id}/{self.region().id}",
318+
metric_name=MetricName.ApproximateAgeOfOldestMessage,
319+
normalization=normalizer_factory.seconds,
320+
stat=stat,
321+
project_id=builder.project.id,
322+
metric_filters={
323+
"resource.labels.subscription_id": self.resource_raw_name,
324+
},
325+
)
326+
for stat in STANDART_STAT_MAP
327+
]
328+
)
329+
return queries
330+
271331

272332
@define(eq=False, slots=False)
273333
class GcpAwsKinesis:

plugins/gcp/fix_plugin_gcp/resources/storage.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44
from attr import define, field
55

66
from fix_plugin_gcp.gcp_client import GcpApiSpec
7-
from fix_plugin_gcp.resources.base import GcpResource, GcpDeprecationStatus, get_client
8-
from fixlib.baseresources import BaseBucket
7+
from fix_plugin_gcp.resources.base import (
8+
GcpMonitoringQuery,
9+
GcpResource,
10+
GcpDeprecationStatus,
11+
GraphBuilder,
12+
get_client,
13+
)
14+
from fix_plugin_gcp.resources.monitoring import STANDART_STAT_MAP, normalizer_factory
15+
from fixlib.baseresources import BaseBucket, MetricName
916
from fixlib.graph import Graph
1017
from fixlib.json_bender import Bender, S, Bend, ForallBend, AsBool
1118

@@ -418,6 +425,48 @@ class GcpBucket(GcpResource, BaseBucket):
418425
requester_pays: Optional[bool] = field(default=None)
419426
lifecycle_rule: List[GcpRule] = field(factory=list)
420427

428+
def collect_usage_metrics(self, builder: GraphBuilder) -> List[GcpMonitoringQuery]:
429+
queries: List[GcpMonitoringQuery] = []
430+
delta = builder.metrics_delta
431+
432+
queries.extend(
433+
[
434+
GcpMonitoringQuery.create(
435+
query_name="storage.googleapis.com/storage/total_bytes",
436+
period=delta,
437+
ref_id=f"{self.kind}/{self.id}/{self.region().id}",
438+
metric_name=MetricName.BucketSizeBytes,
439+
normalization=normalizer_factory.bytes,
440+
stat=stat,
441+
project_id=builder.project.id,
442+
metric_filters={
443+
"resource.labels.bucket_name": self.id,
444+
"resource.labels.location": self.region().id,
445+
},
446+
)
447+
for stat in STANDART_STAT_MAP
448+
]
449+
)
450+
queries.extend(
451+
[
452+
GcpMonitoringQuery.create(
453+
query_name="storage.googleapis.com/storage/object_count",
454+
period=delta,
455+
ref_id=f"{self.kind}/{self.id}/{self.region().id}",
456+
metric_name=MetricName.NumberOfObjects,
457+
normalization=normalizer_factory.count,
458+
stat=stat,
459+
project_id=builder.project.id,
460+
metric_filters={
461+
"resource.labels.bucket_name": self.id,
462+
"resource.labels.location": self.region().id,
463+
},
464+
)
465+
for stat in STANDART_STAT_MAP
466+
]
467+
)
468+
return queries
469+
421470
def pre_delete(self, graph: Graph) -> bool:
422471
client = get_client(self)
423472
objects = client.list(GcpObject.api_spec, bucket=self.name)

0 commit comments

Comments
 (0)