Skip to content

feat: Kubernetes log export ingestion - #33

Merged
leo-aa88 merged 22 commits into
mainfrom
cursor/k8s-log-export-ingest-af93
Aug 16, 2026
Merged

leo-aa88 merged 22 commits into
mainfrom
cursor/k8s-log-export-ingest-af93

Conversation

@leo-aa88

@leo-aa88 leo-aa88 commented Aug 16, 2026 •

Copy link
Copy Markdown
Member

Implements #3: first-class ingestion of Kubernetes log exports through the existing SourceAdapter pipeline (no live cluster API).

Supported inputs

  • Concatenated kubectl logs --prefix --timestamps output
  • Fluent Bit / Vector JSON lines with a nested kubernetes object
  • CRI (kubelet / containerd) node logs: TIMESTAMP stdout|stderr F|P message
  • .gz files and tarballs (.tar, .tar.gz, .tgz, …)
  • Kubelet path conventions: /var/log/pods/<ns>_<pod>_<uid>/<container>/<n>.log (and .log.gz)

Mapping

K8s metadata raglogs field
labels.app / app.kubernetes.io/name / container_name service
namespace environment
pod host

Path-inferred identity is a fallback on RawLogLine; CLI --service / --env still win over adapter defaults. Parsed line fields win over both.

Usage

kubectl logs -n production -l app=billing-worker --all-containers \
  --prefix --timestamps --since=1h > /tmp/billing-export.log
raglogs ingest --adapter k8s /tmp/billing-export.log

raglogs ingest --adapter k8s --recursive ./var/log/pods
raglogs ingest --adapter k8s ./node-logs.tar.gz

API: POST /ingestions with "adapter": "k8s" and paths.

Review follow-up

  • LEVEL_SERVICE_PATTERN runs only on CRI-unwrapped inner messages.
  • Gzipped kubelet paths (…/0.log.gz) infer namespace/pod/container.
  • CRI P/F fragments are concatenated in the k8s adapter read() path.

Merge conflicts with sibling adapter PRs

#31 (Loki), #32 (Datadog), and this PR all touched registry / ingest CLI / API / README / the same unit tests. This branch now merges both of those heads and keeps all five adapters (file, cloudwatch, datadog, loki, k8s) wired together so it is conflict-free against main and against those PRs.

CloudWatch env remains ADAPTER_CLOUDWATCH_REGION (as on main). Loki adds LOKI_*; Datadog adds DATADOG_*.

Tests

Verified: pytest tests/unit/ — 369 passed.

Open in Web Open in Cursor 

cursoragent and others added 5 commits August 16, 2026 08:53
Pull logs from Grafana Loki via query_range so ingest no longer
needs an intermediate file export. Auth and defaults come from
RAGLOGS_ADAPTER_LOKI_* env vars; CLI/API reuse the existing
adapter + window flags.

Closes #2

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Operators can ingest a bounded Datadog window through the existing
SourceAdapter pipeline instead of exporting files first. Events are
mapped onto the JSON field aliases so core parsing stays source-agnostic.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Synthetic ns timestamps before the query window hid the next-page
cursor (max_ts stayed at window.start). Use timestamps inside the
window so pagination and resume-cursor assertions are meaningful.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
@leo-aa88
leo-aa88 marked this pull request as ready for review August 16, 2026 16:06
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

cursoragent and others added 4 commits August 16, 2026 17:40
CI flake8 --select=F82 treats a quoted forward-ref return type as an
undefined name. Drop the annotation so the helper matches the CloudWatch
tests.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Drop the ADAPTER_ segment so keys match RAGLOGS_OPENAI_* rather than
RAGLOGS_ADAPTER_DATADOG_*.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Settings already use the RAGLOGS_ prefix, so adapter_loki_url
became RAGLOGS_ADAPTER_LOKI_URL. Rename fields to loki_* /
cloudwatch_* so env vars match the rest of the config
(RAGLOGS_LOKI_URL, RAGLOGS_CLOUDWATCH_REGION).

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Adapter settings now bind LOKI_URL / CLOUDWATCH_REGION (and the
other LOKI_* keys) instead of RAGLOGS_LOKI_*. Core settings stay
on the RAGLOGS_ prefix.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Code review

