diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 5cd17e2f4c..c63933f658 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/internal/flags" + "github.com/ethereum/go-ethereum/internal/telemetry/tracesetup" "github.com/ethereum/go-ethereum/internal/version" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -239,18 +240,15 @@ func makeFullNode(ctx *cli.Context) *node.Node { cfg.Eth.OverrideVerkle = &v } - // Start metrics export if enabled + // Start metrics export if enabled. utils.SetupMetrics(&cfg.Metrics) - // Setup telemetry if enabled - telemetryService, err := utils.SetupTelemetry(ctx) - if err != nil { - utils.Fatalf("failed to setup telemetry: %v", err) - } - if telemetryService != nil { - utils.RegisterTelemetryService(telemetryService, stack) + // Setup OpenTelemetry reporting if enabled. + if err := tracesetup.StartTelemetry(ctx.Context, cfg.Node.OpenTelemetry, stack); err != nil { + utils.Fatalf("failed to setup OpenTelemetry: %v", err) } + // Add Ethereum service. backend, eth := utils.RegisterEthService(stack, &cfg.Eth) // Create gauge with geth system and build information diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 0c0e066abb..e59f5aebfc 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -198,6 +198,9 @@ var ( utils.RPCGlobalRangeLimitFlag, utils.RPCTelemetryFlag, utils.RPCTelemetryEndpointFlag, + utils.RPCTelemetryUserFlag, + utils.RPCTelemetryPasswordFlag, + utils.RPCTelemetryInstanceIDFlag, utils.RPCTelemetrySampleRatioFlag, } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 8a252c53c3..73eca936cd 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1056,6 +1056,24 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Category: flags.APICategory, } + RPCTelemetryUserFlag = &cli.StringFlag{ + Name: "rpc.telemetry.username", + Usage: "HTTP Basic Auth username for OpenTelemetry", + Category: flags.APICategory, + } + + RPCTelemetryPasswordFlag = &cli.StringFlag{ + Name: "rpc.telemetry.password", + Usage: "HTTP Basic Auth password for OpenTelemetry", + Category: flags.APICategory, + } + + RPCTelemetryInstanceIDFlag = &cli.StringFlag{ + Name: "rpc.telemetry.instance-id", + Usage: "OpenTelemetry instance ID", + Category: flags.APICategory, + } + RPCTelemetrySampleRatioFlag = &cli.Float64Flag{ Name: "rpc.telemetry.sample-ratio", Usage: "Defines the sampling ratio for RPC telemetry (0.0 to 1.0)", @@ -1452,6 +1470,7 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { setNodeUserIdent(ctx, cfg) SetDataDir(ctx, cfg) setSmartCard(ctx, cfg) + setOpenTelemetry(ctx, cfg) if ctx.IsSet(JWTSecretFlag.Name) { cfg.JWTSecret = ctx.String(JWTSecretFlag.Name) @@ -1519,6 +1538,33 @@ func setSmartCard(ctx *cli.Context, cfg *node.Config) { cfg.SmartCardDaemonPath = path } +func setOpenTelemetry(ctx *cli.Context, cfg *node.Config) { + tcfg := &cfg.OpenTelemetry + if ctx.IsSet(RPCTelemetryFlag.Name) { + tcfg.Enabled = ctx.Bool(RPCTelemetryFlag.Name) + } + + if ctx.IsSet(RPCTelemetryEndpointFlag.Name) { + tcfg.Endpoint = ctx.String(RPCTelemetryEndpointFlag.Name) + } + if ctx.IsSet(RPCTelemetryUserFlag.Name) { + tcfg.AuthUser = ctx.String(RPCTelemetryUserFlag.Name) + } + if ctx.IsSet(RPCTelemetryPasswordFlag.Name) { + tcfg.AuthPassword = ctx.String(RPCTelemetryPasswordFlag.Name) + } + if ctx.IsSet(RPCTelemetrySampleRatioFlag.Name) { + tcfg.SampleRatio = ctx.Float64(RPCTelemetrySampleRatioFlag.Name) + } + if ctx.IsSet(RPCTelemetryInstanceIDFlag.Name) { + tcfg.InstanceID = ctx.String(RPCTelemetryInstanceIDFlag.Name) + } + + if tcfg.Endpoint != "" && !tcfg.Enabled { + log.Warn(fmt.Sprintf("OpenTelemetry endpoint configured but telemetry is not enabled, use --%s to enable.", RPCTelemetryFlag.Name)) + } +} + func SetDataDir(ctx *cli.Context, cfg *node.Config) { switch { case ctx.IsSet(DataDirFlag.Name): diff --git a/cmd/utils/telemetry.go b/cmd/utils/telemetry.go deleted file mode 100644 index 2c2cd7a4bb..0000000000 --- a/cmd/utils/telemetry.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package utils - -import ( - "context" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/internal/telemetry/config" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/urfave/cli/v2" -) - -// telemetryService wraps the TelemetryProvider to implement node.Lifecycle. -type telemetryService struct { - telemetryProvider *config.TelemetryProvider -} - -// Start implements node.Lifecycle. -func (t *telemetryService) Start() error { - return nil // TelemetryProvider is already started during setup -} - -// Stop implements node.Lifecycle. -func (t *telemetryService) Stop() error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := t.telemetryProvider.Shutdown(ctx); err != nil { - log.Error("Failed to stop telemetry service", "err", err) - return err - } - log.Info("Telemetry stopped") - return nil -} - -// SetupTelemetry initializes OpenTelemetry tracing based on CLI flags. -func SetupTelemetry(ctx *cli.Context) (*telemetryService, error) { - if !ctx.Bool(RPCTelemetryFlag.Name) { - return nil, nil - } - endpoint := ctx.String(RPCTelemetryEndpointFlag.Name) - if endpoint == "" { - return nil, nil - } - sampleRatio := ctx.Float64(RPCTelemetrySampleRatioFlag.Name) - if sampleRatio < 0 || sampleRatio > 1 { - return nil, fmt.Errorf("invalid sample ratio: %f", sampleRatio) - } - setupCtx := ctx.Context - if setupCtx == nil { - setupCtx = context.Background() - } - - // Configure OpenTelemetry tracing - handle, err := config.Setup( - setupCtx, - endpoint, - sampleRatio, - ) - if err != nil { - return nil, err - } - log.Info("Telemetry enabled", "endpoint", endpoint) - return &telemetryService{telemetryProvider: handle}, nil -} - -// RegisterTelemetryService registers the telemetryService with the node. -func RegisterTelemetryService(service *telemetryService, stack *node.Node) { - stack.RegisterLifecycle(service) -} diff --git a/internal/telemetry/config/config.go b/internal/telemetry/tracesetup/setup.go similarity index 62% rename from internal/telemetry/config/config.go rename to internal/telemetry/tracesetup/setup.go index 99bcbd87f0..e48bce9283 100644 --- a/internal/telemetry/config/config.go +++ b/internal/telemetry/tracesetup/setup.go @@ -14,15 +14,18 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package config +package tracesetup import ( "context" + "encoding/base64" "fmt" "net/url" "time" "github.com/ethereum/go-ethereum/internal/version" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" @@ -32,22 +35,42 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.38.0" ) -// TelemetryProvider wraps a TracerProvider and exposes the Shutdown method. -type TelemetryProvider struct { - tracerProvider *sdktrace.TracerProvider +// Service wraps the provider to implement node.Lifecycle. +type Service struct { + provider *sdktrace.TracerProvider } -// Shutdown shuts down the TracerProvider. -func (t *TelemetryProvider) Shutdown(ctx context.Context) error { - return t.tracerProvider.Shutdown(ctx) +// Start implements node.Lifecycle. +func (t *Service) Start() error { + return nil // provider is already started during setup } -// Setup initializes telemetry with the given parameters. -func Setup(ctx context.Context, endpoint string, sampleRatio float64) (*TelemetryProvider, error) { +// Stop implements node.Lifecycle. +func (t *Service) Stop() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := t.provider.Shutdown(ctx); err != nil { + log.Error("Failed to stop OpenTelemetry service", "err", err) + return err + } + log.Debug("OpenTelemetry stopped") + return nil +} + +// StartTelemetry initializes telemetry with the given parameters. +func StartTelemetry(ctx context.Context, cfg node.OpenTelemetryConfig, stack *node.Node) error { + if !cfg.Enabled { + return nil + } + + if cfg.SampleRatio < 0 || cfg.SampleRatio > 1 { + return fmt.Errorf("invalid sample ratio: %f", cfg.SampleRatio) + } + // Create exporter based on endpoint URL - u, err := url.Parse(endpoint) + u, err := url.Parse(cfg.Endpoint) if err != nil { - return nil, fmt.Errorf("invalid rpc tracing endpoint URL: %w", err) + return fmt.Errorf("invalid rpc tracing endpoint URL: %w", err) } var exporter sdktrace.SpanExporter switch u.Scheme { @@ -61,18 +84,23 @@ func Setup(ctx context.Context, endpoint string, sampleRatio float64) (*Telemetr if u.Path != "" && u.Path != "/" { opts = append(opts, otlptracehttp.WithURLPath(u.Path)) } + if cfg.AuthUser != "" { + opts = append(opts, otlptracehttp.WithHeaders(map[string]string{ + "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(cfg.AuthUser+":"+cfg.AuthPassword)), + })) + } exporter, err = otlptracehttp.New(ctx, opts...) default: - return nil, fmt.Errorf("unsupported telemetry url scheme: %s", u.Scheme) + return fmt.Errorf("unsupported telemetry url scheme: %s", u.Scheme) } if err != nil { - return nil, fmt.Errorf("failed to create telemetry exporter: %w", err) + return fmt.Errorf("failed to create telemetry exporter: %w", err) } // Define sampler such that if no parent span is available, // then sampleRatio of traces are sampled; otherwise, inherit // the parent's sampling decision. - sampler := sdktrace.ParentBased(sdktrace.TraceIDRatioBased(sampleRatio)) + sampler := sdktrace.ParentBased(sdktrace.TraceIDRatioBased(cfg.SampleRatio)) // Define batch span processor options batchOpts := []sdktrace.BatchSpanProcessorOption{ @@ -87,11 +115,14 @@ func Setup(ctx context.Context, endpoint string, sampleRatio float64) (*Telemetr } // Define resource with service and client information - res := resource.NewWithAttributes( - semconv.SchemaURL, + var attr = []attribute.KeyValue{ semconv.ServiceName("geth"), attribute.String("client.name", version.ClientName("geth")), - ) + } + if cfg.InstanceID != "" { + attr = append(attr, semconv.ServiceInstanceID(cfg.InstanceID)) + } + res := resource.NewWithAttributes(semconv.SchemaURL, attr...) // Configure TracerProvider and set it as the global tracer provider tp := sdktrace.NewTracerProvider( @@ -107,6 +138,8 @@ func Setup(ctx context.Context, endpoint string, sampleRatio float64) (*Telemetr propagation.TraceContext{}, propagation.Baggage{}, )) - - return &TelemetryProvider{tracerProvider: tp}, nil + service := &Service{provider: tp} + stack.RegisterLifecycle(service) + log.Info("OpenTelemetry tracing enabled", "endpoint", cfg.Endpoint) + return nil } diff --git a/node/config.go b/node/config.go index dc436876cc..5e0ec63ee5 100644 --- a/node/config.go +++ b/node/config.go @@ -191,9 +191,7 @@ type Config struct { GraphQLVirtualHosts []string `toml:",omitempty"` // Logger is a custom logger to use with the p2p.Server. - Logger log.Logger `toml:",omitempty"` - - oldGethResourceWarning bool + Logger log.Logger `toml:"-,omitempty"` // AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC. AllowUnprotectedTxs bool `toml:",omitempty"` @@ -210,7 +208,28 @@ type Config struct { // EnablePersonal enables the deprecated personal namespace. EnablePersonal bool `toml:"-"` + // Configures database engine used by the node. DBEngine string `toml:",omitempty"` + + // Configures OpenTelemetry reporting. + OpenTelemetry OpenTelemetryConfig `toml:",omitempty"` + + oldGethResourceWarning bool +} + +// OpenTelemetryConfig has settings for +type OpenTelemetryConfig struct { + Enabled bool `toml:",omitempty"` + + InstanceID string `toml:",omitempty"` + + // Exporter endpoint. + Endpoint string `toml:",omitempty"` + AuthUser string `toml:",omitempty"` + AuthPassword string `toml:",omitempty"` + + // Percentage of sampled traces. + SampleRatio float64 `toml:",omitempty"` } // IPCEndpoint resolves an IPC endpoint based on a configured value, taking into