Skip to content

feat(otelcollector): render OTel Collector - #5106

Open
tianfeng92 wants to merge 1 commit into
tigera:masterfrom
tianfeng92:EV-6862-otel-collector
Open

feat(otelcollector): render OTel Collector#5106
tianfeng92 wants to merge 1 commit into
tigera:masterfrom
tianfeng92:EV-6862-otel-collector

Conversation

@tianfeng92

@tianfeng92 tianfeng92 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Render the OTel Collector as a StatefulSet in calico-system, configured via LogCollector.spec.otelCollector.

The collector receives logs from fluent-bit via OTLP and optionally federates Prometheus metrics, forwarding both to user-configured OTLP endpoints. It is added to the LogCollector CR rather than AdditionalStores because it is operator-managed infrastructure (StatefulSet, ConfigMap, RBAC, certs) with its own lifecycle, not a pointer to an external system.

Release Note

Add OpenTelemetry Collector component to export logs and metrics via OTLP.

Test plan

  • Unit tests for render, controller, and fluent-bit pipeline OTel output
  • Validated on MCM cluster — all three log types exporting, clean collector startup

EV-6862

Comment thread internal/controller/otelcollector_controller.go Outdated
Comment on lines +65 to +68
DefaultMemoryLimit = "512Mi"
DefaultMemoryRequest = "128Mi"
DefaultMemoryLimitMiB = 409 // 80% of 512Mi
DefaultMemorySpikeLimitMiB = 100 // ~25% of limit_mib

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.

How are these memory limits decided? Are they from the vendor recommendations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The memory_limiter processor follows the OTel Collector best practices: limit_mib should be ~80% of the container memory limit (409 ≈ 80% of 512Mi), and spike_limit_mib at ~25% of limit_mib (100 ≈ 25% of 409). This headroom lets the GC reclaim memory before the container hits OOMKill. See: https://github.com/open-telemetry/opentelemetry-collector/blob/main/processor/memorylimiterprocessor/README.md

The 512Mi container limit itself is a conservative starting point — the collector is mostly I/O bound (receiving and forwarding telemetry), so memory usage is dominated by in-flight batches rather than computation.

Comment thread pkg/render/otelcollector/component.go Outdated
@tianfeng92
tianfeng92 force-pushed the EV-6862-otel-collector branch 7 times, most recently from 8124957 to 222f3f4 Compare July 27, 2026 23:24
@tianfeng92 tianfeng92 changed the title feat(otelcollector): render OTel Collector from LogCollector.spec.otelCollector feat(otelcollector): render OTel Collector Jul 28, 2026
@tianfeng92
tianfeng92 force-pushed the EV-6862-otel-collector branch from 222f3f4 to 6dd36c8 Compare July 28, 2026 17:12
…lCollector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tianfeng92
tianfeng92 force-pushed the EV-6862-otel-collector branch from 6dd36c8 to a8c2d9e Compare July 28, 2026 17:55
}

if logCollector.Spec.OTelCollector == nil {
r.status.OnCRNotFound()

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.

We shouldn't mark the CR as not found in this case. We should enforce it at the CRD level (via kubebuilder annotations) that the OTelCollector field must be set. We can have a defensive check here if you want, but it should SetDegraded with a message rather that OnCRNotFound

return reconcile.Result{}, nil
}

r.status.OnCRFound()

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.

Move this beneath the block where we find the CR

}
}

