Skip to content

feat(monitoring): auto-route tenant logs using namespace labels - #2195

Merged
Aleksei Sviridkin (lexfrei) merged 2 commits into
cozystack:mainfrom
mattia-eleuteri:monitoring/per-tenant-log-routing
Sep 29, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 2 commits into
cozystack:mainfrom
mattia-eleuteri:monitoring/per-tenant-log-routing

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Mar 10, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

A tenant that runs its own monitoring stack gets a VictoriaLogs instance, but the node fluent-bit in cozy-monitoring sends every container log to global.target (tenant-root), so that instance stays empty. This PR routes each namespace's container logs to the VictoriaLogs named by its namespace.cozystack.io/monitoring label, the label the tenant chart already sets and the per-tenant vmagent already selects on.

Chart (packages/system/monitoring-agents). templates/_tenant-log-routes.tpl looks up the namespaces and keeps those whose label is set and names a tenant other than global.target. For those, fluent-bit gets one rewrite_tag filter with a Rule per target (a single emitter however many tenants are routed), one http output per target to vlinsert-generic.<target>.svc:9481, and a per-target modify Set tenant <target>, because the re-tagged record re-enters the filter chain where the Match * Add tenant cannot overwrite the key. Unlabelled namespaces, empty labels and labels equal to global.target stay on the default outputs.

Controller (internal/controller/tenantlogrouting). lookup results are not part of the digest helm-controller compares, so the chart alone would not pick up a tenant that turns monitoring on, or drop one that is removed, until an unrelated upgrade. The new reconciler in cozystack-controller digests the monitoring label across all namespaces and, when the digest moves, sets reconcile.fluxcd.io/requestedAt and reconcile.fluxcd.io/forceAt on cozy-monitoring/monitoring-agents, which forces an upgrade and re-runs the lookup. It digests every labelled namespace rather than repeating the chart's selection, so it cannot drift from the chart; the cost is an occasional upgrade that renders the same config. The digest it acted on is kept in its own annotation, so a manual flux reconcile does not make it force another upgrade. The existing ClusterRole already grants the verbs it needs.

Behaviour change for the platform operator. Routed records are re-tagged with keep false: once a tenant runs its own monitoring, its namespaces' container logs go to that tenant's VictoriaLogs and no longer to the root one. Events and audit logs are not routed and stay on the root store.

Why not a per-tenant collector. An earlier comment on this PR proposed a fluent-bit per tenant, following the per-tenant vmagent and the OTLP collector from #3763. That collector receives OTLP over the network; a log collector has to tail /var/log through hostPath, which the PodSecurity level of tenant namespaces rejects, and running it in a privileged system namespace instead would mean one DaemonSet per tenant tailing every node's logs. The routing therefore stays in the one node DaemonSet, with the reconciler closing the reconcile gap.

Tests: tests/fluent-bit_tenant_log_routing_test.yaml mocks the namespaces with kubernetesProvider and pins which get a rule and an output, which are skipped (unlabelled, empty label, label equal to global.target), and that a routed record carries the target tenant. Three of its cases fail against the previous revision of this branch. tests/fluent-bit_tenant_log_routing_offline_test.yaml pins that an offline render routes nothing. internal/controller/tenantlogrouting/reconciler_test.go covers first sight, no-op while labels are unchanged (including after a manual reconcile), a tenant turning monitoring on, a routed namespace being deleted, and a missing release.

Refs #2194

Screenshots

Downstream repositories

No entry of the trigger map matches the diff: it touches packages/system/monitoring-agents (values, a template helper, tests), a new reconciler in internal/controller, its registration in cmd/cozystack-controller, and a comment in the cozystack-controller ClusterRole. The one open question is cozystack/website: content/en/docs/next/operations/services/monitoring/logs.md describes log collection in general terms and does not say where a tenant's container logs land, so the behaviour change above could deserve a sentence there. I have left every box empty for a maintainer to decide.

Release note

feat(monitoring): container logs from a tenant that runs its own monitoring stack now go to that tenant's VictoriaLogs instead of the root one. The node fluent-bit routes them by the namespace.cozystack.io/monitoring label, and cozystack-controller re-renders the node agents when that label changes on any namespace.

@dosubot dosubot Bot added size/S This PR changes 10-29 lines, ignoring generated files kind/feature Categorizes issue or PR as related to a new feature labels Mar 10, 2026
@coderabbitai

coderabbitai Bot commented Mar 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes update Fluent Bit configuration rendered from packages/system/monitoring-agents/values.yaml to retarget several existing outputs from vlogs-generic.*:9428 to vlinsert-generic.*:9481, and add dynamically-generated tenant-specific rewrite_tag filters and per-tenant HTTP OUTPUT blocks based on Namespace labels and deduplicated targets.

Changes

Cohort / File(s) Summary
Fluent Bit config (values.yaml)
packages/system/monitoring-agents/values.yaml
Change default HTTP output host/port from vlogs-generic.{{ .Values.global.target }}.svc:9428 to vlinsert-generic.{{ .Values.global.target }}.svc:9481. Add logic that: (1) queries Namespace resources, extracts namespace.cozystack.io/monitoring labels, deduplicates targets (excluding empty and default), (2) emits per-namespace FILTER rewrite_tag rules to retag kube.* into tenant.{target}.{namespace}.$TAG when monitor target differs from default, and (3) generates per-tenant OUTPUT blocks Match tenant.{target}.* sending JSON to vlinsert-generic.{target}.svc:9481. Existing modify filter adding tenant {{ .Values.global.target }} for Match * remains. Pay attention to templating, label lookup, and escaping in the Helm-rendered config.

Sequence Diagram(s)

