mcpField
K
kubesense-logs

kubesense-logs

github.com/kubesense-aiCloud & DevOps3101 installsUpdated 4d ago
Repository

About kubesense-logs

kubesense-logs is a skill for querying logs through the KubeSense MCP server. It helps users choose between reading individual rows and analyzing aggregates, with guidance on fields, filters, and pagination limits.

Key features

  1. 01Look up the log field catalog
  2. 02Retrieve individual log rows
  3. 03Aggregate counts, trends, and percentiles
  4. 04Filter with catalog labels and WHERE clauses
  5. 05Explain time-window pagination limits

Use cases

  1. 01Inspect errors for a workload
  2. 02Trend error counts by workload
  3. 03Count pods with fatal errors
  4. 04Search log message text for timeouts

MCP support

The skill requires the KubeSense MCP server and guides calls to its log-field catalog, search, and analysis tools.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/kubesense-ai/kubesense-mcp-skills --skill kubesense-logs

After installing, type/kubesense-logsin Claude Code to confirm it loaded.

Details

From the project README

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

ToolUse whenReturns
get-trace-or-log-fields (signal: "logs")Always, firstField catalog for your window
search-logsYou want to read individual log linesUp to page_size rows (default 10)
analyze-logsYou want counts, rates, trends, percentilesAggregated 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
ConceptUse this labelStorage column (rejected)
log severitytypelevel
podinstancepod_name
containercontainercontainer_name
node / hostnodehost
workloadworkload—
namespacenamespace—
clustercluster—
log messagebody—
log formatformat—
sourcesource—
regionregion—
app versionapp_version—
environmentenv_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 service field on logs. service exists only on traces (→ app_service). service = checkout against logs returns unknown field "service" for signal=logs. Use workload for the application name.

node on logs, node_name on traces. The two signals disagree — logs use node (→ host), traces use node_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_values column of get-trace-or-log-fields, not the example column. The example generator misses several enum fields (including type) and returns a blank cell for them. enum_values is 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 operators column of get-trace-or-log-fields over-advertises. HAS_TOKEN, HAS_ALL, HAS_ANY, LIKE_AND, ILIKE_LOG, IS_IP_ADDRESS appear in the legend but have no WHERE-string syntax — body HAS_TOKEN oom fails 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 an EXIST operator 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:

  1. Query your real window.
  2. Take the minimum timestamp among the returned rows — do not assume the last row is the oldest.
  3. Call again with the same from_time and where, and to_time = that minimum, copied verbatim.
  4. 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 than page_size across several clusters, advancing to_time can skip rows in a cluster the scan hadn't reached. To read exhaustively, pin clusters to a single cluster and use a large page_size. Otherwise prefer a bigger page_size over more pages, and analyze-logs over 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.

body ILIKE "…" here is rewritten server-side to use the body text index. Raw SQL gets no such rewrite, so do not move a message search into execute-sql unless the shape needs it — and if it does, follow Searching Log Text in kubesense-sql.

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: range for a time series, instant for one number per series.
  • value_operation: row_count, unique_count, avg, sum, min, max, p50, p90, p95, p99.
  • fields supplies the operand for everything except row_count: unique_count of pods → value_operation: "unique_count", fields: [{"field": "instance"}].
  • limit caps series on range queries (top-N by max value, default 20); ignored for instant.

Warning

value_operation: "count" is not valid — the correct name is row_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

  1. Call get-trace-or-log-fields with signal: "logs" first, passing the same window you will query — attribute keys are window-scoped.
  2. Use catalog labels: type, instance, container, node. Never level, pod_name, container_name, host.
  3. There is no service field on logs — use workload.
  4. type values are UPPERCASE (ERROR, not error). Read casing from enum_values, never from example.
  5. Quote any value containing /, :, %, or a space.
  6. NOT (x IN (...)), not x NOT IN (...).
  7. Stick to = != < > <= >= LIKE ILIKE SUBSTR_ILIKE IN — the advertised token/index operators have no WHERE syntax.
  8. value_operation is row_count, not count.
  9. analyze-logs for counts and trends; search-logs only to read messages. Never paginate to compute an aggregate.
  10. Page by narrowing to_time to the minimum timestamp returned; there is no offset. Treat multi-cluster paged scans as samples unless you pin one cluster.
  11. body is filter-only — it cannot be a group-by.

Comments

Markdown supported