Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ for a fuller example including the S3 backend.
| `cluster.peerPort` | Peer replication port (default 7946). |
| `cluster.etcdPrefix` | etcd key prefix (storage "root", default `/oteldb`). |
| `cluster.staticZone` | Fixed failure-domain label for the cluster's nodes (ring zone-spreading). |
| `signals` | Which signals to serve (all default on; `profiles` is the meaningful toggle). |
| `signals` | Which signals to serve (all default on). Disabling one drops its backend, its API bind and its ports; disabling all is rejected. |
| `engine` | Storage engine tuning: `flushInterval`, `readCacheSize`, `decodeCacheSize`, `decodeMemoryLimit`, `aggregateStats`. |
| `service.type` / `annotations` | Client Service exposing the query/ingest APIs. |
| `resources`, `nodeSelector`, `affinity`, `tolerations`, `topologySpreadConstraints`, `podSecurityContext`, `securityContext`, `podAnnotations`, `podLabels`, `serviceAccountName` | Standard pod scheduling/security knobs. |
Expand Down
13 changes: 11 additions & 2 deletions api/v1alpha1/oteldbcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ type OtelDBClusterSpec struct {
Cluster ClusterSpec `json:"cluster,omitempty"`

// Signals selects which telemetry signals the cluster serves (all default to enabled and are
// served from the embedded clustered storage engine).
// served from the embedded clustered storage engine). Disabling every signal is rejected.
// +optional
Signals SignalsSpec `json:"signals,omitempty"`

Expand Down Expand Up @@ -263,17 +263,26 @@ type ClusterSpec struct {
StaticZone string `json:"staticZone,omitempty"`
}

// SignalsSpec selects which signals the cluster serves.
// SignalsSpec selects which signals the cluster serves. Disabling a signal drops its backend and
// its API bind from the rendered config, and its ports from the pods and the client Service. At
// least one signal must stay enabled. Unset means enabled.
type SignalsSpec struct {
// Metrics serves the Prometheus query API and remote-write ingest.
// +kubebuilder:default=true
// +optional
Metrics *bool `json:"metrics,omitempty"`

// Logs serves the Loki query API.
// +kubebuilder:default=true
// +optional
Logs *bool `json:"logs,omitempty"`

// Traces serves the Tempo query API.
// +kubebuilder:default=true
// +optional
Traces *bool `json:"traces,omitempty"`

// Profiles serves the Pyroscope query and ingest API.
// +kubebuilder:default=true
// +optional
Profiles *bool `json:"profiles,omitempty"`
Expand Down
7 changes: 6 additions & 1 deletion config/crd/bases/db.oteldb.io_oteldbclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1639,19 +1639,24 @@ spec:
signals:
description: |-
Signals selects which telemetry signals the cluster serves (all default to enabled and are
served from the embedded clustered storage engine).
served from the embedded clustered storage engine). Disabling every signal is rejected.
properties:
logs:
default: true
description: Logs serves the Loki query API.
type: boolean
metrics:
default: true
description: Metrics serves the Prometheus query API and remote-write
ingest.
type: boolean
profiles:
default: true
description: Profiles serves the Pyroscope query and ingest API.
type: boolean
traces:
default: true
description: Traces serves the Tempo query API.
type: boolean
type: object
storage:
Expand Down
34 changes: 19 additions & 15 deletions internal/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,28 @@ import (
// zone) is intentionally omitted — it is injected per pod via environment variables so the whole
// StatefulSet can share this single ConfigMap.
func renderConfig(cr *dbv1alpha1.OtelDBCluster, etcdEndpoints []string) (string, error) {
if err := validateSignals(cr); err != nil {
return "", err
}

cfg := map[string]any{
// Serve every signal from the embedded clustered storage engine; no ClickHouse.
"metrics_backend": valStorage,
"traces_backend": valStorage,
"logs_backend": valStorage,
"tempo": map[string]any{keyBind: "0.0.0.0:3200"},
"prometheus": map[string]any{keyBind: "0.0.0.0:9090"},
"loki": map[string]any{keyBind: "0.0.0.0:3100"},
"health_check": map[string]any{keyBind: "0.0.0.0:13133"},
"health_check": map[string]any{keyBind: "0.0.0.0:13133"},
}

// Every enabled signal is served from the embedded clustered storage engine; no ClickHouse.
// A disabled signal drops both its backend and its API bind, so the API is not served at all.
if metricsEnabled(cr) {
cfg["metrics_backend"] = valStorage
cfg["prometheus"] = map[string]any{keyBind: "0.0.0.0:9090"}
}
if tracesEnabled(cr) {
cfg["traces_backend"] = valStorage
cfg["tempo"] = map[string]any{keyBind: "0.0.0.0:3200"}
}
if logsEnabled(cr) {
cfg["logs_backend"] = valStorage
cfg["loki"] = map[string]any{keyBind: "0.0.0.0:3100"}
}
if profilesEnabled(cr) {
cfg["profiles_backend"] = valStorage
cfg["pyroscope"] = map[string]any{keyBind: "0.0.0.0:4040"}
Expand Down Expand Up @@ -127,13 +138,6 @@ func renderConfig(cr *dbv1alpha1.OtelDBCluster, etcdEndpoints []string) (string,
return string(out), nil
}

func profilesEnabled(cr *dbv1alpha1.OtelDBCluster) bool {
if p := cr.Spec.Signals.Profiles; p != nil {
return *p
}
return true // default on
}

func backendOf(cr *dbv1alpha1.OtelDBCluster) dbv1alpha1.StorageBackend {
if b := cr.Spec.Storage.Backend; b != "" {
return b
Expand Down
14 changes: 14 additions & 0 deletions internal/controller/naming.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ const (
portPeer = 7946
)

// Port names, shared by the container ports, the Services and the probes.
const (
portNameOTLPGRPC = "otlp-grpc"
portNameOTLPHTTP = "otlp-http"
portNamePromRW = "prom-rw"
portNamePromHTTP = "prom-http"
portNameTempoHTTP = "tempo-http"
portNameLokiHTTP = "loki-http"
portNamePyroscope = "pyroscope"
portNameSelfMetric = "metrics"
portNameHealth = "health-check"
portNamePeer = "peer"
)

const (
configVolumeName = "config"
dataVolumeName = "data"
Expand Down
53 changes: 34 additions & 19 deletions internal/controller/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,32 @@ type namedPort struct {
port int32
}

// apiPorts are the client-facing API ports served by every node.
func apiPorts() []namedPort {
return []namedPort{
{"otlp-grpc", portOTLPGRPC},
{"otlp-http", portOTLPHTTP},
{"prom-rw", portPromRW},
{"prom-http", portPromHTTP},
{"tempo-http", portTempoHTTP},
{"loki-http", portLokiHTTP},
{"pyroscope", portPyroscope},
{"metrics", portSelfMetric},
{"health-check", portHealth},
// apiPorts are the client-facing API ports served by every node. Ports belonging to a disabled
// signal are omitted; OTLP ingest, self-metrics and the health check are always exposed.
func apiPorts(cr *dbv1alpha1.OtelDBCluster) []namedPort {
ports := []namedPort{
{portNameOTLPGRPC, portOTLPGRPC},
{portNameOTLPHTTP, portOTLPHTTP},
}
if metricsEnabled(cr) {
ports = append(ports,
namedPort{portNamePromRW, portPromRW},
namedPort{portNamePromHTTP, portPromHTTP},
)
}
if tracesEnabled(cr) {
ports = append(ports, namedPort{portNameTempoHTTP, portTempoHTTP})
}
if logsEnabled(cr) {
ports = append(ports, namedPort{portNameLokiHTTP, portLokiHTTP})
}
if profilesEnabled(cr) {
ports = append(ports, namedPort{portNamePyroscope, portPyroscope})
}
return append(ports,
namedPort{portNameSelfMetric, portSelfMetric},
namedPort{portNameHealth, portHealth},
)
}

// buildConfigMap renders the shared oteldb config into a ConfigMap.
Expand Down Expand Up @@ -83,7 +96,7 @@ func buildPeerService(cr *dbv1alpha1.OtelDBCluster) *corev1.Service {
PublishNotReadyAddresses: true, // peers must resolve each other before readiness
Selector: selectorLabels(cr),
Ports: []corev1.ServicePort{{
Name: "peer",
Name: portNamePeer,
Port: peerPortOf(cr),
TargetPort: intstr.FromInt32(peerPortOf(cr)),
Protocol: corev1.ProtocolTCP,
Expand All @@ -99,8 +112,9 @@ func buildClientService(cr *dbv1alpha1.OtelDBCluster) *corev1.Service {
if svcType == "" {
svcType = corev1.ServiceTypeClusterIP
}
ports := make([]corev1.ServicePort, 0, len(apiPorts()))
for _, p := range apiPorts() {
api := apiPorts(cr)
ports := make([]corev1.ServicePort, 0, len(api))
for _, p := range api {
ports = append(ports, corev1.ServicePort{
Name: p.name,
Port: p.port,
Expand Down Expand Up @@ -247,18 +261,19 @@ func podEnv(cr *dbv1alpha1.OtelDBCluster) []corev1.EnvVar {
}

func containerPorts(cr *dbv1alpha1.OtelDBCluster) []corev1.ContainerPort {
ports := make([]corev1.ContainerPort, 0, len(apiPorts())+1)
for _, p := range apiPorts() {
api := apiPorts(cr)
ports := make([]corev1.ContainerPort, 0, len(api)+1)
for _, p := range api {
ports = append(ports, corev1.ContainerPort{Name: p.name, ContainerPort: p.port, Protocol: corev1.ProtocolTCP})
}
ports = append(ports, corev1.ContainerPort{Name: "peer", ContainerPort: peerPortOf(cr), Protocol: corev1.ProtocolTCP})
ports = append(ports, corev1.ContainerPort{Name: portNamePeer, ContainerPort: peerPortOf(cr), Protocol: corev1.ProtocolTCP})
return ports
}

func httpProbe(path string) *corev1.Probe {
return &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{Path: path, Port: intstr.FromString("health-check")},
HTTPGet: &corev1.HTTPGetAction{Path: path, Port: intstr.FromString(portNameHealth)},
},
PeriodSeconds: 10,
TimeoutSeconds: 3,
Expand Down
55 changes: 55 additions & 0 deletions internal/controller/signals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2026.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"fmt"

dbv1alpha1 "github.com/oteldb/operator/api/v1alpha1"
)

// signalEnabled reads a signal toggle; an unset toggle means enabled.
func signalEnabled(toggle *bool) bool {
if toggle != nil {
return *toggle
}
return true
}

func metricsEnabled(cr *dbv1alpha1.OtelDBCluster) bool {
return signalEnabled(cr.Spec.Signals.Metrics)
}

func logsEnabled(cr *dbv1alpha1.OtelDBCluster) bool {
return signalEnabled(cr.Spec.Signals.Logs)
}

func tracesEnabled(cr *dbv1alpha1.OtelDBCluster) bool {
return signalEnabled(cr.Spec.Signals.Traces)
}

func profilesEnabled(cr *dbv1alpha1.OtelDBCluster) bool {
return signalEnabled(cr.Spec.Signals.Profiles)
}

// validateSignals rejects a cluster that would serve no signal at all.
func validateSignals(cr *dbv1alpha1.OtelDBCluster) error {
if metricsEnabled(cr) || logsEnabled(cr) || tracesEnabled(cr) || profilesEnabled(cr) {
return nil
}
return fmt.Errorf("spec.signals: at least one signal must be enabled")
}
Loading
Loading