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.

HTTP → query → tx Observation one Micrometer instrument correlation id attached Timer /prometheus (scrape) OTLP push Span traceparent JSON log traceId / requestId OpenTelemetry Collector traces + metrics Prometheus pull / scrape stdout log shipper

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

arcadedb.http.requests

method, path (route template, e.g. /query/{database} — never the raw URI), status, db (the database name, or unknown when the request names a database that does not exist)

arcadedb_http_requests_seconds_*

arcadedb.query.duration

protocol (http, ws, bolt, postgres, mongo, grpc, redis, gremlin, mcp, internal), db, language (sql, opencypher, gremlin, graphql, mongo), type (query or command)

arcadedb_query_duration_seconds_*

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

arcadedb.engine.page.cache.hits / .page.cache.misses

Page cache hit/miss counters

arcadedb.engine.pages.read / .pages.written

Pages read from / written to disk

arcadedb.engine.wal.bytes.written / .wal.files

Write-ahead-log bytes and file count

arcadedb.engine.mvcc.conflicts

MVCC concurrent-modification conflicts

arcadedb.engine.files.open

Open file handles

arcadedb.engine.tx.write / .tx.read / .tx.rollbacks

Write/read transactions and rollbacks

arcadedb.engine.queries / .commands

Query and command counts

arcadedb.engine.databases

Number of open databases

arcadedb.engine.async.boundary.commits

Asynchronous batch transactions cut short by a durability-flag change

arcadedb.engine.async.workers.retiring

Asynchronous workers a lowered parallel level retired and that are still draining

arcadedb.engine.graph.light.edge.census.fallbacks

Lightweight-edge lists the census could not count, whose edges each walk the list alone

arcadedb.engine.bucket.recount.publishes.refused

Bucket count() recomputes not cached because a replicated apply ran under the scan (Since v26.10.2)

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.endpoint is an OTLP/gRPC endpoint (default port 4317).

  • tracing.samplingRate is a parent-based ratio in [0.0, 1.0]: 1.0 samples 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

arcadedb.http.server.requests

method, path (route template), db, status

arcadedb.query

protocol, db, language, type, and db.statement (the full query/command text)

The db.statement span attribute contains the full query/command text, which may include sensitive data. Secure your OTLP collector endpoint and apply collector-side redaction if required.

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

GET, HEAD /api/v1/health

liveness

Returns 204 whenever the HTTP layer is up. Performs no database I/O and never returns a busy/503 status, so a node that is merely warming up is not killed. On a High Availability cluster it returns 503 if the node has given up recovering from a crash loop, so the orchestrator restarts a node that otherwise cannot recover on its own.

GET, HEAD /api/v1/ready

readiness

Returns 204 once the server is ONLINE; 503 (Server not started yet) before that. On a High Availability cluster it also returns 503 while this node’s replication log writer has failed — a node in that state cannot catch up, so everything it serves is frozen at the moment the writer failed. It rejoins on its own once the writer recovers. (Since v26.10.1; this check is not behind server.readinessRequiresHA, see below.) Otherwise unchanged by default.

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 cluster or 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