Adapter + gzip/tar discovery look solid, and RawLogLine defaults (default_service / host / extra) are the right extension. Nested Fluent Bit / Vector kubernetes JSON aliases in core are fine — that is a field-alias problem, not an adapter branch.

Do not merge yet. k8s-specific parsing was pushed into shared parse_text_line, which every file ingest uses.

Blocking

  1. LEVEL_SERVICE_PATTERN mis-parses ordinary text logs (src/core/parsing/text_parser.py). A line like ERROR failed to connect to database today keeps the full message and uses --service / filename. After this PR it becomes service="failed", message="to connect to database". Fingerprints, clustering, and explain all shift for non-k8s file ingest. Apply that pattern only on the CRI recurse path (or inside the k8s adapter), and add a regression test.

CRI (TIMESTAMP stdout|stderr F|P) and kubectl [pod/container] prefixes are specific enough to live in core if you want; the untimestamped LEVEL service message rule is not.

Suggestions

  1. CRI P/F fragments are not reassembled — one kubelet error becomes two fingerprints.
  2. Gzipped kubelet paths (.../0.log.gz) are read as gzip but miss PODS_PATH (\d+\.log$), so namespace/pod/container stay empty.

Merge note

#31, #32, and #33 all touch registry.py, ingest CLI/API, README, and the same unit tests. Land one and rebase the others. Suggested order: #32, then #31, then this PR after the parser fix.

Comment thread src/core/parsing/text_parser.py Outdated
# Try structured pattern first. When service is already known (kubectl --prefix),
# skip the service-token patterns so "ERROR connection timeout" keeps its message.
m = STRUCTURED_TEXT_PATTERN.match(line_stripped) if infer_service else None
m_ls = LEVEL_SERVICE_PATTERN.match(line_stripped) if infer_service and not m else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this runs for every text log, not just k8s.

ERROR failed to connect to database currently keeps the full message and uses --service / filename. After this match it becomes service="failed", message="to connect to database" — fingerprints and clustering change for ordinary file ingest.

Keep LEVEL_SERVICE_PATTERN on the CRI recurse path (or in the k8s adapter) only. Add a regression test that parse_text_line("ERROR failed to connect to database", default_service="api").service == "api" and that the message is unchanged.

