- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| cmd/graph-loader | ||
| deploy | ||
| internal/s3util | ||
| vendor | ||
| .gitignore | ||
| apko.yaml | ||
| catalog-info.yaml | ||
| go.mod | ||
| go.sum | ||
| melange.yaml | ||
| README.md | ||
qlever — SPARQL store over lab-graph
The query tier of the observability-as-linked-data pipeline (collector #9). Stands up
QLever as a rebuildable materialized view over the RDF
triplify writes to s3://semantix/lab-graph, so correlating logs is a SPARQL query, not a
per-signal dashboard.
collector → mover (s3 lab-podlogs) → triplify (RDF → s3 lab-graph) → [ qlever: SPARQL ] → queries
Design rules
- The index is a materialized view, not a database of record. Source of truth is
lab-graph(triplify's sealed.ttl.gzsegments). The index rebuilds from them — nothing here is backed up. - Triples only, default graph (matches triplify's Turtle output; QLever holds ~10.7M triples /
~1.4M
LogEntrys from the currentlab-graph). - QLever engine = upstream image,
docker.io/adfreiburg/qlever(pinned by digest). It's admitted because Kyvernorequire-signed-sbomonly verifiesforgejo.192.168.10.46.sslip.io/*refs — the same reasonmonitoringruns upstream prometheus/grafana. Onlygraph-loaderis a lab-built, signed image. - Serial Turtle parse (
qlever-index -F ttl -p false): triplify's serializer declares@prefixlazily (scattered through each file), so QLever's parallel parser can't be used.
How it runs
A StatefulSet (namespace pipelines, Longhorn PVC at /index) whose (re)start does three steps:
- graph-loader (
cmd/graph-loader) — listslab-graph/**.ttl.gz, gunzips + concatenates to/index/input.ttl, writesinput.ttl.sha256(a hash over the sorted object+etag list). - qlever-index — builds
/index/idx/graph.*; skips the rebuild when the input hash is unchanged, then removes the (~700 MiB) input to reclaim space. - qlever-server — serves SPARQL on
:7001.
Force a refresh (pick up new lab-graph objects): kubectl -n pipelines rollout restart statefulset/qlever.
Configuration (env — graph-loader)
| Env | Default | Meaning |
|---|---|---|
SRC_PREFIX |
lab-graph |
S3 prefix of the RDF to index |
OUT_FILE |
/index/input.ttl |
concatenated Turtle written for the indexer |
S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY |
— | Hetzner S3 (from Secret mover-s3) |
Query
- Web UI:
https://qlever.192.168.10.46.sslip.io— the QLever query interface (autocomplete + results table), pre-pointed at thelab-graphbackend. - SPARQL API:
https://qlever.192.168.10.46.sslip.io/api(in-clusterqlever.pipelines.svc:7001),GET|POST ?query=<sparql>withAccept: application/sparql-results+json.
No auth (behind the ingress, like Temporal). The same host serves both — the ingress routes / to
the UI and /api to the SPARQL server.
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:
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
Build & deploy
graph-loader builds via the Temporal BuildOnly pipeline (webhook POST /build, melange/apko,
signed SBOM). Pin the published graph-loader@sha256:… digest in deploy/statefulset.yaml, then
kubectl apply -f deploy/statefulset.yaml. The QLever image is already digest-pinned.
Out of scope (phase 2)
Incremental / git-manifest-driven rebuild + col:inSegment / col:epochSecond load (the full #9
segment model); /metrics; SSO in front of the endpoint; mirror+sign the QLever image into the lab
registry for supply-chain coverage.