ES|QL Query Skill
Query Elasticsearch via the Kibana ES|QL async search API. Run ES|QL queries directly against Kibana clusters without the user needing to copy-paste results.
Setup
Staging / Production — ,es-web CLI
The ,es-web CLI at /Users/meain/.local/bin/utils/,es-web handles Kibana ES|QL queries with automatic async polling, session management, and table output.
First-time setup for a cluster: the user provides a curl command from Kibana's network tab:
pbpaste | ,es-web init stg-us
pbpaste | ,es-web init prd-eu
Check configured clusters:
,es-web list
Session cookies expire after a few hours. If you get a 401/session expired error:
- Look up the Kibana URL for the target cluster from the Confluence Speed Dial page (
confluence page 405504500) - Open it with
open <url>(withdangerouslyDisableSandbox: true) - Ask the user to copy a curl from the network tab and run
! pbpaste | ,es-web init <cluster>
How to query with ,es-web
# Simple: service + time window
,es-web -c stg-us -s earn -t 1h -n 20
# With additional WHERE clause
,es-web -c stg-us -s earn -t 1h -q 'message LIKE "*validation*"'
# Custom fields
,es-web -c stg-us -s earn -t 1h -f '@timestamp, message, labels.error'
# Raw ES|QL query
,es-web -c stg-us 'FROM logs-* | WHERE service.name == "earn" | STATS count = COUNT(*) BY labels.event_type | SORT count DESC | LIMIT 10'
# JSON output for further processing
,es-web -c stg-us -s earn -t 30m --json
# Debug: print generated query
,es-web -c stg-us -s earn -t 1h --debug
,es-web options
-c— Cluster alias (e.g.stg-us,prd-eu)-s—service.namefilter-t— Time window (e.g.30m,1h,2d). Defaults to4hif omitted (a warning is printed to stderr)-n— Max rows (default: 20)-q— Additional WHERE clause-f— Comma-separated fields for KEEP clause-i— Index pattern (default:logs-*)--json— Raw JSON output--debug— Print generated query to stderr
Important notes
labels.statusis a keyword field — compare with strings, not integers (e.g.,labels.status >= "500")- When no
-fis specified, default fields are:@timestamp, service.name, message, labels.error, labels.error_message, labels.path, labels.status, trace.id - For raw ES|QL queries, pass the full query as a positional argument — no default KEEP is added
- LIKE queries on
messageacross many services can be slow — scope with service and tight time windows
Known service names
These are the service.name values for control-plane-backend services:
| Service | service.name |
|---|---|
| Workload Tenants | workload-tenants-svc |
| Subscriptions | subscriptions-svc |
| User Management | user-management |
| Routing | routing |
| EARN | earn |
| EARN Telemetry | earn-telemetry |
| Usage Reports | usage_reports |
| Onboarding | onboarding |
| Scheduler | scheduler |
The nginx ingress controller logs under service.name == "nginx".
Common log fields
Standard fields
@timestamp— event timestamp (ISO 8601)service.name— service identifier (see table above)service.environment—prod,prd,qa,devmessage— log message textlevel— log level (info,warn,error)trace.id— distributed trace ID for correlating across services
Label fields (keyword/string type)
labels.status— HTTP status code (string, not int)labels.path— request pathlabels.method— HTTP methodlabels.error/labels.error_message— error detailslabels.organization_id/labels.org_id— organization identifierlabels.organization.id— alternative org ID fieldlabels.workload_tenant.id— workload tenant identifierlabels.user.id/labels.principal.id— user/principal identifierslabels.operation— operation being performedlabels.handler— handler namelabels.earn.event.type— EARN event typelabels.earn.event.dedupe_key— EARN dedup keylabels.scheduler.event.id— scheduler event IDlabels.request.url— outgoing request URLlabels.http.router.request_id— request ID
Numeric label fields
numeric_labels.*— numeric values (latency, counts, etc.)
Overview labels (for filtering structured log categories)
These are set via clog.Label() and appear in a labels field:
http_server_overview,dispatcher_overview,queue_dispatcher_overviewearn_event_delivery,events_processorpanic_recovery,panic_recoveredazure_sdk_logging,secrets_handlingdeprecated_behavior_overview
Common query patterns
List services in a time window
FROM logs-*
| WHERE service.environment == "prod"
AND @timestamp >= "2026-04-07T07:00:00.000Z"
AND @timestamp <= "2026-04-07T08:00:00.000Z"
| STATS count = COUNT(*) BY service.name
| SORT count DESC
| LIMIT 30
Find 5xx errors from nginx
FROM logs-*
| WHERE service.name == "nginx"
AND service.environment == "prod"
AND labels.status >= "500"
| SORT @timestamp DESC
| LIMIT 20
| KEEP @timestamp, labels.status, labels.path, labels.method, message
Find errors for a specific service
FROM logs-*
| WHERE service.name == "earn"
AND service.environment == "prod"
AND level == "error"
AND @timestamp >= "START"
AND @timestamp <= "END"
| SORT @timestamp DESC
| LIMIT 30
| KEEP @timestamp, message, labels.error, labels.error_message, labels.operation, trace.id
Search by org/tenant ID across all services
FROM logs-*
| WHERE service.environment == "prod"
AND @timestamp >= "START"
AND @timestamp <= "END"
AND (labels.organization_id == "ORG_ID" OR labels.org_id == "ORG_ID" OR labels.organization.id == "ORG_ID")
| SORT @timestamp DESC
| LIMIT 20
| KEEP @timestamp, service.name, message, labels.error, trace.id
Search by workload tenant ID
FROM logs-*
| WHERE service.environment == "prod"
AND @timestamp >= "START"
AND @timestamp <= "END"
AND labels.workload_tenant.id == "TENANT_ID"
| SORT @timestamp DESC
| LIMIT 30
| KEEP @timestamp, service.name, message, labels.error, labels.operation
Trace a request across services
FROM logs-*
| WHERE trace.id == "TRACE_ID"
| SORT @timestamp ASC
| LIMIT 100
| KEEP @timestamp, service.name, level, message, labels.error, labels.status, labels.path
Check latency metrics
FROM logs-*
| WHERE service.name == "SERVICE"
AND service.environment == "prod"
AND message LIKE "*metric*latency*"
| SORT @timestamp DESC
| LIMIT 30
| KEEP @timestamp, message, numeric_labels.value_ms
Error rate by service (last hour)
FROM logs-*
| WHERE service.environment == "prod"
AND @timestamp >= "START"
AND @timestamp <= "END"
AND level == "error"
| STATS error_count = COUNT(*) BY service.name
| SORT error_count DESC
| LIMIT 20
Find panics
FROM logs-*
| WHERE service.environment == "prod"
AND @timestamp >= "START"
AND @timestamp <= "END"
AND (message LIKE "*panic*" OR labels.panic_recovered == "true" OR labels.panic_recovery == "true")
| SORT @timestamp DESC
| LIMIT 20
| KEEP @timestamp, service.name, message, labels.error, trace.id
Dev / Personal Environments — ,es CLI
The ,es CLI at /Users/meain/.local/bin/utils/,es provides a simpler way to query Elasticsearch, but it only works for the dev environment. Do not use it for staging or production.
Common queries
Search by trace ID:
,es -q 'trace.id:<TRACE_ID>' -n 50
Filter for errors:
,es -q 'trace.id:<TRACE_ID> AND event.type:error' -n 10
Broader service-level search (no trace ID):
,es -q 'service.name:<SERVICE_NAME> AND (level:error OR event.type:error)' -n 20
,es CLI options
-q— Lucene query string-n— Number of results-f— Comma-separated fields to display-s— Sort field (e.g.@timestamp:desc)
Choosing the right approach
| Environment | Method |
|---|---|
| Dev / personal | Use ,es CLI directly |
| Staging / production | Use ,es-web -c <cluster> (requires session cookie from user) |
| No session available | Fall back to clipboard mode (see below) |
Cluster naming convention: <env>-<region>, e.g. stg-us, stg-eu, prd-us, prd-eu, prd-apac.
When no ,es-web session is configured for the target cluster:
- Look up the Kibana URL on the Confluence Speed Dial page (
confluence page 405504500), open<kibana-base-url>/app/discover#/withopen <url>(withdangerouslyDisableSandbox: true), then ask the user to copy a curl from the network tab and run! pbpaste | ,es-web init <cluster> - If that's not possible, fall back to clipboard mode:
- Copy the ES|QL query to the user's clipboard with
pbcopy - Tell the user to open Kibana, run the query in Discover (ES|QL mode)
- Have them copy the response JSON from the network tab
- Copy the ES|QL query to the user's clipboard with
Investigation workflow tips
- Start broad, narrow down. First identify which services are involved, then drill into specific errors.
- nginx → upstream. nginx logs show external-facing status codes; upstream service logs show root cause.
- Use trace.id to correlate requests across services when available.
- Error details live in
labels.errorandlabels.error_message. - Field types matter:
numeric_labels.*fields are numeric;labels.*fields are keywords (strings). - Time windows: Default is 4h when
-tis omitted. If a query times out, retry with a narrower window (e.g.,1h→30m). For very broad searches (e.g., no service filter), start at30mand expand only if needed. - Overview labels (like
http_server_overview,dispatcher_overview) are useful for filtering to structured log categories without needing to match on message text.