Showing posts with label Monitoring. Show all posts
Showing posts with label Monitoring. Show all posts

Wednesday, 5 August 2026

kube-state-metrics

 


kube-state-metrics (KSM) is an official Kubernetes add-on agent that listens to the Kubernetes API server and generates Prometheus-format metrics about the state of Kubernetes objects (such as Pods, Deployments, Nodes, StatefulSets, and CronJobs).

Unlike agent metrics collectors like cAdvisor or node_exporter, KSM does not measure resource usage (like CPU usage, memory consumption, or disk I/O). Instead, it translates the raw state of objects in the Kubernetes API into structured metrics.

Core Concept: Usage vs. State


cAdvisor / Node Exporter (Usage): "How much CPU is Pod X consuming right now?"
kube-state-metrics (State): "How many replicas are ready in Deployment Y?", "When did CronJob Z last succeed?", "What phase is Pod A in?"


How kube-state-metrics Exposes Metrics


┌─────────────────────────┐
│ Kubernetes API Server   │
└────────────┬────────────┘
             │ Watch / List (Informer Cache)
             ▼
┌─────────────────────────┐
│   kube-state-metrics    │ ─── (Generates OpenMetrics in-memory)
└────────────┬────────────┘
             │ HTTP GET /metrics (Port 8080)
             ▼
┌─────────────────────────┐
│    Prometheus Server    │
└─────────────────────────┘

1. Consuming the API via Kubernetes Informers

KSM connects directly to the Kubernetes API server using standard Client-GO Informers.
  • Instead of constantly polling the API server with heavy requests, KSM maintains an in-memory cache updated in real-time via long-polling watch streams.
  • When a resource changes (e.g., a Deployment scales down or a Pod enters CrashLoopBackOff), the local cache updates instantly.

2. Generating Prometheus OpenMetrics In-Memory

KSM parses the fields of the Kubernetes object specs and status subresources (such as .status.phase, .spec.replicas, .status.conditions) and converts them directly into Prometheus gauge/counter metrics.

3. Serving via HTTP Endpoint

KSM hosts a lightweight HTTP server (default port :8080 at path /metrics).

It does not store metrics over time, nor does it push metrics out.

When a Prometheus server scrapes KSM's /metrics endpoint, KSM reads its in-memory snapshot, formats the raw text payload, and streams it back.

Key Technical Properties


Property
Details

Port & Path
Port 8080 (/metrics for cluster state), Port 8081 (/metrics for KSM internal performance telemetry)

Data Format
Prometheus text format / OpenMetrics standard

Metric Types
Predominantly Gauge metrics representing instantaneous state (1 or 0 state representations)

Scaling
Supports Horizontal Sharding and resource filtering (--resources, --namespaces) for large clusters


Example Metric Output


A scrape request to kube-state-metrics for a Deployment produces plain-text metrics like this:

# HELP kube_deployment_spec_replicas The desired number of pods declared in the deployment spec.
# TYPE kube_deployment_spec_replicas gauge
kube_deployment_spec_replicas{namespace="default",deployment="api-server"} 3

# HELP kube_deployment_status_replicas_available The number of available pods created by the deployment.
# TYPE kube_deployment_status_replicas_available gauge
kube_deployment_status_replicas_available{namespace="default",deployment="api-server"} 2


Do we need kube-state-metrics if we have Prometheus deployed in the cluster?


Yes, you still need kube-state-metrics (KSM) even if Prometheus is deployed in your cluster.

Prometheus is the engine that collects, stores, and queries time-series data, while kube-state-metrics is the agent that generates metrics about the state of Kubernetes API objects.

Prometheus does not natively inspect Kubernetes API objects on its own—it relies on exporters like KSM to expose that data.

What Prometheus collects out-of-the-box vs. with KSM?


Metric Source
Responsible Agent
Examples of Metrics Provided

Node / OS Metrics
node_exporter
Node CPU utilization, RAM usage, disk space, network traffic.

Container Usage
cAdvisor (built into kubelet)
Container CPU throttling, memory usage (RSS), network bytes per container.

Control Plane
kube-apiserver, etcd, coredns
API latency, request counts, etcd commit durations.

Kubernetes State
kube-state-metrics
Deployment replica counts, CronJob last success times, Pod restart counts, pending PVCs, ingress statuses, TLS certificate secret expiration.


What happens without kube-state-metrics?


If you run Prometheus without KSM, you lose visibility into the high-level health and configuration of your cluster resources. You will not be able to:

  1. Alert on Deployment Health: You won't know if spec.replicas (desired) doesn't match status.available_replicas.
  2. Track Job / CronJob Success: Metrics like kube_cronjob_status_last_successful_time or kube_job_status_failed won't exist.
  3. Monitor Pod Lifecycle States: You won't have metrics for Pods stuck in Pending, CrashLoopBackOff status codes, or ImagePullBackOff.
  4. Track Resource Requests vs. Limits: You won't be able to compare requested CPU/memory (kube_pod_container_resource_requests) against node capacity to measure cluster overcommit.

How They Work Together


┌─────────────────────────────────────────────────────────┐
│                    Kubernetes API                       │
└────────────────────────────┬────────────────────────────┘
                             │ Watch API Objects
                             ▼
                 ┌───────────────────────┐
                 │  kube-state-metrics   │
                 └───────────┬───────────┘
                             │ Exposes /metrics
                             ▼
                 ┌───────────────────────┐
                 │   Prometheus Server   │ ◄── (Scrapes KSM)
                 └───────────────────────┘


  1. KSM listens to the API server and generates metrics representing resource states.
  2. Prometheus scrapes KSM's /metrics endpoint along with cAdvisor, node-exporter, and your application endpoints.
  3. Prometheus evaluates Alertmanager rules and stores the metrics for Grafana dashboards.

