How to View Your Twilio Account Usage Using Rust

January 25, 2023
Written by
Reviewed by

How to View Your Twilio Account Usage Using Rust

Recently, I’ve stepped through how to view your Twilio account usage information using Go. This time, I’m going to show you how to do it using Rust.

But why Rust? After all, it can be harder to learn than a number of other languages; at least in my experience.

Because I feel that many of Rust’s features – especially borrowing and scoped resource management functionality – make it an extremely compelling language to learn. Learning them can help you better appreciate the languages that you already know. What’s more, it’s a very safe language with speed approaching that of C.

Prerequisites

To follow along with this tutorial, you will need the following:

Application overview

This won’t be a big Rust tutorial, but rather one that’s short, sweet, and mostly straight to the point. You’ll learn how to retrieve your Twilio account usage for the last month, sum up the total cost, and print that information to the terminal.

The code will make direct API calls and be more verbose than you would likely expect as, at least for the time being, there are no official Twilio Rust crates. That said, I’m thinking of creating them.

Scaffold a new Rust project

The first thing to do is to scaffold a new Rust project using Cargo and change in to the new project directory, by running the commands below.

cargo new twilio_account_usage
cd twilio_account_usage

Add the required dependencies

Next, you need to install the dependencies that the application needs. These are:

  • dotenv: This crate merges variables defined in .env into the application’s environment variables provided by the operating system. Among other things, this ensures that secure credentials, such as your Twilio credentials, are kept out of code.
  • reqwest: This crate provides a convenient, higher-level HTTP Client.
  • serde, serde_derive, and serde_json: These crates, collectively, serialise responses from the Twilio API into a series of structs, making them easier to work with, programmatically.

To install them, add the following configuration to the [dependencies] section of Cargo.toml.

dotenv = "0.15.0"
reqwest = {version="0.11.13", features = ["blocking", "json"]}
serde = { version = "1.0.152", features = ["derive"] }
serde_derive = "1.0.152"
serde_json = "1.0.91"

Then, in the terminal, run the following command to download them.

cargo build

Allowing for the speed of your network connection, they’ll be installed relatively quickly.

Set the required environment variables

The next thing to do is to set the two required environment variables, your Twilio Account SID and Auth Token, so that the code can make authenticated requests to Twilio’s APIs. To do that, create a new file named .env in the project’s top-level directory. In the new file, paste the following configuration.

TWILIO_ACCOUNT_SID="<TWILIO_ACCOUNT_SID>"
TWILIO_AUTH_TOKEN="<TWILIO_AUTH_TOKEN>"

Make sure you add .env to .gitignore in your normal projects, so that it’s not accidentally tracked.

The Twilio Console Account Info panel

Next, from the Twilio Console's Dashboard, copy your Account SID and Auth Token and paste them in place of the respective placeholders in .env for TWILIO_ACCOUNT_SID, and TWILIO_AUTH_TOKEN.

Now, it’s time to write some Rust code!

Write the Rust code

In your text editor or IDE of choice, open src/main.rs and paste the following code.

use dotenv::dotenv;
use reqwest::{blocking::Client, Error, StatusCode};
use serde::{Deserialize, Serialize};
use std::env;

#[derive(Serialize, Deserialize)]
struct AccountUsage {
    first_page_uri: String,
    last_page_uri: Option<String>,
    next_page_uri: Option<String>,
    previous_page_uri: Option<String>,
    num_pages: Option<u8>,
    page: u8,
    page_size: u8,
    start: u8,
    end: u8,
    total: Option<u8>,
    uri: String,
    usage_records: Vec<UsageRecord>,
}

#[derive(Serialize, Deserialize)]
struct UsageRecord {
    category: String,
    description: String,
    account_sid: String,
    start_date: String,
    end_date: String,
    as_of: String,
    count: String,
    count_unit: String,
    usage: String,
    usage_unit: String,
    price: String,
    price_unit: String,
    api_version: String,
    uri: String,
    subresource_uris: SubresourceUris,
}

#[derive(Serialize, Deserialize)]
struct SubresourceUris {
    all_time: String,
    today: String,
    yesterday: String,
    this_month: String,
    last_month: String,
    daily: String,
    monthly: String,
    yearly: String,
}

impl AccountUsage {
    fn get_total_usage_cost(&self, usage_records: &Vec<UsageRecord>) -> f64 {
        let mut usage_total = 0.0;
        let au_iter = usage_records.iter();
        for val in au_iter {
            let price: f64 = val.price.parse().unwrap();
            usage_total = usage_total + price;
        }

        return usage_total;
    }

    fn print_usage_report(&self) {
        let data_iter = self.usage_records.iter();
        for record in data_iter {
            println!(
                "{},{},{},{}",
                record.start_date, record.end_date, record.category, record.price
            );
        }
    }
}

fn main() -> Result<(), Error> {
    dotenv().ok();

    let account_sid = env::var("TWILIO_ACCOUNT_SID")
        .expect("Twilio Account SID could not be retrieved.");
    let auth_token = env::var("TWILIO_AUTH_TOKEN")
        .expect("Twilio Auth Token could not be retrieved.");

    let page_size = 20;
    let request_url = format!(
            "https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Usage/Records/LastMonth.json?PageSize={page_size}"
    );

    let client = Client::new();
    let response = client
        .get(&request_url)
        .basic_auth(account_sid, Some(auth_token))
        .send()?;

    if response.status() != StatusCode::OK {
        println!("Received response status: {:?}", response.status())
    }

    let body_result = response.text();
    let body = match body_result {
        Ok(result) => result,
        Err(error) => panic!("Problem extracting the JSON body content. Reason: {:?}", error),
    };

    let account_usage: AccountUsage = serde_json::from_str(&body).unwrap();
    let usage_total = account_usage
        .get_total_usage_cost(&account_usage.usage_records);
    
    println!("Twilio Account Usage");

    account_usage.print_usage_report();
        
    println!("Total records: {}", account_usage.page_size);
    println!("Total cost: {}", usage_total);

    Ok(())
}

Now, let’s step through what’s going on.

The code starts off by defining three structs to model the JSON data returned from a successful response to Twilio’s API. An AccountUsage struct holds the pagination information, along with a vector of UsageRecord structs.

Each UsageRecord struct contains a SubresourceUris struct. These contain a list of related resources, which are URIs to UsageRecords for the same category for today, yesterday, this month, and this year, etc.

Note that some of AccountUsage's fields can be nullable, being defined as either Option<String> or Option<u8>. This is because responses may not contain values for those fields, so they need to be nullable, if necessary.

The AccountUsage struct defines two methods:

  • get_total_usage_cost(): this calculates and returns the total account usage cost, by iterating over the UsageRecord objects stored in usage_records and summing the price field
  • print_usage_report(): this iterates over the UsageRecord objects stored in usage_records, printing their start and end date, category, and price, comma-separated.

Next, the main() function is defined. It uses the dotenv crate to load the variables defined earlier in .env into the application’s list of environment variables.

After that, it attempts to initialise account_sid with your Twilio Account SID and auth_token with your Twilio Auth Token from the environment variables TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN respectively. If either of them cannot be retrieved, an appropriate error message is printed to the terminal and the application exits.

If both variables were defined and retrieved, however, then two further variables are defined: page_size and request_uri.

  • page_size stores the number of records to be retrieved, or the size of a “single page” of records.
  • request_uri is initialised to store the URI for the usage records endpoint, using captured identifiers to set the Account SID and page size. The page size limits the maximum number of records returned to 20.

As the URI ends with LastMonth.json, any results returned will be limited to those in the last month and returned in JSON format. However, by changing .json to either .csv or .html, the information returned would instead be formatted as CSV or HTML respectively.

In addition, there are several other paths available; these are:

DailyThis returns multiple usage records for each usage category, each representing usage over a daily time-interval.
MonthlyThis returns multiple usage records for each usage category, each representing usage over a monthly time-interval
YearlyThis returns multiple usage records for each usage category, each representing usage over a yearly time-interval.
ThisMonthThis returns a single usage record per usage category, for this month’s usage only.
TodayThis returns a single usage record per usage category, for today’s usage only.
YesterdayThis returns a single usage record per usage category, for yesterday’s usage only.
AllTimeThis returns a single usage record for each usage category, each representing usage over the date-range specified. This is the same as the root /Usage/Records.

After that, a new reqwest::blocking::Client object, client, is initialised and used to make a GET request to the URI defined in request_url. It uses Client’s basic_auth() function to authenticate the request, passing account_sid as the username and auth_token as the password.

As the second parameter (password) to basic_auth is an Option type, it allows for authentication both with and without a password. Because of that, the password (auth_token) needs to be supplied as an Option type. As requests to Twilio’s API require the password to be supplied, auth_token is passed in as the Some variant of Option.

Next up, if the status code of the response was not HTTP 200 (OK), the code panics and exits, printing the error code to the terminal. Otherwise, the code next attempts to retrieve the response’s body as text.

If it can’t do that, the code exits, printing the reason why the body could not be extracted to the terminal. However, if the body was retrieved, Serde JSON parses it into an AccountUsage struct: doing so makes the JSON body easier to work with programmatically.

Following that, the get_total_usage_cost function is called to calculate the total cost of the retrieved usage records.

At this point, there’s not all that much left, but to print out the retrieved and calculated information to the terminal. First, a header indicating the information coming (“Twilio Account Usage”) is printed.

Then, an iterator is instantiated to iterate over the available usage records. This is then used to iterate over them, printing out each record’s start and end dates, category, and cost, comma-separated.

After that, the total number of records retrieved and the total cost are printed before the function exits.

Test the application

With the code completed, it’s time to test it, by running the command below.

cargo run

All being well, you’ll see output similar to the following printed to the terminal.

An example of the application displaying its output in the (Gnome) terminal

That’s how to retrieve your Twilio account usage using Rust

There is a lot more that you can do, such as handling errors gracefully. But you’ve now learned the essentials of retrieving information from Twilio’s APIs with Rust.

What improvements would you make to the code? How would you have implemented the application? Please share your feedback, as I’m really just at the start of my journey with Rust and would love to learn from you as well.

Matthew Setter is a PHP Editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Mezzio Essentials and Docker Essentials. When he's not writing PHP code, he's editing great PHP articles here at Twilio. You can find him at msetter@twilio.com, and on Twitter, and GitHub.