Skip to content
Talk to our solutions team

Observability

data.svc emits four things: metric instruments on the engine and on every route, one span per request, structured logs on stdout, and a set of admin routes that report live runtime state. Only the last two reach you without extra wiring.

SignalEmittedLeaves the process
Engine metrics — operations, transactions, hook errorson every operationno exporter is initialised
HTTP server metrics — count, in-flight, bytes, latencyper wrapped routeno exporter is initialised
Request spansper wrapped routeno exporter is initialised
Logsalwaysyes — stdout
Runtime state (queues, locks, tenant inventory)on requestyes — admin routes
Compiled SQL for one requeston requestyes — ?stats=true
Engine debug logger (AST + SQL + timings)library onlynot wired in this service
EXPLAIN / query planneverdoes not exist

The engine reports lifecycle events to one process-wide observer, built once and shared by every per-tenant engine. Ten instruments are registered. If the meter is unavailable or any registration fails, the observer degrades to a no-op for the life of the process and logs the reason at error level — engine construction never fails on telemetry.

MeasuresKindUnitLabelsFires
Engine operationscounteroperation, entity, statusend of every query, create, update, delete, restore
Operation latencyhistogrammsoperation, entity, statussame call; covers hooks, compile, driver and result scan
Rows returned or affectedhistogramoperation, entitysame call, skipped when the count is unknown
Transactionscountercommitted, statusend of every transaction
Transaction latencyhistogrammscommitted, statusbegin through commit or rollback
Hook errorscounterhook, phase, operation, entityonly when a hook returns an error
Migration runscounternonelibrary migration path only
Schema objects createdcounternonelibrary migration path only
Migration step failurescounternonelibrary migration path only
Migration latencyhistogrammsnonelibrary migration path only

On the operation, latency and rows instruments, operation is one of query, create, update, delete, restore, and status is ok or error. phase is one of BeforeValidate, AfterValidate, BeforeExec, AfterExec, AfterScan. Successful hooks emit nothing, so hook-error count is a rate you can alert on directly.

Nested transactions report once, at the outer call. A commit failure and a rollback both arrive with committed=false; the status label separates a clean rollback from a driver error.

The four migration instruments never fire in data.svc. They are emitted by the library’s own create-if-missing migration call, which this service does not use — its plan and apply pipeline is a separate path with no observer attached.

Every route registered by the service is wrapped in a per-route instrumentation middleware. Five instruments are created on first use:

InstrumentKindRegistered unit
http.server.request_countcounter
http.server.active_requestsup-down counter
http.server.request_content_lengthcounterBy
http.server.response_content_lengthcounterBy
http.server.durationhistogramMs

Each carries http.method, http.scheme, user_agent.original and http.route (the route pattern, not the resolved path). When the request resolved a tenant, it also carries customer, product, env and tenant.

http.status_code is added to the attribute set only after the handler returns, so it is present on the two content-length counters and on http.server.duration, and absent from http.server.request_count.

/data/health and /data/ready are registered directly, ahead of the instrumentation wrapper, so they produce neither metrics nor spans. See Checking a tenant is servable for what the probes actually assert.

The wrapper carries a fallback that degrades to a plain passthrough — no metrics, no span — when the process resolves no service instance id. It never triggers in a booted service: the chassis registers a service-discovery client unconditionally, and when servicediscovery_url is unset that client is a stub whose service id is a non-empty placeholder. Every route is instrumented whether or not you set sid or servicediscovery_url; the placeholder id is what lands on the span as the server name.

One server span per instrumented request. The span name and the http.route attribute are both the route pattern. W3C traceparent and baggage headers on the incoming request are extracted, so the span joins a caller’s trace; a valid remote span already on the context is attached as a link. Tenant identity rides on the span as the same four attributes the metrics use, and any tags request headers are copied onto it. When the handler returns, the span records http.status_code and a span status derived from it. A second span is opened once at boot, around service start.

A panic inside a handler is recorded through the tracer by the router’s panic handler.

The engine adds nothing. It reports through metric events, not spans, so a captured trace is one span per request with no child spans for compile, hooks or the database round trip. Per-operation timing comes from the operation-latency histogram, or from ?stats=true on a single request.

Error responses carry no request id and no trace id. The response envelope is error, code and details only. Correlate a client-side failure with the logs by tenant, route and timestamp.

Logs are structured records written to stdout through a console writer, timestamped RFC 3339. Colour is applied only when stdout is a terminal, so a redirected stream or a supervisor-captured stream is clean. Every line carries service_id, datacenter and cluster, taken from the sid, datacenter and cluster configuration keys.

