Skip to Main Content
Blog

Dashboarding OCI costs: A guide to building a usage API with OCI functions and SquaredUp

Dan Watts

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! 🚀

Why dashboard OCI usage data with SquaredUp?

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.

Why use a Function? The secure gateway to your data

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:

  1. Handles complex authentication: The function runs inside your OCI tenancy and uses a Resource Principal for authentication. This means it's granted a secure identity via IAM, allowing it to generate the necessary API signatures automatically without you ever needing to handle API keys.
  2. Provides a simple endpoint: The function is exposed via a public API Gateway. This gateway presents a simple, standard REST endpoint that SquaredUp can easily connect to without needing any complex security configurations.
  3. Transforms data: The function's Python code takes the detailed, raw response from the OCI Usage API and refines it into a clean, simple JSON format, perfect for dashboarding.

In short, the function acts as a secure translator, converting a complex, signature-based API into a simple, web-friendly one.

The solution architecture

Our solution is simple and leverages serverless components within OCI, making it incredibly cost-effective.

  1. OCI Function: A small, serverless Python application that authenticates using its own identity (a Resource Principal) to fetch data from the OCI Usage API.
  2. IAM Dynamic Group & Policy: This is the security backbone. We'll create a Dynamic Group to grant our function specific, least-privilege permissions to read usage data without needing to store any credentials in the code.
  3. API Gateway: This provides a secure, public HTTPS endpoint that will trigger our function. This is the URL that SquaredUp will connect to.
  4. SquaredUp: The visualization layer. We'll add our API Gateway endpoint as a data source and build tiles to display the data exactly how we want it.

Step-by-step deployment guide

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.)

Step 1: Configure IAM Permissions (Dynamic Group & Policy)

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.

2. Create an IAM Policy: Now, we'll create a policy to grant the members of this group the necessary permissions.

Step 2: Create and deploy the OCI Function

Now let's get the function code in place and deploy it using Cloud Shell.

  1. Launch Cloud Shell from the OCI Console by clicking the >_ icon in the top header.
  2. Create a Directory for the function's files:mkdir oci-usage-proxy &&cd oci-usage-proxy
  3. Create the Function Files. You can use a terminal editor like 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>

Step 3: Expose the Function with an API Gateway

With the function deployed, let's make it accessible from the internet.

  1. In the OCI Console, navigate to Developer Services -> API Gateway.
  2. Click Create Gateway. Fill in the details, selecting a VCN and a public subnet.
  3. Once the gateway is created, click on it and select Deployments from the left-hand menu.
  4. Click Create Deployment.
  5. On the "Basic Information" screen, give it a path prefix, like /usage.
  6. On the "Routes" screen, create your first route:
    • Path: /data (for example)
    • Methods: Select GET and POST.
    • Type: Select Oracle Functions.
    • Application: Choose the application where you deployed your function.
    • Function Name: Select your newly deployed function.
  7. Click Next and then Create to deploy your API. Once it's active, make a note of the Endpoint URL. It will look something like <https://<unique-id>>.apigateway.<region>.oci.customer-oci.com/usage.

Step 4: Test your API

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)"

Step 5: Setting up the SquaredUp Web API data source

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.

  1. In SquaredUp, navigate to the settings menu and select Data Sources.
  2. Click Add data source and choose the Web API source.
  3. On the configuration screen, fill in the following details:
    • Display name: Give it a memorable name like “Oracle Cloud”.
    • Base URL: Paste the Endpoint URL you copied from your API Gateway deployment.
    • Authentication: Set this to None. Our OCI Function handles authentication securely on the backend, so no credentials are needed here.

4. Next, you need to test the connection to ensure everything is working correctly.

5. Click Update. If the test is successful, you’re ready to start building dashboards!

Step 6: Building your dashboard tile

Now for the fun part! Let’s pull this data into a SquaredUp dashboard.

  1. On a dashboard, add a new tile (e.g., a Line Graph or Table).
  2. In the tile configuration, select your new Oracle Cloud data source.
  3. Configure the data request:
    • Path: Enter the path to your route, /usage/data.
    • HTTP Method: Change the method to POST. This allows us to use SquaredUp’s dynamic time-frame variables in the request body.

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:

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.

Share this article to LinkedInShare this article on XShare this article to Facebook
Dan Watts

DevRel Engineer, SquaredUp

Visualize over 60 data sources, including:

View all 60+ plugins