Claude Code subagent imported from Ramilito/kubectl.nvim (
.claude/agents/logs.md). Copyright stays with the author.
Pod Logs Feature Guidance
ALWAYS use this subagent for ANY task involving:
- Log streaming, tailing, or fetching
- JSON expand/collapse in log output
- Log view keybindings or syntax highlighting
- Histogram visualization
LogSessionUserData or related mlua bindings
For general mlua FFI patterns (async functions, JSON serialization, error handling), use the rust subagent.
File Map
| Layer | File | Purpose |
|---|---|---|
| Rust | kubectl-client/src/cmd/log_session.rs |
Streaming session, histogram, log fetching, toggle_json() |
| Rust | kubectl-client/src/cmd/mod.rs:124-128 |
Registers log_stream_async and log_session exports |
| Rust | kubectl-client/src/lib.rs:474-488 |
Exposes toggle_json to Lua |
| Rust | kubectl-client/src/structs.rs |
LogConfig/PodRef structs + custom FromLua impl |
| Lua | lua/kubectl/client/init.lua |
client.log_session() / client.toggle_json() wrappers |
| Lua | lua/kubectl/client/types.lua |
kubectl.LogSession / kubectl.ToggleJsonResult annotations |
| Lua | lua/kubectl/resources/pods/init.lua |
Entry points: Logs(), TailLogs(), LogsWithPods(), LogsForFilter(), get_pods_for_logs() |
| Lua | lua/kubectl/views/logs/session.lua |
Session manager: options, timer polling, lifecycle |
| Lua | lua/kubectl/resources/pod_logs/mappings.lua |
Keybindings only — no sibling init.lua/definition.lua |
| Vim | syntax/k8s_pod_logs.vim |
Syntax highlighting |
pod_logs is not a BaseResource.extend module. Its view is assembled ad hoc inside pods/init.lua's LogsWithPods() (a framed buffer built via resource_factory's view_framed), and its mappings are loaded generically from the k8s_pod_logs filetype (strip k8s_ prefix → require kubectl.resources.pod_logs.mappings).
Data Flow
Two distinct code paths share one pod-list resolver but differ in transport:
Lua Transport Rust (dylib)
-------------------------------------------------------------------------------------------
Logs() / LogsWithPods() --> commands.run_async --> fetch_logs_async
one-shot, full buffer (libuv worker thread, (async fn) resolves
replace via buffers.set_content args JSON-encoded) targets, merges streams,
renders histogram
TailLogs() / session:start() --> client.log_session --> log_session
follow, incremental append (direct sync FFI call, blocks (sync fn) LogConfig via
--> views/logs/session.lua timer custom FromLua, spawns one
(vim.uv.new_timer, 200ms) polls Tokio task per container
session:read_chunk(), appends lines into a shared LogSession
toggle_json() -----------------------------------------------------> cmd::log_session::toggle_json
Pod-list resolution (get_pods_for_logs() in pods/init.lua, shared by both paths, in priority order):
- Buffer-local vars
kubectl_log_pods/kubectl_log_display— used when already inside ak8s_pod_logsbuffer (option toggles, refresh). - Tab multi-selections —
state.getSelections(bufnr)(populated by the generic<Plug>(kubectl.tab)mapping, not logs-specific). - Single selection fallback —
M.selection.pod/M.selection.nson thepodsmodule.
Workload flow (gl on Deployments/StatefulSets): their <Plug>(kubectl.logs) override calls the workload module's M.Logs(name, ns), which builds a label filter via child_view.predicate(name, ns) and calls pods.LogsForFilter(filter_key, ns, source). That helper warms the pod store via start_reflector_async (idempotent, waits for initial sync — required because get_table_async is store-only with no API fallback), resolves matching pods via get_table_async + filter_key (the Rust key-filter engine), sorts by name, then hands the explicit list to LogsWithPods after resetting the container selection. Zero matches → vim.notify, no view opened. Follow is a snapshot: f re-follows the pods captured at gl time — press gl again after a rollout to pick up replacement pods.
Rust: LogSession UserData
LogSession wraps a generic StreamingSession<String> (kubectl-client/src/streaming.rs) and exposes it to Lua as a stateful object:
impl UserData for LogSession {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("read_chunk", |_, this, ()| this.read_chunk());
methods.add_method("open", |_, this, ()| Ok(this.is_open()));
methods.add_method("close", |_, this, ()| {
this.close();
Ok(())
});
}
}
Interior mutability: StreamingSession holds Mutex<mpsc::UnboundedReceiver<String>> plus Arc<AtomicBool>/Arc<AtomicUsize>, since UserData methods receive &self.
Struct-from-table conversion: LogConfig's custom FromLua impl (structs.rs) is the reference pattern for turning a nested array-of-tables field (pods: Vec<mlua::Table> → Vec<PodRef>) into a typed struct — use it as the template when a new FFI config needs a list-of-tables field.
Rust: toggle_json Return Pattern
Returns table or nil based on Option<T>. See lib.rs:474-488:
match cmd::log_session::toggle_json(&input) {
Some(result) => {
let tbl = lua.create_table()?;
tbl.set("json", result.json)?;
tbl.set("start_idx", result.start_idx)?; // 1-based for Lua
tbl.set("end_idx", result.end_idx)?;
Ok(mlua::Value::Table(tbl))
}
None => Ok(mlua::Value::Nil),
}
Lua: Session Manager (views/logs/session.lua)
Owns per-buffer session objects plus module-level global_options (since/prefix/timestamps/previous — shared across all log buffers, not per-buffer). Public API: get_or_create(buf, win, options), get(buf), stop(buf), stop_all(), is_active(buf), get_options()/set_options()/reset_options().
Creating/starting: session:start(pods, container) calls client.log_session() (client/init.lua:111-113) synchronously — a direct FFI call, not offloaded to a worker thread — with follow = true.
Polling loop: session:start() also creates a vim.uv.new_timer() at a 200ms interval; each tick calls rust_session:read_chunk() and appends any returned lines to the buffer via nvim_buf_set_lines.
Cleanup (session:stop()): closes the Rust session, stops/closes the timer, removes the manager entry. Triggered by: the manual f toggle, self-detection when session:is_active() goes false, or a BufWinLeave autocmd registered per-session when the session starts.
Type Definitions
--- @class kubectl.LogSession
--- @field open fun(self: kubectl.LogSession): boolean
--- @field close fun(self: kubectl.LogSession)
--- @field read_chunk fun(self: kubectl.LogSession): string[]?
--- @class kubectl.ToggleJsonResult
--- @field json string
--- @field start_idx integer -- 1-based
--- @field end_idx integer -- 1-based
Keybindings
| Key | Plug | Action |
|---|---|---|
f |
<Plug>(kubectl.follow) |
Toggle follow mode |
gw |
<Plug>(kubectl.wrap) |
Toggle line wrap |
gp |
<Plug>(kubectl.prefix) |
Toggle pod prefix |
gt |
<Plug>(kubectl.timestamps) |
Toggle timestamps |
gh |
<Plug>(kubectl.history) |
Set since duration |
gpp |
<Plug>(kubectl.previous_logs) |
Previous container logs |
gj |
<Plug>(kubectl.expand_json) |
Expand/collapse JSON |
gl (Deployments/StatefulSets views) |
<Plug>(kubectl.logs) |
Logs for all pods of the workload |
Syntax Highlighting
Defined in syntax/k8s_pod_logs.vim. Key patterns:
| Pattern | Group | Matches |
|---|---|---|
kubectlLogContainer |
KubectlPending |
[pod-name] prefix |
kubectlLogTimestamp |
KubectlGray |
ISO timestamps |
kubectlLogError |
KubectlError |
ERROR, FATAL, PANIC |
kubectlLogWarn |
KubectlWarning |
WARN, WARNING |
Uses syn sync minlines=100 for performance on large buffers.
Durable Quirks & Invariants
- Container selection is global, not tied to the pod list.
M.selection.containerlives on thepodsmodule (set viapods.selectPod), independent of the multi-pod array built byget_pods_for_logs(). The pods-viewglmapping resets it tonilon every press — any new entry point that jumps intoLogs()must do the same or it will carry over a stale container filter. - Log options are one global table, not per-buffer.
views/logs/session.lua'sglobal_optionsis module-level and shared by every log buffer — changing prefix/timestamps/since/previous in one log view changes the default for the next one too. LogConfigis deserialized two different ways. The sync follow path (client.log_session) goes throughLogConfig's customFromLuaimpl (direct Lua table → struct, no JSON). The async one-shot path (log_stream_async→fetch_logs_async) JSON-encodes the args in Lua and doesserde_json::from_str::<LogConfig>in Rust. Keep both conversions in sync when adding a field.- Any option toggle drops follow mode.
LogsWithPods()unconditionally stops the active session for the current buffer before doing a one-shot fetch, so toggling prefix/timestamps/history/previous while following stops streaming; the user has to pressfagain to resume. - Histogram is one-shot only. It's computed solely inside
fetch_logs_async/render_histogramfrom timestamps found in line text — follow mode never recomputes it, and it renders nothing if no line contains a parseable ISO-8601 timestamp. - Follow streams emit disconnect markers.
spawn_container_log_tasksends--- log stream ended ---on clean stream end and--- log stream error: <e> ---on error, routed throughformat_log_line(so[pod]-prefixed in multi-pod views) — this is how a pod dying in the background becomes visible mid-follow. Only the follow/session path emits them; the one-shotfetch_logs_asyncnever does, and manual stops (ftoggle, closing the view) break the loop before those arms are reached. A marker can also appear on benign long-connection terminations (kubelet closing the stream) — pressingfreconnects; there is deliberately no auto-retry. - Teardown relies on the WinClosed → BufWinLeave chain. Session cleanup is wired to a
BufWinLeaveautocmd on the log buffer; that fires because closing the framed view's main pane cascades to close its other windows, and pane buffers arebufhidden=wipe. Any new teardown path should go through (or explicitly call)session:stop()/log_session.stop()rather than assume buffer deletion alone triggers cleanup.
Common Tasks
Adding a Log Option
- Add the default in
views/logs/session.lua'sget_default_options()(andconfig.options.logsinlua/kubectl/config.luaif it should be user-configurable). - Add a hint in
pods/init.lua'sLogsWithPods()hints array. - Add a keybinding + toggle callback in
pod_logs/mappings.lua(callupdate_option()thenpod_view.Logs()). - If Rust processing is needed: add the field to
LogConfig(structs.rs) and handle it in bothfetch_logs_asyncandLogSession::new(log_session.rs).
Adding Syntax Pattern
In syntax/k8s_pod_logs.vim:
syn match kubectlLogNewPattern /regex/
hi def link kubectlLogNewPattern HighlightGroup
Adding LogSession Method
- Add method in
log_session.rsUserData impl - Update type in
types.lua