{"id":365745,"date":"2026-09-10T08:37:18","date_gmt":"2026-09-10T08:37:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/ops-kit\/"},"modified":"2026-09-10T08:36:57","modified_gmt":"2026-09-10T08:36:57","slug":"klarsmith-ops-kit","status":"publish","type":"plugin","link":"https:\/\/ko.wordpress.org\/plugins\/klarsmith-ops-kit\/","author":23562605,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.1.7","stable_tag":"0.1.7","tested":"7.1","requires":"6.4","requires_php":"8.3","requires_plugins":null,"header_name":"Klarsmith Ops Kit","header_author":"klarsmith","header_description":"Makes WordPress legible to Kubernetes and Prometheus \u2014 honest readiness, snapshot-backed metrics, structured JSON logs.","assets_banners_color":"06202e","last_updated":"2026-09-10 08:36:57","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/klarsmith\/wp-ops-kit","header_author_uri":"https:\/\/klarsmith.com","rating":0,"author_block_rating":0,"active_installs":0,"downloads":27,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.1.7":{"tag":"0.1.7","author":"klarsmith","date":"2026-09-10 08:36:57","revision":3689566}},"upgrade_notice":{"0.1.7":"<p>Name change only; no configuration changes.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3689566,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3689566,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3689566,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3689566,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.1.7"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[34356,194475,1522,5603,248561],"plugin_category":[54],"plugin_contributors":[280067],"plugin_business_model":[],"class_list":["post-365745","plugin","type-plugin","status-publish","hentry","plugin_tags-health-check","plugin_tags-kubernetes","plugin_tags-logging","plugin_tags-monitoring","plugin_tags-prometheus","plugin_category-security-and-spam-protection","plugin_contributors-klarsmith","plugin_committers-klarsmith"],"banners":{"banner":"https:\/\/ps.w.org\/klarsmith-ops-kit\/assets\/banner-772x250.png?rev=3689566","banner_2x":"https:\/\/ps.w.org\/klarsmith-ops-kit\/assets\/banner-1544x500.png?rev=3689566","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/klarsmith-ops-kit\/assets\/icon-128x128.png?rev=3689566","icon_2x":"https:\/\/ps.w.org\/klarsmith-ops-kit\/assets\/icon-256x256.png?rev=3689566","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>WordPress has no honest health endpoint. The front page, <code>admin-ajax.php<\/code> and the usual\n    health.php stubs all return 200 while the database is down, the object cache is gone, or\nWordPress is serving its \"database update required\" interstitial. Every probe in common\nuse calls that pod ready.<\/p>\n\n<p>Klarsmith Ops Kit gives a containerised WordPress site three things operators actually need:<\/p>\n\n<ul>\n<li><strong>Honest readiness<\/strong> \u2014 <code>GET \/wp-json\/ops\/v1\/readyz<\/code> returns 200 only when the database\nanswers, the schema matches the running core version, the object cache round-trips, and\nthe uploads directory is writable. Anything else is a 503 with the failing check names.\nLiveness is deliberately left alone: a database outage must drain pods, never restart\nthem.<\/li>\n<li><strong>Prometheus metrics<\/strong> \u2014 <code>GET \/wp-json\/ops\/v1\/metrics<\/code> serves the text exposition\nformat from a snapshot that <code>wp ops collect<\/code> writes to the object cache on a schedule.\nNothing expensive runs on scrape. <code>wp_ops_snapshot_age_seconds<\/code> tells you when the\ncollector has stopped. Pod-level series (<code>wp_ops_pod_*<\/code>) and site-level series\n(<code>wp_ops_site_*<\/code>) are split so replicas do not duplicate site numbers.<\/li>\n<li><strong>Structured JSON logs<\/strong> \u2014 with <code>WP_OPS_LOG_JSON=true<\/code>, PHP errors and fatals go to\nstderr as one JSON object per line, ready for a log pipeline.<\/li>\n<\/ul>\n\n<p>The plugin fails closed. <code>\/metrics<\/code> is disabled until a token is configured, and the\nanonymous readiness response names failing checks without leaking detail.<\/p>\n\n<p>Configuration is by environment variable, which is how containers are configured:<\/p>\n\n<ul>\n<li><code>WP_OPS_TOKEN<\/code> \u2014 bearer token for <code>\/metrics<\/code> (<code>Authorization: Bearer<\/code> or\n  X-Ops-Token). Unset disables the endpoint.<\/li>\n<li><code>WP_OPS_SITE_NAME<\/code> \u2014 <code>site<\/code> label on JSON log records (metrics carry no site\nlabel; the scraper's own labels identify the site).<\/li>\n<li><code>WP_OPS_EXPECT_OBJECT_CACHE<\/code> \u2014 fail readiness when no external object cache is active.<\/li>\n<li><code>WP_OPS_REQUIRED_PLUGINS<\/code> \u2014 comma-separated plugin files (<code>dir\/plugin.php<\/code>) that\nmust be active for readiness.<\/li>\n<li><code>WP_OPS_REST_BYPASS_AUTH<\/code> \u2014 set to <code>false<\/code> to stop the plugin allowing anonymous\naccess to its own REST namespace.<\/li>\n<li><code>WP_OPS_LOG_JSON<\/code> \u2014 set to <code>true<\/code> to emit JSON log lines on stderr.<\/li>\n<\/ul>\n\n<p>WP-CLI commands: <code>wp ops check<\/code> (exit 1 on any failing check), <code>wp ops collect<\/code>\n(refresh the snapshot), <code>wp ops metrics<\/code> (print the exposition locally).<\/p>\n\n<p>Source, issue tracker and Kubernetes manifests for probes, the collector CronJob and\nscraping live at https:\/\/github.com\/klarsmith\/wp-ops-kit.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install from the WordPress plugin directory, or with Composer\n(<code>composer require klarsmith\/wp-ops-kit<\/code>; the Composer package installs as\n   wp-content\/plugins\/wp-ops-kit).<\/li>\n<li>Activate the plugin.<\/li>\n<li>Point your readiness probe at <code>\/wp-json\/ops\/v1\/readyz<\/code>.<\/li>\n<li>Run <code>wp ops collect<\/code> on a schedule (a five-minute CronJob is the reference setup).<\/li>\n<li>Set <code>WP_OPS_TOKEN<\/code> and scrape <code>\/wp-json\/ops\/v1\/metrics<\/code> with that bearer token.<\/li>\n<\/ol>\n\n<p>If a security plugin blocks anonymous REST requests, the plugin allows its own <code>ops\/v1<\/code>\nnamespace through at priority 1 unless <code>WP_OPS_REST_BYPASS_AUTH=false<\/code>. Add <code>ops<\/code> to the\nsecurity plugin's allowlist as well where it has one.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"why%20not%20use%20the%20front%20page%20as%20the%20readiness%20probe%3F\"><h3>Why not use the front page as the readiness probe?<\/h3><\/dt>\n<dd><p>Because it returns 200 while the site is broken. The database-upgrade interstitial, a\nmissing object cache and a read-only uploads volume all serve a 200 front page.<\/p><\/dd>\n<dt id=\"why%20does%20readiness%20not%20cover%20liveness%20too%3F\"><h3>Why does readiness not cover liveness too?<\/h3><\/dt>\n<dd><p>If liveness consulted the database, one database blip would fail liveness on every pod\nof every site at once and restart-storm the fleet. Readiness drains; liveness restarts.\nOnly readiness should depend on WordPress.<\/p><\/dd>\n<dt id=\"why%20is%20%2Fmetrics%20computed%20from%20a%20snapshot%3F\"><h3>Why is \/metrics computed from a snapshot?<\/h3><\/dt>\n<dd><p>Walking the cron array and summing autoloaded option sizes on every scrape would load a\nshared database for no benefit. The collector does the work once; scrapes read the\nresult. If the collector dies the snapshot age climbs, which is alertable.<\/p><\/dd>\n<dt id=\"does%20it%20work%20behind%20wpml%20or%20a%20language%20prefix%3F\"><h3>Does it work behind WPML or a language prefix?<\/h3><\/dt>\n<dd><p>Yes. Route detection uses the REST route query variable with a request-URI fallback, so\n    \/en\/wp-json\/ops\/v1\/readyz works the same as <code>\/wp-json\/ops\/v1\/readyz<\/code>.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>0.1.7<\/h4>\n\n<ul>\n<li>Directory listing name is \"Klarsmith Ops Kit\" (slug <code>klarsmith-ops-kit<\/code>);\nComposer\/GitHub name unchanged.<\/li>\n<\/ul>\n\n<h4>0.1.6<\/h4>\n\n<ul>\n<li>Earlier directory listing name \"Ops Kit\" (superseded by 0.1.7).\nTested up to WordPress 7.1.<\/li>\n<\/ul>\n\n<h4>0.1.3<\/h4>\n\n<ul>\n<li>Code conforms to the WordPress Coding Standards; PHPStan level 6 clean. No\nbehaviour change beyond sanitising two request inputs used for route detection.<\/li>\n<li>Copy-paste manifests in <code>examples\/<\/code> for Kubernetes, stock Prometheus,\nVictoriaMetrics and a Grafana dashboard.<\/li>\n<\/ul>\n\n<h4>0.1.2<\/h4>\n\n<ul>\n<li><code>\/metrics<\/code> no longer appends its exposition to a response another handler has already\nserved.<\/li>\n<li>Zero-valued post statuses are dropped at collection time; <code>publish<\/code> is always exported\nso a drop to zero stays alertable.<\/li>\n<\/ul>\n\n<h4>0.1.1<\/h4>\n\n<ul>\n<li>Allow the plugin's own <code>ops\/v1<\/code> REST namespace through site-level anonymous-REST\nlockdowns (hooked at priority 1, never overriding an earlier decision).\nEscape hatch: <code>WP_OPS_REST_BYPASS_AUTH=false<\/code>.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<ul>\n<li>Initial release: readiness endpoint, snapshot-backed Prometheus metrics, JSON logging,\n  wp ops check|collect|metrics commands.<\/li>\n<\/ul>","raw_excerpt":"Makes WordPress legible to Kubernetes and Prometheus: honest readiness, snapshot-backed metrics, structured JSON logs.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/365745","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=365745"}],"author":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/klarsmith"}],"wp:attachment":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=365745"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=365745"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=365745"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=365745"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=365745"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=365745"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}