Observability
A repository you depend on needs to answer three questions: what is it doing, is it healthy, and is it configured safely. Jenesis Repository answers them over plain HTTP, so a console, a script or a monitoring system reads the same endpoints. This chapter covers the log tail, the health and metrics endpoints, the security-posture report, and the consistency check that keeps several nodes honest.
Who may read these endpoints
GET /api/logs, GET /api/consistency and the /actuator endpoints show deployment-wide state - every
repository's log lines, every node, every metric. They are therefore gated to a key that holds a
deployment-wide * grant; a key scoped to one repository is refused, and so is a keyless caller even when
anonymous read rights are granted. On a deployment that runs with authentication off, anyone can read them.
/actuator/health is always open, so probes need no credential.
Recent logs
The server keeps its most recent log entries in memory and serves them at GET /api/logs, which lets you read
the tail without shell access to the host:
curl -H 'Jenesis-Repository-Key: jenk_…' \
'http://localhost:8080/api/logs?level=WARN&q=proxy&limit=50'
{"cursor":1834,"count":2,"entries":[
{"seq":1821,"timestamp":"2026-08-21T09:14:02.118Z","level":"WARN",
"logger":"build.jenesis.observation","message":"jenreg.proxy.fetch [format=maven, outcome=miss] failed: …",
"tenant":"default"}]}
| Parameter | Meaning |
|---|---|
level |
The minimum level to return (INFO, WARN, ERROR). |
q |
A substring matched against the logger name and the message. |
since |
A sequence number; only entries after it are returned. Use the cursor of a previous response to tail. |
limit |
The most entries to return (default 200). |
tenant |
Restrict to entries stamped with this tenant. |
The buffer holds the last 1 000 entries by default (jenreg.logs-buffer); older lines are gone from this
view, but still reach your normal log output. Each entry carries a seq, so polling with since=<cursor>
never repeats or skips a line.
The server's own operations log under the logger build.jenesis.observation: a completed operation at
INFO, a failed one at WARN with its error. Today the one operation instrumented this way is the proxy
fetch, jenreg.proxy.fetch, tagged with the format it served and the outcome - hit (served locally),
miss (fetched from the upstream), negative (a remembered upstream 404), verified or withheld. Filter
on q=proxy.fetch to watch your pull-through cache work. The same logger writes one line per HTTP request,
http.server.requests, with its method, path and status - the server's access log; nothing else is logged
per request.
Health and metrics
The server exposes three Spring Boot Actuator endpoints: /actuator/health, /actuator/info and
/actuator/metrics. Health serves the liveness and readiness probes (/actuator/health/liveness,
/actuator/health/readiness) a container platform expects, is never rate-limited, and is readable without a
credential. Its detail is shown only to an authorised caller (management.endpoint.health.show-details is
when-authorized); an anonymous probe sees up or down alone.
/actuator/metrics lists the JVM and HTTP request meters Spring Boot collects - request counts by URI and
status, memory, threads. The proxy-fetch operation described above is also an observation, so a Micrometer
registry on the module path receives it as a timer tagged with format and outcome. The server ships no
registry of its own; the endpoint serves what the registry you install collects.
Security posture
GET /api/posture lists every setting that leaves the deployment less safe than it could be, each with the
reason and the exact key and value that fix it. It never echoes a secret, so it is safe to surface on a
dashboard:
curl -H 'Jenesis-Repository-Key: jenk_…' http://localhost:8080/api/posture
{"count":2,"critical":1,"warn":1,"info":0,"advisories":[
{"id":"jenreg.auth.open","severity":"CRITICAL","scope":"DEPLOYMENT","tenant":null,
"title":"Authorization is disabled - the instance is fully open",
"why":"…","fix":"…","settingKey":"jenreg.auth","settingValue":"true","docs":"…"}]}
The advisories the server raises:
| Id | Severity | Raised when |
|---|---|---|
jenreg.auth.open |
critical | jenreg.auth=false - every request is served anonymously. |
jenreg.profile.dev |
critical | The dev Spring profile is active, so the console runs its local-only form login. |
jenreg.anonymous.write |
critical | jenreg.anonymous-rights grants a keyless caller write or manage rights. |
jenreg.anonymous.enabled |
warn | jenreg.anonymous-rights grants a keyless caller read rights (the public-mirror pattern). |
jenreg.importer.ssrf |
warn | jenreg.block-private-import-hosts=false - an import may reach internal hosts or run over plaintext. |
jenreg.ratelimit.unset |
warn | jenreg.rate-limit is unset or 0, so nothing throttles a client. |
jenreg.console.wildcard |
warn | jenreg.ui.admins contains *, making every signed-in console user an admin. |
jenreg.demo.writable |
warn | jenreg.demo=true without jenreg.read-only=true - a seeded demo anyone can write to. |
A clean deployment returns an empty list. The same report is shown in the console's Security posture
panel, and the jenreg.auth.open advisory is also logged once at boot.
* grant, like the
other reads above. Unlike them, it is also readable by a keyless caller once
jenreg.anonymous-rights grants read rights, since it never reveals a secret. With authentication
off, anyone reads it.
Running more than one node
Every node is stateless; the store is the only state, so several nodes behind a load balancer share one bucket or one mounted directory and serve the same content. Each node derives some state from the store - its view of the index, its configuration - and a node that falls behind or is configured differently would answer differently from its peers. The consistency check makes that visible.
Switch it on with jenreg.consistency.enabled=true on every node. Each node then publishes a small
fingerprint of its derived state to the store on a heartbeat, and GET /api/consistency on any node compares
all of them:
curl -H 'Jenesis-Repository-Key: jenk_…' http://localhost:8080/api/consistency
{"localNodeId":"repo-2","nodeCount":3,"liveCount":3,"converged":true,"singleNode":false,"truncated":false,
"nodes":[{"nodeId":"repo-1","live":true,"stale":false,"heartbeatAgeMillis":4120,
"indexCursor":128934,"snapshotVersion":"…","configGeneration":"3f2a…",
"inventoryTotal":12894,"quotaUsed":73400320,"local":false}],
"divergences":[]}
A node is live while its heartbeat is younger than jenreg.consistency.dead-after; only live nodes take
part in the comparison, and a fleet of one live node is always converged. A node whose last heartbeat is older
than staleness-window is flagged stale but stays in the comparison until dead-after. Three kinds of
divergence are reported, and each also appears in the security-posture report under its own id:
| Id | Meaning |
|---|---|
jenreg.consistency.config |
A live node's configuration generation differs from the freshest node's - it missed a configuration change or is split from the fleet. |
jenreg.consistency.stuck |
A node's index cursor has lagged behind the furthest node for longer than sweep-interval × sweep-intervals without advancing. Lag within that budget is not a divergence. |
jenreg.consistency.pointer |
Two live nodes resolve the same pointer to different content - a client would get different bytes depending on which node answers. |
The check only reports; it never blocks a request.
A node names itself with jenreg.consistency.node-id; unset, it uses the hostname, and falls back to a
generated id (with a warning) only when no hostname is available. Give each node a stable id so a restart
re-uses its fingerprint. Without the setting switched on, a node publishes nothing and writes no operational
keys into the store.
A fingerprint whose node has been silent for longer than jenreg.consistency.forget-after (a day by default)
is deleted by the next node that publishes, so a fleet that gives every restart a fresh hostname does not
accumulate a fingerprint per host it ever ran on. A report reads at most 1 000 fingerprints; past that it
answers with "truncated":true and compares the ones it read.
Settings
| Key | Default | Effect |
|---|---|---|
jenreg.logs-buffer |
1000 |
Entries the in-memory log ring keeps for GET /api/logs. |
jenreg.consistency.enabled |
false |
Publish this node's fingerprint and take part in the consistency check. |
jenreg.consistency.node-id |
the hostname | The node's stable name in the report. |
jenreg.consistency.heartbeat |
the sweep interval | Milliseconds between fingerprint publications (at least 1 000). |
jenreg.consistency.sweep-interval |
60000 |
Milliseconds per sweep; with sweep-intervals, the budget a lagging node has before it is stuck. |
jenreg.consistency.sweep-intervals |
3 |
Sweeps a lagging node may take to catch up. |
jenreg.consistency.staleness-window |
300000 |
Milliseconds since a node's last heartbeat after which it is flagged stale. |
jenreg.consistency.dead-after |
900000 |
Milliseconds of silence after which a node leaves the live comparison. |
management.endpoints.web.exposure.include |
health,info,metrics |
The Actuator endpoints served. |
management.endpoint.health.probes.enabled |
true |
Serve the liveness and readiness probe groups. |
management.endpoint.health.show-details |
when-authorized |
Show health detail only to an authorised caller. |
Every jenreg.* key is also an environment variable (JENREG_CONSISTENCY_ENABLED=true) or a -D system
property.