Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/capability/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (c *config) bind(root *cobra.Command) (*cli.Binder, error) {
}
return b, errors.Join(
b.Register(root, &c.observability.TelemetryConfig),
b.RegisterInNamespace(root, pyroscopeNamespace, &c.observability.pyroscope),
b.Register(root, &c.observability.PyroscopeConfig),
b.RegisterInNamespace(root, httpNamespace, &c.observability.http),
b.RegisterInNamespace(root, capabilitiesNamespace, &c.capabilities),
b.RegisterInNamespace(root, grpcNamespace, &c.grpc),
Expand Down
8 changes: 0 additions & 8 deletions libs/capability/doccomments_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion libs/capability/internal/docgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ func main() {
if err := commentparsing.Run(commentparsing.RunArgs{
Markup: tomlmarkup.New(),
Roots: []any{
&capability.PyroscopeConfig{},
&capability.HTTPConfig{},
&capability.CapabilitiesConfig{},
&capability.GRPCConfig{},
Expand Down
5 changes: 3 additions & 2 deletions libs/capability/observability.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"go.opentelemetry.io/otel/sdk/metric"

"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/loop/pyroscope"
"github.com/smartcontractkit/chainlink-common/pkg/loop/telemetry"
)

Expand All @@ -16,8 +17,8 @@ const (
// the command runs and they have been decoded.
type observability struct {
telemetry.TelemetryConfig
pyroscope PyroscopeConfig
http HTTPConfig
pyroscope.PyroscopeConfig
http HTTPConfig

otelViews []metric.View // supplied through capability.Run
}
Expand Down
5 changes: 3 additions & 2 deletions libs/capability/observability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/loop/pyroscope"
"github.com/smartcontractkit/chainlink-common/pkg/services"
)

Expand All @@ -21,7 +22,7 @@ import (
// Nil rather than a no-op matters to the caller: the root would take a no-op and report on it, and
// a health report mentioning profiling that is not happening is worse than not mentioning it.
func TestProfilerWithoutAServer(t *testing.T) {
assert.Nil(t, newProfiler(logger.Test(t), "cron", PyroscopeConfig{}))
assert.Nil(t, newProfiler(logger.Test(t), "cron", pyroscope.PyroscopeConfig{}))
}

// TestProfilerWithAServer covers the other side: a configured one is a service the root can take,
Expand All @@ -30,7 +31,7 @@ func TestProfilerWithoutAServer(t *testing.T) {
// It is built rather than started. Starting one dials a pyroscope server, which a test has no
// business standing up to prove a constructor returned something.
func TestProfilerWithAServer(t *testing.T) {
p := newProfiler(logger.Test(t), "cron", PyroscopeConfig{ServerAddress: "pyro:4040"})
p := newProfiler(logger.Test(t), "cron", pyroscope.PyroscopeConfig{PyroscopeServerAddress: "pyro:4040"})

require.NotNil(t, p)
assert.Equal(t, "Profiler", p.Name())
Expand Down
56 changes: 9 additions & 47 deletions libs/capability/pyroscope.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,20 @@ package capability

import (
"context"
"fmt"
"runtime/debug"

"github.com/grafana/pyroscope-go"

commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/loop/pyroscope"
"github.com/smartcontractkit/chainlink-common/pkg/services"
)

// PyroscopeConfig configures continuous profiling. An empty ServerAddress leaves profiling off.
type PyroscopeConfig struct {
// ServerAddress is the pyroscope server address. Profiling is disabled when unset.
ServerAddress string
// AuthToken is the pyroscope auth token.
AuthToken commonconfig.SecretString
// Environment is the environment tag attached to profiles.
Environment string
}

// newProfiler returns continuous profiling as a service, or nil when no pyroscope server is
// configured.
func newProfiler(lggr logger.Logger, appName string, cfg PyroscopeConfig) *profilerService {
if cfg.ServerAddress == "" {
func newProfiler(lggr logger.Logger, appName string, cfg pyroscope.PyroscopeConfig) *profilerService {
if cfg.PyroscopeServerAddress == "" {
return nil
}

p := &profilerService{appName: appName, cfg: cfg}
p := &profilerService{lggr: lggr, appName: appName, cfg: cfg}
p.Service, _ = services.Config{
Name: "Profiler",
Start: p.start,
Expand All @@ -38,7 +24,7 @@ func newProfiler(lggr logger.Logger, appName string, cfg PyroscopeConfig) *profi
return p
}

func startProfiler(ctx context.Context, lggr logger.Logger, appName string, cfg PyroscopeConfig) (*profilerService, error) {
func startProfiler(ctx context.Context, lggr logger.Logger, appName string, cfg pyroscope.PyroscopeConfig) (*profilerService, error) {
profiler := newProfiler(lggr, appName, cfg)
if profiler == nil {
return nil, nil
Expand All @@ -50,43 +36,19 @@ func startProfiler(ctx context.Context, lggr logger.Logger, appName string, cfg
type profilerService struct {
services.Service

lggr logger.Logger
appName string
cfg PyroscopeConfig
cfg pyroscope.PyroscopeConfig

// profiler is what start made and close stops. Written by one hook and read by the other, which
// the state machine's lock orders: a service cannot be closed unless it started.
profiler *pyroscope.Profiler
}

func (p *profilerService) start(context.Context) error {
var ver, sha string
if bi, ok := debug.ReadBuildInfo(); ok {
ver = bi.Main.Version
sha = bi.Main.Sum
if len(sha) > 7 {
sha = sha[:7]
}
}

profiler, err := pyroscope.Start(pyroscope.Config{
ApplicationName: p.appName,
ServerAddress: p.cfg.ServerAddress,
AuthToken: string(p.cfg.AuthToken),
Tags: map[string]string{
"version": ver,
"sha": sha,
"environment": p.cfg.Environment,
},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
},
})
profiler, err := pyroscope.Start(p.lggr, p.cfg, p.appName, false)
if err != nil {
return fmt.Errorf("failed to start profiler: %w", err)
return err
}

p.profiler = profiler
Expand Down
47 changes: 47 additions & 0 deletions libs/capability/pyroscope_compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package capability

import (
"reflect"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/pkg/loop/pyroscope"
)

// TestPyroscopeConfigRoundTripsThroughLoopEnv is the contract with a LOOP host: the pyroscope env it
// sets with loop.EnvConfig.AsCmdEnv starts the same profiler here as loop.Server would.
func TestPyroscopeConfigRoundTripsThroughLoopEnv(t *testing.T) {
want := pyroscope.PyroscopeConfig{
PyroscopeAuthToken: "token",
PyroscopeServerAddress: "pyroscope:4040",
PyroscopeEnvironment: "staging",
PyroscopeLinkTracesToProfiles: true,
PyroscopePPROFBlockProfileRate: 7,
PyroscopePPROFMutexProfileFraction: 9,
}
requireNoZeroPyroscopeFields(t, want)

for _, kv := range want.AsCmdEnv() {
k, v, ok := strings.Cut(kv, "=")
require.True(t, ok, "malformed env entry %q", kv)
if strings.HasPrefix(k, "CL_PYROSCOPE_") {
t.Setenv(k, v)
}
}

assert.Equal(t, want, decoded(t).observability.PyroscopeConfig)
}

// requireNoZeroPyroscopeFields fails on any field but AppName left at its zero value, so a field
// added to pyroscope.Config can't go untested by defaulting through on both sides.
func requireNoZeroPyroscopeFields(t *testing.T, cfg pyroscope.PyroscopeConfig) {
t.Helper()
v := reflect.ValueOf(cfg)
for i := range v.NumField() {
name := v.Type().Field(i).Name
require.False(t, v.Field(i).IsZero(), "%s is unset; give it a non-default value", name)
}
}
2 changes: 1 addition & 1 deletion libs/capability/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func run(ctx context.Context, lggr logger.Logger, name string, cfg *config, newC

mux := http.NewServeMux()

profiler, err := startProfiler(ctx, lggr, name, cfg.observability.pyroscope)
profiler, err := startProfiler(ctx, lggr, name, cfg.observability.PyroscopeConfig)
if err != nil {
return fmt.Errorf("failed to start profiler: %w", err)
}
Expand Down
20 changes: 13 additions & 7 deletions libs/capability/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ var undocumentedLoopFlags = []string{
"chip-ingress-max-grpc-request-size",
"chip-ingress-send-interval",
"chip-ingress-send-timeout",
"pyroscope-auth-token",
"pyroscope-environment",
"pyroscope-link-traces-to-profiles",
"pyroscope-pprof-block-profile-rate",
"pyroscope-pprof-mutex-profile-fraction",
"pyroscope-server-address",
"telemetry-attribute",
"telemetry-attribute.<key>",
"telemetry-auth-header",
Expand Down Expand Up @@ -128,9 +134,9 @@ func TestObservabilityFlags(t *testing.T) {
"chip-ingress-endpoint",
"chip-ingress-insecure-connection",

"pyroscope.server-address",
"pyroscope.auth-token",
"pyroscope.environment",
"pyroscope-server-address",
"pyroscope-auth-token",
"pyroscope-environment",

"http.port",

Expand Down Expand Up @@ -203,8 +209,8 @@ func TestObservabilityDecodesFromFlags(t *testing.T) {
"--tracing-tls-cert-path", "/certs/trace.pem",
"--chip-ingress-endpoint", "chip:9000",
"--chip-ingress-insecure-connection", "true",
"--pyroscope.server-address", "pyro:4040",
"--pyroscope.environment", "staging",
"--pyroscope-server-address", "pyro:4040",
"--pyroscope-environment", "staging",
"--telemetry-endpoint", "otel:4317",
"--telemetry-insecure-connection", "true",
"--telemetry-attribute", "env=staging",
Expand All @@ -226,8 +232,8 @@ func TestObservabilityDecodesFromFlags(t *testing.T) {
assert.Equal(t, "chip:9000", obs.ChipIngressEndpoint)
assert.True(t, obs.ChipIngressInsecureConnection)

assert.Equal(t, "pyro:4040", obs.pyroscope.ServerAddress)
assert.Equal(t, "staging", obs.pyroscope.Environment)
assert.Equal(t, "pyro:4040", obs.PyroscopeServerAddress)
assert.Equal(t, "staging", obs.PyroscopeEnvironment)
}

// TestTelemetryDecodesFromEnv covers why the namespace and field names are what they are: a
Expand Down
4 changes: 2 additions & 2 deletions libs/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ require (
github.com/cenkalti/backoff/v5 v5.0.3
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/grafana/pyroscope-go v1.2.8
github.com/hashicorp/go-plugin v1.8.0
github.com/jonboulle/clockwork v0.5.0
github.com/prometheus/client_golang v1.23.2
github.com/samber/do/v2 v2.1.0
github.com/samber/go-type-to-string v1.8.0
github.com/shopspring/decimal v1.4.0
github.com/smartcontractkit/chainlink-common v0.11.2-0.20261022105827-74f78e92fb70
github.com/smartcontractkit/chainlink-common v0.11.2-0.20261022160347-6812ecc7b2db
github.com/smartcontractkit/chainlink-common/x/config v0.0.0-20261014172408-e5560b3eeb33
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20261013212015-e622e8b83b6b
github.com/smartcontractkit/libocr v0.0.0-20260810200708-618b5bf7f342
Expand Down Expand Up @@ -50,6 +49,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go v1.2.8 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions libs/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading