Cloud Observability
ArcadeDB exposes a vendor-neutral observability stack for production (Kubernetes) deployments: always-on RED metrics, OpenTelemetry distributed tracing, structured JSON logging with correlation IDs, and standard health probes. Everything described here is opt-in and behavior-preserving — with no new configuration, an upgraded server behaves exactly as before. The existing /prometheus scrape endpoint, /api/v1/server JSON, and /api/v1/ready semantics are never changed, only added to.
The design follows an instrument-once principle: each hot path (HTTP request, query/command, transaction) is wrapped in a single Micrometer Observation. When only metrics are enabled, an Observation is a timer; when the optional tracing plugin is loaded, the same instrumentation also emits a span. A correlation ID then ties the resulting metric, span, and log line together.
Metrics depth
ArcadeDB builds on the Micrometer framework already in the server. Metrics are enabled by default (arcadedb.serverMetrics=true) and exposed on the Prometheus scrape endpoint /prometheus (see Monitoring for scrape and Grafana setup). The series below are additive — existing metrics and the /prometheus output shape are unchanged.
RED latency timers
Two always-on timers record request/query latency with a percentile histogram (server-side p50/p95/p99). Query text is never used as a tag, keeping cardinality bounded.
| Timer | Tags | Prometheus series |
|---|---|---|
|
|
|
|
|
|
Queries that an MCP tool runs are reported as mcp on both MCP transports. Before 26.11.1 they were reported as http (the HTTP transport) or internal (stdio), so an agent’s traffic could not be told from a REST client’s, and a protocol!="internal" filter dropped the stdio traffic. The same tag is what tells a client request from the engine’s own work under HA, where a database whose directory is being replaced from the leader’s snapshot refuses client requests.
|
The protocol tag defaults to internal for embedded/in-process callers; dashboards typically filter protocol!="internal" to isolate client traffic. Redis commands and unfiltered Mongo scans do not pass through the query engine and are therefore not recorded by arcadedb.query.duration.
|
Since v26.9.1 the db tag of arcadedb.http.requests carries a database name only when that database
exists on the server, and the constant unknown otherwise. The {database} path parameter matches any URL
segment, and the timer is also recorded for requests answered 401, so echoing it verbatim let any caller
register one permanent series per invented name. Requests against a non-existent database are therefore
grouped under db="unknown" rather than appearing one series per name.
|
Engine gauges
EngineMetricsBinder bridges the internal engine Profiler into Micrometer gauges (engine-wide aggregates, refreshed from a one-second-memoized snapshot):
| Gauge | Meaning |
|---|---|
|
Page cache hit/miss counters |
|
Pages read from / written to disk |
|
Write-ahead-log bytes and file count |
|
MVCC concurrent-modification conflicts |
|
Open file handles |
|
Write/read transactions and rollbacks |
|
Query and command counts |
|
Number of open databases |
|
Asynchronous batch transactions cut short by a durability-flag change |
|
Asynchronous workers a lowered parallel level retired and that are still draining |
|
Lightweight-edge lists the census could not count, whose edges each walk the list alone |
|
Bucket |
The two async.* metrics describe the asynchronous executor and are worth reading together with the throughput of your ingestion.
async.boundary.commits counts how often a change to setTransactionUseWAL() / setTransactionSync() forced a worker
to close its open batch transaction early. Occasional increments are normal - a bulk load flips those flags at its start
and end. A rate that stays high while asynchronous throughput drops means two callers are flipping them against each
other on the same database, so batching is degrading towards one commit per task.
async.workers.retiring is a gauge, not a counter: it goes back to zero as the retired workers finish. Zero is the
normal reading, and a brief spike during a setParallelLevel() call is expected. A reading that stays above zero means a
retired worker is stuck inside user code and will never finish draining - a resize deliberately never interrupts one -
so the thread stays alive until the database is closed.
graph.light.edge.census.fallbacks is a counter of how often counting the lightweight edges of one vertex’s edge list
failed, so that every lightweight edge read off that list resolves its occurrence by walking the list itself - the
quadratic cost the census exists to avoid, which on a super-node is the difference between milliseconds and minutes.
Zero is the normal reading. The first failure of each list of a database is logged at WARNING (at most 64 distinct
lists per database; the rest at FINE), so the log shows which list could not be read and this counter shows how often
it keeps happening. (Since v26.11.1: one WARNING per JVM and no counter.)
bucket.recount.publishes.refused (Prometheus arcadedb_engine_bucket_recount_publishes_refused_total; in the Profiler
JSON, recountPublishesRefused: {count: N}) counts the times a bucket recomputed its record count but could not cache
the result, because a replicated apply wrote to the bucket while it was being scanned. One or two increments are the
normal cost of an HA follower catching up. A rate that never returns to zero means a follower on which every
count(*) is a full scan. (Since v26.10.2)
Optional OTLP metrics export
To push metrics to an OpenTelemetry Collector (in addition to the unchanged /prometheus scrape), enable the OTLP registry:
arcadedb.serverMetrics.otlp.enabled=true
arcadedb.serverMetrics.otlp.endpoint=http://otel-collector:4318/v1/metrics
# optional: push every 15 seconds instead of every minute (minimum 1000 ms)
arcadedb.serverMetrics.otlp.step=15000
The metrics exporter speaks OTLP/HTTP (protobuf), not gRPC, so the endpoint is the collector’s HTTP receiver: port 4318, path /v1/metrics (this is also the default). Port 4317 is the gRPC receiver and does not work for metrics; the server logs a warning if it is configured. A URL without a path gets /v1/metrics appended. Tracing is different: it uses OTLP/gRPC on port 4317 (see below).
The metrics and tracing plugins report the OpenTelemetry service.name resource attribute. The standard OTEL_SERVICE_NAME environment variable is honored and takes precedence over OTEL_RESOURCE_ATTRIBUTES and over the arcadedb.serverMetrics.serviceName setting (default arcadedb), so in a container set it with -e OTEL_SERVICE_NAME=my-arcadedb.
This registers an OTLP meter registry alongside Prometheus; the scrape path is unaffected whether or not OTLP is enabled. The OTLP exporter lives in the optional metrics module bundled with the standard distributions.
Distributed tracing
Distributed tracing is an optional plugin (the tracing module, shipped only in the full distribution). When absent or disabled, the in-core Observation calls are no-ops with zero span overhead. When enabled, the same instrumentation points used by the metrics timers also emit OpenTelemetry spans.
Enable tracing and point it at your OTLP collector:
arcadedb.serverMetrics.tracing.enabled=true
arcadedb.serverMetrics.tracing.endpoint=http://otel-collector:4317
arcadedb.serverMetrics.tracing.samplingRate=0.1
-
tracing.endpointis an OTLP/gRPC endpoint (default port4317). -
tracing.samplingRateis a parent-based ratio in[0.0, 1.0]:1.0samples every trace,0.0(default) disables sampling. -
If the endpoint is unreachable, tracing self-disables and logs an error — server startup is never blocked.
Two span types are produced and nest correctly (query spans are children of the HTTP span within one trace):
| Span | Attributes |
|---|---|
|
|
|
|
|
The |
ArcadeDB continues an upstream trace by reading the inbound W3C traceparent HTTP header. Resource attributes (service.name, service.instance.id), OTEL_RESOURCE_ATTRIBUTES, and trace-context propagation over the replication (Raft) and binary wire protocols are not yet implemented; each non-HTTP request starts a fresh root span.
Structured logging and correlation IDs
By default the server logs human-readable text, byte-identical to previous releases. Set arcadedb.server.logFormat=json to emit one JSON object per line instead, suitable for log shippers (Loki, ELK):
arcadedb.server.logFormat=json
See Logging for the configuration underneath this setting: log levels, rotation, log directory, and routing the engine through SLF4J when embedding.
Each JSON line contains: timestamp (local time, yyyy-MM-dd’T’HH:mm:ss.SSS), level, logger, thread, message, and — when present — requestId, db, traceId, spanId, and exception (full stack trace). Example:
{"timestamp":"2026-06-16T10:15:30.123","level":"INFO","logger":"com.arcadedb.server.ArcadeDBServer","thread":"http-1","message":"Query executed","requestId":"req-abc","db":"mydb","traceId":"4bf92f3577b34da6a3ce929d0e0e4736","spanId":"0123456789abcdef"}
Correlation IDs
Every HTTP request is tagged with a requestId: ArcadeDB reuses the inbound X-Request-Id header when present (sanitized, capped at 128 characters) or generates a UUID, and echoes it on the response. The db field is taken from the request’s database path parameter. When the tracing plugin is active, traceId/spanId are added too — but requestId works for correlation even with tracing disabled.
For operators who keep text logs, arcadedb.server.logIncludeTrace=true appends ` [traceId=…]` to each line while a trace is active (default off, preserving current output):
arcadedb.server.logIncludeTrace=true
Health probes
ArcadeDB exposes two complementary, unauthenticated endpoints so orchestrators can tell a live process from a ready one. See the HTTP API reference for response details.
| Endpoint | Probe | Behavior |
|---|---|---|
|
liveness |
Returns |
|
readiness |
Returns |
Both endpoints also accept HEAD, so healthcheck tools that probe with a HEAD request (e.g. wget --spider, common in Docker HEALTHCHECK instructions) work without a body being returned.
Use /api/v1/health for the liveness probe and /api/v1/ready for the readiness probe:
livenessProbe:
httpGet:
path: /api/v1/health
port: 2480
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/ready
port: 2480
initialDelaySeconds: 20
periodSeconds: 10
HA-aware readiness
On a High Availability cluster you can make readiness reflect Raft membership so traffic is withheld until a node has joined the group:
arcadedb.server.readinessRequiresHA=true
When this flag is true and HA is active, /api/v1/ready returns 503 (Node has not yet joined the Raft group) until a leader has been elected. Once a leader is known, a second gate checks that the node is a member of the current Raft configuration and, for a follower, that it has caught up to within arcadedb.server.readinessHAMaxLag log entries (default 100) of the commit index; until then it keeps returning 503 (Node is not yet in the Raft configuration or has not caught up). Only then does it return 204. This prevents a (re)joined follower with a wiped or lagging log from being marked ready mid-catch-up, which would otherwise let a rolling restart drop the write quorum. The default (false for readinessRequiresHA) preserves current readiness behavior.
With readinessRequiresHA on, a third reason can hold a node at 503 (since v26.10.1): its users, groups and API tokens
may not be the cluster’s. These live in server-users.jsonl, server-groups.json and server-api-tokens.json, outside
the databases, so no snapshot carries them (see Cluster security documents). Two kinds of node
are held:
-
a node added to the cluster while running (
POST /api/v1/cluster/peer,connect clusteror a Kubernetes auto-join) that has not yet installed the documents the leader seeds to it; -
a node caught up by a snapshot install from the leader, until the leader confirms its copies or sends new ones.
Until then the node enforces its own copies, so a user dropped, a group narrowed or a token revoked while it was away
would still work there. /api/v1/ready answers 503 with Cluster security documents have not reached this node yet:
<documents>. A statically configured member that simply restarts is not held, and neither is a node while it leads,
because nobody else can confirm its copies.
The hold is bounded by arcadedb.ha.securityConvergenceReadinessTimeout (default 30000 ms; 0 turns it off). When the
window expires the node reports ready anyway, so a scale-up or rolling restart cannot stall behind a seed that never
arrives, and logs a single SEVERE line naming the documents:
Reporting READY after waiting 30000ms for the cluster's security documents, which never reached this node: server-groups.json. ...
When you see it, re-run connect cluster or re-POST the node to /api/v1/cluster/peer to send the seed again, or reissue
the security change. The gRPC Ready RPC uses the same check.
The replication-log-writer check described in the table above is deliberately not behind this flag. This flag gates a node that is merely behind and which you may reasonably choose to serve from; a node whose log writer has failed cannot catch up at all, so it is withheld from traffic whether or not the flag is set. See Kubernetes deployment for the full StatefulSet example.
Further reading
-
Monitoring — Prometheus and Grafana setup
-
Kubernetes deployment — Helm chart and probe configuration
-
Settings — all configuration keys