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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ for a fuller example including the S3 backend.
| `cluster.staticZone` | Fixed failure-domain label for the cluster's nodes (ring zone-spreading). |
| `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`. |
| `retention.maxAge` | How long data is kept (e.g. `720h`). Empty retains forever. Enforced at merge time by dropping whole partitions, so data can outlive the window briefly. |
| `retention.maxBytes` | Retained-bytes budget. **Accepted but not enforced yet** by the storage engine ([oteldb/storage#224](https://github.com/oteldb/storage/issues/224)) — use `maxAge` to bound disk growth. |
| `limits` | Per-node admission control: `ingestBytesPerSecond`, `maxInFlightBytes`, `maxSeries`, `maxSeriesSoft`, `maxPartSize`. Over-budget writes are shed as OTLP partial success rather than buffered. |
| `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. |
| `extraConfig` | Arbitrary raw oteldb config **deep-merged** over the generated config — for fields the CRD does not model (auth, retention policy, prometheus tuning, …). Nested objects merge key by key (`storage.policy` does not wipe `storage.backend`); operator-owned paths are [reserved](#reserved-extraconfig-paths). |
Expand Down Expand Up @@ -101,6 +104,11 @@ spec field to use instead.
| `storage.s3` | `spec.storage.s3` |
| `storage.cluster` (whole subtree) | `spec.cluster`, `spec.etcd.endpoints` |
| `storage.flush_interval`, `storage.read_cache_bytes`, `storage.decode_cache_bytes`, `storage.decode_memory_bytes`, `storage.aggregate_stats` | `spec.engine` |
| `storage.policy.retention` | `spec.retention` |
| `storage.policy.limits` | `spec.limits` |

The rest of `storage.policy` — `precision`, `downsample`, `recompress` — is not modelled by the
CRD and stays mergeable, as in the example above.

### Status

Expand Down
61 changes: 59 additions & 2 deletions api/v1alpha1/oteldbcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ type OtelDBClusterSpec struct {
// +optional
Engine EngineSpec `json:"engine,omitempty"`

// Retention bounds how long ingested data is kept. Empty retains forever.
// +optional
Retention RetentionSpec `json:"retention,omitempty"`

// Limits are the per-node admission-control limits. Empty means unlimited.
// +optional
Limits LimitsSpec `json:"limits,omitempty"`

// Service configures the client-facing Service that exposes the query and ingest APIs.
// +optional
Service ServiceSpec `json:"service,omitempty"`
Expand Down Expand Up @@ -135,8 +143,10 @@ type OtelDBClusterSpec struct {
// instead of being merged: metrics_backend, traces_backend, logs_backend, profiles_backend,
// storage.backend, storage.dir, storage.wal_dir, storage.s3, storage.cluster (and everything
// below it), storage.flush_interval, storage.read_cache_bytes, storage.decode_cache_bytes,
// storage.decode_memory_bytes and storage.aggregate_stats. Configure those through
// spec.storage, spec.cluster, spec.etcd, spec.signals and spec.engine.
// storage.decode_memory_bytes, storage.aggregate_stats, storage.policy.retention and
// storage.policy.limits. Configure those through spec.storage, spec.cluster, spec.etcd,
// spec.signals, spec.engine, spec.retention and spec.limits. The rest of storage.policy
// (precision, downsample, recompress) stays mergeable.
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
ExtraConfig *runtime.RawExtension `json:"extraConfig,omitempty"`
Expand Down Expand Up @@ -324,6 +334,53 @@ type EngineSpec struct {
AggregateStats *bool `json:"aggregateStats,omitempty"`
}

// RetentionSpec bounds how long data is kept. Enforcement happens at merge time and drops whole
// partitions — never individual rows — so data can outlive the window until the partition holding
// it has fully expired.
type RetentionSpec struct {
// MaxAge is the maximum age of retained data (e.g. "720h"). Empty retains forever.
// +optional
MaxAge *metav1.Duration `json:"maxAge,omitempty"`

// MaxBytes is the total retained-bytes budget across every signal on a node.
//
// oteldb accepts it, but the storage engine does not enforce it yet (oteldb/storage#224), so
// setting it alone bounds nothing today. Use MaxAge to bound disk growth.
// +optional
MaxBytes *resource.Quantity `json:"maxBytes,omitempty"`
}

// LimitsSpec are the per-node admission-control limits. They shed over-budget writes and report
// them as OTLP partial success (RESOURCE_EXHAUSTED), so an overload degrades rather than OOMs.
type LimitsSpec struct {
// IngestBytesPerSecond caps the ingest rate, bursting to one second of budget.
// +optional
IngestBytesPerSecond *resource.Quantity `json:"ingestBytesPerSecond,omitempty"`

// MaxInFlightBytes caps the unflushed in-flight bytes buffered before backpressure sheds.
// +optional
MaxInFlightBytes *resource.Quantity `json:"maxInFlightBytes,omitempty"`

// MaxSeries is the hard active-series ceiling: a sample minting a new series past it is shed.
// Existing series are unaffected.
// +kubebuilder:validation:Minimum=0
// +optional
MaxSeries *int64 `json:"maxSeries,omitempty"`

// MaxSeriesSoft is a soft cardinality budget (metrics only): past it a new series' samples go
// to a synthetic per-metric overflow series instead of being shed, until MaxSeries is reached.
// It must not exceed MaxSeries, and needs MaxSeries set to have any effect.
// +kubebuilder:validation:Minimum=0
// +optional
MaxSeriesSoft *int64 `json:"maxSeriesSoft,omitempty"`

// MaxPartSize caps an immutable part's approximate uncompressed size; flush and merge split
// their output to respect it. It is structural: fixed when a node's engine is first created,
// so changing it does not affect existing data.
// +optional
MaxPartSize *resource.Quantity `json:"maxPartSize,omitempty"`
}

// ServiceSpec configures the client-facing Service.
type ServiceSpec struct {
// Type of the client Service.
Expand Down
67 changes: 67 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 70 additions & 2 deletions config/crd/bases/db.oteldb.io_oteldbclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1078,8 +1078,10 @@ spec:
instead of being merged: metrics_backend, traces_backend, logs_backend, profiles_backend,
storage.backend, storage.dir, storage.wal_dir, storage.s3, storage.cluster (and everything
below it), storage.flush_interval, storage.read_cache_bytes, storage.decode_cache_bytes,
storage.decode_memory_bytes and storage.aggregate_stats. Configure those through
spec.storage, spec.cluster, spec.etcd, spec.signals and spec.engine.
storage.decode_memory_bytes, storage.aggregate_stats, storage.policy.retention and
storage.policy.limits. Configure those through spec.storage, spec.cluster, spec.etcd,
spec.signals, spec.engine, spec.retention and spec.limits. The rest of storage.policy
(precision, downsample, recompress) stays mergeable.
type: object
x-kubernetes-preserve-unknown-fields: true
image:
Expand Down Expand Up @@ -1113,6 +1115,52 @@ spec:
type: object
x-kubernetes-map-type: atomic
type: array
limits:
description: Limits are the per-node admission-control limits. Empty
means unlimited.
properties:
ingestBytesPerSecond:
anyOf:
- type: integer
- type: string
description: IngestBytesPerSecond caps the ingest rate, bursting
to one second of budget.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
maxInFlightBytes:
anyOf:
- type: integer
- type: string
description: MaxInFlightBytes caps the unflushed in-flight bytes
buffered before backpressure sheds.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
maxPartSize:
anyOf:
- type: integer
- type: string
description: |-
MaxPartSize caps an immutable part's approximate uncompressed size; flush and merge split
their output to respect it. It is structural: fixed when a node's engine is first created,
so changing it does not affect existing data.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
maxSeries:
description: |-
MaxSeries is the hard active-series ceiling: a sample minting a new series past it is shed.
Existing series are unaffected.
format: int64
minimum: 0
type: integer
maxSeriesSoft:
description: |-
MaxSeriesSoft is a soft cardinality budget (metrics only): past it a new series' samples go
to a synthetic per-metric overflow series instead of being shed, until MaxSeries is reached.
It must not exceed MaxSeries, and needs MaxSeries set to have any effect.
format: int64
minimum: 0
type: integer
type: object
logLevel:
description: LogLevel sets OTEL_LOG_LEVEL for the oteldb process (e.g.
DEBUG, INFO, WARN, ERROR).
Expand Down Expand Up @@ -1433,6 +1481,26 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
retention:
description: Retention bounds how long ingested data is kept. Empty
retains forever.
properties:
maxAge:
description: MaxAge is the maximum age of retained data (e.g.
"720h"). Empty retains forever.
type: string
maxBytes:
anyOf:
- type: integer
- type: string
description: |-
MaxBytes is the total retained-bytes budget across every signal on a node.

oteldb accepts it, but the storage engine does not enforce it yet (oteldb/storage#224), so
setting it alone bounds nothing today. Use MaxAge to bound disk growth.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
securityContext:
description: SecurityContext for the oteldb container.
properties:
Expand Down
14 changes: 14 additions & 0 deletions config/samples/db_v1alpha1_oteldbcluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ spec:
traces: true
profiles: true

# Keep 30 days of data. Enforced at merge time by dropping whole partitions, so data can
# outlive the window until the partition holding it has fully expired.
retention:
maxAge: 720h
# maxBytes is accepted but not enforced by the storage engine yet (oteldb/storage#224).

# Per-node admission control: over-budget writes are shed and reported as OTLP partial
# success, so an overload degrades instead of OOMing.
limits:
maxSeries: 2000000
maxSeriesSoft: 1500000 # past this, new series fold into a per-metric overflow series
maxInFlightBytes: 1Gi
maxPartSize: 256Mi

resources:
requests:
cpu: "1"
Expand Down
8 changes: 8 additions & 0 deletions internal/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func renderConfig(cr *dbv1alpha1.OtelDBCluster, etcdEndpoints []string) (string,
if err := validateSignals(cr); err != nil {
return "", err
}
if err := validatePolicy(cr); err != nil {
return "", err
}

cfg := map[string]any{
"health_check": map[string]any{keyBind: "0.0.0.0:13133"},
Expand Down Expand Up @@ -119,6 +122,11 @@ func renderConfig(cr *dbv1alpha1.OtelDBCluster, etcdEndpoints []string) (string,
storage["aggregate_stats"] = *eng.AggregateStats
}

// Retention and admission-control limits ride the per-tenant storage policy.
if policy := renderPolicy(cr); policy != nil {
storage[keyPolicy] = policy
}

cfg["storage"] = storage

// Merge user-supplied ExtraConfig over the generated config. The merge is recursive so that,
Expand Down
4 changes: 4 additions & 0 deletions internal/controller/extraconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ var reservedConfigPaths = map[string]string{
"storage.decode_cache_bytes": "use spec.engine.decodeCacheSize",
"storage.decode_memory_bytes": "use spec.engine.decodeMemoryLimit",
"storage.aggregate_stats": "use spec.engine.aggregateStats",

// The rest of storage.policy (precision, downsample, recompress) stays mergeable.
"storage.policy.retention": "use spec.retention",
"storage.policy.limits": "use spec.limits",
}

// validationError marks a spec problem that no amount of retrying can fix: the reconcile is
Expand Down
Loading
Loading