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-inspiredcis-benchsubset. Theprodprofile promotes hygiene findings toward critical;devdemotes 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 warnProduce a SARIF report for GitHub code scanning, with a production profile:
esops-doctor scan --profile prod --output sarif > findings.sarifExplain a single rule — what it checks, why, and how to fix it:
esops-doctor explain heap_sizeList the rule catalog, filtered to the security checks:
esops-doctor list-rules --tags securityScan several clusters in one go, and render an HTML report:
esops-doctor scan --targets prod,staging --output html > report.htmlIn 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 errorConfiguration
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 neededIn 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.