diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 59a8a8297e3..b9aab6f75b2 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -41,8 +41,10 @@ const ( optionCORSAllowedOrigins = "cors-allowed-origins" optionNameTracingEnabled = "tracing-enable" optionNameTracingEndpoint = "tracing-endpoint" - optionNameTracingHost = "tracing-host" - optionNameTracingPort = "tracing-port" + optionNameTracingInsecure = "tracing-insecure" + optionNameTracingCAFile = "tracing-ca-file" + optionNameTracingProtocol = "tracing-protocol" + optionNameTracingSamplingRatio = "tracing-sampling-ratio" optionNameTracingServiceName = "tracing-service-name" optionNameVerbosity = "verbosity" optionNamePaymentThreshold = "payment-threshold" @@ -104,6 +106,15 @@ const ( configKeyBlockchainRpcTLSTimeout = "blockchain-rpc.tls-timeout" configKeyBlockchainRpcIdleTimeout = "blockchain-rpc.idle-timeout" configKeyBlockchainRpcKeepalive = "blockchain-rpc.keepalive" + + // tracing + configKeyTracingEnabled = "tracing.enable" + configKeyTracingEndpoint = "tracing.endpoint" + configKeyTracingInsecure = "tracing.insecure" + configKeyTracingCAFile = "tracing.ca-file" + configKeyTracingProtocol = "tracing.protocol" + configKeyTracingSamplingRatio = "tracing.sampling-ratio" + configKeyTracingServiceName = "tracing.service-name" ) var blockchainRpcConfigPairs = []struct{ flat, dotted string }{ @@ -114,11 +125,44 @@ var blockchainRpcConfigPairs = []struct{ flat, dotted string }{ {optionNameBlockchainRpcKeepalive, configKeyBlockchainRpcKeepalive}, } +var tracingConfigPairs = []struct{ flat, dotted string }{ + {optionNameTracingEnabled, configKeyTracingEnabled}, + {optionNameTracingEndpoint, configKeyTracingEndpoint}, + {optionNameTracingInsecure, configKeyTracingInsecure}, + {optionNameTracingCAFile, configKeyTracingCAFile}, + {optionNameTracingProtocol, configKeyTracingProtocol}, + {optionNameTracingSamplingRatio, configKeyTracingSamplingRatio}, + {optionNameTracingServiceName, configKeyTracingServiceName}, +} + +// Deprecated tracing options, removed in the OpenTelemetry migration. They are +// kept only as hidden no-op flags so that existing configs do not break node +// startup; their values are ignored (see deprecatedTracingKeys). +// +// Note: tracing-endpoint is intentionally NOT here — it is reused as the active +// OTLP endpoint flag (see optionNameTracingEndpoint). Its meaning changed from a +// Jaeger agent address to an OTLP collector endpoint. +const ( + optionNameTracingHost = "tracing-host" + optionNameTracingPort = "tracing-port" +) + +// deprecatedTracingKeys are the pre-OpenTelemetry tracing options. They are +// registered as hidden no-op flags so stale configs still start the node, but +// their values are ignored; operators should migrate to the tracing-* keys. +var deprecatedTracingKeys = []string{ + optionNameTracingHost, + optionNameTracingPort, +} + var knownNestedKeys = func() map[string]bool { - m := make(map[string]bool, len(blockchainRpcConfigPairs)) + m := make(map[string]bool, len(blockchainRpcConfigPairs)+len(tracingConfigPairs)) for _, p := range blockchainRpcConfigPairs { m[p.dotted] = true } + for _, p := range tracingConfigPairs { + m[p.dotted] = true + } return m }() @@ -285,10 +329,18 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().Uint64(optionNameNetworkID, chaincfg.Mainnet.NetworkID, "ID of the Swarm network") cmd.Flags().StringSlice(optionCORSAllowedOrigins, []string{}, "origins with CORS headers enabled") cmd.Flags().Bool(optionNameTracingEnabled, false, "enable tracing") - cmd.Flags().String(optionNameTracingEndpoint, "127.0.0.1:6831", "endpoint to send tracing data") - cmd.Flags().String(optionNameTracingHost, "", "host to send tracing data") - cmd.Flags().String(optionNameTracingPort, "", "port to send tracing data") + cmd.Flags().String(optionNameTracingEndpoint, "127.0.0.1:4318", "OTLP collector endpoint to send tracing data (host:port); default port is 4318 for http, 4317 for grpc") + cmd.Flags().Bool(optionNameTracingInsecure, false, "disable TLS for the OTLP exporter (useful for a local collector); when false, set --tracing-ca-file to verify the collector certificate against a private CA") + cmd.Flags().String(optionNameTracingCAFile, "", "path to a PEM-encoded CA bundle used to verify the OTLP collector certificate; ignored when --tracing-insecure=true") + cmd.Flags().String(optionNameTracingProtocol, "http", "OTLP exporter transport: http or grpc") + cmd.Flags().Float64(optionNameTracingSamplingRatio, 1.0, "head-based sampling ratio in [0,1]; 0 samples nothing, 1 samples everything") cmd.Flags().String(optionNameTracingServiceName, "bee", "service name identifier for tracing") + // Deprecated, no-op tracing flags kept for backward compatibility so that + // existing configs do not break node startup. Their values are ignored. + for _, name := range deprecatedTracingKeys { + cmd.Flags().String(name, "", "deprecated and ignored; use the tracing-* options") + _ = cmd.Flags().MarkDeprecated(name, "no longer used after the OpenTelemetry migration; use the tracing-* options") + } cmd.Flags().String(optionNameVerbosity, "info", "log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace") cmd.Flags().String(optionWelcomeMessage, "", "send a welcome message string during handshakes") cmd.Flags().String(optionNamePaymentThreshold, "13500000", "threshold in BZZ where you expect to get paid from your peers") @@ -372,7 +424,39 @@ func (c *command) initLogger(cmd *cobra.Command) error { // bindBlockchainRpcConfig supports both flat (blockchain-rpc-endpoint) and // nested (blockchain-rpc.endpoint) YAML forms, with nested taking precedence. func (c *command) bindBlockchainRpcConfig(cmd *cobra.Command) { - for _, p := range blockchainRpcConfigPairs { + c.bindNestedConfig(cmd, blockchainRpcConfigPairs) +} + +// bindTracingConfig supports both flat (tracing-endpoint) and nested +// (tracing.endpoint) YAML forms, with nested taking precedence. +func (c *command) bindTracingConfig(cmd *cobra.Command) { + c.bindNestedConfig(cmd, tracingConfigPairs) +} + +// warnDeprecatedTracingKeys logs a warning for each pre-OpenTelemetry tracing +// key found in the config file. The keys are accepted to keep stale configs +// bootable, but their values are ignored. Keys passed on the command line are +// already handled by cobra's deprecation notice. +func (c *command) warnDeprecatedTracingKeys() { + for _, key := range deprecatedTracingKeys { + if c.config.InConfig(key) { + c.logger.Warning("deprecated tracing config key is ignored; use the tracing-* options", "key", key) + } + } +} + +// warnTracingTLSWithoutCA warns when tracing runs over TLS without an explicit +// CA bundle, in which case the OTLP exporter falls back to the system root CAs. +func (c *command) warnTracingTLSWithoutCA() { + if c.config.GetBool(configKeyTracingEnabled) && + !c.config.GetBool(configKeyTracingInsecure) && + c.config.GetString(configKeyTracingCAFile) == "" { + c.logger.Warning("tracing: TLS is enabled but no CA bundle is configured; the OTLP exporter will rely on the system root CAs. Provide --tracing-ca-file, set --tracing-insecure=true for a plaintext local collector, or disable tracing.") + } +} + +func (c *command) bindNestedConfig(cmd *cobra.Command, pairs []struct{ flat, dotted string }) { + for _, p := range pairs { nested := c.config.Get(p.dotted) conflict := c.config.InConfig(p.flat) && c.config.IsSet(p.dotted) _ = c.config.BindPFlag(p.dotted, cmd.Flags().Lookup(p.flat)) diff --git a/cmd/bee/cmd/deprecated_tracing_test.go b/cmd/bee/cmd/deprecated_tracing_test.go new file mode 100644 index 00000000000..2ddda4647a9 --- /dev/null +++ b/cmd/bee/cmd/deprecated_tracing_test.go @@ -0,0 +1,68 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd_test + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestDeprecatedTracingKeysAccepted verifies that the pre-OpenTelemetry tracing +// keys are tolerated in a config file (so stale configs still boot) while a +// genuinely unknown key is still rejected. +func TestDeprecatedTracingKeysAccepted(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + config string + wantError string + }{ + { + name: "deprecated tracing keys accepted", + config: "tracing-enable: false\n" + + "tracing-endpoint: my-jaeger:6831\n" + + "tracing-host: \"\"\n" + + "tracing-port: \"\"\n", + }, + { + name: "unknown key rejected", + config: "totally-made-up-key: 1\n", + wantError: "totally-made-up-key", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cfgFile := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(cfgFile, []byte(tc.config), 0o644); err != nil { + t.Fatal(err) + } + + c := newCommand(t) + c.SetCfgFileForTest(cfgFile) + startCmd := c.SubCommandForTest("start") + if startCmd == nil { + t.Fatal("start subcommand not found") + } + + err := c.CheckUnknownParamsForTest(startCmd) + if tc.wantError == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantError) + } + if !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("expected error containing %q, got %v", tc.wantError, err) + } + }) + } +} diff --git a/cmd/bee/cmd/export_test.go b/cmd/bee/cmd/export_test.go index fb5d79cb894..917851c925b 100644 --- a/cmd/bee/cmd/export_test.go +++ b/cmd/bee/cmd/export_test.go @@ -4,7 +4,11 @@ package cmd -import "io" +import ( + "io" + + "github.com/spf13/cobra" +) type ( Command = command @@ -12,6 +16,21 @@ type ( PasswordReader = passwordReader ) +// SubCommandForTest returns the registered subcommand with the given name. +func (c *Command) SubCommandForTest(name string) *cobra.Command { + for _, sc := range c.root.Commands() { + if sc.Name() == name { + return sc + } + } + return nil +} + +// CheckUnknownParamsForTest exposes the unknown-parameter validation. +func (c *Command) CheckUnknownParamsForTest(cmd *cobra.Command) error { + return c.CheckUnknownParams(cmd, nil) +} + var ( NewCommand = newCommand @@ -63,3 +82,10 @@ func WithPasswordReader(r PasswordReader) func(c *Command) { c.passwordReader = r } } + +// SetCfgFileForTest sets the config file path after command construction. It +// must be used instead of WithCfgFile because the global "config" flag's +// StringVar default resets c.cfgFile during flag registration. +func (c *Command) SetCfgFileForTest(f string) { + c.cfgFile = f +} diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index a423c6bbcaf..8e7b9e0356c 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -16,7 +16,6 @@ import ( "os/signal" "path/filepath" "runtime" - "strings" "sync/atomic" "syscall" "time" @@ -164,6 +163,9 @@ func (c *command) initStartCmd() (err error) { return err } c.bindBlockchainRpcConfig(cmd) + c.bindTracingConfig(cmd) + c.warnDeprecatedTracingKeys() + c.warnTracingTLSWithoutCA() return nil }, } @@ -246,12 +248,6 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo networkConfig.blockTime = time.Duration(blockTime) * time.Second } - tracingEndpoint := c.config.GetString(optionNameTracingEndpoint) - - if c.config.IsSet(optionNameTracingHost) && c.config.IsSet(optionNameTracingPort) { - tracingEndpoint = strings.Join([]string{c.config.GetString(optionNameTracingHost), c.config.GetString(optionNameTracingPort)}, ":") - } - staticNodesOpt := c.config.GetStringSlice(optionNameStaticNodes) staticNodes := make([]swarm.Address, 0, len(staticNodesOpt)) for _, p := range staticNodesOpt { @@ -356,9 +352,13 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo SwapFactoryAddress: c.config.GetString(optionNameSwapFactoryAddress), SwapInitialDeposit: c.config.GetString(optionNameSwapInitialDeposit), TargetNeighborhood: c.config.GetString(optionNameTargetNeighborhood), - TracingEnabled: c.config.GetBool(optionNameTracingEnabled), - TracingEndpoint: tracingEndpoint, - TracingServiceName: c.config.GetString(optionNameTracingServiceName), + TracingEnabled: c.config.GetBool(configKeyTracingEnabled), + TracingEndpoint: c.config.GetString(configKeyTracingEndpoint), + TracingInsecure: c.config.GetBool(configKeyTracingInsecure), + TracingCAFile: c.config.GetString(configKeyTracingCAFile), + TracingProtocol: c.config.GetString(configKeyTracingProtocol), + TracingSamplingRatio: c.config.GetFloat64(configKeyTracingSamplingRatio), + TracingServiceName: c.config.GetString(configKeyTracingServiceName), TrxDebugMode: c.config.GetBool(optionNameTransactionDebugMode), WarmupTime: c.config.GetDuration(optionWarmUpTime), WelcomeMessage: c.config.GetString(optionWelcomeMessage), diff --git a/go.mod b/go.mod index 80b5129f2ac..f5bc7086b89 100644 --- a/go.mod +++ b/go.mod @@ -32,26 +32,31 @@ require ( github.com/multiformats/go-multiaddr-dns v0.4.1 github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-multistream v0.6.1 - github.com/opentracing/opentracing-go v1.2.0 github.com/prometheus/client_golang v1.22.0 github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.11.1 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - github.com/uber/jaeger-client-go v2.24.0+incompatible github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/wealdtech/go-ens/v3 v3.5.1 gitlab.com/nolash/go-mockbytes v0.0.7 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.48.0 - golang.org/x/net v0.50.0 - golang.org/x/sync v0.19.0 - golang.org/x/sys v0.41.0 - golang.org/x/term v0.40.0 + golang.org/x/crypto v0.49.0 + golang.org/x/net v0.52.0 + golang.org/x/sync v0.20.0 + golang.org/x/sys v0.42.0 + golang.org/x/term v0.41.0 golang.org/x/time v0.12.0 + google.golang.org/grpc v1.80.0 gopkg.in/yaml.v2 v2.4.0 resenje.org/feed v0.1.2 resenje.org/multex v0.1.0 @@ -59,6 +64,18 @@ require ( resenje.org/web v0.4.3 ) +require ( + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect +) + require ( filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect @@ -72,7 +89,6 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/codahale/hdrhistogram v0.0.0-00010101000000-000000000000 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -87,8 +103,6 @@ require ( github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -169,26 +183,21 @@ require ( github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - github.com/uber/jaeger-lib v2.2.0+incompatible // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wealdtech/go-multicodec v1.4.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.42.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index be253587a4f..ae85aa06dc6 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/HdrHistogram/hdrhistogram-go v0.0.0-20200919145931-8dac23c8dac1 h1:nEjGZtKHMK92888VT6XkzKwyiW14v5FFRGeWq2uV7N0= -github.com/HdrHistogram/hdrhistogram-go v0.0.0-20200919145931-8dac23c8dac1/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -148,6 +146,8 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= @@ -364,6 +364,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -434,6 +436,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -967,10 +971,6 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= -github.com/uber/jaeger-client-go v2.24.0+incompatible h1:CGchgJcHsDd2jWnaL4XngByMrXoGHh3n8oCqAKx0uMo= -github.com/uber/jaeger-client-go v2.24.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= -github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= @@ -1017,14 +1017,24 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -1070,8 +1080,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1110,8 +1120,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1162,8 +1172,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1186,8 +1196,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1260,15 +1270,15 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= -golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1277,8 +1287,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1338,8 +1348,8 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1348,6 +1358,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= @@ -1413,6 +1425,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1429,6 +1445,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 59bcad603af..228d8402178 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -109,16 +109,22 @@ password-file: "/var/lib/bee/password" # swap-initial-deposit: "0" ## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay # target-neighborhood: "" -## enable tracing -# tracing-enable: false -## endpoint to send tracing data -# tracing-endpoint: 127.0.0.1:6831 -## host to send tracing data -# tracing-host: "" -## port to send tracing data -# tracing-port: "" -## service name identifier for tracing -# tracing-service-name: bee +## tracing settings +# tracing: +# ## enable tracing +# enable: false +# ## OTLP collector endpoint to send tracing data (host:port); default port is 4318 for http, 4317 for grpc +# endpoint: 127.0.0.1:4318 +# ## disable TLS for the OTLP exporter (useful for a local collector) +# insecure: false +# ## path to a PEM-encoded CA bundle used to verify the OTLP collector certificate; ignored when insecure is true +# ca-file: "" +# ## OTLP exporter transport: http or grpc +# protocol: http +# ## head-based sampling ratio in [0,1]; 0 samples nothing, 1 samples everything +# sampling-ratio: 1.0 +# ## service name identifier for tracing +# service-name: bee ## gas limit fallback when estimation fails for contract transactions (default 500000) # gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 01e33f4c529..b315896d50a 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -109,16 +109,22 @@ password-file: "/usr/local/var/lib/swarm-bee/password" # swap-initial-deposit: "0" ## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay # target-neighborhood: "" -## enable tracing -# tracing-enable: false -## endpoint to send tracing data -# tracing-endpoint: 127.0.0.1:6831 -## host to send tracing data -# tracing-host: "" -## port to send tracing data -# tracing-port: "" -## service name identifier for tracing -# tracing-service-name: bee +## tracing settings +# tracing: +# ## enable tracing +# enable: false +# ## OTLP collector endpoint to send tracing data (host:port); default port is 4318 for http, 4317 for grpc +# endpoint: 127.0.0.1:4318 +# ## disable TLS for the OTLP exporter (useful for a local collector) +# insecure: false +# ## path to a PEM-encoded CA bundle used to verify the OTLP collector certificate; ignored when insecure is true +# ca-file: "" +# ## OTLP exporter transport: http or grpc +# protocol: http +# ## head-based sampling ratio in [0,1]; 0 samples nothing, 1 samples everything +# sampling-ratio: 1.0 +# ## service name identifier for tracing +# service-name: bee ## gas limit fallback when estimation fails for contract transactions (default 500000) # gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index 33608073903..4c5d92353ed 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -109,16 +109,22 @@ password-file: "/opt/homebrew/var/lib/swarm-bee/password" # swap-initial-deposit: "0" ## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay # target-neighborhood: "" -## enable tracing -# tracing-enable: false -## endpoint to send tracing data -# tracing-endpoint: 127.0.0.1:6831 -## host to send tracing data -# tracing-host: "" -## port to send tracing data -# tracing-port: "" -## service name identifier for tracing -# tracing-service-name: bee +## tracing settings +# tracing: +# ## enable tracing +# enable: false +# ## OTLP collector endpoint to send tracing data (host:port); default port is 4318 for http, 4317 for grpc +# endpoint: 127.0.0.1:4318 +# ## disable TLS for the OTLP exporter (useful for a local collector) +# insecure: false +# ## path to a PEM-encoded CA bundle used to verify the OTLP collector certificate; ignored when insecure is true +# ca-file: "" +# ## OTLP exporter transport: http or grpc +# protocol: http +# ## head-based sampling ratio in [0,1]; 0 samples nothing, 1 samples everything +# sampling-ratio: 1.0 +# ## service name identifier for tracing +# service-name: bee ## gas limit fallback when estimation fails for contract transactions (default 500000) # gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index 95864803848..06344080b39 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -109,16 +109,22 @@ password-file: "./password" # swap-initial-deposit: "0" ## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay # target-neighborhood: "" -## enable tracing -# tracing-enable: false -## endpoint to send tracing data -# tracing-endpoint: 127.0.0.1:6831 -## host to send tracing data -# tracing-host: "" -## port to send tracing data -# tracing-port: "" -## service name identifier for tracing -# tracing-service-name: bee +## tracing settings +# tracing: +# ## enable tracing +# enable: false +# ## OTLP collector endpoint to send tracing data (host:port); default port is 4318 for http, 4317 for grpc +# endpoint: 127.0.0.1:4318 +# ## disable TLS for the OTLP exporter (useful for a local collector) +# insecure: false +# ## path to a PEM-encoded CA bundle used to verify the OTLP collector certificate; ignored when insecure is true +# ca-file: "" +# ## OTLP exporter transport: http or grpc +# protocol: http +# ## head-based sampling ratio in [0,1]; 0 samples nothing, 1 samples everything +# sampling-ratio: 1.0 +# ## service name identifier for tracing +# service-name: bee ## gas limit fallback when estimation fails for contract transactions (default 500000) # gas-limit-fallback: 500000 ## skips the gas estimate step for contract transactions diff --git a/pkg/api/api.go b/pkg/api/api.go index acd838a3ff6..28586141e75 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -63,6 +63,9 @@ import ( "github.com/gorilla/mux" "github.com/hashicorp/go-multierror" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + "go.opentelemetry.io/otel/trace" "golang.org/x/sync/semaphore" ) @@ -462,8 +465,8 @@ func (s *Service) newTracingHandler(spanName string) func(h http.Handler) http.H // ignore } - span, _, ctx := s.tracer.StartSpanFromContext(ctx, spanName, s.logger) - defer span.Finish() + span, _, ctx := s.tracer.StartSpanFromContext(ctx, spanName, s.logger, trace.WithSpanKind(trace.SpanKindServer)) + defer span.End() err = s.tracer.AddContextHTTPHeader(ctx, r.Header) if err != nil { @@ -472,6 +475,23 @@ func (s *Service) newTracingHandler(spanName string) func(h http.Handler) http.H } h.ServeHTTP(w, r.WithContext(ctx)) + + // The response status code is captured upstream by responseCodeMetricsHandler's + // *responseWriter, which exposes Status(). Read it to annotate the span with the + // standard HTTP server attributes so the request is queryable in Tempo/Grafana. + if sw, ok := w.(interface{ Status() int }); ok { + code := sw.Status() + span.SetAttributes( + semconv.HTTPRequestMethodKey.String(r.Method), + semconv.HTTPRoute(spanName), + semconv.HTTPResponseStatusCode(code), + ) + // OTel server-span convention: 5xx marks the span as Error; 4xx is a client + // error and stays Unset, still queryable via http.response.status_code. + if code >= 500 { + span.SetStatus(codes.Error, http.StatusText(code)) + } + } }) } } diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index babd816dd06..23782bcbc6b 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -90,6 +90,7 @@ func init() { type testServerOptions struct { Storer api.Storer + Tracer *tracing.Tracer StateStorer storage.StateStorer Resolver resolver.Interface Pss pss.Interface @@ -231,12 +232,16 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. s.SetSwarmAddress(&o.Overlay) s.SetProbe(o.Probe) - noOpTracer, tracerCloser, _ := tracing.NewTracer(&tracing.Options{ - Enabled: false, - }) - testutil.CleanupCloser(t, tracerCloser) + tracer := o.Tracer + if tracer == nil { + noOpTracer, tracerCloser, _ := tracing.NewTracer(&tracing.Options{ + Enabled: false, + }) + testutil.CleanupCloser(t, tracerCloser) + tracer = noOpTracer + } - s.Configure(signer, noOpTracer, api.Options{ + s.Configure(signer, tracer, api.Options{ CORSAllowedOrigins: o.CORSAllowedOrigins, WsPingPeriod: o.WsPingPeriod, }, extraOpts, 1, erc20APIService) diff --git a/pkg/api/bytes.go b/pkg/api/bytes.go index 7ba80efdc06..c8e0b318037 100644 --- a/pkg/api/bytes.go +++ b/pkg/api/bytes.go @@ -21,8 +21,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/tracing" "github.com/gorilla/mux" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" ) type bytesPostResponse struct { @@ -31,8 +30,8 @@ type bytesPostResponse struct { // bytesUploadHandler handles upload of raw binary data of arbitrary length. func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { - span, logger, ctx := s.tracer.StartSpanFromContext(r.Context(), "post_bytes", s.logger.WithName("post_bytes").Build()) - defer span.Finish() + span, logger, ctx := s.tracer.StartSpanFromContext(r.Context(), "bytes-post", s.logger.WithName("post_bytes").Build()) + defer span.End() headers := struct { BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` @@ -71,10 +70,10 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { default: jsonhttp.InternalServerError(w, "cannot get or create tag") } - ext.LogError(span, err, olog.String("action", "tag.create")) + tracing.RecordError(span, err, attribute.String("action", "tag.create")) return } - span.SetTag("tagID", tag) + span.SetAttributes(attribute.Int64("tag_id", int64(tag))) } defer s.observeUploadSpeed(w, r, time.Now(), "bytes", deferred) @@ -98,7 +97,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { default: jsonhttp.BadRequest(w, nil) } - ext.LogError(span, err, olog.String("action", "new.StamperPutter")) + tracing.RecordError(span, err, attribute.String("action", "new.StamperPutter")) return } @@ -119,7 +118,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { default: jsonhttp.InternalServerError(ow, "split write all failed") } - ext.LogError(span, err, olog.String("action", "split.WriteAll")) + tracing.RecordError(span, err, attribute.String("action", "split.WriteAll")) return } @@ -143,14 +142,14 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { return } } - span.SetTag("root_address", encryptedReference) + span.SetAttributes(attribute.String("root_address", encryptedReference.String())) err = putter.Done(reference) if err != nil { logger.Debug("done split failed", "error", err) logger.Error(nil, "done split failed") jsonhttp.InternalServerError(ow, "done split failed") - ext.LogError(span, err, olog.String("action", "putter.Done")) + tracing.RecordError(span, err, attribute.String("action", "putter.Done")) return } @@ -158,7 +157,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set(SwarmTagHeader, fmt.Sprint(tag)) } - span.LogFields(olog.Bool("success", true)) + span.SetAttributes(attribute.Bool("success", true)) w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) if headers.Act { diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 1bd3854d4d9..141b3b55d24 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -19,9 +19,8 @@ import ( "strings" "time" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/accesscontrol" @@ -66,8 +65,8 @@ func lookaheadBufferSize(size int64) int { } func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { - span, logger, ctx := s.tracer.StartSpanFromContext(r.Context(), "post_bzz", s.logger.WithName("post_bzz").Build()) - defer span.Finish() + span, logger, ctx := s.tracer.StartSpanFromContext(r.Context(), "bzz-post", s.logger.WithName("post_bzz").Build()) + defer span.End() headers := struct { ContentType string `map:"Content-Type"` @@ -105,10 +104,10 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { default: jsonhttp.InternalServerError(w, "cannot get or create tag") } - ext.LogError(span, err, olog.String("action", "tag.create")) + tracing.RecordError(span, err, attribute.String("action", "tag.create")) return } - span.SetTag("tagID", tag) + span.SetAttributes(attribute.Int64("tag_id", int64(tag))) } putter, err := s.newStamperPutter(ctx, putterOptions{ @@ -130,7 +129,7 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { default: jsonhttp.BadRequest(w, nil) } - ext.LogError(span, err, olog.String("action", "new.StamperPutter")) + tracing.RecordError(span, err, attribute.String("action", "new.StamperPutter")) return } @@ -175,7 +174,7 @@ type bzzUploadResponse struct { func (s *Service) fileUploadHandler( ctx context.Context, logger log.Logger, - span opentracing.Span, + span trace.Span, w http.ResponseWriter, r *http.Request, putter storer.PutterSession, @@ -222,7 +221,7 @@ func (s *Service) fileUploadHandler( default: jsonhttp.InternalServerError(w, errFileStore) } - ext.LogError(span, err, olog.String("action", "file.store")) + tracing.RecordError(span, err, attribute.String("action", "file.store")) return } @@ -331,15 +330,17 @@ func (s *Service) fileUploadHandler( logger.Debug("done split failed", "reference", manifestReference, "error", err) logger.Error(nil, "done split failed") jsonhttp.InternalServerError(w, "done split failed") - ext.LogError(span, err, olog.String("action", "putter.Done")) + tracing.RecordError(span, err, attribute.String("action", "putter.Done")) return } - span.LogFields(olog.Bool("success", true)) - span.SetTag("root_address", reference) + span.SetAttributes( + attribute.Bool("success", true), + attribute.String("root_address", reference.String()), + ) if tagID != 0 { w.Header().Set(SwarmTagHeader, fmt.Sprint(tagID)) - span.SetTag("tagID", tagID) + span.SetAttributes(attribute.Int64("tag_id", int64(tagID))) } w.Header().Set(ETagHeader, fmt.Sprintf("%q", reference.String())) w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) diff --git a/pkg/api/dirs.go b/pkg/api/dirs.go index d3fa10ab1d3..49659a3a419 100644 --- a/pkg/api/dirs.go +++ b/pkg/api/dirs.go @@ -29,9 +29,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/storer" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/tracing" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) var errEmptyDir = errors.New("no files in root directory") @@ -40,7 +39,7 @@ var errEmptyDir = errors.New("no files in root directory") func (s *Service) dirUploadHandler( ctx context.Context, logger log.Logger, - span opentracing.Span, + span trace.Span, w http.ResponseWriter, r *http.Request, putter storer.PutterSession, @@ -96,7 +95,7 @@ func (s *Service) dirUploadHandler( default: jsonhttp.InternalServerError(w, errDirectoryStore) } - ext.LogError(span, err, olog.String("action", "dir.store")) + tracing.RecordError(span, err, attribute.String("action", "dir.store")) return } @@ -126,13 +125,13 @@ func (s *Service) dirUploadHandler( logger.Debug("store dir failed", "error", err) logger.Error(nil, "store dir failed") jsonhttp.InternalServerError(w, errDirectoryStore) - ext.LogError(span, err, olog.String("action", "putter.Done")) + tracing.RecordError(span, err, attribute.String("action", "putter.Done")) return } if tag != 0 { w.Header().Set(SwarmTagHeader, fmt.Sprint(tag)) - span.LogFields(olog.Bool("success", true)) + span.SetAttributes(attribute.Bool("success", true)) } w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) if act { diff --git a/pkg/api/pingpong.go b/pkg/api/pingpong.go index 6cb1c4ae23e..1b8ebbecf4e 100644 --- a/pkg/api/pingpong.go +++ b/pkg/api/pingpong.go @@ -31,7 +31,7 @@ func (s *Service) pingpongHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() span, logger, ctx := s.tracer.StartSpanFromContext(ctx, "pingpong-api", logger) - defer span.Finish() + defer span.End() rtt, err := s.pingpong.Ping(ctx, paths.Address, "ping") if err != nil { diff --git a/pkg/api/tracing_test.go b/pkg/api/tracing_test.go new file mode 100644 index 00000000000..f07fb5565c7 --- /dev/null +++ b/pkg/api/tracing_test.go @@ -0,0 +1,153 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api_test + +import ( + "bytes" + "context" + "net/http" + "testing" + "time" + + "github.com/ethersphere/bee/v2/pkg/api" + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" + "github.com/ethersphere/bee/v2/pkg/log" + mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock" + "github.com/ethersphere/bee/v2/pkg/spinlock" + mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/bee/v2/pkg/tracing" + "gitlab.com/nolash/go-mockbytes" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + "go.opentelemetry.io/otel/trace" +) + +// TestTracingHTTPSpan verifies that the HTTP tracing middleware annotates the +// span with the standard server attributes (method, route, status code), marks +// it as a server span, and maps a 5xx response to an Error status while a 2xx +// response leaves the status Unset. These are the fields used to query spans in +// Tempo/Grafana. +func TestTracingHTTPSpan(t *testing.T) { + t.Parallel() + + const resource = "/bytes" + + sr := tracetest.NewSpanRecorder() + tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + t.Cleanup(func() { _ = tp.Shutdown(context.Background()) }) + + storerMock := mockstorer.New() + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Tracer: tracing.NewTracerFromProvider(tp), + Logger: log.Noop, + Post: mockpost.New(mockpost.WithAcceptAll()), + }) + + // Upload some content so it can be downloaded with a 200 OK. + g := mockbytes.New(0, mockbytes.MockTypeStandard).WithModulus(255) + content, err := g.SequentialBytes(swarm.ChunkSize * 2) + if err != nil { + t.Fatal(err) + } + + var res api.BytesPostResponse + jsonhttptest.Request(t, client, http.MethodPost, resource, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(bytes.NewReader(content)), + jsonhttptest.WithUnmarshalJSONResponse(&res), + ) + + // Successful download -> 200 OK. + jsonhttptest.Request(t, client, http.MethodGet, resource+"/"+res.Reference.String(), http.StatusOK, + jsonhttptest.WithExpectedResponse(content), + ) + + // Invalid address triggers an internal error -> 500. + jsonhttptest.Request(t, client, http.MethodGet, resource+"/abcd", http.StatusInternalServerError, + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: "joiner failed", + Code: http.StatusInternalServerError, + }), + ) + + // span.End() runs in the middleware's deferred call, which can fire after the + // client has already received the response, so wait for both download spans. + var spans []sdktrace.ReadOnlySpan + if err := spinlock.Wait(time.Second, func() bool { + spans = endedSpansByName(sr, "bytes-download") + return len(spans) == 2 + }); err != nil { + t.Fatalf("expected 2 bytes-download spans, got %d", len(spans)) + } + + var ok200, err500 sdktrace.ReadOnlySpan + for _, s := range spans { + switch spanStatusCode(s) { + case http.StatusOK: + ok200 = s + case http.StatusInternalServerError: + err500 = s + } + } + if ok200 == nil || err500 == nil { + t.Fatalf("missing spans by status code: have ok=%t err=%t", ok200 != nil, err500 != nil) + } + + // Both spans carry the standard HTTP server attributes and are server-kind. + for _, s := range []sdktrace.ReadOnlySpan{ok200, err500} { + if s.SpanKind() != trace.SpanKindServer { + t.Errorf("span kind = %v, want server", s.SpanKind()) + } + if got := spanAttrString(s, semconv.HTTPRequestMethodKey); got != http.MethodGet { + t.Errorf("http.request.method = %q, want %q", got, http.MethodGet) + } + if got := spanAttrString(s, semconv.HTTPRouteKey); got != "bytes-download" { + t.Errorf("http.route = %q, want %q", got, "bytes-download") + } + } + + // A 5xx response marks the span as Error; a 2xx response stays Unset. + if got := err500.Status().Code; got != codes.Error { + t.Errorf("500 span status = %v, want %v", got, codes.Error) + } + if got := ok200.Status().Code; got != codes.Unset { + t.Errorf("200 span status = %v, want %v", got, codes.Unset) + } +} + +func endedSpansByName(sr *tracetest.SpanRecorder, name string) []sdktrace.ReadOnlySpan { + var out []sdktrace.ReadOnlySpan + for _, s := range sr.Ended() { + if s.Name() == name { + out = append(out, s) + } + } + return out +} + +func spanStatusCode(s sdktrace.ReadOnlySpan) int { + for _, a := range s.Attributes() { + if a.Key == semconv.HTTPResponseStatusCodeKey { + return int(a.Value.AsInt64()) + } + } + return 0 +} + +func spanAttrString(s sdktrace.ReadOnlySpan, key attribute.Key) string { + for _, a := range s.Attributes() { + if a.Key == key { + return a.Value.AsString() + } + } + return "" +} diff --git a/pkg/node/node.go b/pkg/node/node.go index b085278969a..05977d35bf9 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -26,6 +26,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + bee "github.com/ethersphere/bee/v2" "github.com/ethersphere/bee/v2/pkg/accesscontrol" "github.com/ethersphere/bee/v2/pkg/accounting" "github.com/ethersphere/bee/v2/pkg/addressbook" @@ -193,6 +194,10 @@ type Options struct { TargetNeighborhood string TracingEnabled bool TracingEndpoint string + TracingInsecure bool + TracingCAFile string + TracingProtocol string + TracingSamplingRatio float64 TracingServiceName string TrxDebugMode bool WarmupTime time.Duration @@ -218,6 +223,19 @@ const ( maxAllowedDoubling = 1 ) +// tracingEnvironment maps a network id to the deployment.environment trace +// attribute. Unknown ids are reported as "private". +func tracingEnvironment(networkID uint64) string { + switch networkID { + case config.Mainnet.NetworkID: + return "mainnet" + case config.Testnet.NetworkID: + return "testnet" + default: + return "private" + } +} + func NewBee( ctx context.Context, addr string, @@ -236,15 +254,6 @@ func NewBee( nodeMetrics := newMetrics() - tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{ - Enabled: o.TracingEnabled, - Endpoint: o.TracingEndpoint, - ServiceName: o.TracingServiceName, - }) - if err != nil { - return nil, fmt.Errorf("tracer: %w", err) - } - if err := validatePublicAddress(o.NATAddr); err != nil { return nil, fmt.Errorf("invalid NAT address %s: %w", o.NATAddr, err) } @@ -278,7 +287,6 @@ func NewBee( logger: logger, ctxCancel: ctxCancel, errorLogWriter: sink, - tracerCloser: tracerCloser, syncingStopped: syncutil.NewSignaler(), } @@ -389,6 +397,24 @@ func NewBee( return nil, fmt.Errorf("check overlay address: %w", err) } + tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{ + Enabled: o.TracingEnabled, + Endpoint: o.TracingEndpoint, + Insecure: o.TracingInsecure, + CAFile: o.TracingCAFile, + Protocol: o.TracingProtocol, + SamplingRatio: o.TracingSamplingRatio, + ServiceName: o.TracingServiceName, + ServiceVersion: bee.Version, + Environment: tracingEnvironment(networkID), + InstanceID: swarmAddress.String(), + Logger: logger, + }) + if err != nil { + return nil, fmt.Errorf("tracer: %w", err) + } + b.tracerCloser = tracerCloser + var ( chequebookService chequebook.Service = new(noOpChequebookService) chequeStore chequebook.ChequeStore diff --git a/pkg/p2p/libp2p/tracing_test.go b/pkg/p2p/libp2p/tracing_test.go index 66ebd22aa29..a41aed424c2 100644 --- a/pkg/p2p/libp2p/tracing_test.go +++ b/pkg/p2p/libp2p/tracing_test.go @@ -20,6 +20,8 @@ func TestTracing(t *testing.T) { tracer1, closer1, err := tracing.NewTracer(&tracing.Options{ Enabled: true, + Endpoint: "127.0.0.1:1", + Insecure: true, ServiceName: "bee-test", }) if err != nil { @@ -29,6 +31,8 @@ func TestTracing(t *testing.T) { tracer2, closer2, err := tracing.NewTracer(&tracing.Options{ Enabled: true, + Endpoint: "127.0.0.1:1", + Insecure: true, ServiceName: "bee-test", }) if err != nil { @@ -46,9 +50,9 @@ func TestTracing(t *testing.T) { handled := make(chan struct{}) if err := s1.AddProtocol(newTestProtocol(func(ctx context.Context, _ p2p.Peer, _ p2p.Stream) error { span, _, _ := tracer1.StartSpanFromContext(ctx, "test-p2p-handler", nil) - defer span.Finish() + defer span.End() - handledTracingSpan = fmt.Sprint(span.Context()) + handledTracingSpan = fmt.Sprint(span.SpanContext().TraceID()) close(handled) return nil })); err != nil { @@ -66,9 +70,9 @@ func TestTracing(t *testing.T) { ctx := t.Context() span, _, ctx := tracer2.StartSpanFromContext(ctx, "test-p2p-client", nil) - defer span.Finish() + defer span.End() - if fmt.Sprint(span.Context()) == "" { + if !span.SpanContext().IsValid() { t.Error("not tracing span context to send") } diff --git a/pkg/p2p/p2p.go b/pkg/p2p/p2p.go index b709ffa5113..62c7e92491a 100644 --- a/pkg/p2p/p2p.go +++ b/pkg/p2p/p2p.go @@ -215,7 +215,11 @@ type Headers map[string][]byte // Common header names. const ( - HeaderNameTracingSpanContext = "tracing-span-context" + // HeaderNameTracingSpanContext carries the serialised span context between + // peers. The "-v2" suffix marks the OpenTelemetry carrier format; it differs + // from the legacy OpenTracing/Jaeger payload, so the distinct key prevents + // mixed-version peers from decoding each other's incompatible payloads. + HeaderNameTracingSpanContext = "tracing-span-context-v2" ) // NewSwarmStreamName constructs a libp2p compatible stream name out of diff --git a/pkg/pingpong/pingpong.go b/pkg/pingpong/pingpong.go index d62db98b0ac..858ba4d4f39 100644 --- a/pkg/pingpong/pingpong.go +++ b/pkg/pingpong/pingpong.go @@ -19,6 +19,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/pingpong/pb" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/tracing" + "go.opentelemetry.io/otel/attribute" ) // loggerName is the tree path name of the logger for this package. @@ -65,7 +66,8 @@ func (s *Service) Protocol() p2p.ProtocolSpec { func (s *Service) Ping(ctx context.Context, address swarm.Address, msgs ...string) (rtt time.Duration, err error) { span, _, ctx := s.tracer.StartSpanFromContext(ctx, "pingpong-p2p-ping", s.logger) - defer span.Finish() + span.SetAttributes(attribute.String("peer_address", address.String())) + defer span.End() start := time.Now() stream, err := s.streamer.NewStream(ctx, address, nil, protocolName, protocolVersion, streamName) @@ -104,7 +106,8 @@ func (s *Service) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) er defer stream.FullClose() span, _, ctx := s.tracer.StartSpanFromContext(ctx, "pingpong-p2p-handler", s.logger) - defer span.Finish() + span.SetAttributes(attribute.String("peer_address", p.Address.String())) + defer span.End() var ping pb.Ping for { diff --git a/pkg/pusher/pusher.go b/pkg/pusher/pusher.go index 9c4073f1fcc..b756bb088d6 100644 --- a/pkg/pusher/pusher.go +++ b/pkg/pusher/pusher.go @@ -23,9 +23,9 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/topology" "github.com/ethersphere/bee/v2/pkg/tracing" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) // loggerName is the tree path name of the logger for this package. @@ -35,7 +35,7 @@ type Op struct { Chunk swarm.Chunk Err chan error Direct bool - Span opentracing.Span + Span trace.Span identityAddress swarm.Address } @@ -155,9 +155,9 @@ func (s *Service) chunksWorker(startupStabilizer stabilization.Subscriber) { spanCtx := ctx if op.Span != nil { - spanCtx = tracing.WithContext(spanCtx, op.Span.Context()) + spanCtx = tracing.WithContext(spanCtx, op.Span.SpanContext()) } else { - op.Span = opentracing.NoopTracer{}.StartSpan("noOp") + _, op.Span = noop.NewTracerProvider().Tracer(loggerName).Start(spanCtx, "noOp") } if op.Direct { @@ -169,9 +169,9 @@ func (s *Service) chunksWorker(startupStabilizer stabilization.Subscriber) { if err != nil { s.metrics.TotalErrors.Inc() s.metrics.ErrorTime.Observe(time.Since(startTime).Seconds()) - ext.LogError(op.Span, err) + tracing.RecordError(op.Span, err) } else { - op.Span.LogFields(olog.Bool("success", true)) + op.Span.SetAttributes(attribute.Bool("success", true)) } s.metrics.SyncTime.Observe(time.Since(startTime).Seconds()) diff --git a/pkg/pushsync/pushsync.go b/pkg/pushsync/pushsync.go index 05c4aef3d41..f11dfd55f9c 100644 --- a/pkg/pushsync/pushsync.go +++ b/pkg/pushsync/pushsync.go @@ -29,9 +29,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/topology" "github.com/ethersphere/bee/v2/pkg/tracing" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "golang.org/x/time/rate" ) @@ -208,7 +207,11 @@ func (ps *PushSync) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) chunk := swarm.NewChunk(swarm.NewAddress(ch.Address), ch.Data) chunkAddress := chunk.Address() - span, _, ctx := ps.tracer.StartSpanFromContext(ctx, "pushsync-handler", ps.logger, opentracing.Tag{Key: "address", Value: chunkAddress.String()}, opentracing.Tag{Key: "tagID", Value: chunk.TagID()}, opentracing.Tag{Key: "sender_address", Value: p.Address.String()}) + span, _, ctx := ps.tracer.StartSpanFromContext(ctx, "pushsync-handler", ps.logger, trace.WithAttributes( + attribute.String("address", chunkAddress.String()), + attribute.Int64("tag_id", int64(chunk.TagID())), + attribute.String("sender_address", p.Address.String()), + )) var ( stored bool @@ -217,17 +220,18 @@ func (ps *PushSync) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) defer func() { if err != nil { - ext.LogError(span, err) + tracing.RecordError(span, err) } else { - var logs []olog.Field - logs = append(logs, olog.Bool("success", true)) + attrs := []attribute.KeyValue{attribute.Bool("success", true)} if stored { - logs = append(logs, olog.Bool("stored", true)) - logs = append(logs, olog.String("reason", reason)) + attrs = append(attrs, + attribute.Bool("stored", true), + attribute.String("reason", reason), + ) } - span.LogFields(logs...) + span.SetAttributes(attrs...) } - span.Finish() + span.End() }() stamp := new(postage.Stamp) @@ -535,15 +539,17 @@ func (ps *PushSync) push(parentCtx context.Context, resultChan chan<- receiptRes now := time.Now() - spanInner, _, _ := ps.tracer.FollowSpanFromContext(context.WithoutCancel(parentCtx), "push-chunk-async", ps.logger, opentracing.Tag{Key: "address", Value: ch.Address().String()}) + spanInner, _, _ := ps.tracer.FollowSpanFromContext(context.WithoutCancel(parentCtx), "push-chunk-async", ps.logger, trace.WithAttributes( + attribute.String("address", ch.Address().String()), + )) defer func() { if err != nil { - ext.LogError(spanInner, err) + tracing.RecordError(spanInner, err) } else { - spanInner.LogFields(olog.Bool("success", true)) + spanInner.SetAttributes(attribute.Bool("success", true)) } - spanInner.Finish() + spanInner.End() select { case resultChan <- receiptResult{pushTime: now, peer: peer, err: err, receipt: receipt}: case <-parentCtx.Done(): @@ -552,9 +558,9 @@ func (ps *PushSync) push(parentCtx context.Context, resultChan chan<- receiptRes defer action.Cleanup() - spanInner.LogFields(olog.String("peer_address", peer.String())) + spanInner.SetAttributes(attribute.String("peer_address", peer.String())) - receipt, err = ps.pushChunkToPeer(tracing.WithContext(ctx, spanInner.Context()), peer, ch) + receipt, err = ps.pushChunkToPeer(tracing.WithContext(ctx, spanInner.SpanContext()), peer, ch) if err != nil { return } diff --git a/pkg/retrieval/retrieval.go b/pkg/retrieval/retrieval.go index 9432a058690..f4bc9844a33 100644 --- a/pkg/retrieval/retrieval.go +++ b/pkg/retrieval/retrieval.go @@ -27,9 +27,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/topology" "github.com/ethersphere/bee/v2/pkg/tracing" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "resenje.org/singleflight" ) @@ -259,8 +258,10 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s inflight++ go func() { - span, _, ctx := s.tracer.FollowSpanFromContext(spanCtx, "retrieve-chunk", s.logger, opentracing.Tag{Key: "address", Value: chunkAddr.String()}) - defer span.Finish() + span, _, ctx := s.tracer.FollowSpanFromContext(spanCtx, "retrieve-chunk", s.logger, trace.WithAttributes( + attribute.String("address", chunkAddr.String()), + )) + defer span.End() s.retrieveChunk(ctx, quit, chunkAddr, peer, resultC, action, span) }() @@ -295,7 +296,7 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s return v, nil } -func (s *Service) retrieveChunk(ctx context.Context, quit chan struct{}, chunkAddr, peer swarm.Address, result chan retrievalResult, action accounting.Action, span opentracing.Span) { +func (s *Service) retrieveChunk(ctx context.Context, quit chan struct{}, chunkAddr, peer swarm.Address, result chan retrievalResult, action accounting.Action, span trace.Span) { var ( startTime = time.Now() err error @@ -305,10 +306,10 @@ func (s *Service) retrieveChunk(ctx context.Context, quit chan struct{}, chunkAd defer func() { action.Cleanup() if err != nil { - ext.LogError(span, err) + tracing.RecordError(span, err) s.metrics.TotalErrors.Inc() } else { - span.LogFields(olog.Bool("success", true)) + span.SetAttributes(attribute.Bool("success", true)) } select { case result <- retrievalResult{err: err, chunk: chunk, peer: peer}: @@ -449,15 +450,17 @@ func (s *Service) handler(p2pctx context.Context, p p2p.Peer, stream p2p.Stream) var forwarded bool - span, _, ctx := s.tracer.StartSpanFromContext(ctx, "handle-retrieve-chunk", s.logger, opentracing.Tag{Key: "address", Value: addr.String()}) + span, _, ctx := s.tracer.StartSpanFromContext(ctx, "handle-retrieve-chunk", s.logger, trace.WithAttributes( + attribute.String("address", addr.String()), + )) defer func() { if err != nil { - ext.LogError(span, err) + tracing.RecordError(span, err) } else { - span.LogFields(olog.Bool("success", true)) + span.SetAttributes(attribute.Bool("success", true)) } - span.LogFields(olog.Bool("forwarded", forwarded)) - span.Finish() + span.SetAttributes(attribute.Bool("forwarded", forwarded)) + span.End() }() chunk, err := s.storer.Lookup().Get(ctx, addr) diff --git a/pkg/storer/netstore.go b/pkg/storer/netstore.go index e2becfb5027..f9e43ae59c4 100644 --- a/pkg/storer/netstore.go +++ b/pkg/storer/netstore.go @@ -13,8 +13,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/storage" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/topology" - "github.com/opentracing/opentracing-go/ext" - olog "github.com/opentracing/opentracing-go/log" + "github.com/ethersphere/bee/v2/pkg/tracing" + "go.opentelemetry.io/otel/attribute" "golang.org/x/sync/errgroup" ) @@ -32,11 +32,12 @@ func (db *DB) DirectUpload() PutterSession { defer func() { <-db.directUploadLimiter }() span, logger, ctx := db.tracer.FollowSpanFromContext(ctx, "put-direct-upload", db.logger) + span.SetAttributes(attribute.String("address", ch.Address().String())) defer func() { if err != nil { - ext.LogError(span, err) + tracing.RecordError(span, err) } - span.Finish() + span.End() }() for { @@ -83,22 +84,23 @@ func (db *DB) Download(cache bool) storage.Getter { return getterWithMetrics{ storage.GetterFunc(func(ctx context.Context, address swarm.Address) (ch swarm.Chunk, err error) { span, logger, ctx := db.tracer.StartSpanFromContext(ctx, "get-chunk", db.logger) + span.SetAttributes(attribute.String("address", address.String())) defer func() { if err != nil { - ext.LogError(span, err) + tracing.RecordError(span, err) } else { - span.LogFields(olog.Bool("success", true)) + span.SetAttributes(attribute.Bool("success", true)) } - span.Finish() + span.End() }() ch, err = db.Lookup().Get(ctx, address) switch { case err == nil: - span.LogFields(olog.String("step", "chunk found locally")) + span.AddEvent("chunk found locally") return ch, nil case errors.Is(err, storage.ErrNotFound): - span.LogFields(olog.String("step", "retrieve chunk from network")) + span.AddEvent("retrieve chunk from network") if db.retrieval != nil { // if chunk is not found locally, retrieve it from the network ch, err = db.retrieval.RetrieveChunk(ctx, address, swarm.ZeroAddress) diff --git a/pkg/tracing/cafile_test.go b/pkg/tracing/cafile_test.go new file mode 100644 index 00000000000..9955d0d11ee --- /dev/null +++ b/pkg/tracing/cafile_test.go @@ -0,0 +1,165 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tracing_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/ethersphere/bee/v2/pkg/tracing" + "github.com/ethersphere/bee/v2/pkg/util/testutil" +) + +func TestLoadCAFile(t *testing.T) { + t.Parallel() + + t.Run("valid bundle", func(t *testing.T) { + t.Parallel() + + path, caCert := writeTestCA(t) + + cfg, err := tracing.LoadCAFile(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.MinVersion != tls.VersionTLS12 { + t.Errorf("MinVersion = %#x, want %#x", cfg.MinVersion, tls.VersionTLS12) + } + if cfg.RootCAs == nil { + t.Fatal("RootCAs is nil") + } + // The loaded CA must be present in the pool: a self-signed CA verifies + // against a root pool that actually contains it. + if _, err := caCert.Verify(x509.VerifyOptions{Roots: cfg.RootCAs}); err != nil { + t.Errorf("CA does not verify against the loaded pool: %v", err) + } + }) + + t.Run("missing file", func(t *testing.T) { + t.Parallel() + + if _, err := tracing.LoadCAFile(filepath.Join(t.TempDir(), "does-not-exist.pem")); err == nil { + t.Fatal("expected an error for a missing CA file, got nil") + } + }) + + t.Run("no valid certificates", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "garbage.pem") + if err := os.WriteFile(path, []byte("this is not a PEM certificate"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := tracing.LoadCAFile(path); err == nil { + t.Fatal("expected an error for a file with no valid PEM certificates, got nil") + } + }) +} + +// TestNewTracer_CAFile checks that the CA bundle is wired into both OTLP +// transports when TLS is enabled, and ignored when the exporter is insecure. +func TestNewTracer_CAFile(t *testing.T) { + t.Parallel() + + validCA, _ := writeTestCA(t) + missingCA := filepath.Join(t.TempDir(), "missing.pem") + + for _, proto := range []string{"http", "grpc"} { + t.Run(proto+"/valid CA loads", func(t *testing.T) { + t.Parallel() + + _, closer, err := tracing.NewTracer(&tracing.Options{ + Enabled: true, + Endpoint: "127.0.0.1:1", + ServiceName: "test", + Protocol: proto, + CAFile: validCA, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + testutil.CleanupCloser(t, closer) + }) + + t.Run(proto+"/missing CA errors", func(t *testing.T) { + t.Parallel() + + if _, _, err := tracing.NewTracer(&tracing.Options{ + Enabled: true, + Endpoint: "127.0.0.1:1", + ServiceName: "test", + Protocol: proto, + CAFile: missingCA, + }); err == nil { + t.Fatal("expected an error for a missing CA file, got nil") + } + }) + + t.Run(proto+"/insecure ignores CA", func(t *testing.T) { + t.Parallel() + + _, closer, err := tracing.NewTracer(&tracing.Options{ + Enabled: true, + Endpoint: "127.0.0.1:1", + ServiceName: "test", + Protocol: proto, + Insecure: true, + CAFile: missingCA, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + testutil.CleanupCloser(t, closer) + }) + } +} + +// writeTestCA generates a short-lived self-signed CA, writes it PEM-encoded to a +// temp file, and returns the path alongside the parsed certificate. +func writeTestCA(t *testing.T) (path string, caCert *x509.Certificate) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "bee-test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + + caCert, err = x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + + path = filepath.Join(t.TempDir(), "ca.pem") + if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil { + t.Fatal(err) + } + + return path, caCert +} diff --git a/pkg/tracing/doc.go b/pkg/tracing/doc.go index 3f6bc7853f4..dc06e735aac 100644 --- a/pkg/tracing/doc.go +++ b/pkg/tracing/doc.go @@ -7,15 +7,14 @@ Package tracing helps with the propagation of the tracing span through context in the system. It does this for operations contained to single node, as well as across nodes, by injecting special headers. -To use the tracing package, a Tracer instance must be created, which contains -functions for starting new span contexts, injecting them in other data, and -extracting the active span them from the context. +The package wraps the OpenTelemetry Go SDK and ships spans via the OTLP/HTTP +exporter, so any OTLP-compatible backend (Jaeger v2, Tempo, Honeycomb, …) works. To use the tracing package a Tracer instance must be created: tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{ Enabled: true, - Endpoint: "127.0.0.1:6831", + Endpoint: "127.0.0.1:4318", ServiceName: "bee", }) if err != nil { @@ -25,13 +24,13 @@ To use the tracing package a Tracer instance must be created: // ... The tracer instance contains functions for starting new span contexts, injecting -them in other data, and extracting the active span them from the context: +them in other data, and extracting the active span from the context: span, _, ctx := tracer.StartSpanFromContext(ctx, "operation-name", nil) -Once the operation is finished, the open span should be finished: +Once the operation is finished, the open span should be ended: - span.Finish() + span.End() The tracing package also provides a function for creating a logger which will inject a "traceID" field entry to the log line, which helps in finding out which diff --git a/pkg/tracing/export_test.go b/pkg/tracing/export_test.go new file mode 100644 index 00000000000..d39d46ca0fe --- /dev/null +++ b/pkg/tracing/export_test.go @@ -0,0 +1,14 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tracing + +const ( + LogField = logField +) + +var ( + LoadCAFile = loadCAFile + NewResource = newResource +) diff --git a/pkg/tracing/resource_test.go b/pkg/tracing/resource_test.go new file mode 100644 index 00000000000..84fe283b2c5 --- /dev/null +++ b/pkg/tracing/resource_test.go @@ -0,0 +1,112 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tracing_test + +import ( + "testing" + + "github.com/ethersphere/bee/v2/pkg/tracing" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" +) + +// attrValue returns the string value of key in res, or ("", false) when absent. +func attrValue(t *testing.T, res interface{ Set() *attribute.Set }, key attribute.Key) (string, bool) { + t.Helper() + + v, ok := res.Set().Value(key) + if !ok { + return "", false + } + return v.AsString(), true +} + +func TestNewResource(t *testing.T) { + // Not parallel: a subtest uses t.Setenv, which forbids a parallel parent. + // The parallel subtests below still pause until this function returns, by + // which point the env-mutating subtest has run inline and restored the env. + + t.Run("service name and version", func(t *testing.T) { + t.Parallel() + + res, err := tracing.NewResource(&tracing.Options{ + ServiceName: "bee", + ServiceVersion: "2.8.0-abcdef", + Environment: "mainnet", + InstanceID: "overlay-abc123", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got, ok := attrValue(t, res, semconv.ServiceNameKey); !ok || got != "bee" { + t.Errorf("service.name = %q (present=%v), want %q", got, ok, "bee") + } + if got, ok := attrValue(t, res, semconv.ServiceVersionKey); !ok || got != "2.8.0-abcdef" { + t.Errorf("service.version = %q (present=%v), want %q", got, ok, "2.8.0-abcdef") + } + if got, ok := attrValue(t, res, semconv.DeploymentEnvironmentKey); !ok || got != "mainnet" { + t.Errorf("deployment.environment = %q (present=%v), want %q", got, ok, "mainnet") + } + if got, ok := attrValue(t, res, semconv.ServiceInstanceIDKey); !ok || got != "overlay-abc123" { + t.Errorf("service.instance.id = %q (present=%v), want %q", got, ok, "overlay-abc123") + } + // WithTelemetrySDK must contribute the SDK identity attributes. + if _, ok := attrValue(t, res, semconv.TelemetrySDKNameKey); !ok { + t.Error("telemetry.sdk.name is absent, WithTelemetrySDK not applied") + } + // WithHost must contribute host.name so spans are attributable per node. + if _, ok := attrValue(t, res, semconv.HostNameKey); !ok { + t.Error("host.name is absent, WithHost not applied") + } + }) + + t.Run("empty environment omits attribute", func(t *testing.T) { + t.Parallel() + + res, err := tracing.NewResource(&tracing.Options{ServiceName: "bee"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got, ok := attrValue(t, res, semconv.DeploymentEnvironmentKey); ok { + t.Errorf("deployment.environment = %q, want it to be absent", got) + } + }) + + t.Run("empty version omits attribute", func(t *testing.T) { + t.Parallel() + + res, err := tracing.NewResource(&tracing.Options{ServiceName: "bee"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got, ok := attrValue(t, res, semconv.ServiceVersionKey); ok { + t.Errorf("service.version = %q, want it to be absent", got) + } + }) + + // Configured service name/version are applied after WithFromEnv, so they + // take precedence over a colliding OTEL_SERVICE_NAME while unrelated + // environment attributes still enrich the resource. This subtest mutates the + // process environment, so it must not run in parallel. + t.Run("configured values win over env, env enriches", func(t *testing.T) { + t.Setenv("OTEL_SERVICE_NAME", "from-env") + t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=testnet") + + res, err := tracing.NewResource(&tracing.Options{ServiceName: "configured"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got, ok := attrValue(t, res, semconv.ServiceNameKey); !ok || got != "configured" { + t.Errorf("service.name = %q (present=%v), want configured value to win", got, ok) + } + if got, ok := attrValue(t, res, semconv.DeploymentEnvironmentKey); !ok || got != "testnet" { + t.Errorf("deployment.environment = %q (present=%v), want %q from env", got, ok, "testnet") + } + }) +} diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index e9c563464fa..c1ef0fc614a 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -2,278 +2,441 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package tracing wraps the OpenTelemetry SDK and exposes a small set of +// helpers for starting spans, propagating span context across libp2p streams +// and HTTP requests, and annotating loggers with trace ids. package tracing import ( - "bufio" - "bytes" "context" + "crypto/tls" + "crypto/x509" "errors" + "fmt" "io" "net/http" + "os" "time" "github.com/ethersphere/bee/v2/pkg/log" "github.com/ethersphere/bee/v2/pkg/p2p" - "github.com/opentracing/opentracing-go" - "github.com/uber/jaeger-client-go" - "github.com/uber/jaeger-client-go/config" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/grpc/credentials" ) -var ( - // ErrContextNotFound is returned when tracing context is not present - // in p2p Headers or context. - ErrContextNotFound = errors.New("tracing context not found") +// ErrContextNotFound is returned when tracing context is not present in +// p2p Headers, HTTP headers, or the go context. +var ErrContextNotFound = errors.New("tracing context not found") - // noopTracer is the tracer that does nothing to handle a nil Tracer usage. - noopTracer = &Tracer{tracer: new(opentracing.NoopTracer)} -) +// logField is the key in log message field that holds the tracing id value. +const logField = "traceID" -// contextKey is used to reference a tracing context span as context value. -type contextKey struct{} +// instrumentationName identifies spans produced by this package to the OTel SDK. +const instrumentationName = "github.com/ethersphere/bee/v2/pkg/tracing" -// LogField is the key in log message field that holds tracing id value. -const LogField = "traceID" +// p2pCarrierVersion is the leading byte of the libp2p binary span context +// payload. It exists so the wire format can evolve without silently breaking +// peers running an older binary. +const p2pCarrierVersion byte = 1 -const ( - // TraceContextHeaderName is the http header name used to propagate tracing context. - TraceContextHeaderName = "swarm-trace-id" +// p2pCarrierLen is the encoded payload size: +// 1 version + 16 TraceID + 8 SpanID + 1 TraceFlags. +const p2pCarrierLen = 26 + +// shutdownTimeout bounds how long Close waits for the OTel batch processor to +// flush pending spans on shutdown. +const shutdownTimeout = 5 * time.Second - // TraceBaggageHeaderPrefix is the prefix for http headers used to propagate baggage. - TraceBaggageHeaderPrefix = "swarmctx-" +// Supported OTLP transport values for Options.Protocol. +const ( + protocolHTTP = "http" + protocolGRPC = "grpc" ) -// Tracer connect to a tracing server and handles tracing spans and contexts -// by using opentracing Tracer. +// noopTracer is returned when tracing is disabled or *Tracer is nil so callers +// always operate against a working trace.Tracer. +var noopTracer = &Tracer{tracer: noop.NewTracerProvider().Tracer(instrumentationName)} + +// httpPropagator carries trace context across HTTP via the W3C TraceContext +// standard headers (traceparent, tracestate). +var httpPropagator propagation.TextMapPropagator = propagation.TraceContext{} + +// Tracer wraps an OTel Tracer and provides p2p/HTTP carriers plus helpers +// aligned with bee's tracing API. type Tracer struct { - tracer opentracing.Tracer + tracer trace.Tracer } -// Options are optional parameters for Tracer constructor. +// otel returns the underlying OTel tracer, falling back to a no-op tracer when +// the receiver or its tracer is nil. This keeps the exported Tracer safe to use +// even when obtained without NewTracer (e.g. a zero-value &Tracer{}). +func (t *Tracer) otel() trace.Tracer { + if t == nil || t.tracer == nil { + return noopTracer.tracer + } + return t.tracer +} + +// Options are the constructor parameters for Tracer. type Options struct { - Enabled bool - Endpoint string + // Enabled toggles span recording. When false the tracer is a no-op. + Enabled bool + // Endpoint is the OTLP collector endpoint, e.g. "127.0.0.1:4318" for http + // or "127.0.0.1:4317" for grpc. Required when Enabled is true. + Endpoint string + // ServiceName is reported as the OTel service.name resource attribute. ServiceName string + // ServiceVersion is reported as the OTel service.version resource + // attribute. When empty the attribute is omitted. + ServiceVersion string + // Environment is reported as the OTel deployment.environment resource + // attribute (e.g. "mainnet", "testnet"). When empty the attribute is omitted. + Environment string + // InstanceID is reported as the OTel service.instance.id resource attribute + // (the node's overlay address). When empty the attribute is omitted. + InstanceID string + // Insecure disables TLS for the OTLP exporter (useful for a local collector). + Insecure bool + // CAFile is an optional path to a PEM-encoded CA bundle used to verify + // the OTLP collector certificate. Ignored when Insecure is true. When + // empty and Insecure is false, the system root CAs are used. + CAFile string + // SamplingRatio is the head-based sampling ratio for the parent-based + // sampler in the range [0, 1]. 0 disables sampling for non-parented spans; + // 1 samples everything. Values outside [0, 1] are clamped to the nearest bound. + SamplingRatio float64 + // Protocol selects the OTLP exporter transport: "http" or "grpc". Empty + // defaults to "http". + Protocol string + // Logger, when set, receives a confirmation line once tracing is wired up + // and OTLP exporter errors (e.g. an unreachable collector) via the OTel + // global error handler. Optional. + Logger log.Logger } -// NewTracer creates a new Tracer and returns a closer which needs to be closed -// when the Tracer is no longer used to flush remaining traces. +// NewTracer creates a new Tracer and returns a closer that flushes pending +// spans and shuts down the OTel pipeline. func NewTracer(o *Options) (*Tracer, io.Closer, error) { if o == nil { o = new(Options) } - cfg := config.Configuration{ - Disabled: !o.Enabled, - ServiceName: o.ServiceName, - Sampler: &config.SamplerConfig{ - Type: jaeger.SamplerTypeConst, - Param: 1, - }, - Reporter: &config.ReporterConfig{ - LogSpans: true, - BufferFlushInterval: 1 * time.Second, - LocalAgentHostPort: o.Endpoint, - }, - Headers: &jaeger.HeadersConfig{ - TraceContextHeaderName: TraceContextHeaderName, - TraceBaggageHeaderPrefix: TraceBaggageHeaderPrefix, - }, + if !o.Enabled { + return noopTracer, noopCloser{}, nil } - t, closer, err := cfg.NewTracer() - if err != nil { - return nil, nil, err + if o.Endpoint == "" { + return nil, nil, errors.New("tracing-endpoint is required when tracing is enabled") } - return &Tracer{tracer: t}, closer, nil -} -// StartSpanFromContext starts a new tracing span that is either a root one or a -// child of existing one from the provided Context. If logger is provided, a new -// log Entry will be returned with "traceID" log field. -func (t *Tracer) StartSpanFromContext(ctx context.Context, operationName string, l log.Logger, opts ...opentracing.StartSpanOption) (opentracing.Span, log.Logger, context.Context) { - if t == nil { - t = noopTracer + res, err := newResource(o) + if err != nil { + return nil, nil, fmt.Errorf("otel resource: %w", err) } - var span opentracing.Span - if parentContext := FromContext(ctx); parentContext != nil { - opts = append(opts, opentracing.ChildOf(parentContext)) - span = t.tracer.StartSpan(operationName, opts...) - } else { - span = t.tracer.StartSpan(operationName, opts...) + ratio := o.SamplingRatio + if ratio < 0 { + ratio = 0 + } else if ratio > 1 { + ratio = 1 } - sc := span.Context() - return span, loggerWithTraceID(sc, l), WithContext(ctx, sc) -} -// FollowSpanFromContext starts a new tracing span that is either a root one or -// follows an existing one from the provided Context. If logger is provided, a new -// log Entry will be returned with "traceID" log field. -func (t *Tracer) FollowSpanFromContext(ctx context.Context, operationName string, l log.Logger, opts ...opentracing.StartSpanOption) (opentracing.Span, log.Logger, context.Context) { - if t == nil { - t = noopTracer + client, err := newOTLPClient(o) + if err != nil { + return nil, nil, err + } + exporter, err := otlptrace.New(context.Background(), client) + if err != nil { + return nil, nil, fmt.Errorf("otlp exporter: %w", err) } - var span opentracing.Span - if parentContext := FromContext(ctx); parentContext != nil { - opts = append(opts, opentracing.FollowsFrom(parentContext)) - span = t.tracer.StartSpan(operationName, opts...) - } else { - span = t.tracer.StartSpan(operationName, opts...) + tp := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))), + // Batch processor keeps SDK defaults; tunable via OTEL_BSP_* env vars. + sdktrace.WithBatcher(exporter), + ) + if o.Logger != nil { + // Route async OTLP export failures (e.g. an unreachable collector) to the + // node logger so they are visible instead of silently dropped. + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { + o.Logger.Warning("tracing exporter error", "error", err) + })) + o.Logger.Info("tracing enabled", "endpoint", o.Endpoint, "protocol", o.Protocol, "sampling_ratio", ratio) } - sc := span.Context() - return span, loggerWithTraceID(sc, l), WithContext(ctx, sc) + + return &Tracer{tracer: tp.Tracer(instrumentationName)}, providerCloser{tp: tp}, nil } -// AddContextHeader adds a tracing span context to provided p2p Headers from -// the go context. If the tracing span context is not present in go context, -// ErrContextNotFound is returned. -func (t *Tracer) AddContextHeader(ctx context.Context, headers p2p.Headers) error { - if t == nil { - t = noopTracer +// NewTracerFromProvider wraps an existing OTel TracerProvider in a Tracer. It is +// primarily useful for tests that need a recording tracer (e.g. one backed by an +// in-memory span recorder) rather than the OTLP exporter pipeline NewTracer builds. +func NewTracerFromProvider(tp trace.TracerProvider) *Tracer { + return &Tracer{tracer: tp.Tracer(instrumentationName)} +} + +// newResource builds the OTel resource describing this node. The env options +// come before WithAttributes so the configured values win over colliding +// OTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTES, while the environment can still +// add attributes (e.g. cluster, region). +func newResource(o *Options) (*resource.Resource, error) { + attrs := []attribute.KeyValue{semconv.ServiceName(o.ServiceName)} + if o.ServiceVersion != "" { + attrs = append(attrs, semconv.ServiceVersion(o.ServiceVersion)) + } + if o.Environment != "" { + attrs = append(attrs, semconv.DeploymentEnvironment(o.Environment)) + } + if o.InstanceID != "" { + attrs = append(attrs, semconv.ServiceInstanceID(o.InstanceID)) } - c := FromContext(ctx) - if c == nil { - return ErrContextNotFound + return resource.New(context.Background(), + resource.WithFromEnv(), + resource.WithTelemetrySDK(), + resource.WithHost(), + resource.WithAttributes(attrs...), + ) +} + +// newOTLPClient builds the OTLP client for the configured transport. An empty +// Protocol defaults to HTTP for backward compatibility with the initial OTLP +// rollout. +func newOTLPClient(o *Options) (otlptrace.Client, error) { + switch o.Protocol { + case "", protocolHTTP: + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(o.Endpoint), + otlptracehttp.WithCompression(otlptracehttp.GzipCompression), + } + if o.Insecure { + opts = append(opts, otlptracehttp.WithInsecure()) + } else if o.CAFile != "" { + tlsConfig, err := loadCAFile(o.CAFile) + if err != nil { + return nil, err + } + opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig)) + } + return otlptracehttp.NewClient(opts...), nil + case protocolGRPC: + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(o.Endpoint), + otlptracegrpc.WithCompressor("gzip"), + } + if o.Insecure { + opts = append(opts, otlptracegrpc.WithInsecure()) + } else if o.CAFile != "" { + tlsConfig, err := loadCAFile(o.CAFile) + if err != nil { + return nil, err + } + opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig))) + } + return otlptracegrpc.NewClient(opts...), nil + default: + return nil, fmt.Errorf("unsupported otlp protocol %q (want %q or %q)", o.Protocol, protocolHTTP, protocolGRPC) } +} - var b bytes.Buffer - w := bufio.NewWriter(&b) - if err := t.tracer.Inject(c, opentracing.Binary, w); err != nil { - return err +// loadCAFile reads a PEM-encoded CA bundle from path and returns a *tls.Config +// that uses it as the only root for OTLP collector certificate verification. +func loadCAFile(path string) (*tls.Config, error) { + pem, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read tracing CA file %q: %w", path, err) } - if err := w.Flush(); err != nil { - return err + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pem) { + return nil, fmt.Errorf("tracing CA file %q contains no valid PEM certificates", path) } + return &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}, nil +} - headers[p2p.HeaderNameTracingSpanContext] = b.Bytes() +// StartSpanFromContext starts a new span as a child of any span context already +// present in ctx. If logger is non-nil, a derived logger annotated with the +// trace id is returned alongside the new context. +func (t *Tracer) StartSpanFromContext(ctx context.Context, operationName string, l log.Logger, opts ...trace.SpanStartOption) (trace.Span, log.Logger, context.Context) { + ctx, span := t.otel().Start(ctx, operationName, opts...) + return span, loggerWithTraceID(span.SpanContext(), l), ctx +} - return nil +// FollowSpanFromContext starts a new span with a Link to the span context in +// ctx. Links are the OTel equivalent of OpenTracing's FollowsFrom relation: +// the new span is causally related but not a direct child. +func (t *Tracer) FollowSpanFromContext(ctx context.Context, operationName string, l log.Logger, opts ...trace.SpanStartOption) (trace.Span, log.Logger, context.Context) { + if parent := FromContext(ctx); parent.IsValid() { + opts = append(opts, trace.WithLinks(trace.Link{SpanContext: parent})) + } + + ctx, span := t.otel().Start(ctx, operationName, opts...) + return span, loggerWithTraceID(span.SpanContext(), l), ctx } -// FromHeaders returns tracing span context from p2p Headers. If the tracing -// span context is not present in go context, ErrContextNotFound is returned. -func (t *Tracer) FromHeaders(headers p2p.Headers) (opentracing.SpanContext, error) { - if t == nil { - t = noopTracer +// AddContextHeader serialises the active span context into the bee p2p header. +// It is safe to call on a nil receiver. +func (t *Tracer) AddContextHeader(ctx context.Context, headers p2p.Headers) error { + sc := FromContext(ctx) + if !sc.IsValid() { + return ErrContextNotFound } + headers[p2p.HeaderNameTracingSpanContext] = encodeP2PSpanContext(sc) + return nil +} + +// FromHeaders extracts the span context from the bee p2p header. ErrContextNotFound +// is returned when the header is absent or when its payload is undecodable — +// the latter lets mixed-version peers degrade to per-hop trace continuity loss +// rather than failing the stream. Safe to call on a nil receiver. +func (t *Tracer) FromHeaders(headers p2p.Headers) (trace.SpanContext, error) { v := headers[p2p.HeaderNameTracingSpanContext] if v == nil { - return nil, ErrContextNotFound + return trace.SpanContext{}, ErrContextNotFound } - c, err := t.tracer.Extract(opentracing.Binary, bytes.NewReader(v)) - if err != nil { - if errors.Is(err, opentracing.ErrSpanContextNotFound) { - return nil, ErrContextNotFound - } - return nil, err + sc, ok := decodeP2PSpanContext(v) + if !ok { + return trace.SpanContext{}, ErrContextNotFound } - - return c, nil + return sc, nil } -// WithContextFromHeaders returns a new context with injected tracing span -// context if they are found in p2p Headers. If the tracing span context is not -// present in go context, ErrContextNotFound is returned. +// WithContextFromHeaders extracts a span context from the p2p header and +// returns a new context carrying it. Safe to call on a nil receiver. func (t *Tracer) WithContextFromHeaders(ctx context.Context, headers p2p.Headers) (context.Context, error) { - if t == nil { - t = noopTracer - } - - c, err := t.FromHeaders(headers) + sc, err := t.FromHeaders(headers) if err != nil { return ctx, err } - return WithContext(ctx, c), nil + return WithContext(ctx, sc), nil } -// AddContextHTTPHeader adds a tracing span context to provided HTTP headers -// from the go context. If the tracing span context is not present in -// go context, ErrContextNotFound is returned. +// AddContextHTTPHeader injects the active span context into HTTP headers. +// Safe to call on a nil receiver. func (t *Tracer) AddContextHTTPHeader(ctx context.Context, headers http.Header) error { - if t == nil { - t = noopTracer - } - - c := FromContext(ctx) - if c == nil { + sc := FromContext(ctx) + if !sc.IsValid() { return ErrContextNotFound } - carrier := opentracing.HTTPHeadersCarrier(headers) - return t.tracer.Inject(c, opentracing.HTTPHeaders, carrier) + httpPropagator.Inject(trace.ContextWithSpanContext(ctx, sc), propagation.HeaderCarrier(headers)) + return nil } -// FromHTTPHeaders returns tracing span context from HTTP headers. If the tracing -// span context is not present in go context, ErrContextNotFound is returned. -func (t *Tracer) FromHTTPHeaders(headers http.Header) (opentracing.SpanContext, error) { - if t == nil { - t = noopTracer +// FromHTTPHeaders extracts a span context from a W3C traceparent header. +// Safe to call on a nil receiver. +func (t *Tracer) FromHTTPHeaders(headers http.Header) (trace.SpanContext, error) { + ctx := httpPropagator.Extract(context.Background(), propagation.HeaderCarrier(headers)) + sc := trace.SpanContextFromContext(ctx) + if !sc.IsValid() { + return trace.SpanContext{}, ErrContextNotFound } - - carrier := opentracing.HTTPHeadersCarrier(headers) - c, err := t.tracer.Extract(opentracing.HTTPHeaders, carrier) - if err != nil { - if errors.Is(err, opentracing.ErrSpanContextNotFound) { - return nil, ErrContextNotFound - } - return nil, err - } - - return c, nil + return sc, nil } -// WithContextFromHTTPHeaders returns a new context with injected tracing span -// context if they are found in HTTP headers. If the tracing span context is not -// present in go context, ErrContextNotFound is returned. +// WithContextFromHTTPHeaders extracts a span context from HTTP headers and +// returns a new context carrying it. Safe to call on a nil receiver. func (t *Tracer) WithContextFromHTTPHeaders(ctx context.Context, headers http.Header) (context.Context, error) { - if t == nil { - t = noopTracer - } - - c, err := t.FromHTTPHeaders(headers) + sc, err := t.FromHTTPHeaders(headers) if err != nil { return ctx, err } + return WithContext(ctx, sc), nil +} - return WithContext(ctx, c), nil +// WithContext stores a span context in ctx using the standard OTel context +// key, so any OTel-aware code (propagators, exporters) can find it. +func WithContext(ctx context.Context, sc trace.SpanContext) context.Context { + return trace.ContextWithSpanContext(ctx, sc) } -// WithContext adds tracing span context to go context. -func WithContext(ctx context.Context, c opentracing.SpanContext) context.Context { - return context.WithValue(ctx, contextKey{}, c) +// FromContext returns the span context currently associated with ctx. The +// returned SpanContext's IsValid() reports false when none is present. +func FromContext(ctx context.Context) trace.SpanContext { + return trace.SpanContextFromContext(ctx) } -// FromContext return tracing span context from go context. If the tracing span -// context is not present in go context, nil is returned. -func FromContext(ctx context.Context) opentracing.SpanContext { - c, ok := ctx.Value(contextKey{}).(opentracing.SpanContext) - if !ok { - return nil - } - return c +// RecordError attaches an error event to the span, marks the span status as +// Error, and records the supplied attributes alongside the error event. It is +// the OTel equivalent of the OpenTracing ext.LogError pattern bee used previously. +func RecordError(span trace.Span, err error, attrs ...attribute.KeyValue) { + span.RecordError(err, trace.WithAttributes(attrs...)) + span.SetStatus(codes.Error, err.Error()) } -// NewLoggerWithTraceID creates a new log Entry with "traceID" field added if it -// exists in tracing span context stored from go context. +// NewLoggerWithTraceID returns a logger annotated with the trace id from ctx, +// or the original logger if no valid span context is present. func NewLoggerWithTraceID(ctx context.Context, l log.Logger) log.Logger { return loggerWithTraceID(FromContext(ctx), l) } -func loggerWithTraceID(sc opentracing.SpanContext, l log.Logger) log.Logger { +func loggerWithTraceID(sc trace.SpanContext, l log.Logger) log.Logger { if l == nil { return nil } - jsc, ok := sc.(jaeger.SpanContext) - if !ok { + if !sc.HasTraceID() { return l } - traceID := jsc.TraceID() - if !traceID.IsValid() { - return l + return l.WithValues(logField, sc.TraceID().String()).Build() +} + +// encodeP2PSpanContext writes the trace+span ids and flags into a fixed-width +// payload. TraceState is omitted intentionally — bee does not use vendor +// tracestate routing. +func encodeP2PSpanContext(sc trace.SpanContext) []byte { + buf := make([]byte, p2pCarrierLen) + buf[0] = p2pCarrierVersion + tid := sc.TraceID() + sid := sc.SpanID() + copy(buf[1:17], tid[:]) + copy(buf[17:25], sid[:]) + buf[25] = byte(sc.TraceFlags()) + return buf +} + +func decodeP2PSpanContext(b []byte) (trace.SpanContext, bool) { + if len(b) != p2pCarrierLen || b[0] != p2pCarrierVersion { + return trace.SpanContext{}, false + } + var tid trace.TraceID + var sid trace.SpanID + copy(tid[:], b[1:17]) + copy(sid[:], b[17:25]) + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: tid, + SpanID: sid, + TraceFlags: trace.TraceFlags(b[25]), + Remote: true, + }) + if !sc.IsValid() { + return trace.SpanContext{}, false } - return l.WithValues(LogField, traceID).Build() + return sc, true +} + +// noopCloser is returned when tracing is disabled. +type noopCloser struct{} + +func (noopCloser) Close() error { return nil } + +// providerCloser flushes pending spans and stops the batch processor on Close. +type providerCloser struct { + tp *sdktrace.TracerProvider +} + +func (c providerCloser) Close() error { + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + return c.tp.Shutdown(ctx) } diff --git a/pkg/tracing/tracing_test.go b/pkg/tracing/tracing_test.go index c6acb330b18..b2842eea980 100644 --- a/pkg/tracing/tracing_test.go +++ b/pkg/tracing/tracing_test.go @@ -8,14 +8,13 @@ import ( "bytes" "context" "encoding/json" - "fmt" + "errors" "testing" "github.com/ethersphere/bee/v2/pkg/log" "github.com/ethersphere/bee/v2/pkg/p2p" "github.com/ethersphere/bee/v2/pkg/tracing" "github.com/ethersphere/bee/v2/pkg/util/testutil" - "github.com/uber/jaeger-client-go" ) func TestSpanFromHeaders(t *testing.T) { @@ -24,7 +23,7 @@ func TestSpanFromHeaders(t *testing.T) { tracer := newTracer(t) span, _, ctx := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() headers := make(p2p.Headers) if err := tracer.AddContextHeader(ctx, headers); err != nil { @@ -36,17 +35,20 @@ func TestSpanFromHeaders(t *testing.T) { t.Fatal(err) } - if fmt.Sprint(gotSpanContext) == "" { - t.Fatal("got empty span context") + if !gotSpanContext.IsValid() { + t.Fatal("got invalid span context") } - wantSpanContext := span.Context() - if fmt.Sprint(wantSpanContext) == "" { - t.Fatal("got empty start span context") + wantSpanContext := span.SpanContext() + if !wantSpanContext.IsValid() { + t.Fatal("got invalid start span context") } - if fmt.Sprint(gotSpanContext) != fmt.Sprint(wantSpanContext) { - t.Errorf("got span context %+v, want %+v", gotSpanContext, wantSpanContext) + if gotSpanContext.TraceID() != wantSpanContext.TraceID() { + t.Errorf("got trace id %s, want %s", gotSpanContext.TraceID(), wantSpanContext.TraceID()) + } + if gotSpanContext.SpanID() != wantSpanContext.SpanID() { + t.Errorf("got span id %s, want %s", gotSpanContext.SpanID(), wantSpanContext.SpanID()) } } @@ -56,7 +58,7 @@ func TestSpanWithContextFromHeaders(t *testing.T) { tracer := newTracer(t) span, _, ctx := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() headers := make(p2p.Headers) if err := tracer.AddContextHeader(ctx, headers); err != nil { @@ -69,17 +71,32 @@ func TestSpanWithContextFromHeaders(t *testing.T) { } gotSpanContext := tracing.FromContext(ctx) - if fmt.Sprint(gotSpanContext) == "" { - t.Fatal("got empty span context") + if !gotSpanContext.IsValid() { + t.Fatal("got invalid span context") } - wantSpanContext := span.Context() - if fmt.Sprint(wantSpanContext) == "" { - t.Fatal("got empty start span context") + wantSpanContext := span.SpanContext() + if gotSpanContext.TraceID() != wantSpanContext.TraceID() { + t.Errorf("got trace id %s, want %s", gotSpanContext.TraceID(), wantSpanContext.TraceID()) } +} + +// TestFromHeaders_undecodablePayload exercises the rolling-upgrade tolerance: +// peers running an older binary may send a payload in a different format and we +// must not fail the stream, just return ErrContextNotFound so trace continuity +// silently breaks at that hop. +func TestFromHeaders_undecodablePayload(t *testing.T) { + t.Parallel() + + tracer := newTracer(t) - if fmt.Sprint(gotSpanContext) != fmt.Sprint(wantSpanContext) { - t.Errorf("got span context %+v, want %+v", gotSpanContext, wantSpanContext) + headers := p2p.Headers{ + p2p.HeaderNameTracingSpanContext: []byte("not-a-valid-binary-carrier"), + } + + _, err := tracer.FromHeaders(headers) + if !errors.Is(err, tracing.ErrContextNotFound) { + t.Errorf("got error %v, want %v", err, tracing.ErrContextNotFound) } } @@ -89,20 +106,20 @@ func TestFromContext(t *testing.T) { tracer := newTracer(t) span, _, ctx := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() - wantSpanContext := span.Context() - if fmt.Sprint(wantSpanContext) == "" { - t.Fatal("got empty start span context") + wantSpanContext := span.SpanContext() + if !wantSpanContext.IsValid() { + t.Fatal("got invalid start span context") } gotSpanContext := tracing.FromContext(ctx) - if fmt.Sprint(gotSpanContext) == "" { - t.Fatal("got empty span context") + if !gotSpanContext.IsValid() { + t.Fatal("got invalid span context") } - if fmt.Sprint(gotSpanContext) != fmt.Sprint(wantSpanContext) { - t.Errorf("got span context %+v, want %+v", gotSpanContext, wantSpanContext) + if gotSpanContext.TraceID() != wantSpanContext.TraceID() { + t.Errorf("got trace id %s, want %s", gotSpanContext.TraceID(), wantSpanContext.TraceID()) } } @@ -112,22 +129,17 @@ func TestWithContext(t *testing.T) { tracer := newTracer(t) span, _, _ := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() - wantSpanContext := span.Context() - if fmt.Sprint(wantSpanContext) == "" { - t.Fatal("got empty start span context") + wantSpanContext := span.SpanContext() + if !wantSpanContext.IsValid() { + t.Fatal("got invalid start span context") } - ctx := tracing.WithContext(context.Background(), span.Context()) - + ctx := tracing.WithContext(context.Background(), wantSpanContext) gotSpanContext := tracing.FromContext(ctx) - if fmt.Sprint(gotSpanContext) == "" { - t.Fatal("got empty span context") - } - - if fmt.Sprint(gotSpanContext) != fmt.Sprint(wantSpanContext) { - t.Errorf("got span context %+v, want %+v", gotSpanContext, wantSpanContext) + if gotSpanContext.TraceID() != wantSpanContext.TraceID() { + t.Errorf("got trace id %s, want %s", gotSpanContext.TraceID(), wantSpanContext.TraceID()) } } @@ -139,9 +151,9 @@ func TestStartSpanFromContext_logger(t *testing.T) { buf := new(bytes.Buffer) span, logger, _ := tracer.StartSpanFromContext(context.Background(), "some-operation", log.NewLogger("test", log.WithSink(buf), log.WithJSONOutput())) - defer span.Finish() + defer span.End() - wantTraceID := span.Context().(jaeger.SpanContext).TraceID() + wantTraceID := span.SpanContext().TraceID().String() logger.Info("msg") data := make(map[string]any) @@ -159,8 +171,8 @@ func TestStartSpanFromContext_logger(t *testing.T) { t.Fatalf("log field %q is not string", tracing.LogField) } - if gotTraceID != wantTraceID.String() { - t.Errorf("got trace id %q, want %q", gotTraceID, wantTraceID.String()) + if gotTraceID != wantTraceID { + t.Errorf("got trace id %q, want %q", gotTraceID, wantTraceID) } } @@ -170,7 +182,7 @@ func TestStartSpanFromContext_nilLogger(t *testing.T) { tracer := newTracer(t) span, logger, _ := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() if logger != nil { t.Error("logger is not nil") @@ -183,13 +195,13 @@ func TestNewLoggerWithTraceID(t *testing.T) { tracer := newTracer(t) span, _, ctx := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() buf := new(bytes.Buffer) logger := tracing.NewLoggerWithTraceID(ctx, log.NewLogger("test", log.WithSink(buf), log.WithJSONOutput())) - wantTraceID := span.Context().(jaeger.SpanContext).TraceID() + wantTraceID := span.SpanContext().TraceID().String() logger.Info("msg") data := make(map[string]any) @@ -207,8 +219,8 @@ func TestNewLoggerWithTraceID(t *testing.T) { t.Fatalf("log field %q is not string", tracing.LogField) } - if gotTraceID != wantTraceID.String() { - t.Errorf("got trace id %q, want %q", gotTraceID, wantTraceID.String()) + if gotTraceID != wantTraceID { + t.Errorf("got trace id %q, want %q", gotTraceID, wantTraceID) } } @@ -218,7 +230,7 @@ func TestNewLoggerWithTraceID_nilLogger(t *testing.T) { tracer := newTracer(t) span, _, ctx := tracer.StartSpanFromContext(context.Background(), "some-operation", nil) - defer span.Finish() + defer span.End() logger := tracing.NewLoggerWithTraceID(ctx, nil) @@ -227,11 +239,17 @@ func TestNewLoggerWithTraceID_nilLogger(t *testing.T) { } } +// newTracer returns a Tracer wired to a real OTel SDK with an OTLP exporter +// pointed at a dead local endpoint. Spans receive valid contexts; the batch +// processor's failed exports are silently dropped in the background, which is +// fine for unit tests that only verify context propagation. func newTracer(t *testing.T) *tracing.Tracer { t.Helper() tracer, closer, err := tracing.NewTracer(&tracing.Options{ Enabled: true, + Endpoint: "127.0.0.1:1", + Insecure: true, ServiceName: "test", }) if err != nil {