diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md index 254c8055..ece25e85 100644 --- a/blueprints/cloud-operations/network-dashboard/README.md +++ b/blueprints/cloud-operations/network-dashboard/README.md @@ -26,9 +26,12 @@ Clone this repository, then go through the following steps to create resources: Once the resources are deployed, go to the following page to see the dashboard: https://console.cloud.google.com/monitoring/dashboards?project=. A dashboard called "quotas-utilization" should be created. -The Cloud Function runs every 5 minutes by default so you should start getting some data points after a few minutes. +The Cloud Function runs every 10 minutes by default so you should start getting some data points after a few minutes. +You can use the metric explorer to view the data points for the different custom metrics created: https://console.cloud.google.com/monitoring/metrics-explorer?project=. You can change this frequency by modifying the "schedule_cron" variable in variables.tf. +Note that some charts in the dashboard align values over 1h so you might need to wait 1h to see charts on the dashboard views. + Once done testing, you can clean up resources by running `terraform destroy`. ## Supported limits and quotas @@ -46,6 +49,7 @@ The Cloud Function currently tracks usage, limit and utilization of: - Dynamic routes per VPC peering group - IP utilization per subnet (% of IP addresses used in a subnet) - VPC firewall rules per project (VPC drill down is available for usage) +- Tuples per Firewall Policy It writes this values to custom metrics in Cloud Monitoring and creates a dashboard to visualize the current utilization of these metrics in Cloud Monitoring. @@ -58,4 +62,4 @@ In a future release, we could support: - Static routes per VPC / per VPC peering group - Google managed VPCs that are peered with PSA (such as Cloud SQL or Memorystore) -If you are interested in this and/or would like to contribute, please contact legranda@google.com. \ No newline at end of file +If you are interested in this and/or would like to contribute, please contact legranda@google.com. diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py index ae380c20..84bf007c 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py @@ -14,13 +14,14 @@ # limitations under the License. # CFv2 define whether to use Cloud function 2nd generation or 1st generation +import re from distutils.command.config import config import os import time from google.cloud import monitoring_v3, asset_v1 from google.protobuf import field_mask_pb2 from googleapiclient import discovery -from metrics import ilb_fwrules, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls +from metrics import ilb_fwrules, firewall_policies, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls CFv2 = False if CFv2: @@ -123,6 +124,7 @@ config = { "asset_client": asset_v1.AssetServiceClient(), "monitoring_client": monitoring_v3.MetricServiceClient() }, + "series_buffer": [] } @@ -150,6 +152,7 @@ def main(event, context): project_quotas_dict = limits.get_quota_project_limit(config) firewalls_dict = vpc_firewalls.get_firewalls_dict(config) + firewall_policies_dict = firewall_policies.get_firewall_policies_dict(config) # IP utilization subnet level metrics subnets.get_subnets(config, metrics_dict) @@ -160,51 +163,60 @@ def main(event, context): l7_forwarding_rules_dict = ilb_fwrules.get_forwarding_rules_dict(config, "L7") subnet_range_dict = networks.get_subnet_ranges_dict(config) - # Per Project metrics - vpc_firewalls.get_firewalls_data(config, metrics_dict, project_quotas_dict, - firewalls_dict) + try: - # Per Network metrics - instances.get_gce_instances_data(config, metrics_dict, gce_instance_dict, - limits_dict['number_of_instances_limit']) - ilb_fwrules.get_forwarding_rules_data( - config, metrics_dict, l4_forwarding_rules_dict, - limits_dict['internal_forwarding_rules_l4_limit'], "L4") - ilb_fwrules.get_forwarding_rules_data( - config, metrics_dict, l7_forwarding_rules_dict, - limits_dict['internal_forwarding_rules_l7_limit'], "L7") - peerings.get_vpc_peering_data(config, metrics_dict, - limits_dict['number_of_vpc_peerings_limit']) - dynamic_routes_dict = routes.get_dynamic_routes( - config, metrics_dict, limits_dict['dynamic_routes_per_network_limit']) + # Per Project metrics + vpc_firewalls.get_firewalls_data(config, metrics_dict, project_quotas_dict, + firewalls_dict) + # Per Firewall Policy metrics + firewall_policies.get_firewal_policies_data(config, metrics_dict, + firewall_policies_dict) + # Per Network metrics + instances.get_gce_instances_data(config, metrics_dict, gce_instance_dict, + limits_dict['number_of_instances_limit']) + ilb_fwrules.get_forwarding_rules_data( + config, metrics_dict, l4_forwarding_rules_dict, + limits_dict['internal_forwarding_rules_l4_limit'], "L4") + ilb_fwrules.get_forwarding_rules_data( + config, metrics_dict, l7_forwarding_rules_dict, + limits_dict['internal_forwarding_rules_l7_limit'], "L7") + peerings.get_vpc_peering_data(config, metrics_dict, + limits_dict['number_of_vpc_peerings_limit']) + dynamic_routes_dict = routes.get_dynamic_routes( + config, metrics_dict, limits_dict['dynamic_routes_per_network_limit']) - # Per VPC peering group metrics - metrics.get_pgg_data( - config, - metrics_dict["metrics_per_peering_group"]["instance_per_peering_group"], - gce_instance_dict, config["limit_names"]["GCE_INSTANCES"], - limits_dict['number_of_instances_ppg_limit']) - metrics.get_pgg_data( - config, metrics_dict["metrics_per_peering_group"] - ["l4_forwarding_rules_per_peering_group"], l4_forwarding_rules_dict, - config["limit_names"]["L4"], - limits_dict['internal_forwarding_rules_l4_ppg_limit']) - metrics.get_pgg_data( - config, metrics_dict["metrics_per_peering_group"] - ["l7_forwarding_rules_per_peering_group"], l7_forwarding_rules_dict, - config["limit_names"]["L7"], - limits_dict['internal_forwarding_rules_l7_ppg_limit']) - metrics.get_pgg_data( - config, metrics_dict["metrics_per_peering_group"] - ["subnet_ranges_per_peering_group"], subnet_range_dict, - config["limit_names"]["SUBNET_RANGES"], - limits_dict['number_of_subnet_IP_ranges_ppg_limit']) - routes.get_dynamic_routes_ppg( - config, metrics_dict["metrics_per_peering_group"] - ["dynamic_routes_per_peering_group"], dynamic_routes_dict, - limits_dict['dynamic_routes_per_peering_group_limit']) + # Per VPC peering group metrics + metrics.get_pgg_data( + config, + metrics_dict["metrics_per_peering_group"]["instance_per_peering_group"], + gce_instance_dict, config["limit_names"]["GCE_INSTANCES"], + limits_dict['number_of_instances_ppg_limit']) + metrics.get_pgg_data( + config, metrics_dict["metrics_per_peering_group"] + ["l4_forwarding_rules_per_peering_group"], l4_forwarding_rules_dict, + config["limit_names"]["L4"], + limits_dict['internal_forwarding_rules_l4_ppg_limit']) + metrics.get_pgg_data( + config, metrics_dict["metrics_per_peering_group"] + ["l7_forwarding_rules_per_peering_group"], l7_forwarding_rules_dict, + config["limit_names"]["L7"], + limits_dict['internal_forwarding_rules_l7_ppg_limit']) + metrics.get_pgg_data( + config, metrics_dict["metrics_per_peering_group"] + ["subnet_ranges_per_peering_group"], subnet_range_dict, + config["limit_names"]["SUBNET_RANGES"], + limits_dict['number_of_subnet_IP_ranges_ppg_limit']) + routes.get_dynamic_routes_ppg( + config, metrics_dict["metrics_per_peering_group"] + ["dynamic_routes_per_peering_group"], dynamic_routes_dict, + limits_dict['dynamic_routes_per_peering_group_limit']) + except Exception as e: + print("Error writing metrics") + print(e) + finally: + metrics.flush_series_buffer(config) - return 'Function executed successfully' + return 'Function execution completed' if CFv2: @@ -214,4 +226,4 @@ if CFv2: main(None, None) else: if __name__ == "__main__": - main(None, None) \ No newline at end of file + main(None, None) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml index 2a9c8a4e..b87dc1c3 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml @@ -172,3 +172,17 @@ metrics_per_project: utilization: name: firewalls_per_project_utilization description: Number of VPC firewall rules in a project - utilization. +metrics_per_firewall_policy: + firewall_policy_tuples: + usage: + name: firewall_policy_tuples_per_policy_usage + description: Number of tuples in a firewall policy - usage. + limit: + # This limit is not visibile through Google APIs, set default_value + name: firewall_policy_tuples_per_policy_limit + description: Number of tuples in a firewall policy - limit. + values: + default_value: 2000 + utilization: + name: firewall_policy_tuples_per_policy_utilization + description: Number of tuples in a firewall policy - utilization. diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/firewall_policies.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/firewall_policies.py new file mode 100644 index 00000000..ef4c2518 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/firewall_policies.py @@ -0,0 +1,115 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import re +import time + +from collections import defaultdict +from pydoc import doc +from collections import defaultdict +from google.protobuf import field_mask_pb2 +from . import metrics, networks, limits + + +def get_firewall_policies_dict(config: dict): + ''' + Calls the Asset Inventory API to get all Firewall Policies under the GCP organization + + Parameters: + config (dict): The dict containing config like clients and limits + Returns: + firewal_policies_dict (dictionary of dictionary): Keys are policy ids, subkeys are policy field values + ''' + + firewall_policies_dict = defaultdict(int) + read_mask = field_mask_pb2.FieldMask() + read_mask.FromJsonString('name,versionedResources') + + response = config["clients"]["asset_client"].search_all_resources( + request={ + "scope": f"organizations/{config['organization']}", + "asset_types": ["compute.googleapis.com/FirewallPolicy"], + "read_mask": read_mask, + }) + for resource in response: + for versioned in resource.versioned_resources: + firewall_policy = dict() + for field_name, field_value in versioned.resource.items(): + firewall_policy[field_name] = field_value + firewall_policies_dict[firewall_policy['id']] = firewall_policy + return firewall_policies_dict + + +def get_firewal_policies_data(config, metrics_dict, firewall_policies_dict): + ''' + Gets the data for VPC Firewall lorem ipsum + + Parameters: + config (dict): The dict containing config like clients and limits + metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions. + firewall_policies_dict (dictionary of of dictionary of string: string): Keys are policies ids, subkeys are policies values + Returns: + None + ''' + + current_tuples_limit = None + try: + current_tuples_limit = metrics_dict["metrics_per_firewall_policy"][ + "firewall_policy_tuples"]["limit"]["values"]["default_value"] + except Exception: + print( + f"Could not determine number of tuples metric limit due to missing default value" + ) + if current_tuples_limit < 0: + print( + f"Could not determine number of tuples metric limit as default value is <= 0" + ) + + timestamp = time.time() + for firewall_policy_key in firewall_policies_dict: + firewall_policy = firewall_policies_dict[firewall_policy_key] + + # may either be a org, a folder, or a project + # folder and org require to split {folder,organization}\/\w+ + parent = re.search("(\w+$)", firewall_policy["parent"]).group( + 1) if "parent" in firewall_policy else re.search( + "([\d,a-z,-]+)(\/[\d,a-z,-]+\/firewallPolicies/[\d,a-z,-]*$)", + firewall_policy["selfLink"]).group(1) + parent_type = re.search("(^\w+)", firewall_policy["parent"]).group( + 1) if "parent" in firewall_policy else "projects" + + metric_labels = {'parent': parent, 'parent_type': parent_type} + + metric_labels["name"] = firewall_policy[ + "displayName"] if "displayName" in firewall_policy else firewall_policy[ + "name"] + + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_firewall_policy"] + [f"firewall_policy_tuples"]["usage"]["name"], + firewall_policy['ruleTupleCount'], metric_labels, timestamp=timestamp) + if not current_tuples_limit == None and current_tuples_limit > 0: + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_firewall_policy"] + [f"firewall_policy_tuples"]["limit"]["name"], current_tuples_limit, + metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_firewall_policy"] + [f"firewall_policy_tuples"]["utilization"]["name"], + firewall_policy['ruleTupleCount'] / current_tuples_limit, + metric_labels, timestamp=timestamp) + + print(f"Buffered number tuples per Firewall Policy") diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py index 55490544..9b411b15 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py @@ -14,6 +14,8 @@ # limitations under the License. # +import time + from collections import defaultdict from google.protobuf import field_mask_pb2 from . import metrics, networks, limits @@ -75,15 +77,17 @@ def get_forwarding_rules_data(config, metrics_dict, forwarding_rules_dict, Returns: None ''' - for project in config["monitored_projects"]: - network_dict = networks.get_networks(config, project) + + timestamp = time.time() + for project_id in config["monitored_projects"]: + network_dict = networks.get_networks(config, project_id) current_quota_limit = limits.get_quota_current_limit( - config, f"projects/{project}", config["limit_names"][layer]) + config, f"projects/{project_id}", config["limit_names"][layer]) if current_quota_limit is None: print( - f"Could not write {layer} forwarding rules to metric for projects/{project} due to missing quotas" + f"Could not determine {layer} forwarding rules to metric for projects/{project_id} due to missing quotas" ) continue @@ -95,20 +99,24 @@ def get_forwarding_rules_data(config, metrics_dict, forwarding_rules_dict, usage = 0 if net['self_link'] in forwarding_rules_dict: usage = forwarding_rules_dict[net['self_link']] - metrics.write_data_to_metric( - config, project, usage, metrics_dict["metrics_per_network"] + + metric_labels = { + 'project': project_id, + 'network_name': net['network_name'] + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] [f"{layer.lower()}_forwarding_rules_per_network"]["usage"]["name"], - net['network_name']) - metrics.write_data_to_metric( - config, project, net['limit'], metrics_dict["metrics_per_network"] + usage, metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] [f"{layer.lower()}_forwarding_rules_per_network"]["limit"]["name"], - net['network_name']) - metrics.write_data_to_metric( - config, project, usage / net['limit'], - metrics_dict["metrics_per_network"] + net['limit'], metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] [f"{layer.lower()}_forwarding_rules_per_network"]["utilization"] - ["name"], net['network_name']) + ["name"], usage / net['limit'], metric_labels, timestamp=timestamp) print( - f"Wrote number of {layer} forwarding rules to metric for projects/{project}" - ) + f"Buffered number of {layer} forwarding rules to metric for projects/{project_id}" + ) \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py index ef78e67d..dda94c5a 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py @@ -14,6 +14,8 @@ # limitations under the License. # +import time + from code import interact from collections import defaultdict from . import metrics, networks, limits @@ -61,15 +63,16 @@ def get_gce_instances_data(config, metrics_dict, gce_instance_dict, limit_dict): Returns: gce_instance_dict ''' - - for project in config["monitored_projects"]: - network_dict = networks.get_networks(config, project) + timestamp = time.time() + for project_id in config["monitored_projects"]: + network_dict = networks.get_networks(config, project_id) current_quota_limit = limits.get_quota_current_limit( - config, f"projects/{project}", config["limit_names"]["GCE_INSTANCES"]) + config, f"projects/{project_id}", + config["limit_names"]["GCE_INSTANCES"]) if current_quota_limit is None: print( - f"Could not write number of instances for projects/{project} due to missing quotas" + f"Could not determine number of instances for projects/{project_id} due to missing quotas" ) current_quota_limit_view = metrics.customize_quota_view(current_quota_limit) @@ -81,15 +84,19 @@ def get_gce_instances_data(config, metrics_dict, gce_instance_dict, limit_dict): if net['self_link'] in gce_instance_dict: usage = gce_instance_dict[net['self_link']] - metrics.write_data_to_metric( - config, project, usage, metrics_dict["metrics_per_network"] - ["instance_per_network"]["usage"]["name"], net['network_name']) - metrics.write_data_to_metric( - config, project, net['limit'], metrics_dict["metrics_per_network"] - ["instance_per_network"]["limit"]["name"], net['network_name']) - metrics.write_data_to_metric( - config, project, usage / net['limit'], - metrics_dict["metrics_per_network"]["instance_per_network"] - ["utilization"]["name"], net['network_name']) + metric_labels = { + 'project': project_id, + 'network_name': net['network_name'] + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"]["instance_per_network"] + ["usage"]["name"], usage, metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"]["instance_per_network"] + ["limit"]["name"], net['limit'], metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"]["instance_per_network"] + ["utilization"]["name"], usage / net['limit'], metric_labels, + timestamp=timestamp) - print(f"Wrote number of instances to metric for projects/{project}") + print(f"Buffered number of instances to metric for projects/{project_id}") diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py index a379a5c0..5c5bfc73 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py @@ -14,6 +14,8 @@ # limitations under the License. # +import time + from google.api_core import exceptions from google.cloud import monitoring_v3 from . import metrics @@ -173,6 +175,8 @@ def count_effective_limit(config, project_id, network_dict, usage_metric_name, None ''' + timestamp = time.time() + if network_dict['peerings'] == []: return @@ -215,11 +219,16 @@ def count_effective_limit(config, project_id, network_dict, usage_metric_name, # Calculates effective limit: Step 4: Find maximum from step 1 and step 3 effective_limit = max(limit_step1, limit_step3) utilization = peering_group_usage / effective_limit - - metrics.write_data_to_metric(config, project_id, peering_group_usage, - usage_metric_name, network_dict['network_name']) - metrics.write_data_to_metric(config, project_id, effective_limit, - limit_metric_name, network_dict['network_name']) - metrics.write_data_to_metric(config, project_id, utilization, - utilization_metric_name, - network_dict['network_name']) \ No newline at end of file + metric_labels = { + 'project': project_id, + 'network_name': network_dict['network_name'] + } + metrics.append_data_to_series_buffer(config, usage_metric_name, + peering_group_usage, metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer(config, limit_metric_name, + effective_limit, metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer(config, utilization_metric_name, + utilization, metric_labels, + timestamp=timestamp) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py index 155365b6..e0a4399e 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py @@ -14,12 +14,16 @@ # limitations under the License. # +from curses import KEY_MARK +import re import time import yaml from google.api import metric_pb2 as ga_metric from google.cloud import monitoring_v3 from . import peerings, limits, networks +BUFFER_LEN = 10 + def create_metrics(monitoring_project): ''' @@ -84,35 +88,32 @@ def create_metric(metric_name, description, monitoring_project): print("Created {}.".format(descriptor.name)) -def write_data_to_metric(config, monitored_project_id, value, metric_name, - network_name=None, subnet_id=None): +def append_data_to_series_buffer(config, metric_name, metric_value, + metric_labels, timestamp=None): ''' Writes data to Cloud Monitoring custom metrics. Parameters: config (dict): The dict containing config like clients and limits - monitored_project_id: ID of the project where the resource lives (will be added as a label) - value (int): Value for the data point of the metric. metric_name (string): Name of the metric - network_name (string): Name of the network (will be added as a label) - subnet_id (string): Identifier of the Subnet (region/name of the subnet) + metric_value (int): Value for the data point of the metric. + matric_labels (dictionary of dictionary of string: string): metric labels names and values + timestamp (float): seconds since the epoch, in UTC Returns: usage (int): Current usage for that network. limit (int): Current usage for that network. ''' - client = monitoring_v3.MetricServiceClient() series = monitoring_v3.TimeSeries() series.metric.type = f"custom.googleapis.com/{metric_name}" series.resource.type = "global" - series.metric.labels["project"] = monitored_project_id - if network_name != None: - series.metric.labels["network_name"] = network_name - if subnet_id != None: - series.metric.labels["subnet_id"] = subnet_id - now = time.time() - seconds = int(now) - nanos = int((now - seconds) * 10**9) + for label_name in metric_labels: + if (metric_labels[label_name] != None): + series.metric.labels[label_name] = metric_labels[label_name] + + timestamp = timestamp if timestamp != None else time.time() + seconds = int(timestamp) + nanos = int((timestamp - seconds) * 10**9) interval = monitoring_v3.TimeInterval( {"end_time": { "seconds": seconds, @@ -121,20 +122,39 @@ def write_data_to_metric(config, monitored_project_id, value, metric_name, point = monitoring_v3.Point({ "interval": interval, "value": { - "double_value": value + "double_value": metric_value } }) series.points = [point] # TODO: sometimes this cashes with 'DeadlineExceeded: 504 Deadline expired before operation could complete' error # Implement exponential backoff retries? + config["series_buffer"].append(series) + if len(config["series_buffer"]) >= BUFFER_LEN: + flush_series_buffer(config) + + +def flush_series_buffer(config): + ''' + writes buffered metrics to Google Cloud Monitoring, empties buffer upon failure + config (dict): The dict containing config like clients and limits + ''' try: - client.create_time_series(name=config["monitoring_project_link"], - time_series=[series]) + if config["series_buffer"] and len(config["series_buffer"]) > 0: + client = monitoring_v3.MetricServiceClient() + client.create_time_series(name=config["monitoring_project_link"], + time_series=config["series_buffer"]) + series_names = [ + re.search("\/(.+$)", series.metric.type).group(1) + for series in config["series_buffer"] + ] + print("Wrote time series: ", series_names) except Exception as e: - print("Error while writing data point for metric", metric_name) + print("Error while flushing series buffer") print(e) + config["series_buffer"] = [] + def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): ''' @@ -148,18 +168,18 @@ def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): Returns: None ''' - for project in config["monitored_projects"]: - network_dict_list = peerings.gather_peering_data(config, project) + for project_id in config["monitored_projects"]: + network_dict_list = peerings.gather_peering_data(config, project_id) # Network dict list is a list of dictionary (one for each network) # For each network, this dictionary contains: # project_id, network_name, network_id, usage, limit, peerings (list of peered networks) # peerings is a list of dictionary (one for each peered network) and contains: # project_id, network_name, network_id current_quota_limit = limits.get_quota_current_limit( - config, f"projects/{project}", limit_metric) + config, f"projects/{project_id}", limit_metric) if current_quota_limit is None: print( - f"Could not write number of L7 forwarding rules to metric for projects/{project} due to missing quotas" + f"Could not determine number of L7 forwarding rules to metric for projects/{project_id} due to missing quotas" ) continue @@ -169,10 +189,10 @@ def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): for network_dict in network_dict_list: if network_dict['network_id'] == 0: print( - f"Could not write {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project} due to missing permissions." + f"Could not determine {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project_id} due to missing permissions." ) continue - network_link = f"https://www.googleapis.com/compute/v1/projects/{project}/global/networks/{network_dict['network_name']}" + network_link = f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/networks/{network_dict['network_name']}" limit = networks.get_limit_network(network_dict, network_link, current_quota_limit_view, limit_dict) @@ -197,7 +217,7 @@ def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): limit_metric) if current_peered_quota_limit is None: print( - f"Could not write metrics for peering to projects/{peered_network_dict['project_id']} due to missing quotas" + f"Could not determine metrics for peering to projects/{peered_network_dict['project_id']} due to missing quotas" ) continue @@ -211,13 +231,13 @@ def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): peered_network_dict["usage"] = peered_usage peered_network_dict["limit"] = peered_limit - limits.count_effective_limit(config, project, network_dict, + limits.count_effective_limit(config, project_id, network_dict, metric_dict["usage"]["name"], metric_dict["limit"]["name"], metric_dict["utilization"]["name"], limit_dict) print( - f"Wrote {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project}" + f"Buffered {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project_id}" ) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py index 141364d1..616c7f66 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py @@ -14,6 +14,8 @@ # limitations under the License. # +import time + from . import metrics, networks, limits @@ -28,40 +30,53 @@ def get_vpc_peering_data(config, metrics_dict, limit_dict): Returns: None ''' + timestamp = time.time() for project in config["monitored_projects"]: active_vpc_peerings, vpc_peerings = gather_vpc_peerings_data( config, project, limit_dict) + for peering in active_vpc_peerings: - metrics.write_data_to_metric( - config, project, peering['active_peerings'], - metrics_dict["metrics_per_network"]["vpc_peering_active_per_network"] - ["usage"]["name"], peering['network_name']) - metrics.write_data_to_metric( - config, project, peering['network_limit'], - metrics_dict["metrics_per_network"]["vpc_peering_active_per_network"] - ["limit"]["name"], peering['network_name']) - metrics.write_data_to_metric( - config, project, - peering['active_peerings'] / peering['network_limit'], - metrics_dict["metrics_per_network"]["vpc_peering_active_per_network"] - ["utilization"]["name"], peering['network_name']) - print("Wrote number of active VPC peerings to custom metric for project:", - project) + metric_labels = { + 'project': project, + 'network_name': peering['network_name'] + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] + ["vpc_peering_active_per_network"]["usage"]["name"], + peering['active_peerings'], metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] + ["vpc_peering_active_per_network"]["limit"]["name"], + peering['network_limit'], metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] + ["vpc_peering_active_per_network"]["utilization"]["name"], + peering['active_peerings'] / peering['network_limit'], metric_labels, + timestamp=timestamp) + print( + "Buffered number of active VPC peerings to custom metric for project:", + project) for peering in vpc_peerings: - metrics.write_data_to_metric( - config, project, peering['peerings'], - metrics_dict["metrics_per_network"]["vpc_peering_per_network"] - ["usage"]["name"], peering['network_name']) - metrics.write_data_to_metric( - config, project, peering['network_limit'], - metrics_dict["metrics_per_network"]["vpc_peering_per_network"] - ["limit"]["name"], peering['network_name']) - metrics.write_data_to_metric( - config, project, peering['peerings'] / peering['network_limit'], - metrics_dict["metrics_per_network"]["vpc_peering_per_network"] - ["utilization"]["name"], peering['network_name']) - print("Wrote number of VPC peerings to custom metric for project:", project) + metric_labels = { + 'project': project, + 'network_name': peering['network_name'] + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"]["vpc_peering_per_network"] + ["usage"]["name"], peering['peerings'], metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"]["vpc_peering_per_network"] + ["limit"]["name"], peering['network_limit'], metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"]["vpc_peering_per_network"] + ["utilization"]["name"], + peering['peerings'] / peering['network_limit'], metric_labels, + timestamp=timestamp) + print("Buffered number of VPC peerings to custom metric for project:", + project) def gather_peering_data(config, project_id): diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py index fbe9ff3b..8bca3496 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py @@ -14,6 +14,8 @@ # limitations under the License. # +import time + from collections import defaultdict from . import metrics, networks, limits, peerings, routers @@ -88,16 +90,17 @@ def get_dynamic_routes(config, metrics_dict, limits_dict): routers_dict = routers.get_routers(config) dynamic_routes_dict = defaultdict(int) - for project_id in config["monitored_projects"]: - network_dict = networks.get_networks(config, project_id) + timestamp = time.time() + for project in config["monitored_projects"]: + network_dict = networks.get_networks(config, project) - for network in network_dict: - sum_routes = get_routes_for_network(config, network['self_link'], - project_id, routers_dict) - dynamic_routes_dict[network['self_link']] = sum_routes + for net in network_dict: + sum_routes = get_routes_for_network(config, net['self_link'], project, + routers_dict) + dynamic_routes_dict[net['self_link']] = sum_routes - if network['self_link'] in limits_dict: - limit = limits_dict[network['self_link']] + if net['self_link'] in limits_dict: + limit = limits_dict[net['self_link']] else: if 'default_value' in limits_dict: limit = limits_dict['default_value'] @@ -106,21 +109,21 @@ def get_dynamic_routes(config, metrics_dict, limits_dict): break utilization = sum_routes / limit + metric_labels = {'project': project, 'network_name': net['network_name']} + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] + ["dynamic_routes_per_network"]["usage"]["name"], sum_routes, + metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] + ["dynamic_routes_per_network"]["limit"]["name"], limit, metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_network"] + ["dynamic_routes_per_network"]["utilization"]["name"], utilization, + metric_labels, timestamp=timestamp) - metrics.write_data_to_metric( - config, project_id, sum_routes, metrics_dict["metrics_per_network"] - ["dynamic_routes_per_network"]["usage"]["name"], - network['network_name']) - metrics.write_data_to_metric( - config, project_id, limit, metrics_dict["metrics_per_network"] - ["dynamic_routes_per_network"]["limit"]["name"], - network['network_name']) - metrics.write_data_to_metric( - config, project_id, utilization, metrics_dict["metrics_per_network"] - ["dynamic_routes_per_network"]["utilization"]["name"], - network['network_name']) - - print("Wrote metrics for dynamic routes for VPCs in project", project_id) + print("Buffered metrics for dynamic routes for VPCs in project", project) return dynamic_routes_dict diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py index 8b173dc8..fc5c0490 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py @@ -14,6 +14,8 @@ # limitations under the License. # +import time + from . import metrics from google.protobuf import field_mask_pb2 from google.protobuf.json_format import MessageToDict @@ -225,6 +227,7 @@ def get_subnets(config, metrics_dict): # Updates all_subnets_dict with the IP utilization info compute_subnet_utilization(config, all_subnets_dict) + timestamp = time.time() for project_id in config["monitored_projects"]: if project_id not in all_subnets_dict: continue @@ -236,18 +239,23 @@ def get_subnets(config, metrics_dict): # Building unique identifier with subnet region/name subnet_id = f"{subnet_dict['region']}/{subnet_dict['name']}" - metrics.write_data_to_metric( - config, project_id, subnet_dict['used_ip_addresses'], - metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"]["usage"] - ["name"], subnet_dict['network_name'], subnet_id) - metrics.write_data_to_metric( - config, project_id, subnet_dict['total_ip_addresses'], - metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"]["limit"] - ["name"], subnet_dict['network_name'], subnet_id) - metrics.write_data_to_metric( - config, project_id, ip_utilization, metrics_dict["metrics_per_subnet"] - ["ip_usage_per_subnet"]["utilization"]["name"], - subnet_dict['network_name'], subnet_id) + metric_labels = { + 'project': project_id, + 'network_name': subnet_dict['network_name'], + 'subnet_id': subnet_id + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"] + ["usage"]["name"], subnet_dict['used_ip_addresses'], metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"] + ["limit"]["name"], subnet_dict['total_ip_addresses'], metric_labels, + timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"] + ["utilization"]["name"], ip_utilization, metric_labels, + timestamp=timestamp) - print("Wrote metrics for subnet ip utilization for VPCs in project", + print("Buffered metrics for subnet ip utilization for VPCs in project", project_id) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py index 8d15eacd..e7abade5 100644 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py +++ b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py @@ -15,11 +15,13 @@ # import re +import time + from collections import defaultdict from pydoc import doc from collections import defaultdict from google.protobuf import field_mask_pb2 -from . import metrics, networks, limits, peerings, routers +from . import metrics, networks, limits def get_firewalls_dict(config: dict): @@ -69,43 +71,51 @@ def get_firewalls_data(config, metrics_dict, project_quotas_dict, Parameters: config (dict): The dict containing config like clients and limits metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions. - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value. - firewalls_dict (dictionary of dictionary): Keys are projects, subkeys are networks, values count #of VPC Firewall Rules + project_quotas_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value. + firewalls_dict (dictionary of of dictionary of string: string): Keys are projects, subkeys are networks, values count #of VPC Firewall Rules Returns: None ''' - for project in config["monitored_projects"]: - current_quota_limit = project_quotas_dict[project]['global']["firewalls"] + timestamp = time.time() + for project_id in config["monitored_projects"]: + + current_quota_limit = project_quotas_dict[project_id]['global']["firewalls"] if current_quota_limit is None: print( - f"Could not write VPC firewal rules to metric for projects/{project} due to missing quotas" + f"Could not determine VPC firewal rules to metric for projects/{project_id} due to missing quotas" ) continue - network_dict = networks.get_networks(config, project) + network_dict = networks.get_networks(config, project_id) project_usage = 0 for net in network_dict: usage = 0 - if project in firewalls_dict and net['network_name'] in firewalls_dict[ - project]: - usage = firewalls_dict[project][net['network_name']] + if project_id in firewalls_dict and net['network_name'] in firewalls_dict[ + project_id]: + usage = firewalls_dict[project_id][net['network_name']] project_usage += usage - metrics.write_data_to_metric( - config, project, usage, + metric_labels = { + 'project': project_id, + 'network_name': net['network_name'] + } + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_project"][f"firewalls"]["usage"]["name"], - net['network_name']) + usage, metric_labels, timestamp=timestamp) + metric_labels = {'project': project_id} # firewall quotas are per project, not per single VPC - metrics.write_data_to_metric( - config, project, current_quota_limit['limit'], - metrics_dict["metrics_per_project"][f"firewalls"]["limit"]["name"]) - metrics.write_data_to_metric( - config, project, project_usage / current_quota_limit['limit'] - if current_quota_limit['limit'] != 0 else 0, - metrics_dict["metrics_per_project"][f"firewalls"]["utilization"] - ["name"]) - + metrics.append_data_to_series_buffer( + config, + metrics_dict["metrics_per_project"][f"firewalls"]["limit"]["name"], + current_quota_limit['limit'], metric_labels, timestamp=timestamp) + metrics.append_data_to_series_buffer( + config, metrics_dict["metrics_per_project"][f"firewalls"]["utilization"] + ["name"], project_usage / current_quota_limit['limit'] + if current_quota_limit['limit'] != 0 else 0, metric_labels, + timestamp=timestamp) print( - f"Wrote number of VPC Firewall Rules to metric for projects/{project}") + f"Buffered number of VPC Firewall Rules to metric for projects/{project_id}" + ) diff --git a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json index 923dbc84..0e71bb7f 100644 --- a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json +++ b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json @@ -1,6 +1,6 @@ { "category": "CUSTOM", - "displayName": "quotas_utilization", + "displayName": "quotas_utilization_updated", "mosaicLayout": { "columns": 12, "tiles": [ @@ -22,11 +22,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "1800s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -62,11 +64,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -102,11 +106,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/number_of_instances_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -142,11 +148,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/number_of_vpc_peerings_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -182,11 +190,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/number_of_active_vpc_peerings_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_INTERPOLATE" } } @@ -222,11 +232,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/number_of_subnet_IP_ranges_ppg_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -262,11 +274,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_ppg_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -302,11 +316,13 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_ppg_utilization\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" } } @@ -342,6 +358,7 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, "filter": "metric.type=\"custom.googleapis.com/number_of_instances_ppg_utilization\" resource.type=\"global\"" @@ -378,6 +395,7 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" }, "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_network_utilization\" resource.type=\"global\"" @@ -393,8 +411,8 @@ } }, "width": 6, - "xPos": 6, - "yPos": 16 + "xPos": 0, + "yPos": 20 }, { "height": 4, @@ -420,16 +438,11 @@ ], "perSeriesAligner": "ALIGN_MEAN" }, - "filter": "metric.type=\"custom.googleapis.com/firewalls_per_project_vpc_usage\" resource.type=\"global\"", - "secondaryAggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_NONE" - } + "filter": "metric.type=\"custom.googleapis.com/firewalls_per_project_vpc_usage\" resource.type=\"global\"" } } } ], - "thresholds": [], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", @@ -439,7 +452,7 @@ }, "width": 6, "xPos": 0, - "yPos": 20 + "yPos": 24 }, { "height": 4, @@ -465,16 +478,11 @@ ], "perSeriesAligner": "ALIGN_MAX" }, - "filter": "metric.type=\"custom.googleapis.com/firewalls_per_project_utilization\" resource.type=\"global\"", - "secondaryAggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_NONE" - } + "filter": "metric.type=\"custom.googleapis.com/firewalls_per_project_utilization\" resource.type=\"global\"" } } } ], - "thresholds": [], "timeshiftDuration": "0s", "yAxis": { "label": "y1Axis", @@ -484,7 +492,44 @@ }, "width": 6, "xPos": 6, - "yPos": 20 + "yPos": 24 + }, + { + "height": 4, + "widget": { + "title": "tuples_per_firewall_policy_utilization", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "apiSource": "DEFAULT_CLOUD", + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"custom.googleapis.com/firewall_policy_tuples_per_policy_utilization\" resource.type=\"global\"" + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 0, + "yPos": 28 }, { "height": 4, @@ -504,6 +549,7 @@ "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", "perSeriesAligner": "ALIGN_MEAN" }, "filter": "metric.type=\"custom.googleapis.com/ip_addresses_per_subnet_utilization\" resource.type=\"global\"" @@ -519,8 +565,45 @@ } }, "width": 6, - "xPos": 0, - "yPos": 24 + "xPos": 6, + "yPos": 16 + }, + { + "height": 4, + "widget": { + "title": "dynamic_routes_ppg_utilization", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "apiSource": "DEFAULT_CLOUD", + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_NONE", + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_peering_group_utilization\" resource.type=\"global\"" + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 20 } ] }