Files
zooqueenandHanzo Dev e14ac284db
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Failing after 11m25s
Hanzo CI/CD / test-amd64 (push) Failing after 1h6m41s
Hanzo CI/CD / cicd (push) Skipped
CI/CD / cicd (push) Failing after 1h6m44s
No host serves the console bundle; sign-out ends the IAM session
hanzo.yml stops publishing the o11y-site Sites project: o11y.hanzo.ai,
obs.hanzo.ai and dashboards.hanzo.ai redirect to platform.hanzo.ai/o11y.
Sign-out goes to IAM's end-session endpoint instead of a forward-auth's
/__guard/logout, and the "local" authz comment states the boundary as it is:
only cloud reaches a standalone runtime, with headers it minted from a
validated IAM bearer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-15 05:33:45 -07:00

199 lines
9.9 KiB
Go

// Package community constructs the ONE Hanzo o11y (O11y-community) runtime and
// HTTP server used by BOTH the standalone `cmd/community` binary AND the unified
// hanzoai/cloud binary's in-process embed (via app.Server.PublicHandler).
//
// Keeping the whole construction — config resolution, the O11y provider set,
// and the app server — behind a single exported builder guarantees the two
// deployments run byte-identical middleware, identity (pkg/identn/iamidentn,
// i.e. Hanzo IAM gateway-header auth), authz (iamauthz), telemetry stores, rule
// manager, dashboards and alerts. One construction, one way: the embed cannot
// drift from the standalone pod's auth or wiring, because they are the same code.
package community
import (
"context"
"log/slog"
"github.com/hanzoai/o11y/pkg/alertmanager"
"github.com/hanzoai/o11y/pkg/analytics"
"github.com/hanzoai/o11y/pkg/auditor"
"github.com/hanzoai/o11y/pkg/authz"
"github.com/hanzoai/o11y/pkg/authz/iamauthz"
"github.com/hanzoai/o11y/pkg/authz/localauthz"
"github.com/hanzoai/o11y/pkg/cache"
"github.com/hanzoai/o11y/pkg/config"
"github.com/hanzoai/o11y/pkg/config/envprovider"
"github.com/hanzoai/o11y/pkg/config/fileprovider"
"github.com/hanzoai/o11y/pkg/factory"
"github.com/hanzoai/o11y/pkg/flagger"
"github.com/hanzoai/o11y/pkg/gateway"
"github.com/hanzoai/o11y/pkg/gateway/noopgateway"
"github.com/hanzoai/o11y/pkg/global"
"github.com/hanzoai/o11y/pkg/licensing"
"github.com/hanzoai/o11y/pkg/licensing/nooplicensing"
"github.com/hanzoai/o11y/pkg/meterreporter"
"github.com/hanzoai/o11y/pkg/modules/cloudintegration"
"github.com/hanzoai/o11y/pkg/modules/cloudintegration/implcloudintegration"
"github.com/hanzoai/o11y/pkg/modules/dashboard"
"github.com/hanzoai/o11y/pkg/modules/dashboard/impldashboard"
"github.com/hanzoai/o11y/pkg/modules/metricreductionrule"
"github.com/hanzoai/o11y/pkg/modules/metricreductionrule/implmetricreductionrule"
"github.com/hanzoai/o11y/pkg/modules/organization"
"github.com/hanzoai/o11y/pkg/modules/retention"
"github.com/hanzoai/o11y/pkg/modules/rulestatehistory"
"github.com/hanzoai/o11y/pkg/modules/serviceaccount"
"github.com/hanzoai/o11y/pkg/modules/tag"
"github.com/hanzoai/o11y/pkg/o11y"
"github.com/hanzoai/o11y/pkg/prometheus"
"github.com/hanzoai/o11y/pkg/querier"
"github.com/hanzoai/o11y/pkg/query-service/app"
"github.com/hanzoai/o11y/pkg/queryparser"
"github.com/hanzoai/o11y/pkg/ruler"
"github.com/hanzoai/o11y/pkg/ruler/o11yruler"
"github.com/hanzoai/o11y/pkg/sqlschema"
"github.com/hanzoai/o11y/pkg/sqlstore"
"github.com/hanzoai/o11y/pkg/telemetrystore"
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
"github.com/hanzoai/o11y/pkg/zeus"
"github.com/hanzoai/o11y/pkg/zeus/noopzeus"
)
// NewConfig resolves the O11y config from the given YAML files (if any) plus
// the process environment (env:), applying the Hanzo operator-facing aliases
// (e.g. the flat O11Y_DATASTORE_DSN → telemetrystore.datastore.dsn) inside
// o11y.NewConfig. This is THE config path; cmd.NewO11yConfig delegates here
// so standalone and embed read configuration identically.
func NewConfig(ctx context.Context, logger *slog.Logger, configFiles []string) (o11y.Config, error) {
uris := make([]string, 0, len(configFiles)+1)
for _, f := range configFiles {
uris = append(uris, "file:"+f)
}
uris = append(uris, "env:")
return o11y.NewConfig(
ctx,
logger,
config.ResolverConfig{
Uris: uris,
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
},
)
}
// SQLStoreProviderFactories is the community SQL store provider set (the
// control-plane metadata store — sqlite by default).
func SQLStoreProviderFactories() factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]] {
return o11y.NewSQLStoreProviderFactories()
}
// SQLSchemaProviderFactories is the community SQL schema provider set.
func SQLSchemaProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return o11y.NewSQLSchemaProviderFactories(sqlstore)
}
// NewO11y constructs the O11y runtime with the community provider set: noop
// zeus/licensing/gateway, Hanzo IAM authz (iamauthz — the sole authorizer), the
// Datastore (Hanzo Datastore) telemetry store, sqlite control-plane store, the
// full dashboard/cloudintegration/metricreductionrule/ruler modules, and (wired
// internally by o11y.New) the identN provider set including iamidentn — the
// gateway-header human identity the running pod trusts. The provider list is the
// single source of truth for how o11y boots.
func NewO11y(ctx context.Context, config o11y.Config) (*o11y.O11y, error) {
return o11y.New(
ctx,
config,
zeus.Config{},
noopzeus.NewProviderFactory(),
licensing.Config{},
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return nooplicensing.NewFactory()
},
o11y.NewEmailingProviderFactories(),
o11y.NewCacheProviderFactories(),
o11y.NewWebProviderFactories(config.Global),
SQLSchemaProviderFactories,
SQLStoreProviderFactories(),
o11y.NewTelemetryStoreProviderFactories(),
func(_ context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
// Authorization is selected by the TRUST BOUNDARY, not by pluggable policy —
// the enforced policy is identical either way, only where the relationship
// tuples live differs:
// - "local" (O11Y_AUTHZ_PROVIDER=local): the identity headers were minted
// by cloud from a Hanzo IAM bearer it validated (cloud embeds this
// module, and its /v1/o11y proxies to a standalone runtime with the
// same headers), and the sharder gates cross-org — so authorizing that
// user for their own org is a local decision. See pkg/authz/localauthz.
// - "iam" (default): every decision AND every tuple WRITE is delegated to
// Hanzo IAM's Casbin enforcer.
//
// The boundary is a property of the DEPLOYMENT, not of standalone-vs-embed:
// a standalone runtime that only cloud reaches has the embed's boundary and
// runs "local". No public route may reach one.
//
// A WARNING the default cannot express: "iam" requires IAM to serve
// add-policy / remove-policy / get-policies / batch-enforce, and IAM v1.34 —
// the clean-room zip v2 — does not. Selecting it today makes the founding
// grant fail with authz_unavailable on every sign-in. Choose "local" for a
// runtime only cloud reaches, or restore those routes first.
if config.Provider == "local" {
return localauthz.NewProviderFactory(sqlstore), nil
}
return iamauthz.NewProviderFactory(sqlstore), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return o11y.NewAuditorProviderFactories()
},
func(_ context.Context, _ factory.ProviderSettings, _ flagger.Flagger, _ licensing.Licensing, _ telemetrystore.TelemetryStore, _ retention.Getter, _ organization.Getter, _ zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
return o11y.NewMeterReporterProviderFactories(), "noop"
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
func(_ sqlstore.SQLStore, _ telemetrystore.TelemetryStore, _ dashboard.Module, _ queryparser.QueryParser, _ licensing.Licensing, _ flagger.Flagger, _ telemetrytypes.MetadataStore, _ factory.ProviderSettings, _ int) metricreductionrule.Module {
return implmetricreductionrule.NewModule()
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(o11yruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
},
)
}
// NewServer constructs the O11y runtime and its HTTP server together. Callers
// choose the serving mode:
//
// - standalone (cmd/community): server.Start binds the listeners; O11y.Start
// runs background evaluation; O11y.Wait blocks.
// - embedded (hanzoai/cloud): O11y.Start runs background evaluation, and the
// server's route table is installed via o11y.SetRuntime — cloud's own HTTP
// stack serves /v1/o11y/*, resolving each address against that table; the
// listeners are never bound. A host that runs the runtime as a separate
// Deployment installs o11y.Whole over its proxy instead, because for a
// remote every address really is the same endpoint.
//
// Both paths share this ONE construction, so identity and authz are identical.
func NewServer(ctx context.Context, config o11y.Config) (*app.Server, *o11y.O11y, error) {
sn, err := NewO11y(ctx, config)
if err != nil {
return nil, nil, err
}
server, err := app.NewServer(config, sn)
if err != nil {
return nil, nil, err
}
return server, sn, nil
}