Skip to content

Logs

Read or follow a Pod or workload with the same interface:

from cloudcoil import logs

print(logs.read("deployment/worker", namespace="jobs", tail_lines=50))
with logs.stream("deployment/worker", namespace="jobs") as records:
    for record in records:
        print(record.pod, record.container, record.message)

These built-in workload objects work too, including metadata-only references:

Resource String target (singular/plural also accepted)
Deployment deploy/worker
ReplicaSet rs/worker
StatefulSet sts/database
DaemonSet ds/agent
Job job/migration
CronJob cj/nightly
ReplicationController rc/worker

A workload selects one Pod, preferring non-terminating, running, ready Pods, then breaking ties by name. Container selection uses that Pod's default-container annotation or sole regular container; otherwise specify container=. Selection considers every list page.

To combine discovery and streaming across all matching Pods of any supported workload, use the async helper:

from cloudcoil import logs

async def follow_workers():
    async with logs.async_stream(
        "deployment/worker", namespace="jobs", all_pods=True,
        tail_lines=100, match=logs.LogFilter(contains="error", ignore_case=True),
    ) as records:
        async for record in records:
            print(record.pod, record.container, record.message)

all_pods=True selects one container per Pod, or the named container= on Pods that contain it. Records arrive as each stream produces them, preserving order within each source, with no global timestamp sort. max_streams=10 bounds concurrent requests and tasks; buffering is bounded too. Following more sources than this limit raises before opening any log streams: increase the limit explicitly. With follow=False, larger selections are processed in batches. A source error propagates and closes the other streams; exiting the block, a failing filter, and cancellation also close all responses.

For more control, logs.discover("deployment/worker", namespace="jobs") (or a Deployment object) returns containers across its matching Pods for use with read() / stream(). Discovery uses the full spec.selector, including matchExpressions, and ANDs any additional label_selector= for built-in workloads; it does not guess from workload metadata or template labels. ReplicationControllers use their flat label maps. Job references fetch the server-generated selector when it is missing. Empty selectors are rejected. Workload discovery requires Pod-list permission, and a name or metadata-only reference also requires permission to get that workload. Log reads additionally require pods/log. An empty workload returns no discovered sources; read/stream raise ResourceNotFound if no Pods match. Built-in workload selectors retain Kubernetes label-selector semantics.

Custom resources: pass a generated Resource instance or Unstructured object. If its spec.selector describes its Pods, both the standard matchLabels/matchExpressions shape and a flat label map work automatically. This is a convention, not a universal CRD guarantee: some operators use selectors for other resources. If the selector is absent or has another shape, discovery automatically traverses ownerReferences back from Pods. This supports chains such as custom resource → StatefulSet → Pod, as well as CronJob → Job → Pod. Pass a fetched resource with metadata.uid so ownership can be matched to the exact object; names alone are insufficient when an object has been recreated.

Supply label_selector= to explicitly define the Pod association when the convention does not apply or the operator does not set ownership links. For custom resources this replaces the convention; for built-in workloads it narrows their selector. It works on discover, read, stream, and their async equivalents:

from cloudcoil import logs
from cloudcoil.resources import Resource

async def follow_custom_resource(resource: Resource, pod_labels: str):
    async with logs.async_stream(
        resource, label_selector=pod_labels, all_pods=True, tail_lines=100
    ) as records:
        async for record in records:
            print(record.pod, record.container, record.message)

Use the operator's actual Pod labels. Custom kinds do not have string shortcuts or an automatic resource GET; the supplied object provides its selector and namespace. Use namespace= to select the Pod namespace for cluster-scoped or cross-namespace operators. The ownership fallback lists Pods in the selected namespace, then reads only referenced ancestors, caching lookups for that discovery call. It requests metadata where supported and uses API discovery for unfamiliar owner kinds. It respects namespaced/cluster-scoped ownership rules, skips deleted/recreated ancestors, and stops cycles. It requires permission to read intermediate owners and their API discovery endpoints; permission errors propagate instead of producing a silently incomplete result. Use label_selector= if those reads are unavailable. Resources without selectors or ownership links still need explicit Pod labels.

Discover containers by Kubernetes selectors, then filter their log records:

from cloudcoil import logs

errors = logs.LogFilter(regex=r"error|exception", ignore_case=True)
for source in logs.discover(all_namespaces=True, label_selector="app=worker"):
    # Sources include regular, init, and ephemeral containers. Select before reading.
    if source.container_type != "regular":
        continue
    with logs.stream(source, follow=False, tail_lines=100, timestamps=True, match=errors) as records:
        for record in records:
            print(record.namespace, record.pod, record.container, record.message)

discover() uses the active namespace by default and follows every list page. It returns metadata without downloading logs: labels, pod UID, owner references, node, phase, container type/state, and restart count. Selectors run on the server; container="app" selects an exact container name. A discovered source retains its configuration so it can be passed directly to read() or stream() outside the original context. Discovery is a snapshot of potential sources; a waiting container may not have logs, and discovery permissions do not imply permission to read them. API errors propagate to the caller.

# Read text with original line endings, or follow structured records.
print(logs.read("worker", namespace="jobs", tail_lines=50))
with logs.stream("worker", namespace="jobs", match=logs.LogFilter(contains="failed")) as records:
    for record in records:
        print(record.timestamp, record.message)

# The same operations are available asynchronously.
async for source in logs.async_discover(label_selector="app=worker", container="app"):
    async with logs.async_stream(source, follow=False, match=errors) as records:
        async for record in records:
            print(record.pod, record.message)

A supplied Pod provides its namespace, labels, and sole regular container (or the valid kubectl.kubernetes.io/default-container annotation). Ambiguous Pods require container=. A pod name alone needs only log-read permissions and leaves container selection to the server; labels and the selected container may then be unknown. Records expose raw, message, timestamp, pod, namespace, container, previous, labels, and, when discovered, source. Metadata is a snapshot; timestamps preserve nanosecond precision as strings. Use match=lambda record: ... for custom text or metadata filtering.

stream() follows by default and requests timestamps; follow=False reads a finite snapshot and defaults timestamps off. Both accept timestamps= explicitly. Always use with / async with so breaking, errors, and cancellation close the HTTP response. Following disables only the response read timeout. Deployment membership and source metadata are snapshots: streams do not reconnect, switch Pods, or discover new replicas during a rollout. For manually discovered live sources, run async consumers concurrently under your own concurrency limit; a sequential follow loop stays on its first live source.

All operations reuse the active configuration's authenticated HTTP client, or accept config= explicitly. Reusable LogOptions and typed keywords support previous, tail_lines, since_seconds or timezone-aware since_time, and limit_bytes. Direct keywords override options. Time/tail/byte limits apply on the server before client-side matching; byte limits may truncate a line. Kubernetes log access cannot recover deleted Pods or logs that the node has already rotated away.