Standard Stack Setup

In standard Kubernetes monitoring deployments (such as the kube-prometheus-stack Helm chart or Prometheus Operator), kube-state-metrics is included by default as a core component alongside Prometheus and Alertmanager.


How to install kube-state-metrics?



kube-state-metrics (KSM) is typically deployed using one of four common approaches:

1. As Part of the kube-prometheus-stack (Most Common)


If you are setting up cluster monitoring with Prometheus and Grafana, kube-state-metrics is usually installed automatically as a bundled sub-chart.

Using the kube-prometheus-stack Helm chart:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus-stack prometheus-community/kube-prometheus-stack


kube-state-metrics runs out of the box with ServiceMonitors already configured.

2. Standalone Helm Chart


If you already have a Prometheus instance or another monitoring agent (like Datadog, New Relic, or Grafana Alloy) and just need KSM running, you can deploy the official standalone Helm chart:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-state-metrics prometheus-community/kube-state-metrics --namespace kube-system


3. Native Kustomize / Standard Manifests (kubectl apply)


For gitops workflows or lightweight clusters where Helm isn't used, you can apply the official manifests maintained directly in the upstream Git repository using Kustomize:

git clone https://github.com/kubernetes/kube-state-metrics.git
cd kube-state-metrics
kubectl apply -k examples/standard

Note: This creates the necessary ServiceAccount, ClusterRole, ClusterRoleBinding, Deployment, and Service in the kube-system namespace.


4. Cloud Managed Kubernetes Add-ons


On managed platforms (like GKE, EKS, or AKS), kube-state-metrics is often enabled as a one-click add-on or managed component integrated into cloud-native observability services (e.g., Google Cloud Observability, AWS CloudWatch Container Insights, or Azure Monitor).


What Gets Created in the Cluster?


Regardless of how you deploy it, KSM will create:
  • ServiceAccount & ClusterRole/Binding: Grants read-only access (list, watch, get) to cluster API resources.
  • Deployment: Runs the kube-state-metrics container.  
  • Service: Exposes the HTTP endpoint (usually on port 8080 at /metrics).  


How to inspect which default metrics KSM exposes or how to disable unused ones?


Understanding which metrics kube-state-metrics (KSM) emits and how to prune unwanted metrics is key to controlling Prometheus ingestion volume and storage overhead.

Part 1: Inspecting Exposed Metrics


You can inspect the metrics exposed by KSM using three different methods:

1. Official Documentation Reference


The KSM GitHub repository contains generated documentation for every supported Kubernetes API group.
  • Standard Docs: kube-state-metrics/docs contains dedicated files for each resource (e.g., pod-metrics.md, cronjob-metrics.md).
  • Resource Status: The docs categorize metrics as STABLE (default), EXPERIMENTAL (alpha/beta features), or DEPRECATED.  

2. Direct In-Cluster Scraping (curl / port-forward)


To see the exact payload KSM generates in your active cluster, port-forward to the KSM service and fetch /metrics:

# Port-forward the KSM service locally
kubectl port-forward svc/kube-state-metrics -n monitoring 8080:8080

# In a separate terminal, curl the metrics endpoint
curl http://localhost:8080/metrics


Filter for specific resource types using grep:

# View all Pod-related metrics exposed by KSM
curl -s http://localhost:8080/metrics | grep "^kube_pod_"

# View unique metric family names
curl -s http://localhost:8080/metrics | grep -v "^#" | cut -d'{' -f1 | sort -u


3. Prometheus Metric Explorer


In Prometheus, run the following query in the Expression Browser to list all distinct metric names harvested from the KSM job:

count by (__name__) ({job="kube-state-metrics"})


Part 2: Disabling or Filtering Unused Metrics


There are two primary ways to reduce metric volume: at the source (KSM Flags) or at the scraper (Prometheus Relabeling).

Method 1: Filtering at Source via KSM Flags (Recommended)


You can instruct KSM to only collect or expose specific resources or metric families using command-line arguments. This reduces both KSM memory utilization and network output.

--resources: Restricts KSM to watch specific Kubernetes objects only (e.g., ignore configmaps or secrets).
--metric-allowlist: Explicitly lists metric families to expose (all others are dropped).
--metric-denylist: Drops specific metric families while keeping the rest.

Setting via Helm (values.yaml):

# Helm configuration for prometheus-community/kube-state-metrics
resources:
  # Watch only specific K8s API resources
  resources:
    - pods
    - deployments
    - statefulsets
    - cronjobs
    - nodes

# Disable specific metric families across enabled resources
extraArgs:
  - --metric-denylist=kube_configmap_info,kube_secret_info,kube_pod_labels
Setting via Argo CD Application Manifest:YAMLapiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: kube-state-metrics
  namespace: argocd
spec:
  # ... (repo configuration omitted)
  source:
    helm:
      extraArgs:
        - --resources=pods,deployments,cronjobs,nodes
        - --metric-denylist=kube_pod_created,kube_pod_completion_time


Method 2: Dropping Metrics at Prometheus Ingestion


If you do not manage KSM CLI flags directly, configure Prometheus or Grafana Agent/Alloy to drop metrics during the scrape phase using metric_relabel_configs:

