From 9d3f5aff550d4def4f4c5efb7d0c040386a46f68 Mon Sep 17 00:00:00 2001 From: Long Zhang Date: Wed, 10 Jul 2024 14:30:48 +0200 Subject: [PATCH 1/3] support multiple aggregations use -a or --aggregation multiple times for multiple dimensions, the exporter will expose the dimensions as different labels --- app/exporter.py | 32 ++++++++++++++++++------------ main.py | 52 +++++++++++++++++++++++++++++++++---------------- package.json | 2 +- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/app/exporter.py b/app/exporter.py index 78bbcd7..3271f49 100644 --- a/app/exporter.py +++ b/app/exporter.py @@ -15,11 +15,12 @@ def __init__(self, endpoint, aggregate, interval, name, extra_labels): self.interval = interval self.name = name self.extra_labels = extra_labels - self.labels = set([aggregate]) + self.labels = set(aggregate) if extra_labels is not None: self.labels.update(extra_labels.keys()) self.kubernetes_daily_cost_usd = Gauge( - self.name, "Kubernetes daily cost in USD aggregated by %s" % self.aggregate, self.labels) + self.name, "Kubernetes daily cost in USD aggregated by %s" % ", ".join(self.aggregate), self.labels + ) def run_metrics_loop(self): while True: @@ -29,18 +30,25 @@ def run_metrics_loop(self): time.sleep(self.interval) def fetch(self): - api = (f"/allocation/view?aggregate={self.aggregate}&window=today&shareIdle=true&idle=true&" - "idleByNode=false&shareTenancyCosts=true&shareNamespaces=&shareCost=NaN&" - "shareSplit=weighted&chartType=costovertime&costUnit=cumulative&step=") + api = ( + f"/allocation/view?aggregate={','.join(self.aggregate)}&window=today&shareIdle=true&idle=true&" + "idleByNode=false&shareTenancyCosts=true&shareNamespaces=&shareCost=NaN&" + "shareSplit=weighted&chartType=costovertime&costUnit=cumulative&step=" + ) response = requests.get(self.endpoint + api) - if (response.status_code != 200 or response.json().get("code") != 200): - logging.error("error while fetching data from %s, status code %s, message %s!" % ( - api, response.status_code, response.text)) + if response.status_code != 200 or response.json().get("code") != 200: + logging.error( + "error while fetching data from %s, status code %s, message %s!" + % (api, response.status_code, response.text) + ) items = response.json()["data"]["items"]["items"] for item in items: + aggregation_labels = {} + names = item["name"].split("/") + for i in range(len(self.aggregate)): + aggregation_labels[self.aggregate[i]] = names[i] + if self.extra_labels: - self.kubernetes_daily_cost_usd.labels( - **{self.aggregate: item["name"]}, **self.extra_labels).set(item["totalCost"]) + self.kubernetes_daily_cost_usd.labels(**aggregation_labels, **self.extra_labels).set(item["totalCost"]) else: - self.kubernetes_daily_cost_usd.labels( - **{self.aggregate: item["name"]}).set(item["totalCost"]) + self.kubernetes_daily_cost_usd.labels(**aggregation_labels).set(item["totalCost"]) diff --git a/main.py b/main.py index 9178fda..629f5fb 100644 --- a/main.py +++ b/main.py @@ -9,8 +9,7 @@ class key_value_arg(argparse.Action): - def __call__(self, parser, namespace, - values, option_string=None): + def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, dict()) for kvpair in values: @@ -22,32 +21,51 @@ def __call__(self, parser, namespace, def get_args(): parser = argparse.ArgumentParser( - description="Kubernetes Cost Exporter, exposing Kubernetes cost data as Prometheus metrics.") - parser.add_argument("-p", "--port", default=9090, type=int, - help="Exporter's port (default: 9090)") - parser.add_argument("-i", "--interval", default=60, type=int, - help="Update interval in seconds (default: 60)") - parser.add_argument("-n", "--name", default="kubernetes_daily_cost_usd", - help="Name of the exposed metric (default: kubernetes_daily_cost_usd)") - parser.add_argument("-e", "--endpoint", default="http://kubecost-cost-analyzer.monitoring.svc:9003", - help="Kubecost service endpoint (default: http://kubecost-cost-analyzer.monitoring.svc:9003)") - parser.add_argument("-a", "--aggregate", default="namespace", - help="Aggregation level, e.g., namespace, cluster") - parser.add_argument("-l", "--label", nargs="*", action=key_value_arg, - help="Additional labels that need to be added to the metric, \ - may be used several times, e.g., --label project=foo environment=dev") + description="Kubernetes Cost Exporter, exposing Kubernetes cost data as Prometheus metrics." + ) + parser.add_argument("-p", "--port", default=9090, type=int, help="Exporter's port (default: 9090)") + parser.add_argument("-i", "--interval", default=60, type=int, help="Update interval in seconds (default: 60)") + parser.add_argument( + "-n", + "--name", + default="kubernetes_daily_cost_usd", + help="Name of the exposed metric (default: kubernetes_daily_cost_usd)", + ) + parser.add_argument( + "-e", + "--endpoint", + default="http://kubecost-cost-analyzer.monitoring.svc:9003", + help="Kubecost service endpoint (default: http://kubecost-cost-analyzer.monitoring.svc:9003)", + ) + parser.add_argument( + "-a", + "--aggregate", + action="append", + help="Aggregation level (default: namespace), e.g., cluster, namespace, deployment, pod, etc. Multiple options are supported, e.g., -a namespace -a deployment", + ) + parser.add_argument( + "-l", + "--label", + nargs="*", + action=key_value_arg, + help="Additional labels that need to be added to the metric, \ + may be used several times, e.g., --label project=foo environment=dev", + ) args = parser.parse_args() return args def main(args): + if args.aggregate is None: + args.aggregate = ["namespace"] + app_metrics = MetricExporter( endpoint=args.endpoint, aggregate=args.aggregate, interval=args.interval, name=args.name, - extra_labels=args.label + extra_labels=args.label, ) start_http_server(args.port) app_metrics.run_metrics_loop() diff --git a/package.json b/package.json index 08d1de6..e2caca3 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ { "name": "kubernetes-cost-exporter", - "version": "v1.0.3" + "version": "v1.0.4" } From 2e196054281b30bf08f6e099ea44268f410bbd75 Mon Sep 17 00:00:00 2001 From: Long Zhang Date: Wed, 10 Jul 2024 14:32:31 +0200 Subject: [PATCH 2/3] improve error-handling and logging --- app/exporter.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/exporter.py b/app/exporter.py index 3271f49..c9cf400 100644 --- a/app/exporter.py +++ b/app/exporter.py @@ -41,14 +41,16 @@ def fetch(self): "error while fetching data from %s, status code %s, message %s!" % (api, response.status_code, response.text) ) - items = response.json()["data"]["items"]["items"] - for item in items: - aggregation_labels = {} - names = item["name"].split("/") - for i in range(len(self.aggregate)): - aggregation_labels[self.aggregate[i]] = names[i] + else: + items = response.json()["data"]["items"]["items"] + for item in items: + aggregation_labels = {} + names = item["name"].split("/") + for i in range(len(self.aggregate)): + aggregation_labels[self.aggregate[i]] = names[i] - if self.extra_labels: - self.kubernetes_daily_cost_usd.labels(**aggregation_labels, **self.extra_labels).set(item["totalCost"]) - else: - self.kubernetes_daily_cost_usd.labels(**aggregation_labels).set(item["totalCost"]) + if self.extra_labels: + self.kubernetes_daily_cost_usd.labels(**aggregation_labels, **self.extra_labels).set(item["totalCost"]) + else: + self.kubernetes_daily_cost_usd.labels(**aggregation_labels).set(item["totalCost"]) + logging.info(f"cost metric {self.name} updated successfully") From f46631d9535be89862760f950b3063d6a7b9d9c6 Mon Sep 17 00:00:00 2001 From: Long Zhang Date: Wed, 10 Jul 2024 14:41:56 +0200 Subject: [PATCH 3/3] doc: mention multi-dimension aggregations in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a03d320..8d89fa6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ kubernetes_daily_cost_usd{namespace="kube-system"} 1.1914006049581642 ... ``` -*ps: As the metric name indicate, the metric shows the daily costs in USD. `Daily` is based a fixed 24h time window, from UTC 00:00 to UTC 24:00. `namespace` is a label based on `--aggregate` option. Users can also add custom labels using the `--label` option.* +*ps: As the metric name indicate, the metric shows the daily costs in USD. `Daily` is based a fixed 24h time window, from UTC 00:00 to UTC 24:00. `namespace` is a label based on `--aggregate` option, which can be used several times for multiple aggregations. Users can also add custom labels using the `--label` option.* ## How Does This Work @@ -45,4 +45,4 @@ The Kubernetes Cost Exporter needs to be deployed to the same cluster where the ``` kubectl create --namespace -f ./deployment/k8s/deployment.yaml -``` \ No newline at end of file +```