-
Notifications
You must be signed in to change notification settings - Fork 3
Added integration sample for CloudZero and OpsLevel #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tozach
wants to merge
3
commits into
main
Choose a base branch
from
tozach-cloudzero
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# CloudZero to OpsLevel Cost Integration | ||
|
||
This repository contains a sample Python script to integrate cost data from CloudZero into your OpsLevel service catalog. By leveraging the CloudZero and OpsLevel APIs, this integration provides visibility into infrastructure cloud costs associated with services in your catalog. | ||
|
||
## How it Works | ||
|
||
The integration follows these steps: | ||
1. **Fetch Cost Data:** The script calls the CloudZero Billing Costs API (`/v2/billing/costs`) to retrieve cost data for a specified date range. | ||
2. **Group by Tag:** It groups the cost data using a specific tag, such as "Tag:Name", which helps associate infrastructure resources with services in your catalog. The example script specifically ignores costs associated with "__NULL_PARTITION_VALUE__". | ||
3. **Aggregate Costs:** For each unique tag name, the script aggregates the total cost. | ||
4. **Update OpsLevel Properties:** It then calls the OpsLevel GraphQL API using the `propertyAssign` mutation to update a custom property on the corresponding service in your OpsLevel catalog. The tag name from CloudZero is used as the service alias (owner alias) in OpsLevel, and the aggregated cost is assigned as the property value. | ||
|
||
## Prerequisites | ||
|
||
To use this integration, you need: | ||
* **A CloudZero Account:** With an API key enabled. | ||
* **Integrated Cloud Infrastructure:** Your cloud infrastructure (e.g., AWS) should be integrated with CloudZero. | ||
* **Cost Allocation Tags:** **Cost allocation tags must be enabled and used on your cloud resources**. The integration leverages these tags, particularly the "Name" tag in the example, to relate infrastructure costs to services in OpsLevel. | ||
* **An OpsLevel Account:** With an API token. | ||
* **OpsLevel Custom Property:** A custom property definition must be created in OpsLevel to hold the cost data, for example, a JSON property named "aws_cost". | ||
* **Python Environment:** A Python environment with the `requests` library installed. | ||
|
||
## Setup | ||
|
||
1. **Configure Environment Variables:** | ||
* Set the `CLOUDZERO_API_KEY` environment variable with your CloudZero API key. | ||
* Set the `OPSLEVEL_API_TOKEN` environment variable with your OpsLevel API token. | ||
* If these variables are not set, the script will print an error and exit. | ||
|
||
2. **Define OpsLevel Custom Property:** | ||
* Ensure you have a custom property defined in OpsLevel. The example script uses the alias `"aws_cost"`. This property will store the cost value for each service. The property value is stored as a JSON string in OpsLevel. | ||
|
||
3. **Configure Script Parameters:** | ||
* Adjust the config dictionary within the main() function to match your requirements: | ||
|
||
``` | ||
config = { | ||
'opslevel_api_url': 'https://api.opslevel.com/graphql', # Your OpsLevel GraphQL API URL | ||
'opslevel_token_env_var': "OPSLEVEL_API_TOKEN", # Environment variable for OpsLevel API token | ||
'opslevel_definition_alias': "aws_cost", # Alias of the OpsLevel property definition (e.g., "aws_cost") | ||
'cloudzero_api_key_env_var': "CLOUDZERO_API_KEY", # Environment variable for CloudZero API key | ||
'cloudzero_start_date': "2025-05-01T00:00:00Z", # Start date for CloudZero costs (ISO 8601 format) | ||
'cloudzero_end_date': "2025-05-09T00:00:00Z", # End date for CloudZero costs (ISO 8601 format) | ||
'cloudzero_granularity': "daily", # Granularity for CloudZero costs ("daily", "monthly") | ||
'cloudzero_cost_type': "real_cost" # Cost type for CloudZero costs ("real_cost", "amortized_cost") | ||
} | ||
``` | ||
|
||
## Script Usage | ||
|
||
1. **Install Dependencies:** | ||
```bash | ||
pip install requests | ||
``` | ||
2. **Run the Script:** | ||
```bash | ||
python get_cloudzero_billing_costs.py | ||
``` | ||
|
||
The script will fetch data from CloudZero, process it, and attempt to update the specified custom property on services in OpsLevel whose aliases match the "Tag:Name" value from CloudZero. | ||
|
||
The script includes error handling for API calls and JSON decoding. It will print status updates and results. | ||
|
||
## Visualizing Costs in OpsLevel | ||
|
||
Once the script has populated the custom property, you can view the cost information within OpsLevel: | ||
|
||
* **Service Details Page:** The cost will appear as a custom property on the individual service's page. | ||
* **Team Dashboard Widget:** You can add a custom widget to a team's dashboard to display a breakdown of costs for services owned by that team. This widget uses a GraphQL query to fetch the custom property value for all services within the team. The example shows this as a pie chart widget providing a cost breakdown by service. | ||
|
||
## Extensibility | ||
|
||
While this example specifically integrates with CloudZero and uses "Tag:Name" for grouping, the approach of fetching cost data and pushing it to OpsLevel custom properties can be extended. You could use other data sources like the AWS Cost Explorer APIs or APIs from different infrastructure cost management tools. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import requests | ||
import json | ||
import os # Import the os module | ||
|
||
def update_property(api_url, opslevel_token, alias, definition_alias, value): | ||
""" | ||
Calls the update_property mutation. | ||
|
||
Args: | ||
api_url: The URL of the GraphQL API. | ||
opslevel_token: The OpsLevel API token. | ||
alias: The alias of the owner. | ||
definition_alias: The alias of the property definition. | ||
value: The new value for the property. | ||
|
||
Returns: | ||
The JSON response from the mutation, or None if an error occurs. | ||
""" | ||
|
||
mutation = """ | ||
mutation update_property($alias:String, $definition_alias:String, $value:JsonString!){ | ||
propertyAssign(input: {owner: {alias: $alias}, definition: {alias: $definition_alias}, | ||
value: $value, runValidation: false}) { | ||
property{ | ||
value | ||
owner{ | ||
...on Service{ | ||
name | ||
} | ||
} | ||
} | ||
errors{ | ||
message | ||
path | ||
} | ||
} | ||
} | ||
""" | ||
|
||
variables = { | ||
"alias": alias, | ||
"definition_alias": definition_alias, | ||
"value": json.dumps(value) # Important: Convert value to JSON string | ||
} | ||
|
||
headers = { | ||
"Authorization": f"Bearer {opslevel_token}", | ||
"Content-Type": "application/json", | ||
} | ||
|
||
payload = { | ||
"query": mutation, | ||
"variables": json.dumps(variables) | ||
} | ||
|
||
try: | ||
response = requests.post(api_url, json=payload, headers=headers) | ||
response.raise_for_status() | ||
return response.json() | ||
except requests.exceptions.RequestException as e: | ||
print(f"Error calling update_property mutation: {e}") | ||
return None | ||
except json.JSONDecodeError as e: | ||
print(f"Error decoding JSON response: {e}. Response text: {response.text}") | ||
return None | ||
|
||
def get_cloudzero_billing_costs(start_date, end_date, api_key, granularity="daily", cost_type="real_cost"): | ||
""" | ||
Calls the CloudZero Billing Costs API to retrieve cost data. | ||
|
||
Args: | ||
start_date (str): The start date for the cost data in ISO 8601 format (e.g., "2025-01-01T00:00:00Z"). | ||
end_date (str): The end date for the cost data in ISO 8601 format (e.g., "2025-01-31T23:59:59Z"). | ||
api_key (str): The CloudZero API key. | ||
granularity (str, optional): The granularity of the cost data (e.g., "daily", "monthly"). Defaults to "daily". | ||
cost_type (str, optional): The type of cost to retrieve (e.g., "real_cost", "amortized_cost"). Defaults to "real_cost". | ||
|
||
Returns: | ||
dict: The JSON response from the API if the request is successful, None otherwise. | ||
""" | ||
if not api_key: | ||
print("Error: CloudZero API key is missing.") | ||
return None | ||
|
||
url = "https://api.cloudzero.com/v2/billing/costs" | ||
headers = { | ||
"accept": "application/json", | ||
"Authorization": api_key # Assuming API key is passed as a header | ||
} | ||
params = { | ||
"start_date": start_date, | ||
"end_date": end_date, | ||
"granularity": granularity, | ||
"cost_type": cost_type, | ||
"group_by": ["Tag:Name"] | ||
} | ||
|
||
try: | ||
response = requests.get(url, headers=headers, params=params) | ||
response.raise_for_status() # Raise an exception for bad status codes | ||
return response.json() | ||
except requests.exceptions.RequestException as e: | ||
print(f"Error calling CloudZero API: {e}") | ||
if response is not None: | ||
print(f"Response status code: {response.status_code}") | ||
try: | ||
print(f"Response body: {response.json()}") | ||
except: | ||
print(f"Response body: {response.text}") | ||
return None | ||
|
||
def main(): | ||
""" | ||
Main function to retrieve CloudZero billing costs, process them, | ||
and update OpsLevel properties. | ||
""" | ||
# --- Configuration Variables Block --- | ||
# Set all configurable parameters here for easy modification. | ||
config = { | ||
'opslevel_api_url': 'https://api.opslevel.com/graphql', # OpsLevel GraphQL API URL | ||
'opslevel_token_env_var': "OPSLEVEL_API_TOKEN", # Environment variable for OpsLevel API token | ||
'opslevel_definition_alias': "aws_cost", # Alias of the OpsLevel property definition | ||
'cloudzero_api_key_env_var': "CLOUDZERO_API_KEY", # Environment variable for CloudZero API key | ||
'cloudzero_start_date': "2025-05-01T00:00:00Z", # Start date for CloudZero costs | ||
'cloudzero_end_date': "2025-05-09T00:00:00Z", # End date for CloudZero costs | ||
'cloudzero_granularity': "daily", # Granularity for CloudZero costs | ||
'cloudzero_cost_type': "real_cost" # Cost type for CloudZero costs | ||
} | ||
# --- End Configuration Variables Block --- | ||
|
||
opslevel_token = os.environ.get(config['opslevel_token_env_var']) | ||
if not opslevel_token: | ||
print(f"Error: {config['opslevel_token_env_var']} environment variable not set.") | ||
return | ||
|
||
cloudzero_api_key = os.environ.get(config['cloudzero_api_key_env_var']) | ||
if not cloudzero_api_key: | ||
print(f"Error: {config['cloudzero_api_key_env_var']} environment variable not set.") | ||
return | ||
|
||
print("Retrieving CloudZero billing costs...") | ||
billing_data = get_cloudzero_billing_costs( | ||
config['cloudzero_start_date'], | ||
config['cloudzero_end_date'], | ||
cloudzero_api_key, # Pass the API key here | ||
config['cloudzero_granularity'], | ||
config['cloudzero_cost_type'] | ||
) | ||
|
||
if billing_data: | ||
print("CloudZero Billing Costs:") | ||
# Group data by tag and sum costs, ignoring "__NULL_PARTITION_VALUE__" | ||
grouped_data = {} | ||
for item in billing_data['costs']: | ||
tag_name = item.get("Tag:Name") | ||
cost = item.get("cost") | ||
if tag_name and tag_name != "__NULL_PARTITION_VALUE__" and cost is not None: | ||
if tag_name in grouped_data: | ||
grouped_data[tag_name]["cost"] += cost | ||
grouped_data[tag_name]["usage_dates"].append(item.get("usage_date")) | ||
else: | ||
grouped_data[tag_name] = {"cost": cost, "usage_dates": [item.get("usage_date")]} | ||
|
||
# Convert the grouped data to the desired JSON format | ||
result = [{"Tag_Name": tag, "Cost": data["cost"], "Usage_Dates": data["usage_dates"]} for tag, data in grouped_data.items()] | ||
|
||
print("\n--- Processed CloudZero Costs ---") | ||
print(json.dumps(result, indent=2)) | ||
|
||
print("\nUpdating OpsLevel properties...") | ||
for item in result: | ||
print(f" Updating property for service '{item['Tag_Name']}' with cost: {item['Cost']}") | ||
ol_prop_update_result = update_property( | ||
config['opslevel_api_url'], | ||
opslevel_token, | ||
item["Tag_Name"], | ||
config['opslevel_definition_alias'], | ||
item["Cost"] | ||
) | ||
if ol_prop_update_result: | ||
print(" Update Result:", json.dumps(ol_prop_update_result, indent=2)) | ||
else: | ||
print(f" Property update failed for service '{item['Tag_Name']}'.") | ||
else: | ||
print("Failed to retrieve CloudZero billing costs. No OpsLevel properties updated.") | ||
|
||
if __name__ == "__main__": | ||
main() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.