feat(api): add an admin only Prometheus metrics endpoint - #416
Open
matanbaruch wants to merge 1 commit into
Open
feat(api): add an admin only Prometheus metrics endpoint#416matanbaruch wants to merge 1 commit into
matanbaruch wants to merge 1 commit into
Conversation
Add a GET /api/v1/metrics operation returning the device, user and group counters of the platform using the Prometheus text exposition format, so device usage can be graphed in Prometheus and Grafana. The operation is tagged admin and the controller checks the privilege of the caller as well, since the device channel branch of accessTokenAuth() does not set req.user. The counters cover all the objects of the platform whatever the group they belong to, which a simple user is not allowed to see. Counters are computed on scrape rather than on a timer, so no extra unit or background job is needed and the returned values are always those of the very moment the Prometheus server asked for them. Device states are computed with the same state machine the device table uses in get-device-state.util.ts, including the Apple PREPARING and the UNHEALTHY cases, and every label value is known in advance so that a counter dropping to zero is exported as zero instead of vanishing. Signed-off-by: matanbaruch <matan.baruch@unity3d.com>
matanbaruch
force-pushed
the
feature/devicehub-prometheus-metrics
branch
from
July 25, 2026 21:31
6113db2 to
020546f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@DaniilSmirnov you offered to take this on your fork back in DeviceFarmer/stf#860, so here it is, ported to devicehub rather than copied over. The upstream version is DeviceFarmer#893.
Adds
GET /api/v1/metrics, returning the device, user and group counters of the platform in the Prometheus text exposition format, so device usage can be graphed in Prometheus and Grafana.Counters are computed on scrape, not on a timer, so there is no extra unit, no background job and no polling interval to tune. A scrape is three Mongo reads.
What is devicehub specific
This is not the stf patch with the names changed. The parts that had to be rewritten:
ui/src/lib/utils/get-device-state.util.ts, not stf's. That includes the two cases stf does not have: statusPREPARING(6) on an Apple device counts asavailable, and statusUNHEALTHY(7) gets its own state.getDevicesForMetrics()in the device model reads the collection with a projection limited topresent,status,ready,owner,manufacturerandprovider.name, so a scrape does not pull whole device documents.paths/metrics.jsis generated, not hand written. I ranpython3 lib/units/api/gen_routes.py. Heads up that the generator also rewrites the header comment of eightpaths/team*.jsfiles that were edited by hand at some point, so their committed form no longer matches the generator output. I reverted those to keep this diff clean, but they will keep showing up for anyone who runs it.DeviceModel,UserModel,GroupModel) rather than the deprecated concatenateddbapidefault export.Access control
The operation is tagged
admin, and the controller checksreq.user.privilegeitself. The second check is not redundant here.accessTokenAuth()has a branch forreq.headers.channelandreq.headers.devicethat returnstruewithout ever settingreq.user, so a caller coming through that path would otherwise reach the endpoint with no privilege check and crash onreq.user. The controller answers 403 for that and for simple users.Worth knowing: the counters cover every device, user and group regardless of group membership, which is exactly what a simple user must not see. That is why this is admin only rather than scoped per user. If you want per group metrics later, that is a different endpoint with a group label and a membership filter, and it should not reuse this one.
What it returns
devicehub_devices_totaldevicehub_devices_by_statestatedevicehub_devices_availableavailablestatedevicehub_devices_busybusystatedevicehub_providers_totaldevicehub_users_totaldevicehub_users_by_privilegeprivilegedevicehub_groups_totaldevicehub_groups_activedevicehub_groups_by_statestatedevicehub_groups_by_classclassstatevalues areabsent,offline,unauthorized,preparing,busy,available,unhealthyandpresent. Theusingandautomationstates of the device table are not exposed, since they are relative to the user looking at the device and mean nothing to a scraper.Every label value is known up front, so a counter that drops to zero is exported as zero instead of disappearing from the output, which is what you want if you are alerting on it. No metric is labeled with a user email, so there is no PII in the endpoint and no unbounded cardinality.
doc/METRICS.mdhas the scrape config, a docker compose snippet for Prometheus and Grafana, the curl command and the expected output.Verification
npm run lint: 0 errors, no new warnings in the added filesnpm run typecheck: cleannpm run build, then the metrics module exercised against the built output: 10 device state cases including the ApplePREPARINGandUNHEALTHYones, the aggregation, the exposition format and the label reset behaviourreq.userboth get 403 JSON, an admin gets 200 withContent-Type: text/plain; version=0.0.4; charset=utf-8and correct countersThere is no
testscript inpackage.jsonand no JS unit test harness in the repo, so there was nowhere to land the state machine tests as a committed suite.test/apiis pytest against a running stack and would need the generated client regenerated for the new operation. If you want either the pytest coverage or a JS unit runner, tell me which and I will send it separately.I have not run this against a live devicehub with Mongo and real devices. If you have a staging environment, that is the one thing left to confirm.
Unrelated finding
While verifying the above I hit a latent import cycle that is already on
masterand has nothing to do with this PR, but you probably want to know about it.Importing any model index as the first module of a process throws:
db/api.jsbuilds its concatenated default export from the five model indexes, and a model index reaches back intodb/api.jsbefore that export is initialized. It is harmless in the running app becauseapi/index.jspullshelpers/securityHandlers.js, which importsdb/api.jsand initializes the graph in a safe order beforeexpress-openapiloads any controller. It bites anything that loads a model in isolation, which is most of the ways you would write a unit test. Reproduced onfd1eee58with no changes applied.