Files
Antje WorringandClaude Opus 4.8 a5a86ce17a feat: per-tenant isolation via X-Org-Id (v0.3.0)
Every /v1/{metrics,logs,traces} request is scoped to a tenant — the
gateway-minted X-Org-Id header, falling back to the deployment brand. Each
tenant gets its own store set with per-org WALs under <DataDir>/orgs/<org>/o11y/
(HIP-0302), so lux.cloud and zoo.cloud share one binary with hard data
isolation. registry.go lazily creates+durably-backs each tenant on first use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:43:45 -07:00

64 lines
1.8 KiB
Go

package metrics
import (
"path/filepath"
"sync"
)
// tenantSet holds the three signal stores for one org/tenant.
type tenantSet struct {
Metrics *Store
Logs *LogStore
Traces *TraceStore
}
// Registry lazily creates per-tenant store sets. Each tenant's WALs live under
// <DataDir>/orgs/<org>/o11y/ (the HIP-0302 per-org convention), so one tenant's
// metrics/logs/traces never mingle with another's — the same binary serves
// lux.cloud and zoo.cloud with hard data isolation. A single-tenant deployment
// simply uses one org (the deployment brand).
type Registry struct {
mu sync.Mutex
dataDir string
tenants map[string]*tenantSet
}
// NewRegistry returns a registry rooted at dataDir ("" = in-memory, no durability).
func NewRegistry(dataDir string) *Registry {
return &Registry{dataDir: dataDir, tenants: make(map[string]*tenantSet)}
}
// For returns the store set for org, creating it (and enabling per-org WAL
// durability when dataDir is set) on first use. An empty org collapses to
// "default".
func (r *Registry) For(org string) *tenantSet {
if org == "" {
org = "default"
}
r.mu.Lock()
defer r.mu.Unlock()
if ts := r.tenants[org]; ts != nil {
return ts
}
ts := &tenantSet{Metrics: NewStore(), Logs: NewLogStore(), Traces: NewTraceStore()}
if r.dataDir != "" {
dir := filepath.Join(r.dataDir, "orgs", org, "o11y")
_ = ts.Metrics.EnableDurability(filepath.Join(dir, "metrics.wal"))
_ = ts.Logs.EnableDurability(filepath.Join(dir, "logs.wal"))
_ = ts.Traces.EnableDurability(filepath.Join(dir, "traces.wal"))
}
r.tenants[org] = ts
return ts
}
// Tenants returns the org slugs that currently have stores (for /health/admin).
func (r *Registry) Tenants() []string {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]string, 0, len(r.tenants))
for k := range r.tenants {
out = append(out, k)
}
return out
}