Add ingestionCompression to LogCollector and NonClusterHost, and compress fluent-bit log ingestion to Linseed - #5224
Conversation
1cb24e7 to
da81c9f
Compare
da81c9f to
e7a1c7c
Compare
| if v := compressValue(c.cfg.LogCollector.Spec.IngestionCompression); v != "" { | ||
| out["compress"] = v |
| if instance.Spec.IngestionCompression == nil { | ||
| // The CRD default only applies on write, so materialize it for CRs | ||
| // created before the field existed. | ||
| compression := operatorv1.IngestionCompressionZstd | ||
| instance.Spec.IngestionCompression = &compression | ||
| modifiedFields = append(modifiedFields, "IngestionCompression") |
| if instance.Spec.IngestionCompression == nil { | ||
| // Hosts read this CR directly and treat an unset field as None, and | ||
| // the CRD default only applies on write, so materialize the default | ||
| // for CRs created before the field existed. | ||
| preDefaultPatchFrom := client.MergeFrom(instance.DeepCopy()) |
| // Default: Zstd | ||
| // +optional | ||
| // +kubebuilder:default=Zstd | ||
| IngestionCompression *IngestionCompressionOption `json:"ingestionCompression,omitempty"` |
e7a1c7c to
2317d83
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (2)
pkg/enterprise/render/logcollector/pipeline.go:432
- Enabling compression here as soon as this operator runs can lose logs during an upgrade. The LogCollector controller rolls Fluent Bit independently of the Linseed controller; Linseed's rolling deployment can still route requests to old ready pods, and managed clusters may target a separately upgraded management cluster. Because an unsupported encoding returns a 4xx and
out_httpdrops that chunk, shipping both changes in one release does not provide the required ordering. Gate this on verified receiver compatibility, or retain an uncompressed default until compatibility is guaranteed.
if v := compressValue(c.cfg.LogCollector.Spec.IngestionCompression); v != "" {
out["compress"] = v
api/v1/logcollector_types.go:51
- The PR description calls this a config-only change and says
compressis the only addition, but this line introduces a new public LogCollector API field; the PR also adds a shared enum, a NonClusterHost field, controller defaulting, and generated CRD changes. Update the description and API-change checklist so reviewers and release tooling account for the actual public API scope.
// +kubebuilder:default=Zstd
IngestionCompression *IngestionCompressionOption `json:"ingestionCompression,omitempty"`
| // +kubebuilder:default=Zstd | ||
| IngestionCompression *IngestionCompressionOption `json:"ingestionCompression,omitempty"` |
The linseed http outputs post plain NDJSON. Setting compress makes fluent-bit zstd-encode each batch, which cuts ingest bandwidth by roughly 12x on real traffic. Both codecs are already compiled into the fluent-bit image, so this is config only. Requires the Linseed-side decompression from calico-private (EV-6973); those must ship together, since fluent-bit drops a chunk on a 4xx rather than retrying it. EV-6973
Compression for the fluent-bit to Linseed ingestion path was hardcoded to zstd in the rendered config, with no API to turn it off. Both resources gain ingestionCompression: None | Gzip | Zstd, defaulting to Zstd through the CRD, which fills the value on every read. LogCollector drives the in-cluster outputs: the rendered compress option follows the field, and None omits it so out_http posts plain NDJSON. NonClusterHost drives non-cluster hosts, which read the resource directly and hot-reload the value; on a cluster whose CRD predates the field they fall back to gzip, which every release's ingestion listener inflates. The field is named for the ingestion path and "in transit" deliberately: felix already zstd-compresses rotated log files on disk, and the additional stores (S3, Syslog, Splunk) and OpenTelemetry exporters are unaffected. EV-6973.
2317d83 to
5647600
Compare
|
Closing: this repo is no longer where operator changes land. tigera/operator has been imported into the Calico monorepos (tigera/calico-private#13606, projectcalico/calico#13674), both at Both commits here have moved to tigera/calico-private#13367, which already carried the Linseed, host-plugin and Go-client sides of EV-6973 — so the change that had to ship together is now one PR in one repo, rather than two PRs with an ordering constraint between them. The port is unchanged in substance; paths mapped as Nothing goes to projectcalico/calico: LogCollector and NonClusterHost are Enterprise resources, and OSS ships neither CRD outside Review continues on tigera/calico-private#13367. |
Description
The fluent-bit outputs that ship logs to Linseed now compress each batch — zstd by default — cutting log ingestion bandwidth by roughly 12x measured on a kubeadm cluster, and both LogCollector and NonClusterHost gain an
ingestionCompressionfield to control it.ingestionCompression: None | Gzip | Zstd, defaultZstdthrough the CRD (+kubebuilder:default), which the API server fills on every read — so no reconciler write-back is needed, matching the other CRD-defaulted fields in this API.compressfollows the field, andNoneomits it soout_httpposts plain NDJSON. Only the Linseedhttpoutputs carry it — S3, Splunk, Syslog and OpenTelemetry are unaffected, asserted per output in the render test.Both zstd and gzip are compiled into the fluent-bit image, so nothing about the build changes. zstd is the default because it beat gzip on both axes on real Calico log data (43x vs 28x compression, roughly 6x less CPU); gzip is kept for symmetry with Linseed's accepted encodings and for hosts attached to older clusters.
Requires the Linseed-side change
Depends on tigera/calico-private#13367, which teaches Linseed to inflate zstd and gzip request bodies (and adds the host plugin and Go client sides). Ship together, or Linseed first: fluent-bit's
out_httpdrops a chunk on a 4xx rather than retrying it.Upgrade order
Clusters are upgraded before non-cluster host packages, and a management plane before its managed clusters — the same rule for MCM and Calico Cloud. A host at version N only meets a cluster at N or newer.
ingestionCompression: Noneis the escape hatch.CPU cost
Decompression costs Linseed roughly 5–20% more CPU per request (less for bigger batches), measured on a dev laptop and a cluster node (
e2-standard-2). It runs at 488 MB/s of logs per node vCPU against about 30 MB/s for the NDJSON parsing that follows, so it is ~15x cheaper per byte than work Linseed already does. Sender-side cost is distributed across nodes: a 10KB batch encodes in 254us.Release Note
For PR author
UPDATE_RENDERED_CONFIGS=1.make gen-filesrun — CRDs and deepcopy regenerated for the new field.make gen-versions— n/a, no version change.Validated end to end on a real cluster: the new operator asserted the CRDs and both resources read
Zstd; in-cluster outputs renderedcompress: zstd; settingNonere-rendered the ConfigMap without it and ingestion stayed clean (38 bulk responses,"failed":0); non-cluster hosts hot-reloaded on every field change.AI assistance: Written with Claude Code (Claude Fable 5); design decisions, review and cluster validation by the author.