Kubernetes Search adds five search commands. Four of them - k8s, k8slogs, k8sevents, and k8sdescribe - are generating commands: they start a search (they go first, after the leading |) and produce rows from the live Kubernetes API. The fifth, k8syaml, is a streaming formatter you pipe results into.
All four generating commands require the run_k8s_search capability. See Access control.
Conventions shared by all commands
A few arguments and behaviors work the same everywhere, so they’re documented once here.
Resource kinds (kind=). Anywhere a kind is accepted you can use the canonical name (Pod, Deployment), lowercase (pod), plural (pods, deployments), or the short name (po, deploy). The command resolves the kind and its API version through the cluster’s discovery API, so you rarely need api_version=. On Kubernetes 1.27 and later, or 1.26 with the AggregatedDiscoveryEndpoint feature gate on, every kind the cluster serves resolves this way, custom resources included. Any other cluster, including one with aggregated discovery turned off, resolves only the core, apps, batch, and networking.k8s.io kinds: any other kind needs api_version= on | k8s and | k8sdescribe, and | k8sevents for= matches it only by its exact Kind (HorizontalPodAutoscaler, not hpa). Append /<name> to address a single object: kind=pods/payments-api-7d9f8.
Namespaces (namespace=). Accepts an exact name (payments), * for all namespaces, or a glob (prod-*, *-system). A glob or * is expanded client-side into one query per namespace, so a wide pattern over many namespaces means more API calls.
Clusters (context=). Targets the cluster you registered under that name. A glob (prod-*) or * fans out to every matching cluster in parallel, and every result row carries a cluster field naming the one it came from. Without context=, the command uses your single registered cluster (or errors if you have several and didn’t pick one). See Clusters.
concurrency= (default 8, at most 128) caps how many clusters are queried at once during a fan-out. The default is the Fan-out concurrency setting; see Configuration.
Quote values that contain commas or spaces. SPL splits unquoted arguments on commas and spaces, so labels=app=web,tier=frontend reaches the command as two arguments. Quote the whole value: labels="app=web,tier=frontend". An unquoted value like that fails the search with an error that names the stray piece and shows the fix. When the piece after the comma happens to spell another accepted argument - labels=app=web,context=prod - the search runs, with a warning pointing at the quoting fix; check for it before trusting the result. An argument given more than once is refused too, since only one value could apply.
Normal time ranges only. The generating commands do not run in real-time searches; a real-time range fails the search with “This command is not supported in a real-time search”.
cache= overrides the freshness of the on-disk result cache for this search: cache=0 bypasses it and always hits the API; cache=5m accepts a result up to five minutes old. Rows from | k8s, | k8sevents, and | k8sdescribe report _cache_hit (true/false) and, on a hit, _cache_age_seconds (| k8slogs reads log content live, so its rows don’t carry them). See Concepts - caching.
Errors are per-cluster. During a fan-out, a cluster that fails (unreachable, expired credential, RBAC denial) emits a single _cluster_error row and the search continues against the others, so one bad cluster never sinks the whole query.
| k8s - list and get resources
Use k8s whenever you’d reach for kubectl get. It lists resources of a kind, or fetches one object by name, and emits one row per object with the full JSON in _raw.
| k8s kind=<kind>[/<name>]
[api_version=<group/version>]
[namespace=<name|*|glob>]
[labels=<selector>] [fields=<selector>]
[view=metadata] [limit=<int>]
[context=<name|*|glob>] [concurrency=<int>] [cache=<duration>]| Argument | Description |
|---|---|
kind= | Required. Resource kind, optionally /<name> for a single object. |
api_version= | Pin the API version (e.g. apps/v1). Defaults to what discovery reports. Must be an apiVersion - group/version, or v1 - since the value is placed into the API path; anything else is refused. |
namespace= | Namespace, *, or glob. Optional: without it, a list reads the cluster’s Default namespace, or every namespace when the cluster has none. A single object of a namespaced kind needs one exact namespace, given here or by the cluster’s default; a glob or * is refused there. Cluster-scoped kinds ignore it. |
labels= | Server-side label selector, e.g. labels="app=payments,tier=api". Quote a selector that holds a comma or a space. |
fields= | Server-side field selector, e.g. status.phase=Running. |
view=metadata | Return the object’s metadata only - no spec or status - lighter and faster when you don’t need them. |
limit= | Cap the number of rows emitted per cluster, across all its namespaces. A fan-out returns up to this many from each cluster. |
Key fields on each row: kind, name, namespace, and _raw (the full object JSON, ready for spath).
List pods in a namespace:
| k8s kind=pods namespace=paymentsGet one object:
| k8s kind=pods/payments-api-7d9f8 namespace=paymentsFilter server-side with a label selector, then summarize:
| k8s kind=pods namespace=payments labels=app=payments-api
| spath path=status.phase output=phase
| stats count by phaseFind pods that aren’t Running, cluster-wide:
| k8s kind=pods namespace=* fields=status.phase!=Running
| table namespace nameCheap inventory across every cluster (metadata only):
| k8s kind=deployments namespace=* view=metadata context=*
| stats count by clusterRead a custom resource (discovery resolves the group automatically):
| k8s kind=certificates namespace=webportal| k8slogs - pod logs
Use k8slogs like kubectl logs. It returns a snapshot of container logs at query time - one row per log line, with the line text in _raw. There is no live follow; re-run to see newer lines.
| k8slogs namespace=<name> pods=<pattern>
[containers=<pattern>]
[tail=<int>] [since=<duration>] [previous=<bool>]
[context=<name|*|glob>] [concurrency=<int>] [cache=<duration>]| Argument | Description |
|---|---|
namespace= | Required. The namespace to read from. |
pods= | Required. Wildcard pattern matching pod names, e.g. payments-api-* or *proxy*. |
containers= | Wildcard pattern for container names (default *). Matches init, regular, and ephemeral containers. |
tail= | Return only the last N lines per container (like kubectl --tail). A value that is not a whole number fails the search before any log is read. |
since= | Only lines newer than this. Go duration: 30s, 5m, 2h. Without it, the start of the search’s time range sets the bound; under All time there is none. |
previous= | true returns the log of the previous, crashed instance of the container (like kubectl --previous). |
cache= | Caches only the pod list - which pods and containers match - never the log lines. Off by default; cache=30s lets a re-run skip listing the pods again. |
Key fields: pod, container, _time (the log line’s own timestamp), and _raw (the line).
In each cluster, k8slogs reads up to fan_out_concurrency containers’ logs at once (8 by default; see Configuration). concurrency= sets only how many clusters it queries at once.
One container that cannot be read does not fail the search. The readable containers return their lines. A container whose log cannot be read - a missing pods/log permission, an unreachable node - arrives as a _container_error record naming the namespace, pod, and container, the per-container counterpart of _cluster_error. A container that has not started yet (ContainerCreating, PodInitializing, an image still pulling), or whose pod has already gone, has no logs and returns nothing. To see the failures alongside the lines:
| k8slogs namespace=webportal pods=* tail=50
| where isnotnull(_container_error)
| table pod container _container_errorLast 200 lines from every pod of a deployment:
| k8slogs namespace=payments pods=payments-api-* tail=200Why did it crash - read the previous container’s log:
| k8slogs namespace=payments pods=payments-api-7d9f8 previous=trueErrors in the last 15 minutes across a whole namespace:
| k8slogs namespace=webportal pods=* since=15m
| search "ERROR" OR "panic"
| table _time pod container _rawJust the gateway sidecar across both clusters:
| k8slogs namespace=payments pods=payments-api-* containers=*gateway* context=prod-* tail=100| k8sevents - cluster events
Kubernetes events explain why things happened: why a pod won’t schedule, why an image pull failed, why a deployment rolled. k8sevents lists them, with server-side filtering by type, reason, and the object they target.
| k8sevents [namespace=<name|*|glob>]
[type=<Normal|Warning>] [reason=<string>]
[for=<kind>[/<name>]]
[context=<name|*|glob>] [concurrency=<int>] [cache=<duration>]| Argument | Description |
|---|---|
namespace= | Namespace, * for cluster-wide, or glob. Defaults to the cluster’s configured namespace. |
type= | Normal or Warning. Omit for both. |
reason= | Event reason code, e.g. Failed, BackOff, FailedScheduling, SuccessfulCreate. |
for= | Restrict to events about one object: Pod/payments-api-7d9f8, or a bare kind like Node for all events of that kind. |
Key fields: type, reason, message, involved_kind, involved_name, involved_namespace, count, first_timestamp, last_timestamp.
A repeating event is reported by its most recent occurrence, whichever Kubernetes API recorded it - the core one that counts repeats on the event, or events.k8s.io, which keeps them in a separate series. _time is when the event last happened, count is how many times it has been observed (1 for an event that reports no count, never 0), and first_timestamp and last_timestamp are its first and most recent occurrence.
Kubernetes keeps events for about an hour by default.
k8seventsreturns what the API server still has, not a full history.
type=, reason= and for= refuse , and =: those are field-selector syntax, and no real event type, reason or object name contains either.
All warnings in a namespace:
| k8sevents namespace=payments type=WarningCluster-wide scheduling failures right now:
| k8sevents namespace=* reason=FailedScheduling
| table involved_namespace involved_name message countEverything Kubernetes has to say about one pod:
| k8sevents for=Pod/payments-api-7d9f8 namespace=paymentsWarnings across every cluster, grouped:
| k8sevents namespace=* type=Warning context=*
| stats count by cluster reason| k8sdescribe - object and its events
k8sdescribe mirrors kubectl describe: it fetches a single object and the events that reference it, in one search. It always addresses one object, so kind= takes the Kind/Name form.
| k8sdescribe kind=<kind>/<name> [namespace=<name>]
[api_version=<group/version>]
[context=<name|*|glob>] [concurrency=<int>] [cache=<duration>]| Argument | Description |
|---|---|
kind= | Required. A single object as Kind/Name, e.g. pod/payments-api-7d9f8 or deploy/payments-api. |
namespace= | One exact namespace for a namespaced kind, given here or by the cluster’s Default namespace; a glob or * is refused. Ignored for cluster-scoped kinds (Node, Namespace, PersistentVolume). |
api_version= | Pin the API version. Defaults to discovery. Must be an apiVersion; anything else is refused. |
Rows carry a _describe_section field: object for the resource itself and event for each related event. Event rows have the same fields as | k8sevents, read the same way for a repeating event, plus target_kind, target_name, and target_namespace naming the object they describe. Split them with where:
| k8sdescribe kind=pod/payments-api-7d9f8 namespace=payments
| where _describe_section="event"
| table _time type reason messageDescribe a node (cluster-scoped, no namespace):
| k8sdescribe kind=node/ip-10-2-14-7Describe the same deployment across clusters:
| k8sdescribe kind=deploy/payments-api namespace=payments context=*| k8syaml - format JSON as YAML
The generating commands return JSON in _raw. Pipe through k8syaml to read it the way kubectl get -o yaml shows it. This is a streaming command - it transforms rows, it doesn’t fetch anything.
... | k8syaml [field=<field>] [output=<field>]| Argument | Description |
|---|---|
field= | Input field holding the JSON (default _raw). |
output= | Field to write the YAML to (default: rewrite the input field in place). |
Rows whose field is empty or isn’t JSON pass through untouched.
Read a pod as YAML:
| k8s kind=pods/payments-api-7d9f8 namespace=payments
| k8syamlKeep the JSON, add a YAML column:
| k8s kind=pods namespace=payments
| k8syaml field=_raw output=yamlRecipes
A few patterns that combine the commands with everyday SPL.
Pods stuck pending, with the reason:
| k8s kind=pods namespace=* fields=status.phase=Pending view=metadata
| eval key=namespace.":".name
| join key
[ | k8sevents namespace=* reason=FailedScheduling
| eval key=involved_namespace.":".involved_name ]
| table namespace name messageRestart counts across a cluster, worst first:
| k8s kind=pods namespace=*
| spath path=status.containerStatuses{}.restartCount output=restarts
| stats sum(restarts) as restarts by namespace name
| sort - restartsCorrelate ingested logs with live state during an incident - search what Monitoring Kubernetes already stored, then describe the offending pod as it is right now, in the same session:
index=kubernetes namespace=payments "OOMKilled" earliest=-1h
| stats count by pod_name| k8sdescribe kind=pod/payments-api-7d9f8 namespace=paymentsThis pairing - history from ingestion, current state from Kubernetes Search - is the heart of why you’d run both.