sequenceDiagram
    participant Pod as App Pod
    participant FB as Fluent Bit
    participant VInsert as vlinsert-generic (tenant)

    Pod->>FB: emit logs (kube.* tags)
    FB->>FB: FILTER rewrite_tag (per-namespace) -> retag to tenant.{target}.{namespace}.<stream>
    FB->>FB: FILTER modify (Match *) -> add tenant {{ .Values.global.target }}
    FB->>VInsert: OUTPUT Match tenant.{target}.* -> HTTP/JSON POST to vlinsert-generic.{target}.svc:9481
    VInsert-->>FB: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements objective 1 from issue #2194 by adding per-tenant log routing that directs logs to tenant-specific VLogs instances instead of all logs going to the parent tenant.
Out of Scope Changes check ✅ Passed All changes in this PR are directly within scope: the modifications update fluent-bit routing configuration to implement per-tenant log routing as specified in issue #2194.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: automatic per-tenant log routing based on Kubernetes namespace labels.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/system/monitoring-agents/values.yaml (1)

382-394: Consider adding input validation for tenantLogRouting entries.

If a user provides a tenantLogRouting entry without the required namespace or vlogs fields, the template will generate an invalid fluent-bit config that could cause deployment failures with unhelpful error messages.

🛡️ Suggested validation approach

Add validation at the start of the range loop:

      {{- range .Values.tenantLogRouting }}
