The GraphQL Services
Two GraphQL front doors sit on the HD Proxy. One is generic: its schema is generated from every dataset every Proxy it knows serves. The other is purpose-built for the network-flow dataset and speaks in the shapes a dashboard wants — time series, top-N, aggregates. Both translate a GraphQL query into the HD query underneath, receive the records directly from the Storage Nodes, and evaluate the rest locally with the SDK's engine. The rules they share with every other door are on the querying page.
The generic service: hd_graphql
The service reads a proxy list and builds its schema from what each Proxy's
/v1/hd/schema returns, watching the file for changes:
# proxies.yaml
proxies:
prod:
url: https://proxy.hd.internal:6986
Every dataset of every Proxy becomes a query field, <proxy>.<dataset>, with
an object type carrying the dataset's fields (integers as Int, floats as
Float, booleans, everything else as String) and an enum of field names for
filters. The argument is one queryElements object:
| Argument | |
|---|---|
startTime, endTime |
the window, in Unix seconds. Given either, the batches for that window are named; given neither, the whole dataset is read — the service acknowledges the full scan on the caller's behalf, because it has no interface of its own to ask through |
filters |
groups of {fieldName, op, value}, AND within a group and OR between groups, op one of EQUALITY, NOT_EQUALITY, GREATER, LESS. These are what the Storage Nodes evaluate, except a condition on a time stored as a delta from its batch, which is applied to the fetched records |
queryQos |
priority weight 1–255 |
query {
prod {
ipflow(queryElements: {
startTime: 1788660000, endTime: 1788663600,
filters: [{ andFilters: [{ fieldName: ip_src, op: EQUALITY, value: "10.0.0.1" }] }]
}) { timestamp ip_src ip_dst port_dst bytes }
}
}
Three mutations are escape hatches for a client that wants the SDK's engines
directly on the records already fetched: executeRawSQL, executeRawGraph and
executeRawVector, each taking a proxy and a query string and returning the
engine's answer as text.
An address literal in a filter is converted to the integer it is before the
Storage Nodes see it, and an address column is rendered as an address in the
result. healthCheck is the liveness field. The service listens on
GRAPHQL_LISTEN_PORT (8083 by default) and reads PROXY_CONFIG_PATH; a
GraphiQL page can be served beside it. It receives results on
FERMIHDI_DATA_RETURN_PORT (6987) at the address it discovers inside
FERMIHDI_DATA_PLANE_SUBNET, or at FERMIHDI_DATA_RETURN_HOST when discovery
cannot pick the right interface.
The IP-flow service: hd_ipflow_graphql
One dataset, three query fields, at POST /query on HD_SERVER_PORT (8080):
| Field | Returns |
|---|---|
getFlowsTS(queryElements, paging) |
the matching flows, or their aggregates, as a time series |
getFlowsTopXTS(queryElements, topXElements, paging) |
the top N by the ordering given, as a time series — for a "top talkers" panel |
getScalers(queryElements, dataPrepFilters) |
single figures over the selection: the maximum, the total |
queryElements carries the whole question:
| Part | |
|---|---|
when |
fromDateTime and toDateTime, each as an ISO string, Unix seconds or milliseconds. Both ends |
filters |
groups of andFilters, OR-ed together; each {fieldName, type, value} with type EQUALITY, GRATER or LESS. A value written as an address is converted for you |
aggregates |
{fieldName, function, returnType} — SUM, and the rest of the aggregate set — computed after the records arrive |
groupBy |
field names to group the aggregates by |
rollUp |
{intervalMs}: the time-series bucket width |
timeoutMs, queryQos |
the wait, and the priority weight |
paging is {first, skip} and is honoured: the page asked for is the page
returned. Address fields — ipSrc, ipDst, peerIpSrc — come back as
addresses, an IPv4-mapped value as its dotted IPv4.
query TopTalkers($from: Float!, $to: Float!, $interval: Float!) {
getFlowsTS(queryElements: {
when: { fromDateTime: { fromDateTimeMs: $from }, toDateTime: { toDateTimeMs: $to } },
filters: [{ andFilters: [{ fieldName: asSrc, type: GRATER, value: "0" }] }],
aggregates: [{ fieldName: bytes, function: SUM, returnType: uint64 },
{ fieldName: packets, function: SUM, returnType: uint64 }],
groupBy: [asSrc, asDst, ipSrc, ipDst],
rollUp: { intervalMs: $interval }
}) { timestampMS asSrc asDst ipSrc ipDst bytes packets bitsPerSecond packetsPerSecond }
}
The service is configured by HD_-prefixed environment variables or a JSON
file: HD_FERMIHDI_HD_PROXY_ENDPOINT is the Proxy, and
HD_SERVER_AUTH_ENABLED with HD_SERVER_API_TOKEN_SECRET turns on bearer-token
checking for a deployment that does not put it behind the authenticating front
door. It shuts down cleanly on POST /v1/shutdown, draining in-flight queries
first. Its data-return settings are the same three as the generic service's.
Placing either
Both are engine roles in the installer's terms — they carry the SDK, so they want 2 MB hugepages and pinned cores like the other engine roles, and the Administrator's Guide sizes them. Both advertise a data-return address to the Storage Nodes and both fail silently when it is wrong: the query is accepted, the results go to an address nobody is listening on, and the caller waits. The operators manual walks through choosing it.