Balena API Interaction

The Balena API interaction code is responsible for retrieving metrics from the Balena cloud. It handles authentication, API requests, and data parsing to provide a Prometheus-compatible output.

Authentication

The Balena API requires authentication using a Balena API token. The token is stored in the BALENA_TOKEN environment variable.

BALENA_TOKEN = os.getenv("BALENA_TOKEN", False)
          

Retrieving Fleets

The get_balena_fleets function retrieves all the fleets that the user has access to. It uses the Balena API endpoint /v6/application with a filter to select only directly accessible fleets.

def get_balena_fleets(self):
              """
              Get all the fleets that the user has access to
              and return all corresponding fleet IDs
              """
          
              fleets = []
              headers = {
                  "Authorization": f"Bearer {BALENA_TOKEN}",
                  "Content-Type": "application/json",
              }
          
              response = requests.get(
                  "https://api.balena-cloud.com/v6/application?$filter=is_directly_accessible_by__user/any(dau:1 eq 1)",
                  headers=headers,
              )
          
              if not response.ok:
                  print("Error: {}".format(response.text))
                  sys.exit(1)
          
              for fleet in response.json()["d"]:
                  fleets.append((fleet["id"]))
          
              return fleets
          

Retrieving Fleet Metrics

The get_fleet_metrics function retrieves the number of online devices in a given fleet. It uses the Balena API endpoint /v6/application/{fleet_id} with an expansion to include the count of online devices in the fleet.

def get_fleet_metrics(self, fleet_id):
              headers = {
                  "Authorization": f"Bearer {BALENA_TOKEN}",
                  "Content-Type": "application/json",
              }
          
              response = requests.get(
                  f"https://api.balena-cloud.com/v6/application({fleet_id})?$expand=owns__device/$count($filter=is_online eq true)",
                  headers=headers,
              )
          
              if not response.ok:
                  print("Error: {}".format(response.text))
                  sys.exit(1)
          
              device_online_count = response.json()["d"][0]["owns__device"]
              fleet_name = response.json()["d"][0]["app_name"]
          
              return fleet_name, device_online_count
          

Data Collection

The collect function gathers metrics for each fleet and constructs a Prometheus metric family. It iterates through the list of fleets, retrieves metrics for each fleet using get_fleet_metrics, and adds the metrics to the GaugeMetricFamily object.

def collect(self):
              gauge = GaugeMetricFamily(
                  "balena_devices_online", "Devices by status", labels=["fleet_name"]
              )
          
              for fleet_id in self.get_balena_fleets():
                  fleet_name, device_online_count = self.get_fleet_metrics(str(fleet_id))
          
                  gauge.add_metric([fleet_name], float(device_online_count))
          
              return [gauge]
          

Data Exporter

The main function of the code starts an HTTP server on port 8000 and registers the BalenaCollector with the Prometheus registry. The while True loop sleeps for the specified CRAWL_INTERVAL before collecting metrics again. The CRAWL_INTERVAL is set to 60 seconds by default, but can be overridden with the CRAWL_INTERVAL environment variable.

def main():
              if not BALENA_TOKEN:
                  print("Please set the BALENA_TOKEN environment variable")
                  sys.exit(1)
          
              start_http_server(8000)
              REGISTRY.register(BalenaCollector())
              while True:
                  time.sleep(int(CRAWL_INTERVAL))
          

Testing

Unit tests are available in the tests/test_exporter.py file. The tests cover the functionality of the get_balena_fleets, get_fleet_metrics, and collect functions.

def setUp(self):
              self.collector = BalenaCollector()
          
          class TestBalenaCollector(unittest.TestCase):
              def setUp(self):
                  self.collector = BalenaCollector()
          
              @mock.patch("main.BalenaCollector.get_fleet_metrics")
              def test_collect(self, mock_metrics):
                  with mock.patch.object(self.collector, "get_balena_fleets") as mock_fleets:
                      mock_fleets.return_value = ["fleet1", "fleet2"]
                      mock_metrics.side_effect = [("test_fleet", 3), ("test_fleet", 3)]
          
                      result = list(self.collector.collect()[0].samples)
                      expected = [('balena_devices_online', {'fleet_name': 'test_fleet'}, 3),
                                  ('balena_devices_online', {'fleet_name': 'test_fleet'}, 3)]
          
                      result = [(s.name, s.labels, s.value) for s in result]
          
                      self.assertEqual(result, expected)
          
              def test_get_balena_fleets(self):
                  with mock.patch("main.requests.get") as mock_get:
                      mock_get.return_value.ok = True
                      mock_get.return_value.json.return_value = {"d": [{"id": "fleet1"}, {"id": "fleet2"}]}
                      result = list(self.collector.get_balena_fleets())
                      expected = ["fleet1", "fleet2"]
                      self.assertEqual(result, expected)
          
          
              def test_get_fleet_metrics(self):
                  with mock.patch("main.requests.get") as mock_get:
                      mock_get.return_value.ok = True
                      mock_get.return_value.json.return_value = {"d": [{"owns__device": 3, "app_name": "test_fleet"}]}
                      result = self.collector.get_fleet_metrics("fleet1")
                      expected = ("test_fleet", 3)
                      self.assertEqual(result, expected)
          

Top-Level Directory Explanations

tests/ - This directory contains all the unit and integration tests for the project. It includes the __init__.py file which makes it a package, and specific test files like test_exporter.py.