qlever — SPARQL store over lab-graph
The query tier of the observability-as-linked-data pipeline (collector #9). It stands up
QLever as a rebuildable materialized view over the RDF
that triplify writes to s3://semantix/lab-graph, so correlating logs is a SPARQL query
instead of a per-signal dashboard.
collector → mover (s3 lab-podlogs) → triplify (RDF → s3 lab-graph) → [ qlever: SPARQL ] → queries
Where it runs
- Backend: StatefulSet
qlever(podqlever-0) in namespacepipelineson the RKE2 cluster, Longhorn PVC (data, 20Gi) at/index. QLever engine = upstreamdocker.io/adfreiburg/qlever. - Web UI: Deployment
qlever-ui(upstreamadfreiburg/qlever-ui, Django/gunicorn :7000) in the same namespace. - One host, two paths:
https://qlever.192.168.10.46.sslip.io/serves the UI;**/api**serves the SPARQL server (the ingress rewrites/api→ the qlever-server root — same origin, no CORS). In-cluster the backend isqlever.pipelines.svc:7001. - No auth (behind the ingress, like Temporal). Only the
graph-loaderis a lab-built, signed image; both QLever images are upstream (admitted because Kyverno only verifiesforgejo…/*refs). - Repo: https://forgejo.192.168.10.46.sslip.io/forgejo_admin/qlever
How it works (loader → index → server, + UI)
Each qlever-0 (re)start runs three stages sharing the PVC at /index:
- graph-loader (initContainer) — lists
lab-graph/**.ttl.gz, gunzips + concatenates into/index/input.ttland writesinput.ttl.sha256(hash over the sorted object key+etag list). Uses themover-s3Secret for S3 creds. - qlever-index (initContainer) —
qlever-index -i /index/idx/graph -f /index/input.ttl -F ttl -p falsebuilds the SPARQL index; skips the rebuild when the hash is unchanged, then removes the (~700 MiB) input. - qlever-server —
qlever-server -i /index/idx/graph -p 7001 …serves SPARQL.
qlever-ui seeds a single lab-graph backend (pointing at …/api) on startup, so the UI works with
no manual setup. Current scale: ~10.7M triples / ~1.39M LogEntrys.
Query it
See the Example queries page for a full, tested catalog (explore, per-workload, time, errors, HTTP-by-reqId, text search, correlation).
Open https://qlever.192.168.10.46.sslip.io for the UI (autocomplete + results table), or hit the
SPARQL API directly at /api (GET|POST ?query=<sparql>, Accept: application/sparql-results+json).
Count:
PREFIX col: <https://simon.services/ns/collector/v1#>
SELECT (COUNT(?e) AS ?n) WHERE { ?e a col:LogEntry }
Correlation — one container's logs in a window (the point of #9):
PREFIX col: <https://simon.services/ns/collector/v1#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?t ?msg WHERE {
?e col:resource <https://simon.services/id/k8s/lab/<ns>/pod/<uid>/container/<c>> ;
col:occurredAt ?t ; col:message ?msg .
FILTER(?t >= "2026-08-10T00:00:00Z"^^xsd:dateTime)
} ORDER BY ?t
Log volume per container:
PREFIX col: <https://simon.services/ns/collector/v1#>
SELECT ?c (COUNT(?e) AS ?n) WHERE { ?e col:resource ?c } GROUP BY ?c ORDER BY DESC(?n)
Operate
- Refresh (pick up new
lab-graphobjects):kubectl -n pipelines rollout restart statefulset/qlever. The loader re-pulls; the index rebuilds only if the input changed. - Health:
kubectl -n pipelines get pod qlever-0; per-stage logskubectl -n pipelines logs qlever-0 -c graph-loader | -c qlever-index | qlever-0(server); UI:kubectl -n pipelines logs deploy/qlever-ui. - Nothing here is backed up — the index is a rebuildable view; source of truth is
s3://semantix/lab-graph.
Build & release (graph-loader)
graph-loader builds via the Temporal BuildOnly pipeline (webhook POST /build → melange/apko →
signed SBOM + vuln scan). After a source change: push to main, let BuildOnly publish
forgejo_admin/qlever@sha256:…, pin that digest in deploy/statefulset.yaml, kubectl apply. Both
QLever images (adfreiburg/qlever, adfreiburg/qlever-ui) are upstream and digest-pinned.
Troubleshooting (learned the hard way)
GET /returns "Unknown path" — that's the bare qlever-server (a SPARQL API). The browsable UI is theqlever-uiDeployment; the ingress serves it at/and the API at/api.CreateContainerConfigError: image has non-numeric user (qlever)— the QLever image declares its USER by name; underrunAsNonRootkubelet needs a numeric uid. Fixed withrunAsUser: 999+fsGroup: 999.- Turtle parse must be serial (
-p false) — triplify declares@prefixlazily (scattered), so QLever's parallel parser can't be used. - Kyverno admits the upstream images because
require-signed-sbomonly verifiesforgejo.192.168.10.46.sslip.io/*refs (same as monitoring's prometheus/grafana).
Not yet (phase 2)
Incremental / git-manifest-driven rebuild + col:inSegment / col:epochSecond enrichment in
triplify (the full #9 segment model); /metrics for the loader + Prometheus scrape; SSO in front of
the endpoint; mirror+sign the QLever images into the lab registry.