Prometheus CustomResource (Prometheus / ServiceMonitor):


apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: kube-state-metrics
  namespace: monitoring
spec:
  endpoints:
    - port: http-metrics
      metricRelabelings:
        # Drop entire metric families matching a regex pattern
        - action: drop
          sourceLabels: [__name__]
          regex: "(kube_configmap_.*|kube_secret_.*|kube_lease_.*)"
        
        # Drop high-cardinality container state metrics if unused
        - action: drop
          sourceLabels: [__name__]
          regex: "kube_pod_container_state_started"


kube_job_status_failed Source Filtering vs. Scraper DroppingFeatureKSM CLI Flags (--resources, --metric-denylist)Prometheus metric_relabel_configsKSM Memory UsageLower (KSM does not store or process ignored objects in memory)Unchanged (KSM processes objects regardless)Network TrafficLower (HTTP payload size is smaller)Unchanged (KSM transmits full payload; Prometheus discards post-fetch)Prometheus DB StorageLowerLowerConfig PlacementKSM Deployment manifestPrometheus scrape configuration



Prometheus Metrics Exposed by kube-state-metrics


kube_job_status_succeeded


kube_job_status_succeeded is a Prometheus metric exposed by kube-state-metrics that tracks the status of Kubernetes Jobs.

It returns a gauge value representing whether a Job execution succeeded, partitioned by label selectors.

Metric Breakdown


  • Metric Name: kube_job_status_succeeded
  • Type: Gauge
  • Value:
    • 1: The Job has completed successfully.
    • 0: The Job is in progress, failed, or has not succeeded.

Key Labels


Label                  Description
====                  =========
job_name        The name of the Kubernetes Job resource
namespace      The namespace where the Job resides
condition      Condition status (typically true when checking successful completion)


Common PromQL Queries


1. List All Currently Succeeded Jobs


kube_job_status_succeeded{condition="true"} == 1

2. Detect Jobs That Failed or Did Not Succeed


kube_job_status_succeeded{condition="true"} == 0

3. Alerting Rule: Job Failure


Alert when a Job has completed its execution attempt without succeeding:

- alert: KubernetesJobFailed
  expr: kube_job_status_failed{condition="true"} == 1
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Kubernetes Job {{ $labels.namespace }}/{{ $labels.job_name }} failed"


4. Track Job Completion Rate Over Time


Calculate the rate of successful Job completions across a cluster:

sum(increase(kube_job_status_succeeded{condition="true"}[1h]))


kube_job_status_failed


kube_job_status_failed is a Prometheus metric exposed by kube-state-metrics that tracks whether a Kubernetes Job has failed to execute successfully.

It returns a gauge value representing the failure status of a Job, partitioned by label selectors.

Metric Breakdown


  • Metric Name: kube_job_status_failed
  • Type: Gauge
  • Value:
    • 1: The Job reached its failure condition (e.g., exceeded backoffLimit or failed execution).
    • 0: The Job has not failed (it is currently running, pending, or succeeded).

Key Labels


Label                Description
====                =========
job_name      The name of the Kubernetes Job resource
namespace    The namespace where the Job resides
condition    Condition status (typically true when evaluating an active failure state)
reason           The reason for failure if populated (e.g., BackoffLimitExceeded, DeadlineExceeded)


Common PromQL Queries



1. List All Currently Failed Jobs


kube_job_status_failed{condition="true"} == 1

2. Count Failed Jobs by Namespace


sum by (namespace) (kube_job_status_failed{condition="true"} == 1)

3. Prometheus Alerting Rule for Job Failures


Alert when a Job has reached a failed condition and remained failed for 5 minutes:

- alert: KubernetesJobFailed
  expr: kube_job_status_failed{condition="true"} == 1
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Kubernetes Job {{ $labels.namespace }}/{{ $labels.job_name }} failed"
    description: "Job {{ $labels.job_name }} in namespace {{ $labels.namespace }} failed to complete."


4. Filter CronJobs / Generated Jobs by Prefix


If Jobs are generated dynamically by CronJobs, group or filter using regular expressions:

kube_job_status_failed{condition="true", job_name=~"nightly-backup-.*"} == 1



kube_cronjob_status_last_successful_time



kube_cronjob_status_last_successful_time is a gauge metric exposed by kube-state-metrics.

It records the Unix timestamp (in seconds) of when a Kubernetes CronJob last completed execution successfully.

Metric Details


Attribute:Value

  • Exporter: kube-state-metrics
  • Metric Type:Gauge
  • Value: Unix timestamp (seconds) or omitted if the CronJob has never completed
  • Labels: cronjob, namespace

Common PromQL Use Cases


1. Time Since Last Successful Run


Calculates how many seconds have elapsed since the CronJob last succeeded:

time() - kube_cronjob_status_last_successful_time

2. Alert on CronJob Failure or Missed Schedule


Alerts if the time since the last successful execution exceeds a target threshold (e.g., 1 day / 86,400 seconds):

(time() - kube_cronjob_status_last_successful_time) > 86400

3. Alerting when Schedule Run Failed


Compares the last scheduled time (kube_cronjob_status_last_schedule_time) against the last successful time. If last_schedule_time is greater than last_successful_time, the most recent run failed or is taking abnormally long:

kube_cronjob_status_last_schedule_time - kube_cronjob_status_last_successful_time > 0

4. Filter out Suspended CronJobs


