
Dan Watts
DevRel Engineer, SquaredUp
DevRel Engineer, SquaredUp
Oracle Cloud Infrastructure (OCI) provides powerful tools for managing your cloud resources, but getting a clear, real-time view of your usage and costs can sometimes feel locked away behind complex reports. What if you could build beautiful, shareable dashboards that show you exactly what you're spending, where you're spending it, and how it trends over time?
In this guide, we'll walk you through deploying a simple, secure OCI Function that acts as a proxy to OCI's Usage API. We'll then expose it with an API Gateway and, finally, connect it to SquaredUp to create insightful cost and usage dashboards. Let's get started! 🚀
Before we dive into the "how," let's talk about the "why." Raw usage data is useful, but a well-designed dashboard transforms that data into actionable intelligence.
You might be wondering, "OCI has a REST API, so why can't I connect SquaredUp to it directly?" That's a great question, and the answer comes down to authentication and simplicity.
The native OCI REST APIs are protected by a unique and robust security protocol called OCI Signature Version 1. To authenticate a request, you must generate a cryptographic signature using a private RSA key and include it in the Authorization
header. This process is great for security but is too complex to be handled by a standard Web API connection in a dashboarding tool.
This is where our OCI Function comes in. It acts as a secure and intelligent proxy that solves three key problems:
In short, the function acts as a secure translator, converting a complex, signature-based API into a simple, web-friendly one.
Our solution is simple and leverages serverless components within OCI, making it incredibly cost-effective.
For the deployment steps, we'll use the OCI Cloud Shell, which comes pre-loaded with all the tools you need. You can also use the OCI Code Editor to create the files and its built-in terminal to run the commands.
(Note: If you have the OCI CLI and fn
CLI configured on your local machine, you can also perform these steps there.)
First, we need to give our function permission to call the Usage API.
1. Create a Dynamic Group: This group will automatically contain our function once it's deployed.
UsageFunctionDynamicGroup
.<your-compartment-ocid>
with the OCID of the compartment where you will deploy your function.ALL {resource.type = 'fnfunc', resource.compartment.id = '<your-compartment-ocid>'}
2. Create an IAM Policy: Now, we'll create a policy to grant the members of this group the necessary permissions.
UsageFunctionPolicy
.<YourDynamicGroupName>
with the name you used above (e.g., UsageFunctionDynamicGroup
). This policy allows the function to read usage data for the entire tenancy.Allow dynamic-group <YourDynamicGroupName> to read usage-data in tenancy
Now let's get the function code in place and deploy it using Cloud Shell.
>_
icon in the top header.mkdir oci-usage-proxy &&cd oci-usage-proxy
nano
or vi
to create the files. For example, run nano func.py
, paste the code, and press Ctrl+X
to save and exit.File 1: func.py
# func.py - Python OCI Usage API Proxy
import io
import json
import logging
from datetime import datetime
from fdk import response
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Try to import OCI, but don't fail if not available
try:
import oci
OCI_AVAILABLE = True
logger.info("OCI SDK loaded successfully")
except ImportError:
OCI_AVAILABLE = False
logger.warning("OCI SDK not available")
def get_usage_client():
"""Initialize OCI Usage client with Resource Principal auth"""
if not OCI_AVAILABLE:
return None
try:
signer = oci.auth.signers.get_resource_principals_signer()
client = oci.usage_api.UsageapiClient(config={}, signer=signer)
logger.info("OCI Usage client initialized")
return client
except Exception as e:
logger.error(f"Failed to initialize OCI client: {str(e)}")
return None
def handler(ctx, data: io.BytesIO = None):
"""Main function handler"""
try:
# Import urllib for URL decoding
from urllib.parse import unquote
# Get the raw request URL from context if available
request_url = ctx.RequestURL() if hasattr(ctx, 'RequestURL') else ""
# Parse query parameters from URL if present
query_params = {}
if '?' in request_url:
query_string = request_url.split('?')[1]
for param in query_string.split('&'):
if '=' in param:
key, value = param.split('=', 1)
# URL decode the values
query_params[key] = unquote(value)
# Parse body
body = {}
if data:
data_str = data.getvalue()
if data_str:
try:
body = json.loads(data_str)
except json.JSONDecodeError:
logger.warning("Could not parse body as JSON")
# Merge query params with body (query params take precedence for API Gateway calls)
params = {**body, **query_params}
# Get endpoint
endpoint = params.get('endpoint', 'unknown')
logger.info(f"Endpoint: {endpoint}")
logger.info(f"Request URL: {request_url}")
logger.info(f"Query params: {query_params}")
logger.info(f"Body: {body}")
logger.info(f"Final params: {params}")
# Health check
if endpoint == 'health':
# Try to get resource principal info
rp_info = {}
if OCI_AVAILABLE:
try:
signer = oci.auth.signers.get_resource_principals_signer()
rp_info = {
"resource_principal": "available",
"tenancy_id": signer.tenancy_id,
"region": signer.region
}
except Exception as e:
rp_info = {
"resource_principal": "error",
"error": str(e)
}
return response.Response(
ctx,
response_data=json.dumps({
"status": "healthy",
"service": "oci-usage-proxy-python",
"timestamp": datetime.now().isoformat(),
"oci_sdk_available": OCI_AVAILABLE,
"resource_principal_info": rp_info
}),
headers={"Content-Type": "application/json"}
)
# Check if OCI SDK is available
if not OCI_AVAILABLE:
return response.Response(
ctx,
response_data=json.dumps({
"error": "OCI SDK not available",
"message": "Install oci package to use this endpoint"
}),
headers={"Content-Type": "application/json"},
status_code=503
)
# Initialize OCI client
client = get_usage_client()
if not client:
return response.Response(
ctx,
response_data=json.dumps({
"error": "Failed to initialize OCI client",
"hint": "Check Dynamic Group and IAM policies"
}),
headers={"Content-Type": "application/json"},
status_code=503
)
# Handle usage summary
if endpoint == 'summary':
# Get tenant ID - REQUIRED parameter
tenant_id = params.get('tenantId', '')
if not tenant_id:
# Try to get tenant ID from signer if not provided
try:
signer = oci.auth.signers.get_resource_principals_signer()
tenant_id = signer.tenancy_id
logger.info(f"Using tenant ID from resource principal: {tenant_id}")
except:
return response.Response(
ctx,
response_data=json.dumps({
"error": "Missing required parameter: tenantId",
"hint": "Provide tenantId in the request parameters"
}),
headers={"Content-Type": "application/json"},
status_code=400
)
time_start = params.get('timeUsageStarted', '')
time_end = params.get('timeUsageEnded', '')
request_details = oci.usage_api.models.RequestSummarizedUsagesDetails(
tenant_id=tenant_id,
time_usage_started=datetime.fromisoformat(time_start.replace('Z', '+00:00')),
time_usage_ended=datetime.fromisoformat(time_end.replace('Z', '+00:00')),
granularity=params.get('granularity', 'DAILY'),
compartment_depth=2
)
result = client.request_summarized_usages(request_details)
# Clean up the response data
items = []
for item in result.data.items:
items.append({
"date": str(item.time_usage_started),
"service": item.service or "All Services",
"compartment": item.compartment_name or "Root",
"amount": item.computed_amount or 0,
"quantity": item.computed_quantity or 0,
"currency": item.currency or "GBP",
"unit": item.unit
})
return response.Response(
ctx,
response_data=json.dumps({
"items": items,
"itemCount": len(items),
"timeRange": {
"start": time_start,
"end": time_end
}
}),
headers={"Content-Type": "application/json"}
)
# Handle costs
if endpoint == 'costs':
# Get tenant ID - REQUIRED parameter
tenant_id = params.get('tenantId', '')
if not tenant_id:
# Try to get tenant ID from signer if not provided
try:
signer = oci.auth.signers.get_resource_principals_signer()
tenant_id = signer.tenancy_id
logger.info(f"Using tenant ID from resource principal: {tenant_id}")
except:
return response.Response(
ctx,
response_data=json.dumps({
"error": "Missing required parameter: tenantId",
"hint": "Provide tenantId in the request parameters"
}),
headers={"Content-Type": "application/json"},
status_code=400
)
time_start = params.get('timeUsageStarted', '')
time_end = params.get('timeUsageEnded', '')
request_details = oci.usage_api.models.RequestSummarizedUsagesDetails(
tenant_id=tenant_id,
time_usage_started=datetime.fromisoformat(time_start.replace('Z', '+00:00')),
time_usage_ended=datetime.fromisoformat(time_end.replace('Z', '+00:00')),
granularity='DAILY',
query_type='COST',
group_by=[params.get('groupBy', 'service').lower()], # Ensure lowercase
compartment_depth=2
)
result = client.request_summarized_usages(request_details)
# Clean up the response data
costs = []
total = 0.0
for item in result.data.items:
cost_amount = item.computed_amount or 0
total += cost_amount
costs.append({
"date": str(item.time_usage_started),
"service": item.service or "Unknown",
"cost": cost_amount,
"quantity": item.computed_quantity or 0,
"currency": item.currency or "GBP",
"unit": item.unit
})
return response.Response(
ctx,
response_data=json.dumps({
"costs": costs,
"total": round(total, 2),
"currency": costs[0]["currency"] if costs else "GBP",
"itemCount": len(costs)
}),
headers={"Content-Type": "application/json"}
)
# Unknown endpoint
return response.Response(
ctx,
response_data=json.dumps({
"error": f"Unknown endpoint: {endpoint}",
"available": ["health", "summary", "costs"]
}),
headers={"Content-Type": "application/json"},
status_code=404
)
except Exception as e:
logger.error(f"Function error: {str(e)}")
return response.Response(
ctx,
response_data=json.dumps({
"error": "Internal server error",
"details": str(e)
}),
headers={"Content-Type": "application/json"},
status_code=500
)
File 2: func.yaml
schema_version: 20180708
name: oci-usage-proxy-python
version: 0.0.1
runtime: python
build_image: fnproject/python:3.11-dev
run_image: fnproject/python:3.11
entrypoint: /python/bin/fdk /function/func.py handler
memory: 256
timeout: 30
File 3: requirements.txt
fdk>=0.1.0
oci>=2.100.0
4. Deploy the Function. The fn
CLI in Cloud Shell is already authenticated. Run the deploy command, making sure to replace <your-app-name>
with the name of the OCI Functions application you want to deploy to (this must exist in the same compartment you specified in the Dynamic Group).
# Deploy the function to your OCI Functions application
fn deploy --app <your-app-name>
With the function deployed, let's make it accessible from the internet.
/usage
./data
(for example)GET
and POST
.Oracle Functions
.<https://<unique-id>>.apigateway.<region>.oci.customer-oci.com/usage
.You can quickly test that everything is working by calling the health
endpoint. In your browser or using curl
in the Cloud Shell, call your API Gateway endpoint URL, appending the route and query parameter:
curl "https://<unique-id>.apigateway.<region>[.oci.customer-oci.com/usage/data?endpoint=health](https://.oci.customer-oci.com/usage/data?endpoint=health)"
With a working API endpoint, we can now add it to SquaredUp as a Web API data source. This will allow any dashboard to query our OCI function for usage and cost data.
4. Next, you need to test the connection to ensure everything is working correctly.
/usage/data
.costs
endpoint in our function.endpoint
: costs
tenantId
: Your tenancy OCID (e.g., ocid1.tenancy.oc1..aaaaaa...
).timeUsageStarted
: A valid start date in ISO 8601 format (e.g., 2025-09-20T00:00:00Z
).timeUsageEnded
: A valid end date in ISO 8601 format (e.g., 2025-09-21T00:00:00Z
).groupBy
: service
.5. Click Update. If the test is successful, you’re ready to start building dashboards!
Now for the fun part! Let’s pull this data into a SquaredUp dashboard.
/usage/data
.Data (POST body): Add the following JSON. This payload calls the costs
endpoint and will automatically use the dashboard’s time frame picker.
{
"endpoint": "costs",
"timeUsageStarted": "{{timeframe.start}}",
"timeUsageEnded": "{{timeframe.end}}",
"groupBy": "service"
}
4. Customize the Visualization:
costs
).date
property and the Y-axis to the cost
property, split by the service
property.That’s it! You now have a dynamic, auto-refreshing dashboard tile showing your OCI costs directly from the source. You can create multiple tiles to show usage summaries, costs by compartment, and more – all by changing the JSON payload sent to your function.