Skip to content
This repository was archived by the owner on Oct 20, 2024. It is now read-only.

Commit 9e4372f

Browse files
authored
Instrument traces and metrics with opentelemetry (#232)
1 parent 817933b commit 9e4372f

File tree

8 files changed

+354
-20
lines changed

8 files changed

+354
-20
lines changed

go.mod

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/ethereum/go-ethereum v1.11.5
99
github.com/gin-contrib/cors v1.4.0
1010
github.com/gin-gonic/gin v1.9.0
11-
github.com/go-logr/logr v1.2.3
11+
github.com/go-logr/logr v1.2.4
1212
github.com/go-logr/zerologr v1.2.3
1313
github.com/go-playground/validator/v10 v10.12.0
1414
github.com/google/go-cmp v0.5.9
@@ -18,15 +18,26 @@ require (
1818
github.com/spf13/cobra v1.6.1
1919
github.com/spf13/viper v1.15.0
2020
github.com/wangjia184/sortedset v0.0.0-20220209072355-af6d6d227aa7
21+
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.42.0
22+
go.opentelemetry.io/otel v1.16.0
23+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0
24+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0
25+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0
26+
go.opentelemetry.io/otel/metric v1.16.0
27+
go.opentelemetry.io/otel/sdk v1.16.0
28+
go.opentelemetry.io/otel/sdk/metric v0.39.0
29+
go.opentelemetry.io/otel/trace v1.16.0
2130
golang.org/x/sync v0.1.0
22-
golang.org/x/text v0.8.0
31+
golang.org/x/text v0.9.0
32+
google.golang.org/grpc v1.55.0
2333
)
2434

2535
require (
2636
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
2737
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
2838
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
2939
github.com/bytedance/sonic v1.8.0 // indirect
40+
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
3041
github.com/cespare/xxhash v1.1.0 // indirect
3142
github.com/cespare/xxhash/v2 v2.2.0 // indirect
3243
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
@@ -35,19 +46,21 @@ require (
3546
github.com/dustin/go-humanize v1.0.0 // indirect
3647
github.com/fsnotify/fsnotify v1.6.0 // indirect
3748
github.com/gin-contrib/sse v0.1.0 // indirect
49+
github.com/go-logr/stdr v1.2.2 // indirect
3850
github.com/go-ole/go-ole v1.2.1 // indirect
3951
github.com/go-playground/locales v0.14.1 // indirect
4052
github.com/go-playground/universal-translator v0.18.1 // indirect
4153
github.com/go-stack/stack v1.8.1 // indirect
4254
github.com/goccy/go-json v0.10.0 // indirect
4355
github.com/gogo/protobuf v1.3.2 // indirect
44-
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
56+
github.com/golang/glog v1.1.0 // indirect
4557
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
46-
github.com/golang/protobuf v1.5.2 // indirect
58+
github.com/golang/protobuf v1.5.3 // indirect
4759
github.com/golang/snappy v0.0.4 // indirect
4860
github.com/google/flatbuffers v1.12.1 // indirect
4961
github.com/google/uuid v1.3.0 // indirect
5062
github.com/gorilla/websocket v1.4.2 // indirect
63+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
5164
github.com/hashicorp/hcl v1.0.0 // indirect
5265
github.com/holiman/uint256 v1.2.0 // indirect
5366
github.com/inconshreveable/mousetrap v1.0.1 // indirect
@@ -73,11 +86,15 @@ require (
7386
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
7487
github.com/ugorji/go/codec v1.2.9 // indirect
7588
go.opencensus.io v0.24.0 // indirect
89+
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
90+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 // indirect
91+
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
7692
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
7793
golang.org/x/crypto v0.7.0 // indirect
78-
golang.org/x/net v0.8.0 // indirect
79-
golang.org/x/sys v0.6.0 // indirect
80-
google.golang.org/protobuf v1.28.1 // indirect
94+
golang.org/x/net v0.10.0 // indirect
95+
golang.org/x/sys v0.8.0 // indirect
96+
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
97+
google.golang.org/protobuf v1.30.0 // indirect
8198
gopkg.in/ini.v1 v1.67.0 // indirect
8299
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
83100
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 79 additions & 13 deletions
Large diffs are not rendered by default.

internal/config/values.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,29 @@ type Values struct {
3232
EthBuilderUrl string
3333
BlocksInTheFuture int
3434

35+
// Observability variables.
36+
OTELServiceName string
37+
OTELCollectorHeaders map[string]string
38+
OTELCollectorUrl string
39+
OTELInsecureMode bool
40+
3541
// Undocumented variables.
3642
DebugMode bool
3743
GinMode string
3844
}
3945

46+
func envKeyValStringToMap(s string) map[string]string {
47+
out := map[string]string{}
48+
for _, pair := range strings.Split(s, "&") {
49+
kv := strings.Split(pair, "=")
50+
if len(kv) != 2 {
51+
break
52+
}
53+
out[kv[0]] = kv[1]
54+
}
55+
return out
56+
}
57+
4058
func envArrayToAddressSlice(s string) []common.Address {
4159
env := strings.Split(s, ",")
4260
slc := []common.Address{}
@@ -62,6 +80,7 @@ func GetValues() *Values {
6280
viper.SetDefault("erc4337_bundler_max_batch_gas_limit", 25000000)
6381
viper.SetDefault("erc4337_bundler_max_ops_for_unstaked_sender", 4)
6482
viper.SetDefault("erc4337_bundler_blocks_in_the_future", 25)
83+
viper.SetDefault("erc4337_bundler_otel_insecure_mode", false)
6584
viper.SetDefault("erc4337_bundler_debug_mode", false)
6685
viper.SetDefault("erc4337_bundler_gin_mode", gin.ReleaseMode)
6786

@@ -92,6 +111,10 @@ func GetValues() *Values {
92111
_ = viper.BindEnv("erc4337_bundler_relayer_banned_time_window")
93112
_ = viper.BindEnv("erc4337_bundler_eth_builder_url")
94113
_ = viper.BindEnv("erc4337_bundler_blocks_in_the_future")
114+
_ = viper.BindEnv("erc4337_bundler_otel_service_name")
115+
_ = viper.BindEnv("erc4337_bundler_otel_collector_headers")
116+
_ = viper.BindEnv("erc4337_bundler_otel_collector_url")
117+
_ = viper.BindEnv("erc4337_bundler_otel_insecure_mode")
95118
_ = viper.BindEnv("erc4337_bundler_debug_mode")
96119
_ = viper.BindEnv("erc4337_bundler_gin_mode")
97120

@@ -119,6 +142,12 @@ func GetValues() *Values {
119142
}
120143
}
121144

145+
// Validate O11Y variables
146+
if viper.IsSet("erc4337_bundler_otel_service_name") &&
147+
variableNotSetOrIsNil("erc4337_bundler_otel_collector_url") {
148+
panic("Fatal config error: erc4337_bundler_otel_service_name is set without a collector URL")
149+
}
150+
122151
// Return Values
123152
privateKey := viper.GetString("erc4337_bundler_private_key")
124153
ethClientUrl := viper.GetString("erc4337_bundler_eth_client_url")
@@ -133,6 +162,10 @@ func GetValues() *Values {
133162
relayerBannedTimeWindow := viper.GetInt("erc4337_bundler_relayer_banned_time_window") * int(time.Second)
134163
ethBuilderUrl := viper.GetString("erc4337_bundler_eth_builder_url")
135164
blocksInTheFuture := viper.GetInt("erc4337_bundler_blocks_in_the_future")
165+
otelServiceName := viper.GetString("erc4337_bundler_otel_service_name")
166+
otelCollectorHeader := envKeyValStringToMap(viper.GetString("erc4337_bundler_otel_collector_headers"))
167+
otelCollectorUrl := viper.GetString("erc4337_bundler_otel_collector_url")
168+
otelInsecureMode := viper.GetBool("erc4337_bundler_otel_insecure_mode")
136169
debugMode := viper.GetBool("erc4337_bundler_debug_mode")
137170
ginMode := viper.GetString("erc4337_bundler_gin_mode")
138171
return &Values{
@@ -149,6 +182,10 @@ func GetValues() *Values {
149182
RelayerBannedTimeWindow: time.Duration(relayerBannedTimeWindow),
150183
EthBuilderUrl: ethBuilderUrl,
151184
BlocksInTheFuture: blocksInTheFuture,
185+
OTELServiceName: otelServiceName,
186+
OTELCollectorHeaders: otelCollectorHeader,
187+
OTELCollectorUrl: otelCollectorUrl,
188+
OTELInsecureMode: otelInsecureMode,
152189
DebugMode: debugMode,
153190
GinMode: ginMode,
154191
}

internal/o11y/init.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package o11y
2+
3+
import (
4+
"context"
5+
"log"
6+
"math/big"
7+
"time"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"go.opentelemetry.io/otel"
11+
"go.opentelemetry.io/otel/attribute"
12+
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
13+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
14+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
15+
"google.golang.org/grpc/credentials"
16+
17+
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
18+
"go.opentelemetry.io/otel/sdk/resource"
19+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
20+
)
21+
22+
type Opts struct {
23+
ServiceName string
24+
CollectorHeader map[string]string
25+
CollectorUrl string
26+
InsecureMode bool
27+
28+
// Bundler specific attributes
29+
ChainID *big.Int
30+
Address common.Address
31+
}
32+
33+
func initResources(opts *Opts) *resource.Resource {
34+
resources, err := resource.New(
35+
context.Background(),
36+
resource.WithAttributes(
37+
attribute.String("service.name", opts.ServiceName),
38+
attribute.String("library.language", "go"),
39+
attribute.String("bundler.address", opts.Address.Hex()),
40+
attribute.Int64("bundler.chain_id", opts.ChainID.Int64()),
41+
),
42+
)
43+
if err != nil {
44+
log.Fatal(err)
45+
}
46+
47+
return resources
48+
}
49+
50+
func IsEnabled(serviceName string) bool {
51+
return len(serviceName) > 0
52+
}
53+
54+
func InitTracer(opts *Opts) func() {
55+
secureOption := otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, ""))
56+
if opts.InsecureMode {
57+
secureOption = otlptracegrpc.WithInsecure()
58+
}
59+
60+
exporter, err := otlptrace.New(
61+
context.Background(),
62+
otlptracegrpc.NewClient(
63+
secureOption,
64+
otlptracegrpc.WithHeaders(opts.CollectorHeader),
65+
otlptracegrpc.WithEndpoint(opts.CollectorUrl),
66+
),
67+
)
68+
if err != nil {
69+
log.Fatal(err)
70+
}
71+
72+
otel.SetTracerProvider(
73+
sdktrace.NewTracerProvider(
74+
sdktrace.WithSampler(sdktrace.AlwaysSample()),
75+
sdktrace.WithBatcher(exporter),
76+
sdktrace.WithResource(initResources(opts)),
77+
),
78+
)
79+
return func() {
80+
_ = exporter.Shutdown(context.Background())
81+
}
82+
}
83+
84+
func InitMetrics(opts *Opts) func() {
85+
secureOption := otlpmetricgrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, ""))
86+
if opts.InsecureMode {
87+
secureOption = otlpmetricgrpc.WithInsecure()
88+
}
89+
90+
exporter, err := otlpmetricgrpc.New(
91+
context.Background(),
92+
secureOption,
93+
otlpmetricgrpc.WithHeaders(opts.CollectorHeader),
94+
otlpmetricgrpc.WithEndpoint(opts.CollectorUrl),
95+
)
96+
if err != nil {
97+
log.Fatal(err)
98+
}
99+
100+
otel.SetMeterProvider(
101+
sdkmetric.NewMeterProvider(
102+
sdkmetric.WithResource(initResources(opts)),
103+
sdkmetric.WithReader(
104+
sdkmetric.NewPeriodicReader(exporter, sdkmetric.WithInterval(30*time.Second)),
105+
),
106+
),
107+
)
108+
return func() {
109+
_ = exporter.Shutdown(context.Background())
110+
}
111+
}

internal/start/private.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/gin-gonic/gin"
1515
"github.com/stackup-wallet/stackup-bundler/internal/config"
1616
"github.com/stackup-wallet/stackup-bundler/internal/logger"
17+
"github.com/stackup-wallet/stackup-bundler/internal/o11y"
1718
"github.com/stackup-wallet/stackup-bundler/pkg/bundler"
1819
"github.com/stackup-wallet/stackup-bundler/pkg/client"
1920
"github.com/stackup-wallet/stackup-bundler/pkg/gas"
@@ -25,6 +26,8 @@ import (
2526
"github.com/stackup-wallet/stackup-bundler/pkg/modules/paymaster"
2627
"github.com/stackup-wallet/stackup-bundler/pkg/modules/relay"
2728
"github.com/stackup-wallet/stackup-bundler/pkg/signer"
29+
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
30+
"go.opentelemetry.io/otel"
2831
)
2932

3033
func PrivateMode() {
@@ -59,6 +62,24 @@ func PrivateMode() {
5962
log.Fatal(err)
6063
}
6164

65+
if o11y.IsEnabled(conf.OTELServiceName) {
66+
o11yOpts := &o11y.Opts{
67+
ServiceName: conf.OTELServiceName,
68+
CollectorHeader: conf.OTELCollectorHeaders,
69+
CollectorUrl: conf.OTELCollectorUrl,
70+
InsecureMode: conf.OTELInsecureMode,
71+
72+
ChainID: chain,
73+
Address: eoa.Address,
74+
}
75+
76+
tracerCleanup := o11y.InitTracer(o11yOpts)
77+
defer tracerCleanup()
78+
79+
metricsCleanup := o11y.InitMetrics(o11yOpts)
80+
defer metricsCleanup()
81+
}
82+
6283
ov := gas.NewDefaultOverhead()
6384
if chain.Cmp(config.ArbitrumOneChainID) == 0 || chain.Cmp(config.ArbitrumGoerliChainID) == 0 {
6485
ov.SetCalcPreVerificationGasFunc(gas.CalcArbitrumPVGWithEthClient(rpc, conf.SupportedEntryPoints[0]))
@@ -115,6 +136,9 @@ func PrivateMode() {
115136
b.SetGetGasTipFunc(gasprice.GetGasTipWithEthClient(eth))
116137
b.SetGetLegacyGasPriceFunc(gasprice.GetLegacyGasPriceWithEthClient(eth))
117138
b.UseLogger(logr)
139+
if err := b.UserMeter(otel.GetMeterProvider().Meter("bundler")); err != nil {
140+
log.Fatal(err)
141+
}
118142
b.UseModules(
119143
gasprice.SortByGasPrice(),
120144
gasprice.FilterUnderpriced(),
@@ -145,6 +169,9 @@ func PrivateMode() {
145169
if err := r.SetTrustedProxies(nil); err != nil {
146170
log.Fatal(err)
147171
}
172+
if o11y.IsEnabled(conf.OTELServiceName) {
173+
r.Use(otelgin.Middleware(conf.OTELServiceName))
174+
}
148175
r.Use(
149176
cors.Default(),
150177
logger.WithLogr(logr),
@@ -156,6 +183,7 @@ func PrivateMode() {
156183
handlers := []gin.HandlerFunc{
157184
relayer.FilterByClientID(),
158185
jsonrpc.Controller(client.NewRpcAdapter(c, d)),
186+
jsonrpc.WithOTELTracerAttributes(),
159187
relayer.MapUserOpHashToClientID(),
160188
}
161189
r.POST("/", handlers...)

0 commit comments

Comments
 (0)