Combine with kube_cronjob_spec_suspend to suppress false alerts on intentionally paused jobs:

(
  (time() - kube_cronjob_status_last_successful_time) > 86400
) and on (cronjob, namespace) (
  kube_cronjob_spec_suspend == 0
)



Tuesday, 4 August 2026

Monitoring term: Dead-man's switch


It's monitoring inverted: instead of alerting when you observe something bad, you alert when you stop observing something good.

Example: ping on a successful run. If run is unsuccessful, monitoring catches missing ping and triggers alert.

The name comes from industrial safety — the lever on a train's throttle or a chainsaw that has to be actively held down. If the operator dies or lets go, the machine stops. Safety is the default state; it takes continuous positive action to keep running.

Normal alerting is presence-based. Something goes wrong, it emits a signal, you alert on the signal: error rate spikes, latency crosses a threshold, a pod enters CrashLoopBackOff. It works well when failures are noisy.

A dead-man's switch is absence-based. The healthy system periodically says "still fine." You alert when that message doesn't arrive on time.

Thursday, 14 May 2026

Introduction to Checkly



How Checkly works

Checkly is a SaaS synthetic monitoring platform — you define "checks" (HTTP requests or browser scripts), Checkly runs them on a schedule from probe locations around the world (or on-demand from CI), records latency/assertions/screenshots, and alerts you when they fail or get slow.
  
  Two main check types:

  - API checks — a single HTTP request with assertions on status, headers, body, response time.
  - Browser checks — a Playwright script run in a real headless Chromium against your deployed app.

There's also multi-step API checks (chain requests, e.g. login → use token → logout) and heartbeat checks (your job pings Checkly; alert if it stops).

Hearbeat vs Ping 

Heartbeats and pings are both vital network failure-detection mechanisms, but they differ in purpose: Heartbeats are proactive, periodic "I am alive" messages sent by an application to signal it is healthy, while Pings are reactive requests to check if a server is reachable. Heartbeats detect application crashes, while pings detect network downtime.

