Observability

July 11, 2026 · View on GitHub

Saiku ships with opt-in OpenTelemetry instrumentation via the OpenTelemetry Java agent. The agent is side-loaded — it is only attached at JVM start when an OTLP endpoint is configured, so deployments that don't want observability pay no cost (no agent loaded, no startup delay, no runtime overhead).

This is Tier 1: zero-code instrumentation. Custom spans for ThinQueryService, AI Query API, and cellset cache (Tier 2) layer on top of the same SDK once we know what's worth tracing — tracked under a follow-up issue.

What gets instrumented automatically

The agent's bytecode-injected instrumentation covers everything Saiku runs on:

ComponentWhat you get
Jetty 12 (EE10)Server span per HTTP request, populated with http.method, http.route, http.status_code, url.path.
Jersey 3.1Resource method attribution on the request span (code.namespace, code.function).
JDBCA child span per SQL statement Mondrian emits, with db.system and db.statement. This is the big win: every slow MDX query is decomposed into the SQL it generated.
java.net.http.HttpClientOutbound calls to AI providers (Anthropic, OpenAI, etc.) and any other downstream HTTP service appear as child spans.
Log4j 2Trace context (trace_id, span_id) injected into MDC. The Saiku log4j pattern renders these as [trace_id=… span_id=…] when present and elides the bracket entirely when absent. Logs can also be exported as OTLP log records.
JVMProcess metrics: heap usage, GC pause duration, thread count, class loading.
DBCP2Connection pool metrics (active, idle, waits).

Enabling it

Docker

The published ghcr.io/spiculedata/saiku image bakes the agent into /opt/saiku/otel/opentelemetry-javaagent.jar. The entrypoint attaches it only when OTEL_EXPORTER_OTLP_ENDPOINT is set:

docker run -d \
  --name saiku \
  -p 8080:8080 \
  -e OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
  -e OTEL_SERVICE_NAME=saiku-prod \
  -e OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production \
  ghcr.io/spiculedata/saiku:latest

Standalone distribution

Download the agent jar manually and drop it next to run.sh / run.bat:

curl -fsSLO https://repo1.maven.org/maven2/io/opentelemetry/javaagent/opentelemetry-javaagent/2.28.1/opentelemetry-javaagent-2.28.1.jar
mv opentelemetry-javaagent-2.28.1.jar opentelemetry-javaagent.jar
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
./run.sh

The wrapper attaches the agent only when both the jar AND the endpoint env var are present.

Hand-rolled (bare java -jar)

java -javaagent:/path/to/opentelemetry-javaagent.jar \
  -jar saiku-<version>.jar serve

Configuration reference

All settings are standard OpenTelemetry SDK environment variables — Saiku passes them straight through to the agent. The full list is in the OTel SDK env-var spec; the ones you'll actually want:

VariableDefaultPurpose
OTEL_EXPORTER_OTLP_ENDPOINTunsetOTLP collector URL. Setting this is what activates the agent. Supports both gRPC (:4317) and HTTP/protobuf (:4318).
OTEL_EXPORTER_OTLP_PROTOCOLgrpcgrpc or http/protobuf. Pick whichever your collector speaks.
OTEL_SERVICE_NAMEsaikuService identifier in your tracing UI. Saiku sets this default when the agent activates; override per environment.
OTEL_RESOURCE_ATTRIBUTESunsetComma-separated key=value pairs. Typical: deployment.environment=demo,service.version=4.3.4.
OTEL_TRACES_EXPORTERotlpotlp / none.
OTEL_METRICS_EXPORTERotlpotlp / prometheus / none.
OTEL_LOGS_EXPORTERotlpotlp / none.
OTEL_TRACES_SAMPLERparentbased_always_onSampling strategy. See sampling below.
OTEL_TRACES_SAMPLER_ARGunsetSampler argument (e.g. ratio for traceidratio).
OTEL_EXPORTER_OTLP_HEADERSunsetFor SaaS providers needing auth: api-key=….

Sampling

The default parentbased_always_on sampler captures every trace — fine for development and the demo, not what you want under production load. For a real deployment:

# Capture 5% of root traces; child spans follow the root's decision.
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.05

If you ingest into a SaaS provider with usage-based billing, lean toward 1–5%. Self-hosted (Tempo, Jaeger) can comfortably handle 25–100% on Saiku's typical query volume.

Verifying it works

Spin up a quick collector via Docker to sanity-check end-to-end:

docker run --rm -p 4318:4318 \
  otel/opentelemetry-collector:latest \
  --config=/etc/otelcol/config.yaml

Default collector config logs received spans to stdout. Send a query to Saiku and you should see spans named GET /rest/saiku/info, POST /rest/saiku/api/query/execute, and JDBC child spans for the Mondrian-generated SQL.

Log lines for the same request will now include the trace ID so you can correlate logs ↔ traces:

2026-06-03T12:34:56.789 INFO  [ThinQueryService] [trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7] Executing MDX …

Provider bundles (saiku#1429)

Four pre-baked provider integrations ship under observability/ — each with a copy-paste quickstart, a Docker Compose bundle (or env template), and a Grafana starter dashboard everyone reuses.

ProviderPathPipeline
Datadogobservability/datadog/Datadog Agent (OTLP receiver) → Datadog SaaS
Grafana Cloudobservability/grafana-cloud/Direct OTLP → Grafana Cloud
New Relicobservability/new-relic/Direct OTLP → New Relic
AWS CloudWatchobservability/cloudwatch/ADOT collector → CloudWatch / X-Ray

The observability/dashboards/grafana/saiku-overview.json dashboard covers HTTP request rate + latency, Mondrian JDBC statement rate + p95, JVM heap, GC pause, JVM threads, DBCP2 pool utilization, and outbound HTTP calls to AI providers. Import via Grafana → Dashboards → Import → Upload JSON. The PromQL queries are compatible with any Prometheus-speaking data source (Grafana Cloud Mimir, self- hosted Prometheus, Datadog's Prometheus API, etc.).

What this doesn't cover

Tier 1 stops at framework-level auto-instrumentation. The following require custom spans (Tier 2):

  • Per-MDX-query attribution (cube name, axis structure, cell count)
  • AI Query API request metadata (model, provider, token usage)
  • Cellset cache hit/miss attribution
  • Mondrian Calcite-vs-legacy backend selection events
  • Schema discovery vs query-execute attribution beyond the HTTP route

These ship in a follow-up. Tier 1 gives us the request/SQL/JVM visibility that 80% of operational questions need; Tier 2 fills in the domain-specific gaps once we know which gaps actually matter in production.