KeyTypeDefaultEffect
log.levelstringerrorMinimum severity: trace, debug, info, warn, error, fatal, panic

These conditions are logged and are invisible everywhere else — no probe fails, no request errors:

ConditionConsequence
A datastore init script failsLogged; the engine still serves traffic
The materialized-refresh runner fails to installLogged; mutations succeed and refresh tasks are never enqueued
Two schema documents declare the same entity or traitLogged at error; the first declaration stands and the later one is ignored
schemasource: is unset, unknown, or points at a missing directoryLogged as a warning; falls back to the metadata service
A tenant listed under loadtenants: fails to warmLogged; boot continues
The rate limiter fails to build, or a route names an undeclared profileLogged; requests are allowed — the limiter fails open
Engine metric registration failsLogged at error; engine metrics become no-ops
jwks.disabled: trueLogged once at error level, as a SECURITY: line — the service is accepting tokens without signature verification

Two logging knobs do nothing in this service, and both appear in shipped example configurations:

  • log.loglinenumbers is read by no code. The service builds its logger with source line numbers off, unconditionally.
  • realtimedebug, with the _debug_log_ query parameter or the X-Debug-Log header, switches the request context to a debug logger that this service never registers. Every log call in data.svc resolves the service logger directly, so the flag changes no output. Raise log.level instead.

Add ?stats=true to a REST read or write and the response envelope gains a stats block containing the compiled sql, one entry per relation eager-load subquery in include_sql[], duration_ms, and rows_affected on mutations. This is the only per-request SQL visibility that works against a deployed service, and it is available to any caller who can make the request.

Terminal window
curl -s 'https://api.example.com/data/rest/invoice?status=open&stats=true' -H 'Authorization: Bearer <token>' -H 'X-Customer: acme' -H 'X-Product: erp' -H 'X-Env: prod' -H 'X-Tenant: eu1'

The full flag set and the exact envelope are in Request flags.

The engine also carries a debug logger that reports far more: the operation, the entity, the parsed query rendered in pipe syntax, the compiled SQL, the bound arguments, every eager-load subquery, the statement count, the row count and the elapsed milliseconds. It is not wired in data.svc — there is no configuration key that turns it on. It is an option on the in-process engine, so it is reachable only from a Go service that opens the engine itself; see Embedding the engine. The standard implementation writes to stderr and prints argument count rather than argument values unless explicitly told otherwise, because bound arguments carry plaintext user input.

There is no EXPLAIN. No route, no query DSL keyword and no request flag returns a database query plan or a cost estimate, and the service issues no EXPLAIN of its own. To read a plan, take the statement from stats.sql and run it against the database yourself.

These routes report what the process is currently doing for the calling tenant. All require the four CPET headers and a token.

RouteReportsGate
GET /data/admin/data/materialize/statsCross-entity refresh queue depth by status, plus cumulative runner counters. The runner block is absent when no runner exists for this tenantadmin
GET /data/admin/data/materialize/pendingPending and failed refresh tasks with attempt counts — page size fixed at 100admin
GET /data/admin/data/locksCoordination locks with holder, operation kind, acquisition and expiry, is_expired and seconds_to_expiry. Accepts prefix, op_kind and include_expiredadmin
GET /data/superadmin/tenantThe tenant keys this process currently holds live resources for — the closest thing to a cache inventorysuperadmin
GET /data/admin/data/hashesNothing. The compiled-query catalogue is a stub and always returns an empty listadmin

Each /data/admin/data/… route above has a /data/superadmin/tenant/{tenant}/… twin — the same handler, re-rooted, reading the state of a named sibling tenant. The twin path drops the admin/data segment: GET /data/superadmin/tenant/{tenant}/locks. GET /data/superadmin/tenant itself has no admin equivalent, because an admin cannot list tenants it does not belong to. Migration plans, applied-step history and seed state are covered in Migrations.

Not availableDetail
Query plansNo EXPLAIN emitter exists anywhere in the service
Connection pool statisticsThe pool registry tracks reference counts and endpoint snapshots internally; no route exposes them
Per-tenant engine metricsTenant is not a label on any engine instrument, by design
A slow-query logNo latency threshold triggers a log line; latency is only in the histogram and in ?stats=true
The compiled-query cache catalogueThe route is a stub returning an empty list

The security audit stream is a further gap. Access denials and internal errors from the compiler are recorded into a tamper-evident hash chain that is held in memory, in one process-wide sink shared by every tenant engine, with no route that reads it and no eviction — it grows for the life of the process. Successful mutations are not recorded at all: the hook that emits them exists in the library but the deployable service does not register it. A durable, readable audit trail requires either per-entity history and change data capture on the entities you care about, or a Go host that wires its own append-only sink.