# /var/log/pods/<namespace>_<pod>_<uid>/<container>/<restart>.log
PODS_PATH = (
r"(?:^|/)pods/(?P<namespace>[^/_]+)_(?P<pod>[^/]+)_(?P<uid>[0-9a-fA-F-]+)"
r"/(?P<container>[^/]+)/(?P<restart>\d+)\.log$"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: .../0.log.gz is ingested as gzip (_is_gzip_log) but this regex requires .log$, so default_service / namespace / pod stay empty. Strip a trailing .gz before inferring metadata, or allow .log.gz here.

# CRI (containerd / kubelet) : <RFC3339> stdout|stderr F|P <message>
CRI_PATTERN = re.compile(
r"^(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))\s+"
r"(?P<stream>stdout|stderr)\s+(?P<tag>[FP])\s+(?P<msg>.*)$"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: kubelet splits long lines into P (partial) then F (full). Each fragment becomes its own LogEntry, so one error becomes two fingerprints. If node-log dumps are a real input, concatenate P…F in the k8s reader before parsing.

cursoragent and others added 3 commits August 16, 2026 17:47
Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Settings now read DB_URL, LLM_PROVIDER, LOKI_URL, and the rest
without a project prefix. RAGLOGS_ERROR stays as an error code.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Settings now read DB_URL, DATADOG_API_KEY, OPENAI_API_KEY, and the rest
without a project prefix, matching how .env files are typically written.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
@leo-aa88 leo-aa88 linked an issue Aug 16, 2026 that may be closed by this pull request
3 tasks
cursoragent and others added 7 commits August 16, 2026 17:57
Support kubectl captures, Fluent Bit/Vector JSON, CRI node logs,
and tarballs via --adapter k8s. Map namespace/pod/container onto
environment/host/service without special-casing the core pipeline.

Closes #3

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
CRI-unwrapped lines have no leading timestamp, so they need a
LEVEL/service/message pattern. kubectl --prefix already supplies
the workload identity — do not treat the first word after LEVEL
as a service token.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
api_base_url used to pass through arbitrary http(s) origins, so a
params.site override could send API keys off-Datadog. Canonicalize to
https://api.<known-site> and reject anything else at discover time.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Issue #2 asked for labels plus line text; labels were only stored on
source_ref, so service/environment/host stayed empty. Adapters now set
RawLogLine defaults and ingestion fills LogEntry from them when the
parsed line and SourceSpec do not already set those fields.

Also keep Loki's origin settings-only (no params.url), cap query_range
limit at 5000, and reject a non-integer resume cursor as
AdapterUnavailableError. Document that query_range pagination is global
across streams.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Keep isort-style ordering consistent with the rest of the adapter tests.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Resolve conflicts from the RAGLOGS_ prefix removal (#34) while keeping
Datadog env vars, docs, and adapter settings.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
LEVEL_SERVICE_PATTERN was matching every untimestamped "ERROR word rest"
line, so file ingest with default_service stole the first message word.
Apply that pattern only after CRI unwrap.

Also strip .gz before kubelet path metadata, and reassemble CRI P/F
fragments in the k8s adapter so one kubelet line is one fingerprint.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/k8s-log-export-ingest-af93 branch from 2eb3e78 to 9a47bbd Compare August 16, 2026 18:00
Resolve env-prefix overlaps from #34 by keeping unprefixed
CLOUDWATCH_* / LOKI_* names and bringing in main's settings tests.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
cursoragent and others added 2 commits August 16, 2026 18:05
Keep both adapters in the registry, CLI, API, README, and unit tests so
this PR can land after #32 without re-conflicting on shared files.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Keep file, CloudWatch, Datadog, Loki, and k8s adapters in the shared
registry/CLI/API/tests so this PR no longer conflicts with #31 or #32.

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Re-review (follow-up)

Previous blockers and the two suggestions are fixed. CI is green.

  • LEVEL_SERVICE_PATTERN is gated on cri_unwrapped=True, and test_untimestamped_error_does_not_steal_service_token covers ERROR failed to connect to database.
  • CRI P/F fragments are joined in _reassemble_cri_fragments.
  • _strip_gzip_log_suffix infers kubelet metadata from 0.log.gz.

No remaining blockers. Fine to merge after rebasing onto whatever lands first among #31/#32.

Suggestions (non-blocking)

  1. CLI vs path-default precedence. Ingestion uses raw.default_service or spec.service, so a kubelet path wins over --service. Add Loki source adapter for bounded-window ingest #31 does the opposite (spec.service or raw.default_service). Prefer CLI/API --service/--env as the override when you reconcile RawLogLine + _process_line with Loki — both PRs extend those types and will conflict.

  2. KUBECTL_PREFIX_PATTERN still runs on every text line ([app/worker] … can look like kubectl). Acceptable given issue feat: Kubernetes log export ingestion #3; just be aware it is not k8s-adapter-only.

CRI/kubectl handling in parse_text_line plus Fluent Bit aliases in extract_kubernetes_fields are in scope for #3. The CRI-inner gate keeps generic file ingest intact.

@leo-aa88
leo-aa88 merged commit 7976dcd into main Aug 16, 2026
1 check passed
@leo-aa88
leo-aa88 deleted the cursor/k8s-log-export-ingest-af93 branch August 16, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Kubernetes log export ingestion

2 participants