diff --git a/docs/API.md b/docs/API.md index e27bf6817..e63634d6e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1298,3 +1298,33 @@ Show journal logs until the given `until` timestamp, formats are described here: An example project using this endpoint can be found [in this repository](https://github.com/balena-io-playground/device-cloud-logging). + +#### Device metrics + +> **Introduced in supervisor v16.11.0** + +Get current device metrics. + +From an app container: + +```bash +$ curl "$BALENA_SUPERVISOR_ADDRESS/v2/device/metrics?apikey=$BALENA_SUPERVISOR_API_KEY" +``` + +Response: +```json +{ + "status": "success", + "metrics": { + "cpu_usage": 0.5, + "memory_usage": 1024, + "memory_total": 2048, + "storage_usage": 1024, + "storage_total": 2048, + "storage_block_device": "sda", + "cpu_temp": 50, + "cpu_id": "1234567890", + "is_undervolted": false + } +} +``` diff --git a/src/device-api/actions.ts b/src/device-api/actions.ts index 737cbe8c6..b5e7e8c6a 100644 --- a/src/device-api/actions.ts +++ b/src/device-api/actions.ts @@ -27,6 +27,7 @@ import { BadRequestError, } from '../lib/errors'; import { withLock } from '../lib/update-lock'; +import * as systemInfo from '../lib/system-info'; /** * Run an array of healthchecks, outputting whether all passed or not @@ -445,3 +446,15 @@ export const patchHostConfig = async (conf: unknown, force: boolean) => { } await hostConfig.patch(parsedConf, force); }; + +/** + * Get device metrics and checks + * Used by: + * - GET /v2/device/metrics + */ +export const getMetrics = async () => { + return { + ...(await systemInfo.getSystemMetrics()), + ...(await systemInfo.getSystemChecks()), + }; +}; diff --git a/src/device-api/v2.ts b/src/device-api/v2.ts index f9d9cc972..1cffb23b7 100644 --- a/src/device-api/v2.ts +++ b/src/device-api/v2.ts @@ -575,3 +575,15 @@ router.post('/v2/journal-logs', (req, res) => { res.end(); }); }); + +router.get('/v2/device/metrics', async (_req, res, next) => { + try { + const metrics = await actions.getMetrics(); + return res.json({ + status: 'success', + metrics, + }); + } catch (e) { + next(e); + } +}); diff --git a/test/integration/device-api/actions.spec.ts b/test/integration/device-api/actions.spec.ts index 833f3b129..fef83e1fd 100644 --- a/test/integration/device-api/actions.spec.ts +++ b/test/integration/device-api/actions.spec.ts @@ -1338,3 +1338,18 @@ describe('patches host config', () => { expect(hostConfigPatch).to.have.been.calledWith(conf, true); }); }); + +describe('gets metrics', () => { + it('gets system metrics and checks', async () => { + const metrics = await actions.getMetrics(); + expect(metrics).to.have.property('cpu_usage'); + expect(metrics).to.have.property('memory_usage'); + expect(metrics).to.have.property('memory_total'); + expect(metrics).to.have.property('storage_usage'); + expect(metrics).to.have.property('storage_total'); + expect(metrics).to.have.property('storage_block_device'); + expect(metrics).to.have.property('cpu_temp'); + expect(metrics).to.have.property('cpu_id'); + expect(metrics).to.have.property('is_undervolted'); + }); +}); diff --git a/test/integration/device-api/v2.spec.ts b/test/integration/device-api/v2.spec.ts index def563817..6b6791aa9 100644 --- a/test/integration/device-api/v2.spec.ts +++ b/test/integration/device-api/v2.spec.ts @@ -639,4 +639,47 @@ describe('device-api/v2', () => { .expect(503); }); }); + + describe('GET /v2/device/metrics', () => { + // Actions are tested elsewhere so we can stub the dependency here + let getMetricsStub: SinonStub; + beforeEach(() => { + getMetricsStub = stub(actions, 'getMetrics').resolves(); + }); + afterEach(async () => { + getMetricsStub.restore(); + // Remove all scoped API keys between tests + await db.models('apiSecret').whereNot({ appId: 0 }).del(); + }); + + it('responds with 200 if metrics are returned', async () => { + const metrics = { + cpu_usage: 0.5, + memory_usage: 1024, + memory_total: 2048, + storage_usage: 1024, + storage_total: 2048, + storage_block_device: 'sda', + cpu_temp: 50, + cpu_id: '1234567890', + is_undervolted: false, + }; + getMetricsStub.resolves(metrics); + await request(api) + .get('/v2/device/metrics') + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) + .expect(200, { + status: 'success', + metrics, + }); + }); + + it('responds with 503 if there is an error getting metrics', async () => { + getMetricsStub.throws(new Error()); + await request(api) + .get('/v2/device/metrics') + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) + .expect(503); + }); + }); });