func (c *component) statefulSet() *appsv1.StatefulSet {

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.

We need to update the annotations of the pod to use HashAnnotations() and HashAnnotationKey(). We should also add a watch of tigera-ca-private into the controller to ensure we reconcile when the CA rotates.


objs = append(objs, secret.ToRuntimeObjects(secret.CopyToNamespace(OTelCollectorNamespace, c.cfg.PullSecrets...)...)...)

return objs, nil

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.

What is our deletion strategy to ensure that if otel is disabled, we don't keep the otel collector and all of its resources around?


if licenseStatus == utils.LicenseStatusExpired {
r.status.SetDegraded(operatorv1.ResourceValidationError,
"License is expired - OTel collector forwarding is stopped. Contact Tigera support or email licensing@tigera.io", nil, reqLogger)

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.

From what I understand, this message is not true? Forwarding of metrics continues since we do not tear down the deployment.

What is our stance here? It seems like it's that metric forwarding does not require a license. But if the license is missing or doesn't have the feature, we don't deploy the collector at all which conflicts with that stance

endpoint: {{.Endpoint}}
{{- if .TLSInsecure}}
tls:
insecure: true

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.

Claude: This renders the wrong OTel setting. The tlsInsecure field is documented as "disables TLS verification" (keep TLS on, just don't check the cert), but tls.insecure: true means no TLS at all — plaintext. The setting for "skip verification" is insecure_skip_verify, which we never render.

So for a self-signed target like the field doc suggests:

  • http:// or a scheme-less host:port → plaintext; telemetry (incl. flow data) goes out unencrypted.
  • https:// grpc → the scheme forces TLS and overrides insecure, so the self-signed cert fails verification and the connection breaks; tlsInsecure did nothing.

Either way the field can't do what it promises. If plaintext is actually what we want, that's a separate thing (or just an http:// scheme).

// TLSInsecure disables TLS verification for this exporter. Only use for trusted in-cluster targets.
// Default: false
// +optional
TLSInsecure *bool `json:"tlsInsecure,omitempty"`

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.

What is our overall TLS strategy here? Do we need to support a destination that requires mTLS? Do we need to support a user having a custom CA? There is precedent for this in other components (e.g. syslog forwarding, guardian connection)


// Exporters configures the OTLP export endpoints.
// +optional
Exporters []OTelExporter `json:"exporters,omitempty"`

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.

Claude: A few kubebuilder-marker gaps on the new API surface, grouping them here:

  • This list needs list markers. exporters[] is our first named object list, but it has no +listType=map / +listMapKey=name, and nothing enforces name uniqueness. So duplicate names silently collide (same exporter key), and the list merges as atomic-replace under server-side apply. Add both markers and make name required + unique.
  • Required fields aren't marked. Name and Endpoint (otelcollector_types.go:67,70) have no +optional, so they're effectively required — but the doc wants an explicit // +required.
  • Defaults live in prose. metrics.enabled (otelcollector_types.go:52) and tlsInsecure (:80) state their defaults in the comment but have no +kubebuilder:default, unlike protocol (:74) which does. Add the marker (or drop the prose claim so they don't drift).

},
SecurityContext: securitycontext.NewNonRootContext(),
ReadinessProbe: healthProbe(),
LivenessProbe: healthProbe(),

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.

Claude: These two probes are identical and neither sets InitialDelaySeconds, so there's no boot grace — the liveness check starts immediately and could restart the collector while it's still starting up.

The convention in this repo is to give boot grace with InitialDelaySeconds (we don't use startup probes — they show up once in the whole render tree, InitialDelaySeconds ~20 times), and to tune liveness longer than readiness. Guardian/dex/manager/apiserver set liveness to 90 and readiness to 10.

Suggest matching that: give healthProbe a delay param and wire LivenessProbe: healthProbe(90) / ReadinessProbe: healthProbe(10). The 90 on liveness is the boot grace; the 10 on readiness lets it start taking traffic quickly. Keeps the same :13133 endpoint, just splits the timing the way the other components do.

Namespace: OTelCollectorNamespace,
},
Spec: appsv1.StatefulSetSpec{
Replicas: c.cfg.Installation.ControlPlaneReplicas,

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.

Do we have a requirement for supporting HA? Right now there are many gaps to make this support HA in a comprehensive way: no anti-affinity, no pod disruption budgets, no explicit strategy for dealing with multiple replicas causing metric duplication.

The latter needs some research (Claude mentions target allocator could fix the metrics side, and that StatefulSets should use a headless service for PVCs which could eventually break the logs side).

If we don't have this explicit requirement I would recommend moving it out of scope

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants