SKILL.md
KubeSense Logs
Requires the KubeSense MCP server; see [kubesense-mcp](../kubesense-mcp/SKILL.md) for connection, auth, and the shared WHERE-clause reference. Tools here resolve RBAC against the logs module.
Tools
| Tool | Use when | Returns |
|---|---|---|
get-trace-or-log-fields (signal: "logs") |
Always, first | Field catalog for your window |
search-logs |
You want to read individual log lines | Up to page_size rows (default 10) |
analyze-logs |
You want counts, rates, trends, percentiles | Aggregated series |
Reading rows to compute a total is the most common mistake. If the question is "how many", "how often", or "which is worst", use analyze-logs — one call over the whole window beats paginating raw rows, and is more accurate.
Field Names — Catalog Labels Only
The server rejects storage column names at every input slot (where, requiredfields, groupbyfields, fields, sortby) with:
field "pod_name" is a storage column; use the catalog label "instance" instead
| Concept | Use this label | Storage column (rejected) |
|---|---|---|
| log severity | type |
level |
| pod | instance |
pod_name |
| container | container |
container_name |
| node / host | node |
host |
| workload | workload |
— |
| namespace | namespace |
— |
| cluster | cluster |
— |
| log message | body |
— |
| log format | format |
— |
| source | source |
— |
| region | region |
— |
| app version | app_version |
— |
| environment | env_type |
— |
That is the complete set of 13 fields get-trace-or-log-fields advertises for logs. Three more parse but are hidden from discovery: timestamp, patternid, bodylength.
[!WARNING]
There is noservicefield on logs.serviceexists only on traces
(→app_service).service = checkoutagainst logs returnsunknown field "service" for signal=logs. Useworkloadfor the application name.nodeon logs,node_nameon traces. The two signals disagree — logs usenode
(→host), traces usenode_name. Carrying a field name across signals is a
predictable failure.
Enum values are UPPERCASE on logs
type accepts exactly: ERROR, WARN, INFO, DEBUG, TRACE, FATAL, PANIC. Casing is significant — type = error matches nothing.
(Traces use lowercase status = error. The two signals differ in both the field name and the casing.)
format accepts json, klog, nginx — lowercase.
[!NOTE]
Read casing off theenum_valuescolumn ofget-trace-or-log-fields, not theexamplecolumn. The example generator misses several enum fields (includingtype)
and returns a blank cell for them.enum_valuesis always correct.
WHERE Syntax
type = ERROR AND namespace = production
type IN (ERROR, FATAL) AND workload = checkout
NOT (env_type = dev)
body ILIKE "%timeout%"
@user.id = abc-123
Operators the WHERE parser actually accepts: = != < > <= >= LIKE ILIKE SUBSTR_ILIKE IN, combined with AND / OR / NOT and parentheses.
[!WARNING]
Theoperatorscolumn ofget-trace-or-log-fieldsover-advertises.HAS_TOKEN,HASALL,HASANY,LIKEAND,ILIKELOG,ISIPADDRESSappear in the legend but
have no WHERE-string syntax —body HAS_TOKEN oomfails to parse. Stick to the
list above.
Quoting. Bare values may contain only letters, digits, _, ., -. Anything with a slash, colon, %, or space must be double-quoted:
body ILIKE "%connection refused%" ✓
timestamp > "2026-07-30T10:00:00Z" ✓
body ILIKE %timeout% ✗ parse error
NOT IN does not parse as a leaf. Write NOT (namespace IN (kube-system, default)).
Attributes carry an @ prefix in WHERE and work correctly on logs:
@user.id = abc-123
@http.status_code > 400
Attribute operators: = != IN < > <= >= LIKE ILIKE. Unquoted numeric attribute values are coerced to numbers, so @latency > 100 compares numerically.
[!WARNING]
exist(@field)is broken on both signals — it emits anEXISToperator carrying a
value, which the validator rejects (operator "EXIST" takes no values (got 1)). There
is no working existence check. Filter on a concrete value instead.
In groupbyfields / fields / requiredfields / sortby there is no @ — set the flag instead:
{ "field": "user.id", "is_attribute": true }
search-logs
{
"from_time": "2026-07-30T10:00:00Z",
"to_time": "2026-07-30T11:00:00Z",
"where": "type = ERROR AND namespace = production",
"required_fields": [
{ "field": "instance", "is_attribute": false },
{ "field": "body", "is_attribute": false }
],
"page_size": 50
}
Output is TSV: a # totalcount=<N> errorcount=<N> isfreesearch=<bool> line, a header row, then one row per log. totalcount is for the whole window, ignoring pagesize — compare it to the rows you received to know whether more exist.
Pagination is window-based
There is no page or offset parameter. The window is half-open (fromtime <= timestamp < totime), so you page by narrowing to_time:
- Query your real window.
- Take the minimum
timestampamong the returned rows — do not assume the last row
is the oldest.
- Call again with the same
fromtimeandwhere, andtotime= that minimum,
copied verbatim.
- Stop when a call returns fewer rows than
page_size, or the answer is settled.
Timestamps are RFC3339 UTC with millisecond precision. Copy the cell exactly — truncating to whole seconds re-reads or skips rows within that second.
[!IMPORTANT]
A paged scan is a sample, not a census. Rows are orderedtoStartOfHour(timestamp) DESC, cluster DESC, timestamp DESC— hour-bucket first, then
cluster. When one hour holds more matching rows thanpage_sizeacross several
clusters, advancingto_timecan skip rows in a cluster the scan hadn't reached. To
read exhaustively, pinclustersto a single cluster and use a largepage_size.
Otherwise prefer a biggerpage_sizeover more pages, andanalyze-logsover
pagination for anything aggregate.
isfreesearch
Touching body, traceid, spanid, or any attribute routes the query to the raw table instead of the pre-aggregated rollups — slower, but the only way to see message text. It is computed automatically and reported in the output header. Column-only filters stay on the fast rollup path.
analyze-logs
{
"from_time": "2026-07-30T10:00:00Z",
"to_time": "2026-07-30T11:00:00Z",
"query_type": "range",
"where": "type = ERROR",
"group_by_fields": [ { "field": "workload", "is_attribute": false } ],
"value_operation": "row_count",
"sort_direction": "DESC",
"limit": 10
}
query_type:rangefor a time series,instantfor one number per series.valueoperation:rowcount,unique_count,avg,sum,min,max,p50,
p90, p95, p99.
fieldssupplies the operand for everything exceptrow_count:
uniquecount of pods → valueoperation: "unique_count", fields: [{"field": "instance"}].
limitcaps series onrangequeries (top-N by max value, default 20); ignored for
instant.
[!WARNING]
valueoperation: "count"is not valid — the correct name isrowcount. The tool
schema's own summary line says "Common: count, avg, …", which is wrong.
body cannot be used in groupbyfields (it doesn't exist on the rollup tables). It is a filter-only field. Group by pattern_id if you want to cluster similar messages.
Output shape
Columnar JSON keyed by query label ("A"). For range, a single shared timestamps array (Unix seconds) is emitted once and each series carries a positionally-aligned values array (null = no data in that bucket). For instant, each series carries one scalar value. total_series/truncated report how many non-zero series existed before the top-N cap.
Worked Examples
Error count per workload, last hour, as a trend querytype: range, where: "type = ERROR", groupbyfields: [{"field":"workload"}], valueoperation: row_count
How many distinct pods logged a fatal error right now querytype: instant, where: "type = FATAL", valueoperation: unique_count, fields: [{"field":"instance"}]
Read the actual timeout messages search-logs, where: 'body ILIKE "%timeout%" AND workload = checkout', requiredfields: [{"field":"instance"},{"field":"body"}], pagesize: 50
Error rate as a percentage — needs two queries plus a formula, so use analyze-telemetry; see [kubesense-mcp](../kubesense-mcp/SKILL.md).
Rules
- Call
get-trace-or-log-fieldswithsignal: "logs"first, passing the same window
you will query — attribute keys are window-scoped.
- Use catalog labels:
type,instance,container,node. Neverlevel,pod_name,
container_name, host.
- There is no
servicefield on logs — useworkload. typevalues are UPPERCASE (ERROR, noterror). Read casing fromenum_values,
never from example.
- Quote any value containing
/,:,%, or a space. NOT (x IN (...)), notx NOT IN (...).- Stick to
= != < > <= >= LIKE ILIKE SUBSTR_ILIKE IN— the advertised
token/index operators have no WHERE syntax.
valueoperationisrowcount, notcount.analyze-logsfor counts and trends;search-logsonly to read messages. Never
paginate to compute an aggregate.
- Page by narrowing
to_timeto the minimum timestamp returned; there is no offset.
Treat multi-cluster paged scans as samples unless you pin one cluster.
bodyis filter-only — it cannot be a group-by.