Checks are typically authored as code (Checkly CLI, TypeScript) and checkly deploy'd to the cloud. You can tag them (tags: ["auth"]), parametrise them with env vars like ENVIRONMENT_URL, and trigger them on-demand from CI — which is exactly what this PR does with npx checkly trigger --tags=auth.
  
  Runtime model:
  - Scheduled: every N minutes from chosen regions (e.g. us-east-2, eu-west-1) — catches regressions/outages between deploys.
  - Triggered from CI: post-deploy smoke test, results gate (or just annotate) the deploy.
  - Alerts: Slack/PagerDuty/email on failure, with retry/degraded thresholds to avoid flap.

  ---

  What it would check for this auth API
  
  Given the auth API's surface (login, OAuth, JWT issuance, admin endpoints), realistic auth-tagged checks:

  1. Health endpoint — basic liveness

  new ApiCheck("auth-health", {
    name: "Auth API – health",
    tags: ["auth"],
    frequency: 1, // minute
    locations: ["us-east-2", "eu-west-1"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/health`,
      method: "GET",
      assertions: [
        AssertionBuilder.statusCode().equals(200),
        AssertionBuilder.responseTime().lessThan(500),
        AssertionBuilder.jsonBody("$.status").equals("ok"),
      ],
    },
  });

  2. Login flow — happy path, returns a JWT

  new ApiCheck("auth-login", {
    name: "Auth API – login returns JWT",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/auth/login`,
      method: "POST",
      headers: [{ key: "Content-Type", value: "application/json" }],
      body: JSON.stringify({
        email: process.env.SYNTHETIC_USER_EMAIL,
        password: process.env.SYNTHETIC_USER_PASSWORD,
      }),
      assertions: [
        AssertionBuilder.statusCode().equals(200),
        AssertionBuilder.responseTime().lessThan(1500),
        AssertionBuilder.jsonBody("$.token").isNotNull(),
        // structural check on JWT shape
        AssertionBuilder.jsonBody("$.token").matches("^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$"),
      ],
    },
  });
  
  3. Login — wrong password returns 401 (negative path)

  Catches the "accidentally accepts anything" class of regression.

  new ApiCheck("auth-login-bad-pw", {
    name: "Auth API – wrong password = 401",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/auth/login`,
      method: "POST",
      headers: [{ key: "Content-Type", value: "application/json" }],
      body: JSON.stringify({ email: process.env.SYNTHETIC_USER_EMAIL, password: "wrong" }),
      assertions: [AssertionBuilder.statusCode().equals(401)],
    },
  });
  
  4. Multi-step — login then call protected endpoint

  This is the most useful kind for an auth API, because it proves the token actually works.

  new MultiStepCheck("auth-token-roundtrip", {
    name: "Auth API – token works against /me",
    tags: ["auth"],
    code: { entrypoint: path.join(__dirname, "token-roundtrip.spec.ts") },
  });
  // token-roundtrip.spec.ts
  import { test, expect } from "@playwright/test";
  test("login then /me", async ({ request }) => {
    const login = await request.post(`${process.env.ENVIRONMENT_URL}/auth/login`, {
      data: { email: process.env.SYNTHETIC_USER_EMAIL, password: process.env.SYNTHETIC_USER_PASSWORD },
    });
    expect(login.ok()).toBeTruthy();
    const { token } = await login.json();
    
    const me = await request.get(`${process.env.ENVIRONMENT_URL}/me`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    expect(me.status()).toBe(200);
    const body = await me.json();
    expect(body.email).toBe(process.env.SYNTHETIC_USER_EMAIL);
  });
  
  5. TLS & cert expiry

  A pure config check — useful because cert rotation is a classic outage cause.

  new ApiCheck("auth-tls", {
    name: "Auth API – TLS cert valid > 14d",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/health`,
      method: "GET",
      assertions: [AssertionBuilder.statusCode().equals(200)],
    },
    // Checkly surfaces cert expiry on the run; you set a threshold per check
  });
  
  6. Browser check — full login UX

    expect(login.ok()).toBeTruthy();
    const { token } = await login.json();

    const { token } = await login.json();

    const me = await request.get(`${process.env.ENVIRONMENT_URL}/me`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    expect(me.status()).toBe(200);
    const body = await me.json();
    expect(body.email).toBe(process.env.SYNTHETIC_USER_EMAIL);
  });

  5. TLS & cert expiry

  A pure config check — useful because cert rotation is a classic outage cause.

  new ApiCheck("auth-tls", {
    name: "Auth API – TLS cert valid > 14d",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/health`,
      method: "GET",
      assertions: [AssertionBuilder.statusCode().equals(200)],
    },
    // Checkly surfaces cert expiry on the run; you set a threshold per check

  5. TLS & cert expiry

  A pure config check — useful because cert rotation is a classic outage cause.

  new ApiCheck("auth-tls", {
    name: "Auth API – TLS cert valid > 14d",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/health`,
      method: "GET",
      assertions: [AssertionBuilder.statusCode().equals(200)],
    },
    // Checkly surfaces cert expiry on the run; you set a threshold per check
  });

  new ApiCheck("auth-tls", {
    name: "Auth API – TLS cert valid > 14d",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/health`,
      method: "GET",
      assertions: [AssertionBuilder.statusCode().equals(200)],
    },
    // Checkly surfaces cert expiry on the run; you set a threshold per check
  });

  6. Browser check — full login UX

  Runs against the front-end but exercises the auth API end-to-end including redirects, cookies, CSRF.

  new BrowserCheck("auth-ui-login", {
    name: "Login UI works",
    tags: ["auth"],
    code: { entrypoint: path.join(__dirname, "login.spec.ts") },
  });
  import { test, expect } from "@playwright/test";
  test("user can sign in", async ({ page }) => {
    await page.goto(process.env.ENVIRONMENT_URL!);
    await page.getByLabel("Email").fill(process.env.SYNTHETIC_USER_EMAIL!);
    await page.getByLabel("Password").fill(process.env.SYNTHETIC_USER_PASSWORD!);
    await page.getByRole("button", { name: "Sign in" }).click();
    await expect(page.getByText("Dashboard")).toBeVisible({ timeout: 10_000 });
  });

  7. OAuth callback reachability

  Doesn't fully exercise the Google/Microsoft flow (those need real consent), but checks the callback
  endpoint responds correctly to a missing-code request — confirms route + handler are wired.

  new ApiCheck("auth-oauth-google-callback-shape", {
    name: "Auth API – Google OAuth callback exists",
    tags: ["auth"],
    request: {
      url: `${process.env.ENVIRONMENT_URL}/auth/google/callback`,
      method: "GET",
      assertions: [
        // 400 for missing `code`, not 404/500 — proves handler is mounted
        AssertionBuilder.statusCode().equals(400),
      ],
    },
  });

Thursday, 19 March 2026

Monitoring and Observability

 

Monitoring vs Observability

In the world of IT and DevOps, monitoring and observability are two related but distinct concepts used to manage system health and performance. 

Core Difference


The simplest way to distinguish them is:
  • Monitoring tells you what is happening (and when). It is reactive and focuses on known problems using predefined metrics.
  • Observability tells you why it is happening. It is proactive and uses the system's outputs to understand its internal state, especially for "unknown unknowns". 

Key Comparison Table


Feature         Monitoring                 Observability
----------           ---------------                    -----------------    
Purpose         Detect known issues Diagnose root causes
Perspective External (symptoms) Internal (system state)
Question         "Is the system healthy?" "Why is it behaving this way?"
Approach Reactive                         Proactive
Focus         "Known knowns"         "Unknown unknowns"
Data Types Metrics, logs                 Metrics, logs, and traces


Analogy: The Car

  • Monitoring is your dashboard. It has dials for speed and fuel, and a "check engine" light. It tells you if you are speeding or if something is broken.
  • Observability is the mechanic’s diagnostic tool. When the "check engine" light comes on, the mechanic plugs in a tool to see exactly which sensor failed and why, without having to take the entire engine apart. 

Common Tools

  • Monitoring Tools: Nagios, Zabbix, Prometheus.
  • Observability Platforms: Datadog, New Relic, Honeycomb, Dynatrace.





Three Pillars of Observability


The three pillars of observability—metrics, logs, and traces—are essential telemetry data types used to understand the internal state of complex, distributed systems. They enable teams to detect, investigate, and resolve performance issues by providing high-level trends, granular event details, and full request-flow paths. 

Metrics


Quantitative measurements over time (e.g., CPU usage, error rates).

Numerical measurements that describe the health, performance, and behavior of a system over time (e.g., CPU usage, error rates, throughput). They are ideal for alerting, capacity planning, and spotting trends or symptoms.


Logs


Granular, timestamped records of discrete events.

Timestamped, granular records of discrete events. They provide the detailed context (text or structured data) necessary to understand exactly what happened within an application or service.


Traces


End-to-end journeys of a single request through a distributed system, showing how different 

Records showing the journey of a single request as it travels through a distributed system, encompassing multiple services. They are critical for pinpointing bottlenecks, latency, or failures in microservices architectures. 

Why They Are Used Together


While metrics indicate that a problem exists, logs provide the context of why it happened, and traces show where it is occurring. Correlating these three data types provides actionable insights rather than just raw data.


Are Logs concern of Monitoring or Observabilty?

Both monitoring and observability deal with logs, but they do so in fundamentally different ways, representing a shift from simply knowing something is broken to understanding why. 

Monitoring is generally used to detect known issues using logs. It is reactive and focuses on pre-defined metrics or alert thresholds, such as alerting when error logs spike or when a specific error code appears.

Observability is used to investigate and understand the "why" behind issues by exploring logs, metrics, and traces together. It is proactive, allowing you to debug complex, distributed systems without needing to know every question ahead of time. 

Comparison: Logs in Monitoring vs. Observability


Feature                 Log Monitoring                                   Log Observability
----------                  ---------------------                                      -------------------------
Primary Question   What went wrong?                                   Why did it go wrong?
Approach        Reactive: Alerts when logs meet criteria   Proactive: Explores data to find root causes
Log Handling        Searchable, indexed logs for active alerts  Contextualized, correlated logs (with traces)
Data Usage       Simple monitoring and basic dashboards   Deep, ad-hoc, and exploratory analysis
Typical Usage       "Error rate > 5%"                                   "Why did this transaction fail?"


How They Work Together

Logs are one of the "three pillars" of observability—alongside metrics and traces—that provide the detailed, granular context necessary for troubleshooting, notes Grafana. 

Monitoring tells you the system is unhealthy (e.g., an alert fires because of high error rates in log files).
Observability allows you to use tools like Splunk or Datadog to dive into the logs and traces to find the specific line of code or database failure causing the issue

In short, monitoring is a component of observability—you cannot have true observability without comprehensive logging. 


Monitoring vs Observability on the example of AWS Lambda


For AWS Lambda, monitoring identifies what is wrong (e.g., an execution failed), while observability reveals why it happened by connecting logs, metrics, and traces across your entire serverless architecture. 

Monitoring AWS Lambda: Detecting the Known


Monitoring focuses on pre-defined health indicators. You use it to track "known-knowns" and trigger reactive alerts when thresholds are breached. 

  • Key Tool: Amazon CloudWatch collects standard metrics automatically.
  • Monitored Metrics:
    • Invocations: The total number of times your function runs.
    • Errors: The count of failed executions.
    • Duration: How long your function takes to run.
    • Throttles: Occurrences where invocations are blocked due to concurrency limits.
  • Example Scenario: You set a CloudWatch Alarm to notify you if your Lambda's error rate exceeds 5%. Monitoring tells you there is a problem, but not the specific line of code that caused it. 

Observability in AWS Lambda: Investigating the Unknown


Observability is a property of the system that allows you to understand its internal state from external outputs. It uses high-cardinality telemetry to investigate complex, distributed issues. 

  • Key Tool: AWS X-Ray provides distributed tracing to visualize the request path across multiple services.
  • Key Elements:
    • Distributed Traces: Seeing a "waterfall" view of a request as it moves from API Gateway to Lambda, then to DynamoDB.
    • Log Insights: Using CloudWatch Logs Insights to run ad-hoc queries across massive log volumes to find specific patterns.
    • Enhanced Instrumentation: Using libraries like AWS Lambda Powertools to add structured logging and context to your telemetry.
  • Example Scenario: You notice high latency in a specific user's request. Using X-Ray, you see that the Lambda function itself is fast, but it is waiting 2 seconds for a downstream third-party API call. Observability provided the "why". 

Comparison Summary for Lambda


Feature   Monitoring (CloudWatch)           Observability (X-Ray + Logs + Metrics)
----------     ---------------------------------            ---------------------------------------------------
Goal   Track health against thresholds.   Understand root causes and behavior.
Questions  "Is my function failing?"           "Why is this specific request slow?"
Visibility   Isolated metrics for one function.   Request paths across multiple services.
Action   Reactive (Alarms/Notifications).   Proactive (Debugging/Optimisation).


---

Thursday, 12 June 2025

Useful Kibana DevTools Queries






Elasticsearch’s query DSL is structured by query types.

Every top-level query must be one of the defined types:
  • match
  • term
  • range
  • bool
  • wildcard
  • query_string
  • function_score
  • etc.

Main APIs:

  • _cat - for a human-readable summary
  • _stats - for a detailed JSON response

Failed Queries


Example:

{
  "statusCode": 502,
  "error": "Bad Gateway",
  "message": "Client request timeout for: https://my.elastic-system.svc:9200 with request GET /my_index/_search?pretty=true"
}


A 502 Bad Gateway combined with a timeout usually means the Elasticsearch engine is struggling to process the request, or there is a networking bottleneck between Kibana and the database.



Cluster

To check cluster health:

GET /_cluster/health

GET /_cluster/health?level=shards

The output contains status which can be green, yellow or red.

To check status of each shard:

GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node

The output shows if shard is primary (p) or replica (r). It also shows the status which can be e.g. STARTED, UNASSIGNED  and reason which can be e.g. ALLOCATION_FAILED.

To sort the output by some column we can use s parameter:

GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node,store&s=state
GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node,store&s=node
GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node,store&s=index 

To sort in descending order, append :desc to the name of the sorted column:

GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node,store&s=store:desc



To get memory allocation and consumption per node:

GET /_cat/allocation?v&s=node

The output contains the following columns:
  • shards (number)
  • shards.undesired
  • write_load.forecast
  • disk.indices.forecast (in Gb or Tb)
  • disk.indices (in Gb or Tb)
  • disk.used (in Gb or Tb)
  • disk.avail (in Gb or Tb)
  • disk.total (in Gb or Tb)
  • disk.percent (number, %)
  • host (IP address)
  • ip (IP address)
  • node (node name or UNASSIGNED)
  • node.role (combination of cdfhilmrstw)

If some shard is not allocated, we can check the reason:

GET /_cluster/allocation/explain

To manually trigger retry of all previously failed shard allocations:

POST /_cluster/reroute?retry_failed=true

To check the progress, check the health of the cluster and:

GET /_cat/recovery/my_index?v



Index



In Elasticsearch, every index has a Mapping. Think of it like a database table definition. If a field isn't explicitly defined or hasn't been automatically detected from an uploaded document, Elasticsearch acts like that field doesn't exist. You can't sort by a column that the database doesn't know about!

To find out which fields exist in index:

GET /my_index/_mapping


To perform a search operation on a specific index:

GET /my_index/_search 

By itself (without a request body), it returns the first 10 documents by default. This request is the same as the above one:

GET /my_index/_search
{
  "query": {
    "match_all": {}
  }
}

In Kibana's Dev Tools, the query parameter in a GET request refers to the search query that defines which documents we want to retrieve from Elasticsearch. It's part of the request body and specifies the search criteria. The query parameter essentially tells Elasticsearch "find me documents that match these conditions." It's the core part of any search request and determines which documents from our index will be returned in the response.

The query object can contain various types of queries. Common query types:

match_all - Returns all documents:

{
  "query": {
    "match_all": {}
  }
}

match - Full-text search on a specific field:

{
  "query": {
    "match": {
      "field_name": "search_term"
    }
  }
}

term - Exact term matching:

{
  "query": {
    "term": {
      "status": "active"
    }
  }
}



To find all documents written in past 1 minute:

GET my_index/_search
{
  "query": {
    "range": {
        "timestamp": {
            "gte": "now-1m",
            "lte": "now"
        }
    }
  }
}


For X days use: Xd
For X hours use: Xh

bool


bool - Combine multiple queries with logical operators:

{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "elasticsearch"}},
        {"range": {"date": {"gte": "2023-01-01"}}}
      ]
    }
  }
}


bool is the workhorse of combinational querying in Elasticsearch.
It's like the logical brain of the query DSL.

A bool query lets you combine multiple conditions (AND, OR, NOT) into one query. Think of it like this:

(bool)
 ├── must → AND conditions
 ├── filter → AND but cheap
 ├── should → OR conditions
 └── must_not → NOT conditions


Without a bool query, Elasticsearch can only run one query condition at a time. But real searches need multiple conditions.

For example:
  • logs in the last 7 days
  • log_group is X
  • success = false
  • AND NOT status=200
  • AND (message contains A OR B)

You need logical operators → that's what bool provides.

Within bool we can use filter instead of must

filter is not a standalone query. It is only one part of the bool query type.

Filters are:
  • cached
  • more efficient
  • ideal for exact and boolean conditions

Everything inside filter is combined with AND.

"filter": [
  { cond1 },
  { cond2 },
  { cond3 }
]


This means:

cond1 AND cond2 AND cond3



filter is not a query type — it is an instruction telling the bool query how to treat subqueries (as cached, non-scoring, mandatory matches). 

So this:

"filter": [...]

doesn’t make sense by itself — filter what?

Whereas this:

"bool": {
  "filter": [...]
}

...means: "Run these filter queries together inside a boolean query."

Example:

GET logs-*/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "@timestamp": {
              "gte": "now-7d",
              "lte": "now"
            }
          }
        },
        {
          "term": {
            "log_group": "/aws/lambda/my-lambda"
          }
        },
        {
          "term": {
            "mycorp.message.my-lambda.success": false
          }
        }
      ]
    }
  }
}


All must clauses must also match.

"must": [
  { cond1 },
  { cond2 }
]

Meaning:

cond1 AND cond2


Inside should, clauses are OR unless minimum_should_match makes them mandatory.

"should": [
  { cond1 },
  { cond2 }
]

Meaning:

cond1 OR cond2

must_not: All must NOT match.

"must_not": [
  { cond1 }
]

Meaning:

NOT cond1



range - Query for values within a range:

{
  "query": {
    "range": {
      "age": {
        "gte": 18,
        "lte": 65
      }
    }
  }
}


To get the number of documents in an Elasticsearch index, you can use the _count API or the _stats API.

GET /my_index/_count

This will return a response like:

{
  "count": 12345,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  }
}


To get a certain number of documents, use size argument:

GET my_index/_search?size=900

We can also use _cat API:

GET /_cat/count/my_index?v

This will return output like:

epoch      timestamp count
1718012345 10:32:25  12345


GET /my_index/_stats

"indices": {
  "my_index": {
    "primaries": {
      "docs": {
        "count": 12345,
        "deleted": 12
      }
    }
  }
}


To get the union of all values of some field e.g. channel_type field across all documents in the my_index index, we can use an Elasticsearch terms aggregation:


GET my_index/_search
{
  "size": 0, 
  "aggs": {
    "unique_channel_types": {
      "terms": {
        "field": "channel_type.keyword",
        "size": 10000  // increase if you expect many unique values
      }
    }
  }
}


Explanation:
  • "size": 0: No documents returned, just aggregation results.
  • "terms": Collects unique values.
  • "channel_type.keyword": Use .keyword to aggregate on the raw value (not analyzed text).
  • "size": 10000: Max number of buckets (unique values) to return. Adjust as needed.

Response example:

{
  "aggregations": {
    "unique_channel_types": {
      "buckets": [
        { "key": "email", "doc_count": 456 },
        { "key": "push", "doc_count": 321 },
        { "key": "sms", "doc_count": 123 }
      ]
    }
  }
}

The "key" values in the buckets array are your union of channel_type values.


Let's assume that my_index has the timestamp field (as the root field...but it can be at any path in which case we'd need to adjust the query) is correctly mapped as a date type.


To get the oldest document:

GET my_index/_search
{
  "size": 1,
  "sort": [
    { "@timestamp": "asc" }
  ]
}


To get the newest document:

GET my_index/_search
{
  "size": 1,
  "sort": [
    { "@timestamp": "desc" }
  ]
}


Sorting by a field that isn't indexed or doc_values-enabled (especially on a large or unoptimized index) can cause the memory usage to spike and the request to hang until it times out.


How to get all possible values of some field in all documents added to index in last 24 hours?

We can use Terms Aggregation with Range Query:


GET /my_index/_search
{
  "size": 0,
  "query": {
    "range": {
      "@timestamp": {
        "gte": "now-24h/h",
        "lte": "now"
      }
    }
  },
  "aggs": {
    "unique_values": {
      "terms": {
        "field": "my_field.keyword",
        "size": 10000
      }
    }
  }
}


Check number of documents which are older than N days:


POST my_index/_count
{
  "query": {
    "range": {
      "@timestamp": {
        "lt": "now-Nd/d"
      }
    }
  }
}

Delete all documents older than N days:

POST my_index/_delete_by_query?conflicts=proceed&wait_for_completion=false
{
  "query": {
    "range": {
      "@timestamp": {
        "lt": "now-Nd/d"
      }
    }
  }
}

The output of the above command is task ID:

{
  "task": "BeJWGidDTtWkRL9aQEkjhg:26425516"
}

To check all tasks, grouped by node on which they are running:

GET _tasks

The output shows task's id, action name (e.g. "indices:data/write/bulk[s][p]", "cluster:monitor/tasks/lists[n]"), type (e.g. transport, monitoring, ...).

To check the task status:

GET _tasks/<task_id>

To check if any delete_by_query task is running and number of docs deleted so far:

GET _tasks?actions=*delete/byquery&detailed=true


Once delete_by_query task is completed: deletion is done, but disk space might not yet be reclaimed. To free disk space, run a forcemerge:

POST my_index/_forcemerge?only_expunge_deletes=true

For a huge shard, consider doing this after several weekly chunks, not after every single one, to reduce I/O spikes.

Check if any forcemerge tasks are running:

GET _tasks?actions=*forcemerge
GET _tasks?actions=*forcemerge&detailed=true
GET _tasks?actions=*forcemerge&detailed=true&group_by=parents

Check number of merges:

GET my_index/_stats?level=shards



To get number of shards in index:

GET my-index/_settings/?filter_path=**.number_of_shards


To get number of replicas:

GET my-index/_settings/?filter_path=**.number_of_replicas

Output:

{
  "my-index": {
    "settings": {
      "index": {
        "number_of_replicas": "1"
      }
    }
  }
}


How to find out disk size used by some index?

GET /_cat/indices/your-index-name*?v&h=index,docs.count,store.size,pri.store.size&s=store.size:desc
  • store.size: Total size on disk (includes all primary shards and replica shards).
  • pri.store.size: Size of only the primary shards (useful for knowing the "true" data size without redundancy).
  • s=store.size:desc: Sorts the list by size (useful if you are using a wildcard *).

If you need a precise number for an automated script or a deeper dive into memory usage, use the _stats endpoint.

GET /your-index-name*/_stats/store

In the JSON response, look for:
  • total.store.size_in_bytes: The exact byte count for the whole index (primaries + replicas).
  • primaries.store.size_in_bytes: The exact byte count for just the primary data.

To see which fields take up the most space:

POST /my-index/_disk_usage

To get the frequency of writes (document ingestion) over the last 24 hours, broken down by the hour, you should use a Date Histogram aggregation. In Elasticsearch, "writing into an index" typically equates to the creation of new documents with a @timestamp.

GET /your-index-name*/_search
{
  "size": 0, 
  "query": {
    "range": {
      "@timestamp": {
        "gte": "now-24h",
        "lte": "now"
      }
    }
  },
  "aggs": {
    "writes_per_hour": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "1h",
        "extended_bounds": {
          "min": "now-24h",
          "max": "now"
        }
      }
    }
  }
}


Breakdown of the Query:
  • "size": 0: Tells Elasticsearch we don't want to see the actual documents (the "hits"), just the statistical summary.
  • range: Filters the data to only include documents from the last 24 hours.
  • date_histogram: This is the magic part. It buckets your data by time.
  • fixed_interval: "1h": Groups the results into 1-hour chunks.
  • extended_bounds: Ensures that even if an hour had zero writes, it still shows up in your list as a "0" count rather than being skipped entirely.



----