Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac8ca7395 | ||
|
|
eb2de99089 | ||
|
|
12e4cacf03 | ||
|
|
f2ab8a12b0 | ||
|
|
53b455b4a7 | ||
|
|
0ab53eb746 | ||
|
|
8e64601291 | ||
|
|
767533f8ef | ||
|
|
0a13ef6982 | ||
|
|
e9bcd48f5f |
@@ -157,3 +157,31 @@ telemetrystore provider registration all stay (implementation, not surface).
|
||||
`O11Y_OTEL_COLLECTOR_DATASTORE_*` keys in `deploy/` are consumed by the
|
||||
`hanzoai/signoz-otel-collector` fork — renamed here for consistency; that fork must
|
||||
accept the `_DATASTORE_` segment (coordinated cross-repo rename).
|
||||
|
||||
## Native datastore metrics driver (`pkg/datastoremetrics`) — the fork unblock
|
||||
|
||||
- **What**: the o11y-native write path for metrics. `Writer.WriteMetrics` satisfies
|
||||
`zapmetricreceiver.Handler`, decoding a ZAP `MetricBatch` straight into the datastore
|
||||
`time_series_v4` + `samples_v4` tables over **upstream** `clickhouse-go` v2 (via
|
||||
`telemetrystore.TelemetryStore.ClickhouseDB()`), reusing the `telemetrymetrics`
|
||||
table-name constants so a series written here is immediately queryable.
|
||||
- **Why it exists**: the stock `signozclickhousemetrics` exporter serialises OTLP
|
||||
exponential histograms as a DDSketch into `exp_hist.sketch`, which needs the FORKED
|
||||
ch-go (`proto.DD/.Store/.IndexMapping`). That fork conflicts with the upstream ch-go
|
||||
the query plane pins — the reason metrics ingest could not move in-process. This driver
|
||||
sidesteps the fork: the ZAP wire carries CLASSIC Prometheus shapes, so histograms
|
||||
decompose into `<name>.bucket{le=…}` / `.count` / `.sum` samples and summaries into
|
||||
`.quantile{quantile=…}` — no sketch, no `exp_hist`, NO fork. Only the two tables the
|
||||
query plane already reads.
|
||||
- **Fingerprint parity**: the labels→fingerprint hash (FNV-1a, hierarchical
|
||||
resource→scope→point, salted with `__name__`) is ported verbatim from the fork's
|
||||
`internal/common/fingerprint` (which is import-walled), so join keys are byte-identical
|
||||
and the existing reader joins samples to series unchanged. Column strings match too:
|
||||
temporality `Cumulative`/`Unspecified`, type `Sum`/`Gauge`/`Histogram`/`Summary`.
|
||||
- **Wire-in**: `datastoremetrics.NewWriter(store.ClickhouseDB())` +
|
||||
`zapmetricreceiver.New(Config{OnBatch: w.WriteMetrics})`. Consumed by `hanzoai/cloud`'s
|
||||
embedded o11y runtime (opt-in `O11Y_METRICS_ZAP_LISTEN`, fail-soft) so the standalone
|
||||
`signoz-otel-collector` metrics path can later repoint to cloud (verify-then-cutover).
|
||||
- **Boundary unchanged**: the physical `signoz_metrics.*_v4` names still live in
|
||||
`telemetrymetrics` (the source of truth this driver reuses) — renaming them is a
|
||||
datastore migration, out of scope here.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/hanzoai/o11y/cmd"
|
||||
"github.com/hanzoai/o11y/pkg/community"
|
||||
"github.com/hanzoai/o11y/pkg/instrumentation"
|
||||
)
|
||||
|
||||
@@ -14,7 +15,7 @@ func main() {
|
||||
// register a list of commands to the root command
|
||||
registerServer(cmd.RootCmd, logger)
|
||||
cmd.RegisterGenerate(cmd.RootCmd, logger)
|
||||
cmd.RegisterMetastore(cmd.RootCmd, logger, sqlstoreProviderFactories, sqlschemaProviderFactories)
|
||||
cmd.RegisterMetastore(cmd.RootCmd, logger, community.SQLStoreProviderFactories, community.SQLSchemaProviderFactories)
|
||||
|
||||
cmd.Execute(logger)
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/hanzoai/o11y/pkg/factory"
|
||||
"github.com/hanzoai/o11y/pkg/signoz"
|
||||
"github.com/hanzoai/o11y/pkg/sqlschema"
|
||||
"github.com/hanzoai/o11y/pkg/sqlstore"
|
||||
)
|
||||
|
||||
func sqlstoreProviderFactories() factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]] {
|
||||
return signoz.NewSQLStoreProviderFactories()
|
||||
}
|
||||
|
||||
func sqlschemaProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
return signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
}
|
||||
+9
-97
@@ -7,47 +7,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/hanzoai/o11y/cmd"
|
||||
"github.com/hanzoai/o11y/pkg/alertmanager"
|
||||
"github.com/hanzoai/o11y/pkg/analytics"
|
||||
"github.com/hanzoai/o11y/pkg/auditor"
|
||||
"github.com/hanzoai/o11y/pkg/authn"
|
||||
"github.com/hanzoai/o11y/pkg/authz"
|
||||
"github.com/hanzoai/o11y/pkg/authz/iamauthz"
|
||||
"github.com/hanzoai/o11y/pkg/cache"
|
||||
"github.com/hanzoai/o11y/pkg/community"
|
||||
"github.com/hanzoai/o11y/pkg/errors"
|
||||
"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/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/signozruler"
|
||||
"github.com/hanzoai/o11y/pkg/signoz"
|
||||
"github.com/hanzoai/o11y/pkg/sqlstore"
|
||||
"github.com/hanzoai/o11y/pkg/telemetrystore"
|
||||
"github.com/hanzoai/o11y/pkg/types/authtypes"
|
||||
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
|
||||
"github.com/hanzoai/o11y/pkg/version"
|
||||
"github.com/hanzoai/o11y/pkg/zeus"
|
||||
"github.com/hanzoai/o11y/pkg/zeus/noopzeus"
|
||||
)
|
||||
|
||||
func registerServer(parentCmd *cobra.Command, logger *slog.Logger) {
|
||||
@@ -75,62 +38,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
// print the version
|
||||
version.Info.PrettyPrint(config.Version)
|
||||
|
||||
signoz, err := signoz.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()
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
sqlschemaProviderFactories,
|
||||
sqlstoreProviderFactories(),
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
|
||||
},
|
||||
func(_ context.Context, sqlstore sqlstore.SQLStore, _ authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
// Hanzo IAM is the sole authorization provider — every decision is
|
||||
// delegated to IAM's Casbin enforce endpoint. No OpenFGA, no fallback.
|
||||
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 signoz.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 signoz.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(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
|
||||
},
|
||||
)
|
||||
// community.NewServer is the ONE construction shared with the hanzoai/cloud
|
||||
// embed — same providers, same identity (iamidentn gateway-header auth), same
|
||||
// wiring. Standalone owns the process: bind listeners, run background
|
||||
// evaluation, block until shutdown.
|
||||
server, signoz, err := community.NewServer(ctx, config)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := app.NewServer(config, signoz)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "failed to create server", errors.Attr(err))
|
||||
logger.ErrorContext(ctx, "failed to create signoz server", errors.Attr(err))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -146,14 +60,12 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
return err
|
||||
}
|
||||
|
||||
err = server.Stop(ctx)
|
||||
if err != nil {
|
||||
if err := server.Stop(ctx); err != nil {
|
||||
logger.ErrorContext(ctx, "failed to stop server", errors.Attr(err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = signoz.Stop(ctx)
|
||||
if err != nil {
|
||||
if err := signoz.Stop(ctx); err != nil {
|
||||
logger.ErrorContext(ctx, "failed to stop signoz", errors.Attr(err))
|
||||
return err
|
||||
}
|
||||
|
||||
+7
-25
@@ -4,33 +4,15 @@ import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"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/community"
|
||||
"github.com/hanzoai/o11y/pkg/signoz"
|
||||
)
|
||||
|
||||
// NewSigNozConfig resolves the SigNoz config from the given YAML files plus the
|
||||
// process environment. It delegates to community.NewConfig — the ONE config path
|
||||
// shared by the standalone binary and the hanzoai/cloud embed — so both read
|
||||
// configuration (and the Hanzo operator-facing aliases like O11Y_DATASTORE_DSN)
|
||||
// identically.
|
||||
func NewSigNozConfig(ctx context.Context, logger *slog.Logger, configFiles []string) (signoz.Config, error) {
|
||||
uris := make([]string, 0, len(configFiles)+1)
|
||||
for _, f := range configFiles {
|
||||
uris = append(uris, "file:"+f)
|
||||
}
|
||||
uris = append(uris, "env:")
|
||||
|
||||
config, err := signoz.NewConfig(
|
||||
ctx,
|
||||
logger,
|
||||
config.ResolverConfig{
|
||||
Uris: uris,
|
||||
ProviderFactories: []config.ProviderFactory{
|
||||
envprovider.NewFactory(),
|
||||
fileprovider.NewFactory(),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return signoz.Config{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return community.NewConfig(ctx, logger, configFiles)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ require (
|
||||
github.com/hanzoai/clickhouse-go-mock v0.14.1
|
||||
github.com/hanzoai/govaluate v0.1.0
|
||||
github.com/hanzoai/signoz-otel-collector v0.144.6
|
||||
github.com/hanzoai/sqlite v0.2.2
|
||||
github.com/huandu/go-sqlbuilder v1.39.1
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
|
||||
github.com/knadh/koanf v1.5.0
|
||||
@@ -39,7 +40,7 @@ require (
|
||||
github.com/mailru/easyjson v0.9.0
|
||||
github.com/open-telemetry/opamp-go v0.22.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b
|
||||
github.com/perses/spec v0.1.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/alertmanager v0.31.1
|
||||
@@ -64,6 +65,7 @@ require (
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||
github.com/uptrace/bun/extra/bunotel v1.2.9
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/zap-proto/zap2pb v0.2.0
|
||||
go.opentelemetry.io/collector/confmap v1.54.0
|
||||
go.opentelemetry.io/collector/otelcol v0.144.0
|
||||
go.opentelemetry.io/collector/pdata v1.54.0
|
||||
@@ -77,14 +79,13 @@ require (
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/text v0.38.0
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
google.golang.org/api v0.275.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.35.3
|
||||
@@ -92,35 +93,97 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/ThinkInAIXYZ/go-mcp v0.2.24 // indirect
|
||||
github.com/WqyJh/go-cosyvoice v0.1.0 // indirect
|
||||
github.com/WqyJh/go-openai-realtime v0.5.1-0.20250210083616-024eddd5a481 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/ecs-20140526/v4 v4.26.10 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.2 // indirect
|
||||
github.com/alibabacloud-go/resourcecenter-20221201 v1.5.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/vod-20170321/v2 v2.16.10 // indirect
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.7 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/baidubce/bce-qianfan-sdk/go/qianfan v0.0.14 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.264 // indirect
|
||||
github.com/beego/beego v1.12.14 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
|
||||
github.com/carmel/gooxml v0.0.0-20220216072414-40ff56130850 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/cohere-ai/cohere-go/v2 v2.5.2 // indirect
|
||||
github.com/consensys/gnark-crypto v0.20.1 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/digitalocean/go-libvirt v0.0.0-20260217163227-273eaa321819 // indirect
|
||||
github.com/diskfs/go-diskfs v1.9.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20260627200808-0b76000cabdb // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/evanw/esbuild v0.28.1 // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gage-technologies/mistral-go v1.1.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.5 // indirect
|
||||
@@ -136,49 +199,144 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofiber/fiber/v3 v3.2.0 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.4 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/hanzoai/commerce/metering v0.1.2 // indirect
|
||||
github.com/hanzoai/ai v1.802.1-0.20260708185316-0321c35877f0 // indirect
|
||||
github.com/hanzoai/beego/v2 v2.3.10 // indirect
|
||||
github.com/hanzoai/commerce/metering v0.1.4 // indirect
|
||||
github.com/hanzoai/dashscope-go-sdk v0.0.2 // indirect
|
||||
github.com/hanzoai/dashscopego v0.6.0 // indirect
|
||||
github.com/hanzoai/dbx v1.16.0 // indirect
|
||||
github.com/hanzoai/go-openrouter v1.0.0 // indirect
|
||||
github.com/hanzoai/iam v1.31.18 // indirect
|
||||
github.com/hanzoai/pdf v1.2.0 // indirect
|
||||
github.com/hanzoai/search-go v0.36.0 // indirect
|
||||
github.com/hanzoai/tasks v1.49.0 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
github.com/henomis/lingoose v0.1.0 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/pkcs7 v0.2.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.2 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/huandu/go-clone v1.7.3 // indirect
|
||||
github.com/hupe1980/go-huggingface v0.0.15 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/leverly/ChatGLM v1.2.0 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/lithammer/shortuuid v3.0.0+incompatible // indirect
|
||||
github.com/luthermonson/go-proxmox v0.4.0 // indirect
|
||||
github.com/luxfi/accel v1.2.4 // indirect
|
||||
github.com/luxfi/age v1.5.0 // indirect
|
||||
github.com/luxfi/crypto v1.19.20 // indirect
|
||||
github.com/luxfi/kms v1.11.6 // indirect
|
||||
github.com/luxfi/age v1.6.0 // indirect
|
||||
github.com/luxfi/bft v0.1.5 // indirect
|
||||
github.com/luxfi/cache v1.2.1 // indirect
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/consensus v1.35.32 // indirect
|
||||
github.com/luxfi/constants v1.5.8 // indirect
|
||||
github.com/luxfi/container v0.0.4 // indirect
|
||||
github.com/luxfi/crypto v1.19.26 // indirect
|
||||
github.com/luxfi/crypto/ipa v1.2.4 // indirect
|
||||
github.com/luxfi/database v1.19.3 // indirect
|
||||
github.com/luxfi/geth v1.17.11 // indirect
|
||||
github.com/luxfi/ids v1.3.0 // indirect
|
||||
github.com/luxfi/kms v1.11.8 // indirect
|
||||
github.com/luxfi/math v1.4.1 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/mdns v0.1.1 // indirect
|
||||
github.com/luxfi/metric v1.5.8 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/p2p v1.21.1 // indirect
|
||||
github.com/luxfi/pq v1.1.0 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/validators v1.2.0 // indirect
|
||||
github.com/luxfi/version v1.0.1 // indirect
|
||||
github.com/luxfi/warp v1.24.0 // indirect
|
||||
github.com/luxfi/zapdb v1.10.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.47 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.9.5 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.100 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/mr-tron/base58 v1.3.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nxadm/tail v1.4.11 // indirect
|
||||
github.com/openai/openai-go/v2 v2.1.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
|
||||
github.com/oschwald/geoip2-golang v1.11.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/pdfcpu/pdfcpu v0.11.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sashabaranov/go-openai v1.32.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/supranational/blst v0.3.16 // indirect
|
||||
github.com/swaggest/refl v1.4.0 // indirect
|
||||
github.com/swaggest/usecase v1.3.1 // indirect
|
||||
github.com/tealeg/xlsx v1.0.5 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.77 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.1116 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/hunyuan v1.3.48 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tbaas v1.1.13 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ua-parser/uap-go v0.0.0-20251207011819-db9adb27a0b8 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.70.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.241 // indirect
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.141 // indirect
|
||||
github.com/wangbin/jiebago v0.3.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/zap-proto/go v1.3.0 // indirect
|
||||
github.com/zap-proto/http v0.2.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
@@ -191,16 +349,26 @@ require (
|
||||
go.uber.org/goleak v1.3.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
google.golang.org/genai v1.10.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
k8s.io/api v0.35.3 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31 // indirect
|
||||
k8s.io/metrics v0.30.0 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -211,7 +379,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.71.0
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
@@ -225,7 +393,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||
github.com/elastic/lunes v0.2.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.7
|
||||
@@ -241,7 +409,7 @@ require (
|
||||
github.com/go-openapi/analysis v0.24.2 // indirect
|
||||
github.com/go-openapi/errors v0.22.7 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||
github.com/go-openapi/loads v0.23.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/swag v0.25.5 // indirect
|
||||
@@ -258,8 +426,8 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/hanzoai/cloud v0.0.0-20260519044114-66d5d2a6312c
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/hanzoai/cloud v1.786.112
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
|
||||
@@ -281,9 +449,9 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-syslog/v4 v4.3.0 // indirect
|
||||
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
|
||||
github.com/magefile/mage v1.17.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mdlayher/vsock v1.2.1 // indirect
|
||||
@@ -317,7 +485,7 @@ require (
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/segmentio/backo-go v1.0.1 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
|
||||
@@ -325,7 +493,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/swaggest/openapi-go v0.2.60
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
@@ -335,7 +503,7 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zap-proto/zip v1.2.0
|
||||
github.com/zap-proto/zip v1.2.1
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/collector/component v1.54.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componentstatus v0.148.0 // indirect
|
||||
@@ -389,8 +557,8 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0
|
||||
@@ -402,15 +570,17 @@ require (
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
gopkg.in/telebot.v3 v3.3.8 // indirect
|
||||
k8s.io/client-go v0.35.3 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
|
||||
)
|
||||
|
||||
replace github.com/expr-lang/expr => github.com/hanzoai/expr v1.17.8
|
||||
|
||||
replace github.com/hanzoai/cloud => ../cloud
|
||||
|
||||
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
|
||||
@@ -2,11 +2,13 @@ package o11y
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/zap-proto/zip"
|
||||
luxlog "github.com/luxfi/log"
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// Mount registers Hanzo o11y's HTTP surface under /v1/o11y per HIP-0106.
|
||||
@@ -25,9 +27,10 @@ import (
|
||||
// after o11y.New + app.NewServer.
|
||||
// - Until a handler is registered, the routes 503 with a clear error.
|
||||
//
|
||||
// All traffic under /v1/o11y is delegated to the registered http.Handler
|
||||
// via zip.AdaptNetHTTP; the o11y handler internally rewrites /v1/o11y/*
|
||||
// to /api/* so existing controllers stay untouched (see app.createPublicServer).
|
||||
// All traffic under /v1/o11y is delegated to the registered http.Handler via
|
||||
// zip.AdaptNetHTTP; handlerAdapter normalizes the /v1/o11y/<resource> public
|
||||
// contract onto the two internal route families HERE — the ONE Hanzo-owned seam —
|
||||
// so the embedded SigNoz route literals stay untouched (see rewriteExternalPath).
|
||||
func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
log := deps.Logger
|
||||
if log == nil {
|
||||
@@ -55,9 +58,56 @@ func (handlerAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "o11y runtime not initialized", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
rewriteExternalPath(r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// rewriteExternalPath maps the ONE public o11y contract — api.hanzo.ai/v1/o11y/<resource>,
|
||||
// one /v1/, no /api/ — onto the two internal route families, at this single Hanzo-owned
|
||||
// seam. It is done HERE, never by editing the embedded SigNoz route literals: SigNoz's
|
||||
// whole frontend and backend speak /api/vN, and rewriting those literals is a fork diff
|
||||
// that a later upstream re-sync silently reverts (it already happened once — see
|
||||
// o11y/CLAUDE.md).
|
||||
//
|
||||
// SigNoz native (registered at /api/vN/*):
|
||||
// /v1/o11y/vN/… → /api/vN/… (canonical — the /api/ never surfaces)
|
||||
// /v1/o11y/api/vN/… → /api/vN/… (deprecated alias: the leaked form callers emit
|
||||
// today. Drop once every consumer emits the
|
||||
// canonical form — one and one way.)
|
||||
// Hanzo llmobs (registered natively at /v1/o11y/{traces,observations,…}): passed
|
||||
// through unchanged.
|
||||
//
|
||||
// This requires the embedded SigNoz StripPrefix wrapper to be OFF — cloud CR
|
||||
// O11Y_GLOBAL_EXTERNAL__URL="" — so a /v1/o11y/* llmobs path survives to the router.
|
||||
func rewriteExternalPath(u *url.URL) {
|
||||
rest, ok := strings.CutPrefix(u.Path, "/v1/o11y/")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(rest, "api/v"): // deprecated leaked alias: /v1/o11y/api/vN/x
|
||||
setPath(u, "/"+rest) // → /api/vN/x
|
||||
case isVersionSegment(rest): // canonical SigNoz form: /v1/o11y/vN/x
|
||||
setPath(u, "/api/"+rest) // → /api/vN/x
|
||||
default: // Hanzo llmobs / native resource — the router owns /v1/o11y/x directly.
|
||||
}
|
||||
}
|
||||
|
||||
// isVersionSegment reports whether rest begins with a SigNoz API version segment
|
||||
// (v followed by a digit — "v1/health", "v3/query_range"): the marker that tells an
|
||||
// embedded-SigNoz route apart from a Hanzo-native llmobs resource (traces, sessions, …).
|
||||
func isVersionSegment(rest string) bool {
|
||||
return len(rest) >= 2 && rest[0] == 'v' && rest[1] >= '0' && rest[1] <= '9'
|
||||
}
|
||||
|
||||
// setPath rewrites the request path, clearing RawPath so EscapedPath re-derives from the
|
||||
// new value — the rewritten SigNoz paths contain no characters needing escaping, and
|
||||
// llmobs paths (the only ones carrying an {id}) are never rewritten.
|
||||
func setPath(u *url.URL, p string) {
|
||||
u.Path = p
|
||||
u.RawPath = ""
|
||||
}
|
||||
|
||||
var (
|
||||
hmu sync.RWMutex
|
||||
registered http.Handler
|
||||
|
||||
+32
-17
@@ -1,7 +1,6 @@
|
||||
package o11y_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -28,7 +27,10 @@ func TestMountWithoutHandlerReturns503(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountForwardsToRegisteredHandler(t *testing.T) {
|
||||
// TestMountNormalizesExternalPath proves the one public contract /v1/o11y/<resource>
|
||||
// (one /v1/, no /api/) is normalized at the mount seam onto the two internal route
|
||||
// families: SigNoz native (/api/vN/*) and Hanzo llmobs (/v1/o11y/*, passed through).
|
||||
func TestMountNormalizesExternalPath(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := o11y.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
@@ -37,25 +39,38 @@ func TestMountForwardsToRegisteredHandler(t *testing.T) {
|
||||
var sawPath string
|
||||
o11y.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sawPath = r.URL.Path
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
defer o11y.SetHandler(nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/o11y/api/v1/health", nil)
|
||||
resp, err := app.Fiber().Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
cases := []struct {
|
||||
name, external, internal string
|
||||
}{
|
||||
// SigNoz native — canonical Hanzo form; the /api/ never surfaces externally.
|
||||
{"signoz canonical v1", "/v1/o11y/v1/health", "/api/v1/health"},
|
||||
{"signoz canonical v3", "/v1/o11y/v3/query_range", "/api/v3/query_range"},
|
||||
{"signoz canonical v5", "/v1/o11y/v5/query_range", "/api/v5/query_range"},
|
||||
// SigNoz native — deprecated /api/ alias still resolves during migration.
|
||||
{"signoz legacy alias", "/v1/o11y/api/v1/health", "/api/v1/health"},
|
||||
// Hanzo llmobs — registered natively at /v1/o11y/*, passed through untouched.
|
||||
{"llmobs traces", "/v1/o11y/traces", "/v1/o11y/traces"},
|
||||
{"llmobs observations", "/v1/o11y/observations", "/v1/o11y/observations"},
|
||||
{"llmobs score by id", "/v1/o11y/score/abc123", "/v1/o11y/score/abc123"},
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d want 200", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != `{"ok":true}` {
|
||||
t.Fatalf("body=%q want {\"ok\":true}", body)
|
||||
}
|
||||
if sawPath != "/v1/o11y/api/v1/health" {
|
||||
t.Fatalf("sawPath=%q want /v1/o11y/api/v1/health", sawPath)
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
sawPath = ""
|
||||
req := httptest.NewRequest(http.MethodGet, tc.external, nil)
|
||||
resp, err := app.Fiber().Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d want 200", resp.StatusCode)
|
||||
}
|
||||
if sawPath != tc.internal {
|
||||
t.Fatalf("external %s → internal %q, want %q", tc.external, sawPath, tc.internal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Package community constructs the ONE Hanzo o11y (SigNoz-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 SigNoz 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/authn"
|
||||
"github.com/hanzoai/o11y/pkg/authz"
|
||||
"github.com/hanzoai/o11y/pkg/authz/iamauthz"
|
||||
"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/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/signozruler"
|
||||
"github.com/hanzoai/o11y/pkg/signoz"
|
||||
"github.com/hanzoai/o11y/pkg/sqlschema"
|
||||
"github.com/hanzoai/o11y/pkg/sqlstore"
|
||||
"github.com/hanzoai/o11y/pkg/telemetrystore"
|
||||
"github.com/hanzoai/o11y/pkg/types/authtypes"
|
||||
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
|
||||
"github.com/hanzoai/o11y/pkg/zeus"
|
||||
"github.com/hanzoai/o11y/pkg/zeus/noopzeus"
|
||||
)
|
||||
|
||||
// NewConfig resolves the SigNoz 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
|
||||
// signoz.NewConfig. This is THE config path; cmd.NewSigNozConfig delegates here
|
||||
// so standalone and embed read configuration identically.
|
||||
func NewConfig(ctx context.Context, logger *slog.Logger, configFiles []string) (signoz.Config, error) {
|
||||
uris := make([]string, 0, len(configFiles)+1)
|
||||
for _, f := range configFiles {
|
||||
uris = append(uris, "file:"+f)
|
||||
}
|
||||
uris = append(uris, "env:")
|
||||
|
||||
return signoz.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 signoz.NewSQLStoreProviderFactories()
|
||||
}
|
||||
|
||||
// SQLSchemaProviderFactories is the community SQL schema provider set.
|
||||
func SQLSchemaProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
return signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
}
|
||||
|
||||
// NewSigNoz constructs the SigNoz runtime with the community provider set: noop
|
||||
// zeus/licensing/gateway, Hanzo IAM authz (iamauthz — the sole authorizer), the
|
||||
// ClickHouse (Hanzo Datastore) telemetry store, sqlite control-plane store, the
|
||||
// full dashboard/cloudintegration/metricreductionrule/ruler modules, and (wired
|
||||
// internally by signoz.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 NewSigNoz(ctx context.Context, config signoz.Config) (*signoz.SigNoz, error) {
|
||||
return signoz.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()
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
SQLSchemaProviderFactories,
|
||||
SQLStoreProviderFactories(),
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
|
||||
},
|
||||
func(_ context.Context, sqlstore sqlstore.SQLStore, _ authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
// Hanzo IAM is the sole authorization provider — every decision is
|
||||
// delegated to IAM's Casbin enforce endpoint. No OpenFGA, no fallback.
|
||||
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 signoz.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 signoz.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(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NewServer constructs the SigNoz runtime and its HTTP server together. Callers
|
||||
// choose the serving mode:
|
||||
//
|
||||
// - standalone (cmd/community): server.Start binds the listeners; SigNoz.Start
|
||||
// runs background evaluation; SigNoz.Wait blocks.
|
||||
// - embedded (hanzoai/cloud): SigNoz.Start runs background evaluation, and
|
||||
// server.PublicHandler() is installed via o11y.SetHandler — cloud's own HTTP
|
||||
// stack serves /v1/o11y/*; the listeners are never bound.
|
||||
//
|
||||
// Both paths share this ONE construction, so identity and authz are identical.
|
||||
func NewServer(ctx context.Context, config signoz.Config) (*app.Server, *signoz.SigNoz, error) {
|
||||
sn, err := NewSigNoz(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
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package community
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The hanzoai/cloud embed and the standalone binary both resolve config through
|
||||
// NewConfig. The flat operator knob O11Y_DATASTORE_DSN MUST win and land on the
|
||||
// telemetry store DSN — this is the ONE var the cloud CR sets to point the
|
||||
// in-process runtime at the shared ClickHouse (Hanzo Datastore). If this alias
|
||||
// regresses, the embed silently talks to localhost and serves no telemetry.
|
||||
func TestNewConfigAppliesDatastoreDSNAlias(t *testing.T) {
|
||||
const dsn = "tcp://datastore.hanzo.svc:9000?username=u&password=p"
|
||||
t.Setenv("O11Y_DATASTORE_DSN", dsn)
|
||||
t.Setenv("O11Y_TELEMETRYSTORE_DATASTORE_CLUSTER", "insights")
|
||||
|
||||
config, err := NewConfig(context.Background(), slog.Default(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfig: %v", err)
|
||||
}
|
||||
|
||||
if got := config.TelemetryStore.Clickhouse.DSN; got != dsn {
|
||||
t.Fatalf("telemetrystore DSN = %q, want the O11Y_DATASTORE_DSN value %q", got, dsn)
|
||||
}
|
||||
if got := config.TelemetryStore.Clickhouse.Cluster; got != "insights" {
|
||||
t.Fatalf("telemetrystore cluster = %q, want %q", got, "insights")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (C) 2025-2026, Hanzo AI Inc. All rights reserved.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package datastoremetrics
|
||||
|
||||
import "sort"
|
||||
|
||||
// Metric-series fingerprinting — the native port of the labels-hash the
|
||||
// datastore metrics schema uses to join a sample (samples_v4) to its series
|
||||
// metadata (time_series_v4). It is a plain FNV-1a walk over the sorted
|
||||
// (key, value) attribute pairs with a 0xFF separator, seeded by a parent
|
||||
// offset so the hierarchy resource → scope → point composes: the scope hash
|
||||
// seeds the point hash, which is finally salted with the metric __name__.
|
||||
//
|
||||
// This is the minimal, dependency-free equivalent of the histogram-fork's
|
||||
// fingerprint package (which lives behind an internal/ import wall and pulls
|
||||
// OTLP pdata). Reproducing the exact constants here keeps the native writer on
|
||||
// upstream ch-go with NO fork dependency while still producing byte-identical
|
||||
// join keys, so the existing query plane reads what this writer writes.
|
||||
const (
|
||||
// initialOffset is the FNV-1a 64-bit offset basis — the seed for the
|
||||
// top-level (resource) fingerprint.
|
||||
initialOffset uint64 = 14695981039346656037
|
||||
prime64 uint64 = 1099511628211
|
||||
// separatorByte delimits key from value and pair from pair, so
|
||||
// {a=bc} and {ab=c} hash differently.
|
||||
separatorByte byte = 255
|
||||
)
|
||||
|
||||
func hashAdd(h uint64, s string) uint64 {
|
||||
for i := 0; i < len(s); i++ {
|
||||
h ^= uint64(s[i])
|
||||
h *= prime64
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func hashAddByte(h uint64, b byte) uint64 {
|
||||
h ^= uint64(b)
|
||||
h *= prime64
|
||||
return h
|
||||
}
|
||||
|
||||
// fingerprint folds attrs into a hash seeded by offset. attrs is walked in
|
||||
// sorted key order so the result is independent of map iteration order.
|
||||
func fingerprint(offset uint64, attrs map[string]string) uint64 {
|
||||
keys := make([]string, 0, len(attrs))
|
||||
for k := range attrs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
h := offset
|
||||
for _, k := range keys {
|
||||
h = hashAdd(h, k)
|
||||
h = hashAddByte(h, separatorByte)
|
||||
h = hashAdd(h, attrs[k])
|
||||
h = hashAddByte(h, separatorByte)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// hashWithName salts a series fingerprint with the metric name, matching the
|
||||
// schema's __name__ dimension. This is the value stored in the `fingerprint`
|
||||
// column of both samples_v4 and time_series_v4.
|
||||
func hashWithName(h uint64, name string) uint64 {
|
||||
sum := hashAdd(h, "__name__")
|
||||
sum = hashAddByte(sum, separatorByte)
|
||||
sum = hashAdd(sum, name)
|
||||
return sum
|
||||
}
|
||||
|
||||
// mergeAttrs overlays maps left-to-right (later wins) into a fresh map. Used
|
||||
// to compose point + scope + resource attributes for the `labels` JSON with
|
||||
// the same precedence the schema expects (resource overrides point on clash).
|
||||
func mergeAttrs(maps ...map[string]string) map[string]string {
|
||||
out := make(map[string]string)
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// marshalLabels renders attrs as the compact, key-sorted JSON object the
|
||||
// datastore's JSON functions consume for the `labels` column. It is the native
|
||||
// port of the schema's label marshaller: sorted keys, minimal escaping, no
|
||||
// reflection. __name__ is expected to already be present in attrs.
|
||||
func marshalLabels(attrs map[string]string) string {
|
||||
keys := make([]string, 0, len(attrs))
|
||||
for k := range attrs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if len(keys) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
b := make([]byte, 0, 128)
|
||||
b = append(b, '{')
|
||||
for _, name := range keys {
|
||||
b = append(b, '"')
|
||||
b = append(b, name...)
|
||||
b = append(b, '"', ':', '"')
|
||||
for _, c := range []byte(attrs[name]) {
|
||||
switch c {
|
||||
case '\\', '"':
|
||||
b = append(b, '\\', c)
|
||||
case '\n':
|
||||
b = append(b, '\\', 'n')
|
||||
case '\r':
|
||||
b = append(b, '\\', 'r')
|
||||
case '\t':
|
||||
b = append(b, '\\', 't')
|
||||
default:
|
||||
b = append(b, c)
|
||||
}
|
||||
}
|
||||
b = append(b, '"', ',')
|
||||
}
|
||||
b[len(b)-1] = '}' // replace trailing comma
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// labelsJSON builds the `labels` column value for a series: the merged
|
||||
// point/scope/resource attributes plus __name__, key-sorted and JSON-encoded.
|
||||
func labelsJSON(name string, point, scope, resource map[string]string) string {
|
||||
merged := mergeAttrs(point, scope, resource)
|
||||
merged["__name__"] = name
|
||||
return marshalLabels(merged)
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// Copyright (C) 2025-2026, Hanzo AI Inc. All rights reserved.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package datastoremetrics is the o11y-native datastore metrics driver: it
|
||||
// writes metric samples + series metadata to the datastore (the columnar
|
||||
// telemetry backend) over UPSTREAM ch-go / clickhouse-go v2, with NO
|
||||
// histogram-fork dependency.
|
||||
//
|
||||
// It is the write-side companion to pkg/zapmetricreceiver: WriteMetrics
|
||||
// satisfies zapmetricreceiver.Handler, so a receiver dispatches each decoded
|
||||
// batch straight into the datastore:
|
||||
//
|
||||
// w := datastoremetrics.NewWriter(store.ClickhouseDB())
|
||||
// rcv, _ := zapmetricreceiver.New(zapmetricreceiver.Config{OnBatch: w.WriteMetrics})
|
||||
//
|
||||
// WHY THIS EXISTS — the unblock. The stock histogram exporter serialises OTLP
|
||||
// exponential histograms as a DDSketch into the `exp_hist.sketch` column, which
|
||||
// needs a FORKED ch-go exposing proto.DD / proto.Store / proto.IndexMapping.
|
||||
// That fork conflicts with the upstream ch-go the query plane pins, which is why
|
||||
// metrics ingest could not move in-process. This driver sidesteps the fork
|
||||
// entirely: the ZAP wire (luxfi/metric.MetricBatch) already carries CLASSIC
|
||||
// Prometheus shapes — explicit histogram buckets, summary quantiles — so every
|
||||
// series decomposes into plain samples_v4 rows (`<name>.bucket{le=…}`,
|
||||
// `<name>.count`, `<name>.sum`, `<name>.quantile{quantile=…}`). No sketch, no
|
||||
// exp_hist, no fork — just the two tables the query plane already reads
|
||||
// (time_series_v4 + samples_v4), keyed by the identical labels fingerprint.
|
||||
package datastoremetrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"github.com/hanzoai/o11y/pkg/telemetrymetrics"
|
||||
"github.com/hanzoai/o11y/pkg/zapmetricreceiver"
|
||||
)
|
||||
|
||||
// Temporality + type column values — the exact strings the query plane filters
|
||||
// on (metrictypes.Temporality.Value() / OTLP MetricType.String()).
|
||||
const (
|
||||
temporalityCumulative = "Cumulative"
|
||||
temporalityUnspecified = "Unspecified"
|
||||
|
||||
typeSum = "Sum"
|
||||
typeGauge = "Gauge"
|
||||
typeHistogram = "Histogram"
|
||||
typeSummary = "Summary"
|
||||
|
||||
// Series-name suffixes for decomposed complex metrics — the datastore's
|
||||
// dot-suffix convention (NOT Prometheus `_bucket`/`_count`).
|
||||
suffixCount = ".count"
|
||||
suffixSum = ".sum"
|
||||
suffixBucket = ".bucket"
|
||||
suffixQuantile = ".quantile"
|
||||
|
||||
// unitCount is the unit stamped on synthesised `.count` series.
|
||||
unitCount = "1"
|
||||
)
|
||||
|
||||
// INSERT templates — identical column order to the datastore metrics schema.
|
||||
// exp_hist is intentionally absent: the native path never writes a sketch.
|
||||
const (
|
||||
samplesSQLTmpl = "INSERT INTO %s.%s (env, temporality, metric_name, fingerprint, unix_milli, value, flags, inserted_at_unix_milli) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
timeSeriesSQLTmpl = "INSERT INTO %s.%s (env, temporality, metric_name, description, unit, type, is_monotonic, fingerprint, unix_milli, labels, attrs, scope_attrs, resource_attrs, __normalized, inserted_at_unix_milli) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
|
||||
// sampleRow maps 1:1 to a samples_v4 row (minus the write-time inserted_at).
|
||||
type sampleRow struct {
|
||||
env string
|
||||
temporality string
|
||||
metricName string
|
||||
fingerprint uint64
|
||||
unixMilli int64
|
||||
value float64
|
||||
flags uint32
|
||||
}
|
||||
|
||||
// tsRow maps 1:1 to a time_series_v4 row (minus __normalized / inserted_at).
|
||||
type tsRow struct {
|
||||
env string
|
||||
temporality string
|
||||
metricName string
|
||||
description string
|
||||
unit string
|
||||
typ string
|
||||
isMonotonic bool
|
||||
fingerprint uint64
|
||||
unixMilli int64 // hour-floored, matching the schema's series cadence
|
||||
labels string
|
||||
attrs map[string]string
|
||||
scopeAttrs map[string]string
|
||||
resourceAttrs map[string]string
|
||||
}
|
||||
|
||||
// Writer ingests decoded metric batches into the datastore over upstream
|
||||
// clickhouse-go v2. It is safe for concurrent use (clickhouse.Conn is).
|
||||
type Writer struct {
|
||||
conn clickhouse.Conn
|
||||
db string
|
||||
tsTable string
|
||||
samplesTable string
|
||||
nowMilli func() int64
|
||||
}
|
||||
|
||||
// Option configures a Writer.
|
||||
type Option func(*Writer)
|
||||
|
||||
// WithDatabase overrides the target datastore database (default: the query
|
||||
// plane's canonical metrics DB).
|
||||
func WithDatabase(db string) Option { return func(w *Writer) { w.db = db } }
|
||||
|
||||
// WithTables overrides the distributed time-series / samples table names.
|
||||
func WithTables(timeSeries, samples string) Option {
|
||||
return func(w *Writer) { w.tsTable, w.samplesTable = timeSeries, samples }
|
||||
}
|
||||
|
||||
// WithNow injects the inserted-at clock (tests).
|
||||
func WithNow(now func() int64) Option { return func(w *Writer) { w.nowMilli = now } }
|
||||
|
||||
// NewWriter builds a Writer over an existing datastore connection. Defaults
|
||||
// target the SAME database + distributed tables the query plane reads, so a
|
||||
// series written here is immediately queryable.
|
||||
func NewWriter(conn clickhouse.Conn, opts ...Option) *Writer {
|
||||
w := &Writer{
|
||||
conn: conn,
|
||||
db: telemetrymetrics.DBName,
|
||||
tsTable: telemetrymetrics.TimeseriesV4TableName,
|
||||
samplesTable: telemetrymetrics.SamplesV4TableName,
|
||||
nowMilli: func() int64 { return time.Now().UnixMilli() },
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(w)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// WriteMetrics is the zapmetricreceiver.Handler: it decomposes a batch into
|
||||
// series + samples and writes both tables. A nil / empty batch is a no-op.
|
||||
func (w *Writer) WriteMetrics(ctx context.Context, batch *zapmetricreceiver.MetricBatch) error {
|
||||
if batch == nil || len(batch.Families) == 0 {
|
||||
return nil
|
||||
}
|
||||
if batch.TimestampNs == 0 {
|
||||
batch.TimestampNs = w.nowMilli() * 1e6
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
if len(ts) == 0 && len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := w.writeTimeSeries(ctx, ts); err != nil {
|
||||
return fmt.Errorf("datastoremetrics: write time_series: %w", err)
|
||||
}
|
||||
if err := w.writeSamples(ctx, samples); err != nil {
|
||||
return fmt.Errorf("datastoremetrics: write samples: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildRows is the pure batch → rows transformation — all fingerprinting and
|
||||
// classic-shape decomposition, no IO. Kept free of the connection so it is
|
||||
// exhaustively unit-testable.
|
||||
func buildRows(batch *zapmetricreceiver.MetricBatch) (tsRows []tsRow, sampleRows []sampleRow) {
|
||||
resourceAttrs := normalizedResource(batch)
|
||||
env := resourceAttrs["deployment.environment"]
|
||||
|
||||
resourceHash := fingerprint(initialOffset, resourceAttrs)
|
||||
// The ZAP wire carries no instrumentation scope; the scope layer is empty,
|
||||
// so its hash is just the resource hash carried forward.
|
||||
scopeAttrs := map[string]string{}
|
||||
scopeHash := fingerprint(resourceHash, scopeAttrs)
|
||||
|
||||
unixMilli := batch.TimestampNs / 1e6
|
||||
tsUnixMilli := unixMilli / 3600000 * 3600000 // floor to the hour
|
||||
|
||||
seenTS := make(map[[2]uint64]bool)
|
||||
|
||||
emit := func(name, desc, unit, typ, temporality string, isMonotonic bool, baseLabels, extras map[string]string, value float64) {
|
||||
point := mergeAttrs(baseLabels, extras)
|
||||
fp := hashWithName(fingerprint(scopeHash, point), name)
|
||||
|
||||
sampleRows = append(sampleRows, sampleRow{
|
||||
env: env, temporality: temporality, metricName: name,
|
||||
fingerprint: fp, unixMilli: unixMilli, value: value,
|
||||
})
|
||||
|
||||
key := [2]uint64{fp, uint64(tsUnixMilli)}
|
||||
if seenTS[key] {
|
||||
return // same series already described this hour — one metadata row suffices
|
||||
}
|
||||
seenTS[key] = true
|
||||
tsRows = append(tsRows, tsRow{
|
||||
env: env, temporality: temporality, metricName: name, description: desc, unit: unit,
|
||||
typ: typ, isMonotonic: isMonotonic, fingerprint: fp, unixMilli: tsUnixMilli,
|
||||
labels: labelsJSON(name, point, scopeAttrs, resourceAttrs), attrs: point,
|
||||
scopeAttrs: scopeAttrs, resourceAttrs: resourceAttrs,
|
||||
})
|
||||
}
|
||||
|
||||
for i := range batch.Families {
|
||||
fam := &batch.Families[i]
|
||||
desc := fam.Help
|
||||
for j := range fam.Metrics {
|
||||
m := &fam.Metrics[j]
|
||||
labels := m.Labels
|
||||
switch fam.Type {
|
||||
case "counter":
|
||||
if m.Value == nil {
|
||||
continue
|
||||
}
|
||||
emit(fam.Name, desc, "", typeSum, temporalityCumulative, true,
|
||||
labels, tempExtras(temporalityCumulative), *m.Value)
|
||||
case "gauge":
|
||||
if m.Value == nil {
|
||||
continue
|
||||
}
|
||||
emit(fam.Name, desc, "", typeGauge, temporalityUnspecified, false,
|
||||
labels, tempExtras(temporalityUnspecified), *m.Value)
|
||||
case "histogram":
|
||||
emitHistogram(emit, fam.Name, desc, labels, m)
|
||||
case "summary":
|
||||
emitSummary(emit, fam.Name, desc, labels, m)
|
||||
default:
|
||||
// unknown family type: skip rather than write a malformed row
|
||||
}
|
||||
}
|
||||
}
|
||||
return tsRows, sampleRows
|
||||
}
|
||||
|
||||
// emitFunc is the closure buildRows hands to the per-type decomposers.
|
||||
type emitFunc func(name, desc, unit, typ, temporality string, isMonotonic bool, baseLabels, extras map[string]string, value float64)
|
||||
|
||||
// emitHistogram decomposes a classic histogram into `.count`, `.sum` and
|
||||
// cumulative `.bucket{le=…}` samples (including le=+Inf) — the exact series the
|
||||
// query plane's histogram_quantile expects.
|
||||
func emitHistogram(emit emitFunc, name, desc string, labels map[string]string, m *zapmetricreceiver.Metric) {
|
||||
if m.SampleCount != nil {
|
||||
emit(name+suffixCount, desc, unitCount, typeSum, temporalityCumulative, true,
|
||||
labels, tempExtras(temporalityCumulative), float64(*m.SampleCount))
|
||||
}
|
||||
if m.SampleSum != nil {
|
||||
emit(name+suffixSum, desc, "", typeSum, temporalityCumulative, true,
|
||||
labels, tempExtras(temporalityCumulative), *m.SampleSum)
|
||||
}
|
||||
var lastCumulative float64
|
||||
for _, b := range m.Buckets {
|
||||
lastCumulative = float64(b.CumulativeCount)
|
||||
emit(name+suffixBucket, desc, "", typeHistogram, temporalityCumulative, true,
|
||||
labels, leExtras(strconv.FormatFloat(b.UpperBound, 'f', -1, 64), temporalityCumulative),
|
||||
float64(b.CumulativeCount))
|
||||
}
|
||||
// le=+Inf carries the total count. Prefer SampleCount; fall back to the last
|
||||
// cumulative bucket when the sender omitted it.
|
||||
infValue := lastCumulative
|
||||
if m.SampleCount != nil {
|
||||
infValue = float64(*m.SampleCount)
|
||||
} else if len(m.Buckets) == 0 {
|
||||
return // nothing to anchor +Inf to
|
||||
}
|
||||
emit(name+suffixBucket, desc, "", typeHistogram, temporalityCumulative, true,
|
||||
labels, leExtras("+Inf", temporalityCumulative), infValue)
|
||||
}
|
||||
|
||||
// emitSummary decomposes a summary into `.count`, `.sum` and
|
||||
// `.quantile{quantile=…}` samples.
|
||||
func emitSummary(emit emitFunc, name, desc string, labels map[string]string, m *zapmetricreceiver.Metric) {
|
||||
if m.SampleCount != nil {
|
||||
emit(name+suffixCount, desc, unitCount, typeSum, temporalityCumulative, true,
|
||||
labels, tempExtras(temporalityCumulative), float64(*m.SampleCount))
|
||||
}
|
||||
if m.SampleSum != nil {
|
||||
emit(name+suffixSum, desc, "", typeSum, temporalityCumulative, true,
|
||||
labels, tempExtras(temporalityCumulative), *m.SampleSum)
|
||||
}
|
||||
for _, q := range m.Quantiles {
|
||||
emit(name+suffixQuantile, desc, "", typeSummary, temporalityCumulative, true,
|
||||
labels, quantileExtras(strconv.FormatFloat(q.Quantile, 'f', -1, 64), temporalityCumulative),
|
||||
q.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// normalizedResource copies the batch resource map and lifts AppName / Version
|
||||
// into the OTLP service.* attributes when absent, so dashboards can group by
|
||||
// service.name.
|
||||
func normalizedResource(batch *zapmetricreceiver.MetricBatch) map[string]string {
|
||||
r := make(map[string]string, len(batch.Resource)+2)
|
||||
for k, v := range batch.Resource {
|
||||
r[k] = v
|
||||
}
|
||||
if batch.AppName != "" {
|
||||
if _, ok := r["service.name"]; !ok {
|
||||
r["service.name"] = batch.AppName
|
||||
}
|
||||
}
|
||||
if batch.Version != "" {
|
||||
if _, ok := r["service.version"]; !ok {
|
||||
r["service.version"] = batch.Version
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func tempExtras(t string) map[string]string { return map[string]string{"__temporality__": t} }
|
||||
|
||||
func leExtras(le, t string) map[string]string {
|
||||
return map[string]string{"le": le, "__temporality__": t}
|
||||
}
|
||||
|
||||
func quantileExtras(q, t string) map[string]string {
|
||||
return map[string]string{"quantile": q, "__temporality__": t}
|
||||
}
|
||||
|
||||
func (w *Writer) writeTimeSeries(ctx context.Context, rows []tsRow) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
stmt, err := w.conn.PrepareBatch(ctx,
|
||||
fmt.Sprintf(timeSeriesSQLTmpl, w.db, w.tsTable), driver.WithReleaseConnection())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
now := w.nowMilli()
|
||||
for _, r := range rows {
|
||||
if err := stmt.Append(
|
||||
r.env, r.temporality, r.metricName, r.description, r.unit, r.typ,
|
||||
r.isMonotonic, r.fingerprint, r.unixMilli, r.labels,
|
||||
r.attrs, r.scopeAttrs, r.resourceAttrs, false, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return stmt.Send()
|
||||
}
|
||||
|
||||
func (w *Writer) writeSamples(ctx context.Context, rows []sampleRow) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
stmt, err := w.conn.PrepareBatch(ctx,
|
||||
fmt.Sprintf(samplesSQLTmpl, w.db, w.samplesTable), driver.WithReleaseConnection())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
now := w.nowMilli()
|
||||
for _, r := range rows {
|
||||
if err := stmt.Append(
|
||||
r.env, r.temporality, r.metricName, r.fingerprint, r.unixMilli, r.value, r.flags, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return stmt.Send()
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (C) 2025-2026, Hanzo AI Inc. All rights reserved.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package datastoremetrics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/zapmetricreceiver"
|
||||
)
|
||||
|
||||
func f64(v float64) *float64 { return &v }
|
||||
func u64(v uint64) *uint64 { return &v }
|
||||
|
||||
// refFNV is an independent, byte-stream expression of FNV-1a (the ground-truth
|
||||
// definition) used to cross-check the fingerprint primitive without importing
|
||||
// the forked internal package.
|
||||
func refFNV(seed uint64, data []byte) uint64 {
|
||||
const prime = 1099511628211
|
||||
h := seed
|
||||
for _, b := range data {
|
||||
h ^= uint64(b)
|
||||
h *= prime
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func TestFingerprintMatchesFNVReference(t *testing.T) {
|
||||
attrs := map[string]string{"b": "2", "a": "1"}
|
||||
// sorted key order: a,b — stream is key 0xFF value 0xFF per pair.
|
||||
stream := []byte("a")
|
||||
stream = append(stream, 255)
|
||||
stream = append(stream, []byte("1")...)
|
||||
stream = append(stream, 255)
|
||||
stream = append(stream, []byte("b")...)
|
||||
stream = append(stream, 255)
|
||||
stream = append(stream, []byte("2")...)
|
||||
stream = append(stream, 255)
|
||||
|
||||
got := fingerprint(initialOffset, attrs)
|
||||
want := refFNV(initialOffset, stream)
|
||||
if got != want {
|
||||
t.Fatalf("fingerprint=%d want=%d", got, want)
|
||||
}
|
||||
|
||||
// hashWithName appends __name__ 0xFF name (no trailing separator).
|
||||
name := "http_requests"
|
||||
nameStream := append([]byte("__name__"), 255)
|
||||
nameStream = append(nameStream, []byte(name)...)
|
||||
if hashWithName(got, name) != refFNV(got, nameStream) {
|
||||
t.Fatalf("hashWithName mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintOffsetPassthroughAndOrderIndependent(t *testing.T) {
|
||||
if fingerprint(initialOffset, map[string]string{}) != initialOffset {
|
||||
t.Fatalf("empty attrs must pass the offset through unchanged")
|
||||
}
|
||||
a := fingerprint(initialOffset, map[string]string{"x": "1", "y": "2", "z": "3"})
|
||||
b := fingerprint(initialOffset, map[string]string{"z": "3", "y": "2", "x": "1"})
|
||||
if a != b {
|
||||
t.Fatalf("fingerprint must be independent of map order: %d != %d", a, b)
|
||||
}
|
||||
if fingerprint(initialOffset, map[string]string{"x": "1"}) == fingerprint(initialOffset, map[string]string{"x": "2"}) {
|
||||
t.Fatalf("distinct values must hash differently")
|
||||
}
|
||||
}
|
||||
|
||||
func indexTS(rows []tsRow) map[uint64]tsRow {
|
||||
m := make(map[uint64]tsRow, len(rows))
|
||||
for _, r := range rows {
|
||||
m[r.fingerprint] = r
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestBuildRowsCounter(t *testing.T) {
|
||||
batch := &zapmetricreceiver.MetricBatch{
|
||||
AppName: "gateway",
|
||||
Version: "1.2.3",
|
||||
Resource: map[string]string{"deployment.environment": "prod"},
|
||||
TimestampNs: 1_700_000_000_000 * 1e6,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "http_requests_total", Help: "total reqs", Type: "counter",
|
||||
Metrics: []zapmetricreceiver.Metric{{
|
||||
Labels: map[string]string{"method": "GET"}, Value: f64(42),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
if len(samples) != 1 || len(ts) != 1 {
|
||||
t.Fatalf("counter: got %d samples %d ts, want 1/1", len(samples), len(ts))
|
||||
}
|
||||
s := samples[0]
|
||||
if s.metricName != "http_requests_total" || s.value != 42 || s.temporality != temporalityCumulative {
|
||||
t.Fatalf("counter sample wrong: %+v", s)
|
||||
}
|
||||
if s.env != "prod" {
|
||||
t.Fatalf("env must come from deployment.environment: %q", s.env)
|
||||
}
|
||||
tr := ts[0]
|
||||
if tr.fingerprint != s.fingerprint {
|
||||
t.Fatalf("join key broken: ts fp %d != sample fp %d", tr.fingerprint, s.fingerprint)
|
||||
}
|
||||
if tr.typ != typeSum || !tr.isMonotonic {
|
||||
t.Fatalf("counter must be monotonic Sum: %+v", tr)
|
||||
}
|
||||
if tr.resourceAttrs["service.name"] != "gateway" || tr.resourceAttrs["service.version"] != "1.2.3" {
|
||||
t.Fatalf("AppName/Version must lift into service.*: %+v", tr.resourceAttrs)
|
||||
}
|
||||
for _, sub := range []string{`"__name__":"http_requests_total"`, `"method":"GET"`, `"service.name":"gateway"`, `"__temporality__":"Cumulative"`} {
|
||||
if !strings.Contains(tr.labels, sub) {
|
||||
t.Fatalf("labels %q missing %q", tr.labels, sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRowsGauge(t *testing.T) {
|
||||
batch := &zapmetricreceiver.MetricBatch{
|
||||
TimestampNs: 1_700_000_000_000 * 1e6,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "queue_depth", Type: "gauge",
|
||||
Metrics: []zapmetricreceiver.Metric{{Value: f64(7)}},
|
||||
}},
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
if len(samples) != 1 || samples[0].temporality != temporalityUnspecified {
|
||||
t.Fatalf("gauge temporality must be Unspecified: %+v", samples)
|
||||
}
|
||||
if ts[0].typ != typeGauge || ts[0].isMonotonic {
|
||||
t.Fatalf("gauge must be non-monotonic Gauge: %+v", ts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRowsHistogram(t *testing.T) {
|
||||
batch := &zapmetricreceiver.MetricBatch{
|
||||
TimestampNs: 1_700_000_000_000 * 1e6,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "req_duration", Type: "histogram",
|
||||
Metrics: []zapmetricreceiver.Metric{{
|
||||
Labels: map[string]string{"route": "/v1/chat"},
|
||||
SampleCount: u64(10), SampleSum: f64(3.5),
|
||||
Buckets: []zapmetricreceiver.Bucket{
|
||||
{UpperBound: 0.1, CumulativeCount: 3},
|
||||
{UpperBound: 0.5, CumulativeCount: 8},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
tsByFP := indexTS(ts)
|
||||
|
||||
// Expect .count, .sum, and 3 .bucket samples (0.1, 0.5, +Inf).
|
||||
var count, sum int
|
||||
les := map[string]float64{} // le -> sample value
|
||||
for _, s := range samples {
|
||||
tr := tsByFP[s.fingerprint]
|
||||
switch s.metricName {
|
||||
case "req_duration.count":
|
||||
count++
|
||||
if s.value != 10 {
|
||||
t.Fatalf(".count value=%v want 10", s.value)
|
||||
}
|
||||
if tr.unit != unitCount || tr.typ != typeSum {
|
||||
t.Fatalf(".count series wrong: %+v", tr)
|
||||
}
|
||||
case "req_duration.sum":
|
||||
sum++
|
||||
if s.value != 3.5 {
|
||||
t.Fatalf(".sum value=%v want 3.5", s.value)
|
||||
}
|
||||
case "req_duration.bucket":
|
||||
if tr.typ != typeHistogram {
|
||||
t.Fatalf(".bucket type must be Histogram: %+v", tr)
|
||||
}
|
||||
les[tr.attrs["le"]] = s.value
|
||||
default:
|
||||
t.Fatalf("unexpected series %q", s.metricName)
|
||||
}
|
||||
}
|
||||
if count != 1 || sum != 1 {
|
||||
t.Fatalf("want one .count and one .sum, got %d/%d", count, sum)
|
||||
}
|
||||
if les["0.1"] != 3 || les["0.5"] != 8 || les["+Inf"] != 10 {
|
||||
t.Fatalf("cumulative bucket values wrong: %+v", les)
|
||||
}
|
||||
// Every bucket le must produce a distinct fingerprint.
|
||||
seen := map[uint64]bool{}
|
||||
for _, s := range samples {
|
||||
if s.metricName == "req_duration.bucket" {
|
||||
if seen[s.fingerprint] {
|
||||
t.Fatalf("duplicate bucket fingerprint %d", s.fingerprint)
|
||||
}
|
||||
seen[s.fingerprint] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRowsSummary(t *testing.T) {
|
||||
batch := &zapmetricreceiver.MetricBatch{
|
||||
TimestampNs: 1_700_000_000_000 * 1e6,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "rpc_latency", Type: "summary",
|
||||
Metrics: []zapmetricreceiver.Metric{{
|
||||
SampleCount: u64(5), SampleSum: f64(1.2),
|
||||
Quantiles: []zapmetricreceiver.Quantile{
|
||||
{Quantile: 0.5, Value: 0.2},
|
||||
{Quantile: 0.99, Value: 0.9},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
tsByFP := indexTS(ts)
|
||||
quantiles := map[string]float64{}
|
||||
for _, s := range samples {
|
||||
tr := tsByFP[s.fingerprint]
|
||||
if s.metricName == "rpc_latency.quantile" {
|
||||
if tr.typ != typeSummary {
|
||||
t.Fatalf(".quantile type must be Summary: %+v", tr)
|
||||
}
|
||||
quantiles[tr.attrs["quantile"]] = s.value
|
||||
}
|
||||
}
|
||||
if quantiles["0.5"] != 0.2 || quantiles["0.99"] != 0.9 {
|
||||
t.Fatalf("quantile samples wrong: %+v", quantiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeSeriesHourFloor(t *testing.T) {
|
||||
// 1_700_000_123_456 ms is mid-hour; series unix_milli must floor to the hour.
|
||||
batch := &zapmetricreceiver.MetricBatch{
|
||||
TimestampNs: 1_700_000_123_456 * 1e6,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "c", Type: "counter",
|
||||
Metrics: []zapmetricreceiver.Metric{{Value: f64(1)}},
|
||||
}},
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
if samples[0].unixMilli != 1_700_000_123_456 {
|
||||
t.Fatalf("sample keeps exact ms: %d", samples[0].unixMilli)
|
||||
}
|
||||
if ts[0].unixMilli != 1_700_000_123_456/3600000*3600000 {
|
||||
t.Fatalf("series must floor to hour: %d", ts[0].unixMilli)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilValueSkipped(t *testing.T) {
|
||||
batch := &zapmetricreceiver.MetricBatch{
|
||||
TimestampNs: 1,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "c", Type: "counter",
|
||||
Metrics: []zapmetricreceiver.Metric{{Value: nil}},
|
||||
}},
|
||||
}
|
||||
ts, samples := buildRows(batch)
|
||||
if len(ts) != 0 || len(samples) != 0 {
|
||||
t.Fatalf("nil counter value must be skipped, got %d/%d", len(ts), len(samples))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeterministicAcrossCalls(t *testing.T) {
|
||||
mk := func() *zapmetricreceiver.MetricBatch {
|
||||
return &zapmetricreceiver.MetricBatch{
|
||||
AppName: "svc", TimestampNs: 1_700_000_000_000 * 1e6,
|
||||
Families: []zapmetricreceiver.MetricFamily{{
|
||||
Name: "m", Type: "counter",
|
||||
Metrics: []zapmetricreceiver.Metric{{
|
||||
Labels: map[string]string{"a": "1", "b": "2", "c": "3"}, Value: f64(1),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
_, s1 := buildRows(mk())
|
||||
_, s2 := buildRows(mk())
|
||||
if s1[0].fingerprint != s2[0].fingerprint {
|
||||
t.Fatalf("fingerprints must be stable across calls: %d != %d", s1[0].fingerprint, s2[0].fingerprint)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,13 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
_ "modernc.org/sqlite"
|
||||
// The "sqlite" database/sql driver is registered exactly once by the sqlstore
|
||||
// provider (pkg/sqlstore/sqlitesqlstore, which this package transitively
|
||||
// imports and which must import modernc for the *sqlite.Error type). A second
|
||||
// blank _ "modernc.org/sqlite" here was redundant, and re-pointing it at
|
||||
// github.com/hanzoai/sqlite would double-register "sqlite" under CGO_ENABLED=1
|
||||
// (the fork's cgo backend Register()s mattn while modernc's init Register()s
|
||||
// modernc) — the exact "Register called twice" panic. One registration site.
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/contextlinks"
|
||||
traceFunnelsModule "github.com/hanzoai/o11y/pkg/modules/tracefunnel"
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/errors"
|
||||
"github.com/hanzoai/o11y/pkg/sqlstore"
|
||||
"github.com/hanzoai/o11y/pkg/types/opamptypes"
|
||||
@@ -17,6 +15,7 @@ import (
|
||||
|
||||
"github.com/open-telemetry/opamp-go/protobufs"
|
||||
opampTypes "github.com/open-telemetry/opamp-go/server/types"
|
||||
"github.com/zap-proto/zap2pb"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
@@ -125,7 +124,7 @@ func (agent *Agent) agentDescriptionChanged(newStatus *protobufs.AgentToServer)
|
||||
if newStatus.AgentDescription == nil {
|
||||
return false
|
||||
}
|
||||
if proto.Equal(agent.Status.AgentDescription, newStatus.AgentDescription) {
|
||||
if zap2pb.Equal(agent.Status.AgentDescription, newStatus.AgentDescription) {
|
||||
return false
|
||||
}
|
||||
agent.CanLB = ExtractLbFlag(newStatus.AgentDescription)
|
||||
@@ -136,7 +135,7 @@ func (agent *Agent) agentDescriptionChanged(newStatus *protobufs.AgentToServer)
|
||||
// subscribers if the status has changed relative to what we have stored.
|
||||
func (agent *Agent) updateRemoteConfigStatus(newStatus *protobufs.AgentToServer) {
|
||||
if newStatus.RemoteConfigStatus == nil ||
|
||||
proto.Equal(agent.Status.RemoteConfigStatus, newStatus.RemoteConfigStatus) {
|
||||
zap2pb.Equal(agent.Status.RemoteConfigStatus, newStatus.RemoteConfigStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package sqlitesqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/factory/factorytest"
|
||||
"github.com/hanzoai/o11y/pkg/sqlstore"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDriverRegisteredOnce proves the "sqlite" database/sql driver is registered
|
||||
// exactly once — via this provider's modernc import — and opens cleanly. It is the
|
||||
// guard for removing the now-redundant blank _ "modernc.org/sqlite" from
|
||||
// pkg/query-service/app/http_handler.go: registration must survive that removal,
|
||||
// and pointing that blank import at github.com/hanzoai/sqlite instead would
|
||||
// double-register "sqlite" under CGO_ENABLED=1 (the fork's cgo backend Register()s
|
||||
// mattn while modernc's init Register()s modernc) and panic. Opening the provider
|
||||
// here — under whatever CGO mode the test runs — fails loudly if either regression
|
||||
// returns. It also asserts the DSN pragmas take effect (journal_mode=wal,
|
||||
// busy_timeout>0) through the driver.
|
||||
func TestDriverRegisteredOnce(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "o11y.db")
|
||||
store, err := New(context.Background(), factorytest.NewSettings(), sqlstore.Config{
|
||||
Provider: "sqlite",
|
||||
Connection: sqlstore.ConnectionConfig{
|
||||
MaxOpenConns: 1,
|
||||
MaxConnLifetime: 0,
|
||||
},
|
||||
Sqlite: sqlstore.SqliteConfig{
|
||||
Path: dbPath,
|
||||
Mode: "wal",
|
||||
BusyTimeout: 5 * time.Second,
|
||||
TransactionMode: "deferred",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sqldb := store.SQLDB()
|
||||
require.NoError(t, sqldb.Ping())
|
||||
|
||||
var journalMode string
|
||||
require.NoError(t, sqldb.QueryRow("PRAGMA journal_mode").Scan(&journalMode))
|
||||
require.Equal(t, "wal", journalMode, "journal_mode not applied — modernc _pragma DSN form not honored")
|
||||
|
||||
var busyTimeout int
|
||||
require.NoError(t, sqldb.QueryRow("PRAGMA busy_timeout").Scan(&busyTimeout))
|
||||
require.Greater(t, busyTimeout, 0, "busy_timeout not applied")
|
||||
|
||||
t.Logf("journal_mode=%s busy_timeout=%d", journalMode, busyTimeout)
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package sqlitesqlstore
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/hanzoai/o11y/pkg/errors"
|
||||
"github.com/hanzoai/o11y/pkg/factory"
|
||||
@@ -13,8 +13,12 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
|
||||
"modernc.org/sqlite"
|
||||
sqlite3 "modernc.org/sqlite/lib"
|
||||
// hanzoai/sqlite is the one dual-backend driver: importing it registers the
|
||||
// "sqlite" database/sql driver (mattn/SQLCipher under cgo, modernc pure-Go
|
||||
// otherwise) AND exposes backend-neutral constraint-error classification, so
|
||||
// this package no longer imports modernc directly (which would double-register
|
||||
// "sqlite" in the cgo cloud binary).
|
||||
"github.com/hanzoai/sqlite"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
@@ -44,13 +48,20 @@ func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook,
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlstore.Config, hooks ...sqlstore.SQLStoreHook) (sqlstore.SQLStore, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/hanzoai/o11y/pkg/sqlitesqlstore")
|
||||
|
||||
connectionParams := url.Values{}
|
||||
// do not update the order of the connection params as busy_timeout doesn't work if it's not the first parameter
|
||||
connectionParams.Add("_pragma", fmt.Sprintf("busy_timeout(%d)", config.Sqlite.BusyTimeout.Milliseconds()))
|
||||
connectionParams.Add("_pragma", fmt.Sprintf("journal_mode(%s)", config.Sqlite.Mode))
|
||||
connectionParams.Add("_pragma", "foreign_keys(1)")
|
||||
connectionParams.Set("_txlock", config.Sqlite.TransactionMode)
|
||||
sqldb, err := sql.Open("sqlite", "file:"+config.Sqlite.Path+"?"+connectionParams.Encode())
|
||||
// Build the DSN with backend-correct pragma syntax. hanzoai/sqlite.PragmaDSN
|
||||
// emits `_pragma=journal_mode(wal)` under the modernc backend and
|
||||
// `_journal_mode=wal` under the mattn/SQLCipher backend, so the pragmas
|
||||
// actually apply whichever backend the binary links — this package is built
|
||||
// CGO=0 standalone but CGO=1 inside the cloud binary, and a single hardcoded
|
||||
// syntax is silently dropped by the other backend. busy_timeout MUST lead
|
||||
// (WAL cannot be enabled while another connection holds the db). _txlock is a
|
||||
// driver connection param (not a pragma) that both backends honor; append it.
|
||||
dsn := sqlite.PragmaDSN(config.Sqlite.Path, []sqlite.Pragma{
|
||||
{Name: "busy_timeout", Value: strconv.FormatInt(config.Sqlite.BusyTimeout.Milliseconds(), 10)},
|
||||
{Name: "journal_mode", Value: config.Sqlite.Mode},
|
||||
{Name: "foreign_keys", Value: "1"},
|
||||
}) + "&_txlock=" + url.QueryEscape(config.Sqlite.TransactionMode)
|
||||
sqldb, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -102,10 +113,8 @@ func (provider *provider) WrapNotFoundErrf(err error, code errors.Code, format s
|
||||
}
|
||||
|
||||
func (provider *provider) WrapAlreadyExistsErrf(err error, code errors.Code, format string, args ...any) error {
|
||||
if sqlite3Err, ok := err.(*sqlite.Error); ok {
|
||||
if sqlite3Err.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE || sqlite3Err.Code() == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY || sqlite3Err.Code() == sqlite3.SQLITE_CONSTRAINT_FOREIGNKEY {
|
||||
return errors.Wrapf(err, errors.TypeAlreadyExists, code, format, args...)
|
||||
}
|
||||
if sqlite.IsConstraintUnique(err) || sqlite.IsConstraintPrimaryKey(err) || sqlite.IsConstraintForeignKey(err) {
|
||||
return errors.Wrapf(err, errors.TypeAlreadyExists, code, format, args...)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user