+     {{- if not .namespace }}
+     {{- fail "tenantLogRouting entry missing required 'namespace' field" }}
+     {{- end }}
+     {{- if not .vlogs }}
+     {{- fail "tenantLogRouting entry missing required 'vlogs' field" }}
+     {{- end }}
      [OUTPUT]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/system/monitoring-agents/values.yaml` around lines 382 - 394, The
template iterates over .Values.tenantLogRouting without checking required
fields, which can produce invalid fluent-bit OUTPUT blocks when namespace or
vlogs are missing; update the range loop over tenantLogRouting to validate each
entry by testing that .namespace and .vlogs are non-empty (e.g., using
conditional/if checks) and either skip the entry or emit a clear failure/warning
message before rendering the OUTPUT stanza for that item; reference the existing
range over .Values.tenantLogRouting and the fields .namespace and .vlogs when
implementing the checks so only entries with both fields produce the Name http /
Match / Host / header blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/system/monitoring-agents/values.yaml`:
- Around line 444-449: The Rule line in the Helm template that builds the
rewrite_tag filter interpolates .Values.tenantLogRouting[].namespace directly
into a regex (the "Rule $kubernetes_namespace_name ^({{ .namespace }})$
tenant.{{ .namespace }}.$TAG false"), which can break when namespaces contain
regex metacharacters; update that template to escape the namespace using Helm's
regexQuoteMeta (or equivalent) when constructing both the capture group and the
replacement token so the generated regex is literal and safe, ensuring you
reference the template block iterating over .Values.tenantLogRouting and the
rewrite_tag Rule to locate and change the interpolation.

---

Nitpick comments:
In `@packages/system/monitoring-agents/values.yaml`:
- Around line 382-394: The template iterates over .Values.tenantLogRouting
without checking required fields, which can produce invalid fluent-bit OUTPUT
blocks when namespace or vlogs are missing; update the range loop over
tenantLogRouting to validate each entry by testing that .namespace and .vlogs
are non-empty (e.g., using conditional/if checks) and either skip the entry or
emit a clear failure/warning message before rendering the OUTPUT stanza for that
item; reference the existing range over .Values.tenantLogRouting and the fields
.namespace and .vlogs when implementing the checks so only entries with both
fields produce the Name http / Match / Host / header blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ffb4485-2409-4778-bcf4-7be38544caca

📥 Commits

Reviewing files that changed from the base of the PR and between a13481b and 9b044a3.

📒 Files selected for processing (1)
  • packages/system/monitoring-agents/values.yaml

Comment thread packages/system/monitoring-agents/values.yaml Outdated
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses the current limitation where monitoring-agents Fluent Bit sends all container logs to a generic VLogs instance, preventing tenants from receiving their own logs in their dedicated VLogs. By introducing a new tenantLogRouting configuration, it enables per-tenant log routing, allowing logs from specific namespaces to be automatically directed to their respective VLogs instances, thereby enhancing log isolation and management for multi-tenant environments.

Highlights

  • Configuration Parameter: Introduced tenantLogRouting to the monitoring-agents Fluent Bit configuration, allowing specification of tenant namespaces and their corresponding VLogs endpoints.
  • Dynamic Output Generation: Implemented logic to dynamically generate per-tenant HTTP OUTPUT blocks within Fluent Bit, directing logs to each tenant's dedicated VLogs instance based on the new configuration.
  • Log Retagging: Added dynamic generation of rewrite_tag filters that match logs by kubernetes_namespace_name and re-tag them for proper routing to the respective tenant's VLogs.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/system/monitoring-agents/values.yaml
    • Introduced a new tenantLogRouting configuration block under fluent-bit to enable per-tenant log routing.
    • Added a range loop to dynamically generate [OUTPUT] blocks for each configured tenant, directing logs to their specific VLogs instance.
    • Included another range loop to dynamically generate [FILTER] blocks with rewrite_tag rules, matching logs by kubernetes_namespace_name and re-tagging them for tenant-specific routing.
Activity
  • The pull request was created to address the issue of tenants not receiving their logs in their dedicated VLogs instances.
  • Automated summaries were generated by Claude Code and CodeRabbit.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

The pull request introduces a new Fluent Bit configuration for per-tenant log routing, allowing logs from specific namespaces to be routed to dedicated VLogs services. This involves dynamically generating HTTP output and rewrite tag filter blocks based on user-defined tenantLogRouting values. However, the current implementation has several security vulnerabilities: the .vlogs parameter, used as an HTTP output host, is user-controlled and creates a Server-Side Request Forgery (SSRF) risk; the .namespace parameter, embedded in a regular expression for tag rewriting, is user-controlled and leads to a Regular Expression Denial of Service (ReDoS) vulnerability; and the .namespace parameter, embedded in glob patterns for log matching, is user-controlled and creates a log routing injection vulnerability. Remediation requires strict validation and escaping of these user-controlled parameters, along with refactoring duplicated output configurations for better maintainability.

{{- if not .namespace }}{{ fail "tenantLogRouting: namespace is required" }}{{- end }}
[OUTPUT]
Name http
Match tenant.{{ .namespace }}.*

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.

security-high high

The .vlogs parameter is directly used as the Host for an HTTP output, which is a user-controlled value. This creates a Server-Side Request Forgery (SSRF) vulnerability, allowing an attacker to specify an arbitrary hostname for Fluent Bit to send logs, potentially scanning internal networks or exfiltrating data. Implement strict validation and allow-listing for .vlogs to restrict it to trusted hosts. Additionally, the configuration for this [OUTPUT] block is duplicated, increasing maintenance overhead. Consider refactoring for future maintainability.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a Helm values.yaml template — .vlogs is set by the platform operator during tenant provisioning, not by end users. There is no user-controlled input path here, so SSRF is not applicable.

{{- range .Values.tenantLogRouting }}
[FILTER]
Name rewrite_tag
Match kube.*

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.

security-high high

The .namespace parameter is directly embedded into a regular expression within the Rule field of a rewrite_tag filter. This user-controlled value creates a Regular Expression Denial of Service (ReDoS) vulnerability, allowing an attacker to inject malicious regex patterns that could consume excessive CPU and lead to a denial of service. Implement strict validation and escape special characters for .namespace. Additionally, the current regex ^({{ .namespace }})$ uses an unnecessary capturing group; simplifying it could improve clarity and avoid minor overhead.

Rule $kubernetes_namespace_name ^{{ .namespace }}$ tenant.{{ .namespace }}.$TAG false

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — regexQuoteMeta is now applied to escape the namespace value before it's used in the regex pattern.

{{- range .Values.tenantLogRouting }}
{{- if not .namespace }}{{ fail "tenantLogRouting: namespace is required" }}{{- end }}
[OUTPUT]
Name http

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.

security-medium medium

The .namespace parameter is directly embedded into the Match field of an [OUTPUT] block, which uses glob patterns. An attacker controlling this value could inject glob pattern wildcards (*, ?, []) to broaden log matching criteria.

Impact: This could lead to misrouting of logs, potential information leakage between tenants if a malicious pattern matches other tenants' logs, or a denial of service by causing Fluent Bit to process an excessive amount of logs.

Remediation: Implement strict validation for the .namespace parameter to ensure it only contains valid and expected namespace names. Avoid allowing wildcard characters or other special glob pattern characters in user-supplied namespace values.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The Match field uses fluent-bit tag matching, not arbitrary glob. The namespace value is set by the platform during provisioning, not by tenant users, so glob injection is not a concern here.

@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch from 6eb1707 to 70274a7 Compare March 10, 2026 15:57

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/system/monitoring-agents/values.yaml`:
- Around line 382-387: The template validates .namespace but not .vlogs, so
missing vlogs silently defaults Host to localhost; update the tenantLogRouting
loop in values.yaml to check for .vlogs (e.g., add {{- if not .vlogs }}{{ fail
"tenantLogRouting: vlogs is required for namespace <namespace>" }}{{- end }})
before rendering the [OUTPUT] block so rendering fails fast when .vlogs is
omitted; keep the existing Host {{ .vlogs }} usage and include the namespace in
the failure message to aid debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92ca9944-7a8d-42fc-9f26-3d5af186d017

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb1707 and 70274a7.

📒 Files selected for processing (1)
  • packages/system/monitoring-agents/values.yaml

Comment thread packages/system/monitoring-agents/values.yaml Outdated
@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch from 70274a7 to 28a6826 Compare March 11, 2026 09:38

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for tackling this! The problem is real — tenant VLogs instances receiving no logs is a gap we need to close.

However, I think the manual tenantLogRouting approach has some drawbacks:

  1. No automation — someone has to manually add entries to values.yaml every time a tenant with monitoring is created/deleted. This is error-prone and doesn't scale.
  2. No sub-tenant support — nested tenants won't get their logs routed automatically.
  3. Diverges from the existing pattern — vmagent already solves the equivalent problem for metrics automatically.

How vmagent does it (for reference)

  • Each tenant namespace gets a label namespace.cozystack.io/monitoring: <monitoring-namespace> (namespace.yaml)
  • The cozystack-values Secret injects _namespace.monitoring into every tenant, pointing to the correct monitoring stack
  • The monitoring-agents HelmRelease for each Kubernetes cluster uses $targetTenant := .Values._namespace.monitoring to route both metrics and logs to the right destination automatically

For child Kubernetes clusters, fluent-bit already sends all logs to vlinsert-generic.{{ $targetTenant }}.svc — so logs already work automatically there. The gap is only on the root cluster, where all namespace logs go to tenant-root without per-tenant splitting.

Suggested approach

Instead of a manual list in monitoring-agents values, consider generating the fluent-bit routing config at the tenant chart level (packages/apps/tenant/templates/), using the same _namespace.monitoring value that vmagent already relies on. This way:

  • Tenant creation automatically sets up log routing
  • Sub-tenant inheritance works out of the box (child tenants inherit _namespace.monitoring from parent)
  • The pattern stays consistent with how metrics collection already works

For reference, PR #1684 added per-tenant vmagent with automatic wiring — the same pattern should be followed for log routing.

@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch from 28a6826 to 29a4a64 Compare March 19, 2026 13:49
@dosubot dosubot Bot added size/M This PR changes 30-99 lines, ignoring generated files and removed size/S This PR changes 10-29 lines, ignoring generated files labels Mar 19, 2026
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Andrei Kvapil (@kvaps) Thanks for the detailed review and the pointer to how vmagent handles this!

I've reworked the implementation to follow the exact same automatic pattern. Here's what changed:

Approach: Instead of a manual tenantLogRouting list, fluent-bit config now uses Helm lookup to discover namespaces with the namespace.cozystack.io/monitoring label — the same label that vmagent already relies on. No manual configuration needed.

How it works:

  1. lookup "v1" "Namespace" "" "" lists all namespaces at deploy time
  2. Filters those with namespace.cozystack.io/monitoring pointing to a target different from global.target (tenant-root)
  3. Generates rewrite_tag filters per namespace → routes matching logs to tenant.<target>.<ns>.$TAG
  4. Generates deduplicated HTTP OUTPUTs per unique monitoring target → sends to vlinsert-generic.<target>.svc:9481
  5. Flux reconciliation (every 5min) picks up new tenants automatically

Also fixed: Default outputs now correctly use vlinsert-generic:9481 (VLCluster) instead of vlogs-generic:9428.

Edge cases handled:

  • Tenant without monitoring → label = tenant-root → filtered out → logs go to default
  • Sub-tenant inheriting parent monitoring → label = parent target → routed correctly
  • helm template dry-run → lookup returns empty → no dynamic routing → safe
  • Namespace deleted → lookup no longer finds it → automatic cleanup

Only packages/system/monitoring-agents/values.yaml is modified, no changes needed in the tenant chart since namespace.cozystack.io/monitoring labels are already in place.

@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch 2 times, most recently from da4c3ac to 1e8f354 Compare March 19, 2026 13:52

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/system/monitoring-agents/values.yaml (1)

442-459: Consider order of modify and rewrite_tag filters for tenant field accuracy.

The modify filter at line 442-445 adds tenant {{ .Values.global.target }} to all logs (Match *) before the rewrite_tag filters process them. This means logs routed to tenant-specific VLogs instances will still have tenant: cozy-monitoring (or whatever global.target is) rather than the actual tenant target.

If tenants should see their own tenant name in the tenant field, consider either:

  1. Moving the tenant modify filter after rewrite_tag and adjusting the match patterns, or
  2. Adding tenant-specific modify filters after rewrite_tag

This may be intentional if the tenant field is meant for platform-level identification, or could be addressed in the follow-up PR #2196 (metadata redaction).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/system/monitoring-agents/values.yaml` around lines 442 - 459, The
current global modify filter (Name modify that adds tenant {{
.Values.global.target }}) is applied before the rewrite_tag rules, causing
tenant-specific routing to keep the global tenant value; update the config so
tenant is set after tags are rewritten: either move the global "Name modify Add
tenant {{ .Values.global.target }}" block below the rewrite_tag blocks and
narrow its Match to only platform-level logs, or add per-namespace
tenant-specific modify filters immediately after the rewrite_tag Rule (matching
the rewritten tag pattern such as tenant.{{ $monTarget }}.*) and set Add tenant
to the corresponding {{ $monTarget }}; adjust Match patterns accordingly so the
modify that sets tenant runs after the rewrite_tag transformation (refer to the
existing [FILTER] blocks, the Name modify filter and the Name rewrite_tag filter
with Rule $kubernetes_namespace_name ^({{ $ns.metadata.name }})$ tenant.{{
$monTarget }}.{{ $ns.metadata.name }}.$TAG).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/system/monitoring-agents/values.yaml`:
- Around line 347-348: Add a missing ExternalName Service entry for
vlinsert-generic in the monitoring-external-services.yaml template: create an
ExternalName service resource named vlinsert-generic that mirrors the existing
vlogs-generic entry but points to vlinsert-generic.tenant-root.svc.cluster.local
and exposes port 9481 so Fluent Bit outputs (references to vlinsert-generic:9481
in values.yaml) can resolve; update the template where other services are
defined (use the vlogs-generic Service as the pattern) and ensure the same
entries are added for the other occurrences noted (lines near 358-359, 369-370,
392).

---

Nitpick comments:
In `@packages/system/monitoring-agents/values.yaml`:
- Around line 442-459: The current global modify filter (Name modify that adds
tenant {{ .Values.global.target }}) is applied before the rewrite_tag rules,
causing tenant-specific routing to keep the global tenant value; update the
config so tenant is set after tags are rewritten: either move the global "Name
modify Add tenant {{ .Values.global.target }}" block below the rewrite_tag
blocks and narrow its Match to only platform-level logs, or add per-namespace
tenant-specific modify filters immediately after the rewrite_tag Rule (matching
the rewritten tag pattern such as tenant.{{ $monTarget }}.*) and set Add tenant
to the corresponding {{ $monTarget }}; adjust Match patterns accordingly so the
modify that sets tenant runs after the rewrite_tag transformation (refer to the
existing [FILTER] blocks, the Name modify filter and the Name rewrite_tag filter
with Rule $kubernetes_namespace_name ^({{ $ns.metadata.name }})$ tenant.{{
$monTarget }}.{{ $ns.metadata.name }}.$TAG).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5220f786-1b26-476a-87f9-623825339d61

📥 Commits

Reviewing files that changed from the base of the PR and between 28a6826 and 29a4a64.

📒 Files selected for processing (1)
  • packages/system/monitoring-agents/values.yaml

Comment on lines +347 to +348
Host vlinsert-generic.{{ .Values.global.target }}.svc
port 9481

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for vlinsert-generic service definitions in the codebase
echo "=== Searching for vlinsert-generic service definitions ==="
rg -n "vlinsert-generic" --type yaml

echo ""
echo "=== Checking monitoring package templates ==="
fd -e yaml . packages/system/monitoring/templates -x grep -l "vlinsert" {} \;

Repository: cozystack/cozystack

Length of output: 610


🏁 Script executed:

#!/bin/bash
# Search for all external service definitions
echo "=== Checking for external services definitions ==="
rg -n "ExternalName" --type yaml -A 3 | head -50

echo ""
echo "=== Looking for vlogs references ==="
rg -n "vlogs" --type yaml | grep -i service

echo ""
echo "=== Checking monitoring-external-services.yaml ==="
find . -name "monitoring-external-services.yaml" -exec cat {} \;

Repository: cozystack/cozystack

Length of output: 9071


🏁 Script executed:

#!/bin/bash
# Check Victoria Logs configuration and services
echo "=== Searching for victoria logs related configs ==="
fd -name "*.yaml" | xargs grep -l "victoria" | head -20

echo ""
echo "=== Check if vlinsert is mentioned in any documentation ==="
fd -name "*.md" | xargs grep -l "vlinsert" 2>/dev/null || echo "No vlinsert in markdown files"

Repository: cozystack/cozystack

Length of output: 684


🏁 Script executed:

#!/bin/bash
# Search for victoria-logs package
echo "=== Finding victoria-logs related files ==="
find . -type d -name "*victoria*logs*" | head -20

echo ""
echo "=== Searching for any vlinsert mention (case-insensitive) ==="
rg -i "vlinsert" --type yaml

echo ""
echo "=== Check monitoring package structure ==="
ls -la packages/system/monitoring/templates/ | head -30

echo ""
echo "=== Check if monitoring package defines any services ==="
fd -e yaml . packages/system/monitoring/templates -exec basename {} \;

Repository: cozystack/cozystack

Length of output: 2170


🏁 Script executed:

#!/bin/bash
# Check vlogs configuration in monitoring package
echo "=== Content of vlogs.yaml ==="
cat packages/system/monitoring/templates/vlogs/vlogs.yaml

echo ""
echo "=== Content of vlogs grafana-datasource.yaml ==="
cat packages/system/monitoring/templates/vlogs/grafana-datasource.yaml

Repository: cozystack/cozystack

Length of output: 1146


🏁 Script executed:

#!/bin/bash
# Check monitoring-agents deployment/values
echo "=== Check monitoring-agents to understand service discovery ==="
head -100 packages/system/monitoring-agents/values.yaml

echo ""
echo "=== Search for VLogs CRD definitions ==="
rg -i "kind.*vlogs" --type yaml

echo ""
echo "=== Check if VLogs operator creates services automatically ==="
find . -name "*.yaml" -o -name "*.md" | xargs grep -l "VLogs.*service" 2>/dev/null | head -5

Repository: cozystack/cozystack

Length of output: 4352


🏁 Script executed:

#!/bin/bash
# Check VLogs CRD to understand service creation
echo "=== Searching for VLogs CRD service-related fields ==="
rg -A 5 -B 5 "vlinsert\|service" packages/system/victoria-metrics-operator/charts/victoria-metrics-operator/crd.yaml | head -100

echo ""
echo "=== Check if there's any documentation about VLogs services ==="
find . -name "*.md" -o -name "README*" | xargs grep -l "VLogs\|vlinsert" 2>/dev/null | head -5

Repository: cozystack/cozystack

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Look at the full diff context around the changes
echo "=== Check git diff for this file if available ==="
git log --oneline packages/system/monitoring-agents/values.yaml 2>/dev/null | head -5

echo ""
echo "=== Check recent changes context ==="
git show HEAD:packages/system/cozystack-basics/templates/monitoring-external-services.yaml 2>/dev/null || echo "Not available"

Repository: cozystack/cozystack

Length of output: 783


🏁 Script executed:

#!/bin/bash
# Check the actual changes in monitoring-agents/values.yaml
echo "=== Context around lines 340-400 in monitoring-agents/values.yaml ==="
sed -n '340,400p' packages/system/monitoring-agents/values.yaml | cat -n

echo ""
echo "=== Check what the OUTPUT is for ==="
sed -n '330,350p' packages/system/monitoring-agents/values.yaml | cat -n

Repository: cozystack/cozystack

Length of output: 4108


🏁 Script executed:

#!/bin/bash
# Final verification: check if VLogs operator would create vlinsert services
echo "=== Search for any indication that vlinsert services are created ==="
rg -i "vlinsert" . --type yaml

echo ""
echo "=== Check if there's a pattern for how services are named in monitoring ==="
rg -i "port.*948|9428" . --type yaml | grep -v node_modules | head -20

echo ""
echo "=== Verify the monitoring namespace and how external services work ==="
grep -r "cozy-monitoring" packages/system/cozystack-basics/templates/monitoring-external-services.yaml

Repository: cozystack/cozystack

Length of output: 1308


Add vlinsert-generic ExternalName service to monitoring-external-services.yaml.

Fluent Bit outputs (lines 347-348 and others) send logs to vlinsert-generic:9481 (VictoriaLogs insert endpoint), but this service is not defined in packages/system/cozystack-basics/templates/monitoring-external-services.yaml. Without it, logs will fail to be inserted into VictoriaLogs. Add the missing ExternalName service mapping to vlinsert-generic.tenant-root.svc.cluster.local similar to the existing vlogs-generic service.

Also applies to: 358-359, 369-370, 392

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/system/monitoring-agents/values.yaml` around lines 347 - 348, Add a
missing ExternalName Service entry for vlinsert-generic in the
monitoring-external-services.yaml template: create an ExternalName service
resource named vlinsert-generic that mirrors the existing vlogs-generic entry
but points to vlinsert-generic.tenant-root.svc.cluster.local and exposes port
9481 so Fluent Bit outputs (references to vlinsert-generic:9481 in values.yaml)
can resolve; update the template where other services are defined (use the
vlogs-generic Service as the pattern) and ensure the same entries are added for
the other occurrences noted (lines near 358-359, 369-370, 392).

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.

NOT LGTM — the change points all log outputs at a VictoriaLogs service that isn't deployed (breaks log shipping); the per-tenant routing also can't reconcile under Flux and diverges from the linked issue's design.

Business context: route each tenant's container logs to that tenant's VictoriaLogs instance (issue #2194), instead of everything landing in the root tenant.

Blockers

B1: outputs target vlinsert-generic:9481, which is not deployed — breaks all log shipping

File: packages/system/monitoring-agents/values.yaml:347 (also 358, 369, 392)

All three baseline outputs (kube/events/audit) and the new per-tenant output are switched to vlinsert-generic.<target>.svc:9481 — the cluster-mode VictoriaLogs ingest endpoint (VLCluster → vlinsert, port 9481). The platform deploys single-node VictoriaLogs only: packages/system/monitoring/templates/vlogs/vlogs.yaml creates kind: VLogs named generic → service vlogs-generic:9428. There is no VLCluster/vlinsert anywhere in the repo. Corroboration: the tenant kubernetes app ships to vlogs-generic.<tenant>.svc:9428 (packages/apps/kubernetes/templates/helmreleases/monitoring-agents.yaml:76-77), the Grafana datasource reads vlogs-generic:9428, and issue #2194 itself names the target vlogs-generic.<tenant>.svc. After this merges, fluent-bit posts to a non-existent service and root-tenant container/event/audit log shipping stops. Revert host/port to vlogs-generic:9428, or land the VLogs→VLCluster migration in the same PR.

B2: per-tenant routing via lookup can't reconcile under Flux

File: packages/system/monitoring-agents/values.yaml (the two new lookup "v1" "Namespace" blocks)

monitoring-agents is a Flux HelmRelease; helm-controller's upgrade trigger is digest-based on chart + values, and cluster state read via lookup is invisible to that hash. Creating a tenant namespace or toggling the namespace.cozystack.io/monitoring label changes no digest, so the config won't re-render and the new tenant gets no routing until an unrelated change forces a reconcile. It's also why helm template renders both blocks to nothing offline (the PR's three test-plan items are unchecked). Route this through values (the tenantLogRouting list the PR body / issue #2194 describe) instead of lookup.

B3: implementation diverges from the documented contract

The PR body and issue #2194 specify an explicit tenantLogRouting values list of {namespace, vlogs} pairs targeting single-node vlogs-…:9428. The diff instead implements label-driven auto-discovery (namespace.cozystack.io/monitoring) against cluster-mode endpoints. That label is defined nowhere else in the repo (grep finds it only here), so its contract — who sets it, to what value — is undefined and undocumented. Reconcile code and design (the values-list approach also fixes B2).

B4: the re-tagged tenant copy keeps the root's identity labels

File: packages/system/monitoring-agents/values.yaml (the new rewrite_tag filter)

rewrite_tag runs after the Add tenant {{ .Values.global.target }} / Add cluster root-cluster modify filters, and fluent-bit's modify Add does not overwrite an existing key. So the re-emitted tenant.<target>.* record carries tenant=<root target> / cluster=root-cluster, not the destination tenant's identity — which undercuts the per-tenant isolation goal. Verify the routed copy's labels on a live cluster.

Non-blocking

  1. The events output _stream_fields has a pre-existing typo meatdata_namespace → metadata_namespace (the events nest filter adds the metadata_ prefix); this line is rewritten here, so it's in scope. The audit output similarly lists requestUri while _msg_field uses requestURI — confirm the emitted field name and make them consistent.
  2. A new routing label/knob ships with no docs or values-schema description; add docs (and the website docs PR) for a user-facing routing control.

Match kube.*
Host vlogs-generic.{{ .Values.global.target }}.svc
port 9428
Host vlinsert-generic.{{ .Values.global.target }}.svc

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.

This (and the events/audit outputs, and the new per-tenant output) targets vlinsert-generic:9481 — the cluster-mode VictoriaLogs ingest endpoint. The platform only deploys single-node kind: VLogs (vlogs-generic:9428); there's no VLCluster/vlinsert anywhere in the repo, and issue #2194 names the target as vlogs-generic.<tenant>.svc. As-is this posts to a service that doesn't exist and stops log shipping. Revert to vlogs-generic:9428, or land the VLogs→VLCluster migration in this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Checked against current main: VictoriaLogs was migrated VLogs→VLCluster in 62d36ec4e (2026-03-10), so vlogs.yaml:4 is now kind: VLCluster and vlinsert-generic:9481 is what main uses everywhere — the baseline outputs here (values.yaml:347/358/369), the tenant app (apps/kubernetes/templates/helmreleases/monitoring-agents.yaml:71), and the ExternalName (cozystack-basics/templates/monitoring-external-services.yaml:5). vlogs-generic:9428 isn't in the repo anymore, so reverting would break shipping; after a rebase onto main these three lines are a no-op. Full context + the B2/B4 and design path in my PR comment below.

@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Thanks Aleksei Sviridkin (@lexfrei), Andrei Kvapil (@kvaps). Digging into this against current main shifted the picture, so let me lay out findings and propose a direction before reworking.

B1 — vlinsert-generic:9481 target

I believe this is based on a stale tree. main migrated VictoriaLogs VLogs→VLCluster in 62d36ec4e (2026-03-10), so packages/system/monitoring/templates/vlogs/vlogs.yaml:4 is now kind: VLCluster (with a vlinsert: component), and the cluster-mode ingest vlinsert-generic:9481 is exactly what main uses everywhere:

  • baseline outputs in this file — packages/system/monitoring-agents/values.yaml:347/358/369
  • the tenant app — packages/apps/kubernetes/templates/helmreleases/monitoring-agents.yaml:71
  • the ExternalName — packages/system/cozystack-basics/templates/monitoring-external-services.yaml:5

vlogs-generic:9428 no longer exists anywhere in the repo, so reverting to it would break shipping. After rebasing this branch onto main, the three baseline-output lines become a no-op (they already match main) — so B1 resolves on rebase rather than by reverting.

B3 — the routing label

namespace.cozystack.io/monitoring isn't new here — it's the established platform label, set by the tenant chart at packages/apps/tenant/templates/namespace.yaml:89 and consumed by the metrics path (packages/system/monitoring/templates/vm/vmagent.yaml:33,36) and WorkloadMonitorReconciler (internal/controller/workloadmonitor_controller.go:37). Keying log routing off it is consistent with how metrics already discover tenants. The divergence from the PR body's {namespace, vlogs} list is fair though — see the design note below.

B2 — lookup can't reconcile under Flux

Agreed, this is the core problem: lookup results aren't part of helm-controller's release determination, so new/relabeled namespaces won't reliably re-render. It's also why the offline helm template renders these blocks to nothing.

B4 — re-tagged copy keeps the root identity

Agreed — rewrite_tag runs after Add tenant/Add cluster, and fluent-bit's modify Add doesn't overwrite an existing key, so the routed copy carries tenant=<root>. Valid.

The architectural catch

I checked whether B2/B3 could be fixed by switching to a values-driven routing table (per the PR body). It can't, the way the root agent is wired: monitoring-agents is a platform-rendered singleton (packages/core/platform/templates/bundles/system.yaml:206, hardcoded global.target: tenant-root, no valuesFrom, static config). The platform never enumerates tenants at render time; tenants only register via the namespace label, read by operators/controllers at runtime. So nothing can populate a tenantLogRouting list on the root agent automatically — a values list would be manual (the drawback Andrei Kvapil (@kvaps) flagged), and the only render-time discovery is the lookup we're trying to remove.

Proposed direction

Matching how metrics already work: metrics aren't federated at the root — each tenant gets its own vmagent that scrapes its namespaces (label-selected) and writes to its own backend. The log-equivalent is a per-tenant fluent-bit in the tenant monitoring stack shipping to the tenant's own vlinsert-generic via _namespace.monitoring, rather than splitting traffic inside the root DaemonSet. That's values-driven, reconciles per tenant HelmRelease, inherits to sub-tenants, and leaves the root agent untouched (so B1/B4 fall away). Open question: the root-cluster case would mean N per-tenant fluent-bit DaemonSets each tailing all node logs behind a namespace filter (mirroring N per-tenant vmagents) — want to confirm that's acceptable before I build it.

If you'd rather keep the split in the root agent, that needs a small controller (extend WorkloadMonitorReconciler) to watch the label and maintain the routing config — happy to go that way instead.

Andrei Kvapil (@kvaps) Aleksei Sviridkin (@lexfrei) — which direction do you prefer? I'll rework accordingly.

@lexfrei

Copy link
Copy Markdown
Contributor

Two things: the base edit (vlogs-generic:9428 → vlinsert-generic:9481) is already on main after the VLogs to VLCluster migration, so those lines are a no-op now and this needs a rebase. The core per-tenant rewrite_tag routing is still not in main, so the feature is still wanted — it's waiting on a direction from the earlier review (per-tenant fluent-bit at the tenant-chart level vs. the current approach). A steer there would unblock it.

@lexfrei Aleksei Sviridkin (lexfrei) added the area/monitoring Issues or PRs related to the monitoring stack (vlogs, vmstack, grafana, workloadmonitor) label Jul 3, 2026
@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch from 1e8f354 to 0ff2cf2 Compare July 9, 2026 07:31
@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch from 0ff2cf2 to b320401 Compare July 9, 2026 07:32
@mattia-eleuteri mattia-eleuteri changed the title [monitoring] Add per-tenant log routing support in fluent-bit feat(monitoring): auto-route tenant logs using namespace labels Jul 9, 2026
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Direction call, as promised — going with per-tenant fluent-bit at the tenant chart level, mirroring the per-tenant vmagent pattern.

Rationale, building on the earlier synthesis:

  • The current root-level lookup approach has a structural flaw under Flux: helm-controller only re-renders when chart or values change, so a new tenant enabling monitoring doesn't trigger a re-render and its logs keep flowing to root until an unrelated upgrade. That's B2 from Aleksei Sviridkin (@lexfrei)'s review, and it can't be fixed values-side.
  • A controller maintaining the root fluent-bit config would work but adds a new reconciliation loop for something the tenant chart can already own declaratively.
  • Per-tenant fluent-bit is consistent with how metrics already work (one vmagent per tenant selecting its namespaces, no root federation), keeps the root agent config static, and makes tenant log collection self-contained in the tenant's monitoring stack.

I'll rework this PR accordingly: drop the root-agent routing changes and add a fluent-bit collector to the tenant monitoring stack, scoped to the tenant's namespaces via the existing namespace.cozystack.io/monitoring label. The branch is already rebased onto current main (the vlinsert-generic:9481 edits dropped out as no-ops, as expected).

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.

mattia-eleuteri NOT LGTM. The branch still routes tenant logs through a lookup in the root fluent-bit config, which is the approach you dropped in favour of a per-tenant collector in your July comment, and two of the earlier problems are still there.

Two of my earlier points were wrong and are closed. vlinsert-generic:9481 is what main uses after the VLCluster migration, and namespace.cozystack.io/monitoring is an established label set by the tenant chart. I also checked whether a tenant can steer its logs into someone else's store through that label. I found no path: the tenant chart sets it from the parent's values, tenant users only get namespaced RoleBindings so they cannot patch the Namespace, and a label value cannot carry a newline into the config.

Blockers

B1: new tenants are not picked up until something else changes

Helm-controller only upgrades the release when the chart or values digest changes, and lookup results are not part of that digest. A tenant that turns on monitoring gets no routing, and a deleted one keeps its filter, until an unrelated change forces an upgrade. Your July comment reached the same conclusion. Main now has a per-tenant collector for traces (#3763), so the tenant-chart layer already has a pattern to follow.

B2: routed records still say tenant=tenant-root

I rendered the configmap with three mocked namespaces. The rewrite_tag filters come after Add tenant tenant-root and Add cluster root-cluster, and modify Add does not overwrite an existing key. So every record that lands in a tenant's VictoriaLogs is labelled as the root tenant.

  Match *
  Add tenant tenant-root
  ...
  Name rewrite_tag
  Match kube.*
  Rule $kubernetes_namespace_name ^(tenant-a)$ tenant.tenant-a.tenant-a.$TAG false

B3: no tests

monitoring-agents has a helm-unittest suite, and lookup can be mocked with kubernetesProvider (that is how I got the render above). The new logic needs a test that pins which namespaces get a filter and an output, and which are skipped: an unlabelled namespace, an empty label, and a label equal to global.target.

B4: PR body and provenance

The body describes a tenantLogRouting values list and a usage example that the diff does not have. It also ends with a "Generated with Claude Code" line, and the PR body becomes the merge commit message. Please rewrite the body to match the change, drop that line, and add an Assisted-by: LLM trailer to the commit.

Non-blocking

  1. With keep false the tenant's logs leave the root store completely. That may be intended, but it is a visible change for the platform operator and should be stated.
  2. Each rewrite_tag filter creates its own emitter, so this adds one emitter per tenant namespace. One filter with several Rule lines avoids that.

A tenant that runs its own monitoring stack gets a VictoriaLogs instance,
but the node fluent-bit sends every container log to global.target, so
that instance stays empty. The tenant chart already names the stack a
namespace reports to in namespace.cozystack.io/monitoring, the label the
per-tenant vmagent selects on, so the log path keys off the same label.

The routed record leaves the default outputs (keep false): a tenant with
its own monitoring holds its logs, the root store no longer does. The
re-tagged record re-enters the filter chain, where the Match * "Add
tenant" cannot overwrite the key, so a per-target Set labels it with the
tenant it is sent to. One rewrite_tag carries every rule, keeping a
single emitter however many tenants are routed.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <[email protected]>
The node fluent-bit builds its tenant routes from a lookup of namespace
labels, and helm-controller upgrades a release only when its chart or
values digest changes. A tenant turning monitoring on therefore got no
route, and a removed one kept its route, until an unrelated change
upgraded monitoring-agents.

The reconciler digests the label across all namespaces and requests a
forced upgrade of the release when the digest moves. It digests every
labelled namespace rather than repeating the chart's selection, so it
cannot drift from the chart; the cost is an occasional upgrade that
renders the same config.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <[email protected]>
@mattia-eleuteri
mattia-eleuteri force-pushed the monitoring/per-tenant-log-routing branch from 66e4a87 to e510d49 Compare September 29, 2026 15:12
@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files and removed size/M This PR changes 30-99 lines, ignoring generated files labels Sep 29, 2026
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator Author

Aleksei Sviridkin (@lexfrei) Reworked in cdff5ed and e510d49. First, why the branch does not follow the per-tenant collector direction from my July comment: I checked it against the tree and it does not hold for logs. The #3763 collector receives OTLP over the network, while a log collector has to tail /var/log through hostPath, which the PodSecurity level of tenant namespaces rejects (the same constraint packages/apps/opensearch/templates/opensearch.yaml works around). Moving it to a privileged system namespace would mean one DaemonSet per tenant tailing every node's logs. So routing stays in the node DaemonSet, and B1 is fixed where it comes from.

B1. New tenantlogrouting reconciler in cozystack-controller: it digests the namespace.cozystack.io/monitoring label across all namespaces and, when the digest moves, sets requestedAt and forceAt on cozy-monitoring/monitoring-agents, so helm-controller runs a forced upgrade and the lookup is re-evaluated. It digests every labelled namespace instead of repeating the chart's selection, so it cannot drift from the chart; the cost is an occasional upgrade that renders the same config. The digest it acted on lives in its own annotation, so a manual flux reconcile does not trigger an extra forced upgrade. The operator's HelmRelease update merges existing annotations (createOrUpdateHelmRelease), so they survive a Package reconcile, and the existing ClusterRole already grants the verbs.

B2. Each routed target gets a modify filter matching tenant.<target>.* with Set tenant <target>, which does overwrite, so a record in tenant-a's store is labelled tenant=tenant-a.

B3. tests/fluent-bit_tenant_log_routing_test.yaml mocks namespaces with kubernetesProvider and pins the rules, outputs and Set for routed tenants, and the skips for an unlabelled namespace, an empty label and a label equal to global.target, plus a case where global.target itself changes. Three of its cases fail against the previous revision of the branch. A second suite pins that an offline render routes nothing, and the reconciler has its own unit tests.

B4. Body rewritten to match the diff, the byline is gone, and both commits carry Assisted-by: LLM. The branch was rewritten for that, hence the force-push.

Non-blocking 1. Stated in the body: once a tenant runs its own monitoring, its container logs leave the root store; events and audit logs are not routed.

Non-blocking 2. One rewrite_tag filter with one Rule per target (the target's namespaces joined in one regex), so there is a single emitter.

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.

mattia-eleuteri LGTM. The blockers from my last review are fixed.

Business context: a tenant that runs its own monitoring gets a VictoriaLogs instance that stays empty, because the node fluent-bit sends every container log to the root store.

I rendered the configmap with mocked namespaces. The re-tagged record comes back into the chain as tenant.tenant-a.*, the Match * Add leaves the key alone, and Set tenant tenant-a overwrites it before the tenant output. The kube.* filters don't run a second time on the new tag.

The reconciler checks out too. createOrUpdateHelmRelease in the operator merges existing annotations, so the digest survives a Package reconcile. The ClusterRole already allows patch on helmreleases, the Namespace informer is already there for tenantquota, and the controller sits behind the manager's leader election like the others. Removing the digest comparison, the sort, the forceAt token, the global.target guard or the empty-label guard each fails a test.

The hostPath argument against a per-tenant collector holds. Tenant namespaces run under a PodSecurity level that rejects hostPath, which is the same wall the opensearch sysctl DaemonSet goes around.

Non-blocking:

  1. Any change to the routed set changes the configmap, and checksum/config then rolls fluent-bit on every node. The container tail input has no DB, so after a restart fluent-bit starts at the end of the files it already knew, and lines written during the restart are lost. This already happens on platform upgrades. With this PR it also happens each time a namespace joins or leaves a routed tenant, for example a child tenant created under a monitored one. A DB on the tail input, or the chart's hotReload, would close that gap in a follow-up.
  2. The body leaves the website question open. I think logs.md should get a sentence saying that a tenant with its own monitoring no longer has its container logs in the root store.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 018b499 into cozystack:main Sep 29, 2026
56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/monitoring Issues or PRs related to the monitoring stack (vlogs, vmstack, grafana, workloadmonitor) kind/feature Categorizes issue or PR as related to a new feature size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants