KubeSense Logs
Requires the KubeSense MCP server; see kubesense-mcp 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,
required_fields, group_by_fields, fields, sort_by) 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, pattern_id, body_length.
[!WARNING] There is no
servicefield 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 the
enum_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] The
operatorscolumn ofget-trace-or-log-fieldsover-advertises.HAS_TOKEN,HAS_ALL,HAS_ANY,LIKE_AND,ILIKE_LOG,IS_IP_ADDRESSappear 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 group_by_fields / fields / required_fields / sort_by 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 # total_count=<N> error_count=<N> is_free_search=<bool> line, a header
row, then one row per log. total_count is for the whole window, ignoring
page_size — 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
(from_time <= timestamp < to_time), 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
from_timeandwhere, andto_time= 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 ordered
toStartOfHour(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.
is_free_search
Touching body, trace_id, span_id, 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.value_operation:row_count,unique_count,avg,sum,min,max,p50,p90,p95,p99.fieldssupplies the operand for everything exceptrow_count:unique_countof pods →value_operation: "unique_count",fields: [{"field": "instance"}].limitcaps series onrangequeries (top-N by max value, default 20); ignored forinstant.
[!WARNING]
value_operation: "count"is not valid — the correct name isrow_count. The tool schema's own summary line says "Common: count, avg, …", which is wrong.
body cannot be used in group_by_fields (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
query_type: range, where: "type = ERROR",
group_by_fields: [{"field":"workload"}], value_operation: row_count
How many distinct pods logged a fatal error right now
query_type: instant, where: "type = FATAL", value_operation: unique_count,
fields: [{"field":"instance"}]
Read the actual timeout messages
search-logs, where: 'body ILIKE "%timeout%" AND workload = checkout',
required_fields: [{"field":"instance"},{"field":"body"}], page_size: 50
Error rate as a percentage — needs two queries plus a formula, so use
analyze-telemetry; see kubesense-mcp.
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 fromexample.- 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. value_operationisrow_count, 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.