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
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,30 @@ 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. |
| `policy.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. |
| `policy.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. |
| `policy.limits` | Per-node admission control: `ingestBytesPerSecond`, `maxInFlightBytes`, `maxSeries`, `maxSeriesSoft`, `maxPartSize`. Over-budget writes are shed as OTLP partial success rather than buffered. |
| `policy.downsample[]` | Merge-time age-tiered rollup: `{after, interval, agg}`. Samples past `after` collapse to one per `interval` bucket. **Lossy and irreversible.** |
| `policy.precision[]` | Age-tiered lossy float precision: `{after, bits}`. Parts past `after` keep only `bits` mantissa bits. **Lossy and irreversible.** |
| `policy.recompress` | `{after, level}`. Rewrites fully-cold parts with a higher-ratio Zstandard profile. Decode-transparent and lossless. |
| `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). |
| `extraConfig` | Arbitrary raw oteldb config **deep-merged** over the generated config — for fields the CRD does not model (auth, prometheus tuning, …). Nested objects merge key by key (`storage.policy` does not wipe `storage.backend`); operator-owned paths are [reserved](#reserved-extraconfig-paths). |

> `spec.policy` maps onto oteldb's `storage.policy`. `downsample`, `precision` and `recompress`
> work against oteldb v0.48.0; `retention` and `limits` landed upstream **after** it. Older oteldb
> builds — including the operator's current default image — ignore unknown config keys silently,
> so on those the two newer blocks are accepted by the API server and have no effect. Pin a newer
> `spec.image` before relying on them.

### Reserved `extraConfig` paths

`extraConfig` is merged recursively, so it can add keys the CRD does not model:

```yaml
extraConfig:
storage:
policy:
recompress: {after: 3d, level: 19} # keeps backend/dir/cluster
auth:
tenant_header: X-Scope-OrgID # merged in; keeps backend/dir/cluster
```

The paths the operator renders from the spec are **reserved**: an `extraConfig` that sets one is
Expand All @@ -104,11 +112,9 @@ 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` |
| `storage.policy.retention`, `storage.policy.limits`, `storage.policy.downsample`, `storage.policy.precision`, `storage.policy.recompress` | `spec.policy` |

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

### Status

Expand Down
108 changes: 97 additions & 11 deletions api/v1alpha1/oteldbcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,11 @@ 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.
// Policy is the per-tenant storage policy: retention, admission-control limits, and the
// merge-time downsample/precision/recompress tiers. It maps onto oteldb's storage.policy
// block. Empty leaves the engine at its defaults (retain forever, no limits, lossless, raw).
// +optional
Limits LimitsSpec `json:"limits,omitempty"`
Policy PolicySpec `json:"policy,omitempty"`

// Service configures the client-facing Service that exposes the query and ingest APIs.
// +optional
Expand Down Expand Up @@ -135,18 +133,17 @@ type OtelDBClusterSpec struct {

// ExtraConfig is arbitrary additional oteldb config deeply merged over the generated config, as
// a top-level YAML/JSON object. Use it to set fields the CRD does not model directly (auth,
// prometheus tuning, retention policy, ...). Nested objects are merged key by key, so
// prometheus tuning, ...). Nested objects are merged key by key, so
// storage.policy can be added without discarding the generated storage block; any other value
// overrides the generated one.
//
// The paths the operator owns are reserved and rejected with a Degraded/InvalidSpec condition
// 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, 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.
// storage.decode_memory_bytes, storage.aggregate_stats and storage.policy (modelled in full, so
// the whole block is reserved). Configure those through spec.storage, spec.cluster, spec.etcd,
// spec.signals, spec.engine and spec.policy.
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
ExtraConfig *runtime.RawExtension `json:"extraConfig,omitempty"`
Expand Down Expand Up @@ -334,6 +331,95 @@ type EngineSpec struct {
AggregateStats *bool `json:"aggregateStats,omitempty"`
}

// PolicySpec is the per-tenant storage policy, mapping 1:1 onto oteldb's storage.policy block.
//
// Downsample, Precision and Recompress work against oteldb v0.48.0. Retention and Limits landed in
// oteldb's config after it; older builds ignore unknown config keys silently, so against those
// those two are accepted and do nothing — pin a newer Image before relying on them.
type PolicySpec struct {
// 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"`

// Downsample is the age-tiered merge-time rollup: samples older than a tier's After are
// replaced by one representative per Interval-wide bucket. Empty keeps data raw.
//
// This rewrites data in place and cannot be undone: lowering a tier's After re-processes
// existing parts at the next merge, and the replaced samples are gone.
// +optional
// +listType=atomic
Downsample []DownsampleTierSpec `json:"downsample,omitempty"`

// Precision is the age-tiered lossy float-compression policy: the value column of parts older
// than a tier's After is re-encoded to keep only Bits mantissa bits. Empty stays lossless.
//
// This rewrites data in place and cannot be undone: discarded mantissa bits are not
// recoverable, and lowering a tier's After re-processes existing parts at the next merge.
// +optional
// +listType=atomic
Precision []PrecisionTierSpec `json:"precision,omitempty"`

// Recompress rewrites fully-cold parts with a higher-ratio Zstandard profile at merge, trading
// merge CPU for storage. It is decode-transparent and lossless. Nil disables it.
// +optional
Recompress *RecompressSpec `json:"recompress,omitempty"`
}

// DownsampleTierSpec is one age band of the downsampling policy. Tiers are order-independent: a
// sample is rolled up by the coarsest tier whose After it has exceeded, and samples younger than
// every tier stay raw. Buckets align to absolute multiples of Interval, so repeated merges are
// stable.
type DownsampleTierSpec struct {
// After is the age past which this tier applies, relative to merge time (e.g. "24h").
// +required
After metav1.Duration `json:"after"`

// Interval is the rollup bucket width (e.g. "5m"). It must be positive.
// +required
Interval metav1.Duration `json:"interval"`

// Agg combines the samples in a bucket. Defaults to "last".
// +kubebuilder:validation:Enum=last;first;min;max;sum;avg;count
// +optional
Agg string `json:"agg,omitempty"`
}

// PrecisionTierSpec is one age band of the lossy float-precision policy. Tiers are
// order-independent: a part takes the most aggressive tier whose After it has exceeded. The
// encoder keeps whichever of the lossy and lossless encodings is smaller, so a tier can only help
// size.
type PrecisionTierSpec struct {
// After is the age past which this tier applies, relative to merge time (e.g. "168h").
// +required
After metav1.Duration `json:"after"`

// Bits is the number of significant mantissa bits retained. Fewer bits compress better and
// lose more accuracy.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=63
// +required
Bits int32 `json:"bits"`
}

// RecompressSpec configures cold-part recompression.
type RecompressSpec struct {
// After is the age past which a fully-cold part is recompressed at merge. It must be positive
// — the block exists only to enable recompression.
// +required
After metav1.Duration `json:"after"`

// Level is the Zstandard level: 1 is fastest, 19 is the best ratio. Empty uses the best-ratio
// default.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=19
// +optional
Level *int32 `json:"level,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.
Expand Down
89 changes: 87 additions & 2 deletions api/v1alpha1/zz_generated.deepcopy.go

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

Loading
Loading