esops-doctor is a read-only diagnostic linter for self-managed Elasticsearch and OpenSearch clusters. Think kube-bench / kube-score, but for ES/OS: “Tell me what’s wrong.”

It’s the diagnostic counterpart to esops. Where esops is imperative and may change your cluster, esops-doctor is declarative, opinionated, and never mutates. It scans a cluster for known anti-patterns, mis-configurations, and hygiene gaps, then reports each finding with a severity and a remediation hint — often the exact esops command that fixes it.

What it is

A single statically linked Go binary — no JVM, no Python runtime, no Docker. It drops straight into a CI job, a bastion host, or a scratch image and runs anywhere.

The CLI surface is identical whether you point it at Elasticsearch or OpenSearch. The two are not pretended to be the same product — a rule that only makes sense for one is tagged accordingly and silently skipped (and reported as skipped) on the other. It probes the cluster first to find out what it’s actually talking to, rather than trusting your config.

Read-only by construction

This is the headline guarantee, and it’s not a promise — it’s enforced by the build. The cluster is only ever touched through a small set of read-only capabilities, and CI fails the build if any code path so much as references a mutating operation. An operator can run esops-doctor against production with no fear, from CI or a bastion, without any “are you sure?” ceremony, because there is nothing it can change.

Probes read cluster metadata only — settings, mappings, templates, policies, health, and stats. They never read the contents of your documents.

What it can check

  • Resource sanity — heap size (≈50% RAM, ≤31 GB for compressed oops), bootstrap.memory_lock, shards per node, shard-size distribution, disk watermarks, zone awareness, dedicated master nodes, JVM GC ergonomics.
  • Mapping & index anti-patterns — dynamic mapping in production, unbounded keyword cardinality, deeply nested objects, missing templates on time-series patterns, deprecated field types, drift between templates and live indices.
  • Lifecycle & retention — ILM/ISM policy presence on time-series indices, snapshot policy plus a recent successful run, repository configuration, retention gaps, pending-task accumulation.
  • Security posture — anonymous access, TLS on HTTP and transport, default credentials, audit logging, overly permissive roles, stale API keys, deprecated realms.
  • Operational hygiene — cluster-settings drift, version skew / upgrade readiness, deprecation logs, and parity with the cluster’s own bootstrap checks.

Built for pipelines

  • Zero telemetry. No traces, no metrics, no “anonymised crash reports” — nothing leaves the machine. By design, forever.
  • Profiles for prod, staging, dev, ci, and a CIS-inspired cis-bench subset. The prod profile promotes hygiene findings toward critical; dev demotes them.
  • Waivers to silence a known finding, each requiring a justification and supporting an optional expiry. Expired waivers fail loud — the finding re-surfaces rather than staying quietly suppressed.
  • Scriptable output. Table (default), JSON, YAML, SARIF, JUnit, and a self-contained HTML report — all with stable schemas and documented, stable exit codes. Stdout is pure report; logs and progress go to stderr, so it stays pipeable.

Rules are data, not code

Rules are YAML, evaluated with CEL (the same expression language Kubernetes admission controllers use). Adding a rule is a YAML change, not a Go change — a non-Go contributor can write one.

checks:
  - id: heap_size
    name: JVM heap size configuration
    category: resource_sanity
    severity: critical
    description: Heap should be ~50% of RAM and ≤31GB for compressed oops.
    probe: nodes
    condition: |
      size(self) > 0 &&
      self.all(node,
        node.jvm.heap.max <= 31 * 1024 * 1024 * 1024
      )
    message: Heap size misconfigured on {{count}} nodes.
    dialects: [elasticsearch, opensearch]

The binary ships with a baked-in catalog, but you can layer your own rules on top with --rules-dir PATH (or drop them in ~/.config/esops-doctor/rules.d/). A custom rule with the same id as a built-in one shadows it, so tuning a baked-in rule’s severity or threshold is a one-file change rather than a fork.

A few examples

Scan the production cluster:

$ esops-doctor scan --context prod
SEVERITY  RULE              CATEGORY         MESSAGE
critical  heap_size         resource_sanity  Heap size misconfigured on 2 nodes.
error     ilm_on_timeseries lifecycle        3 time-series indices have no ILM policy.
warn      tls_transport     security         Node-to-node TLS is not enabled.

3 findings: 1 critical, 1 error, 1 warn

Produce a SARIF report for GitHub code scanning, with a production profile:

esops-doctor scan --profile prod --output sarif > findings.sarif

Explain a single rule — what it checks, why, and how to fix it:

esops-doctor explain heap_size

List the rule catalog, filtered to the security checks:

esops-doctor list-rules --tags security

Scan several clusters in one go, and render an HTML report:

esops-doctor scan --targets prod,staging --output html > report.html

In a CI pipeline

esops-doctor returns documented, stable exit codes. The interesting one is 20: “findings at or above the --fail-on threshold” — the normal way a pipeline fails a scan. Cluster-unreachable, auth, and usage errors each get their own code, so a red pipeline tells you why without parsing output.

# Fail the job on any finding of 'error' severity or worse
esops-doctor scan --profile ci --fail-on error

Configuration

There’s nothing new to configure. esops-doctor reuses the esops config file (~/.config/esops/config.yaml) and its contexts — same clusters, same auth, same secret indirection. If you have esops set up, you have esops-doctor set up.

esops-doctor scan --context prod          # use a configured context
esops-doctor scan --url http://localhost:9200   # one-off, no config needed

In short

If you operate Elasticsearch or OpenSearch and want a fast, honest answer to “what’s wrong with this cluster?” — without any risk of changing it — esops-doctor is the read-only second opinion. It pairs with esops: doctor tells you what’s wrong, esops fixes it.