Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7bafc6b
adding resource manager for wf size
patrickhuie19 Jun 15, 2026
276ddee
bumping common and protos to remove local replace
patrickhuie19 Jun 15, 2026
937a244
updates from proto and plumbing config
patrickhuie19 Jul 1, 2026
8fd5251
adding numericTenantID + reducing naming complexity in protos
patrickhuie19 Jul 7, 2026
d288672
feat(metering): TOML-only gating, node_id logical name, syncer delta …
patrickhuie19 Jul 9, 2026
1898360
feat(metering): supply deterministic cross-node event_id from workflo…
patrickhuie19 Jul 10, 2026
58607d9
updates from chainlink-common changes
patrickhuie19 Jul 15, 2026
c662958
Merge branch 'develop' into feat/shared-2712
patrickhuie19 Jul 15, 2026
72b90d5
Merge branch 'develop' into feat/shared-2712
patrickhuie19 Jul 15, 2026
a500659
lint
patrickhuie19 Jul 15, 2026
cc1ab09
removing local replaces
patrickhuie19 Jul 15, 2026
1b333be
cleanup
patrickhuie19 Jul 16, 2026
3d48b1c
dependency bumps, refactoring, some correctness changes
patrickhuie19 Jul 30, 2026
af84741
Merge branch 'develop' into feat/shared-2712
patrickhuie19 Jul 30, 2026
9ca0e7f
Merge branch 'develop' into feat/shared-2712
patrickhuie19 Jul 30, 2026
54689c5
fixing tests
patrickhuie19 Jul 30, 2026
d17cd8b
cleaning up reconcile orphan behavior; adding paused state to workflo…
patrickhuie19 Jul 31, 2026
bfb6a4b
removing redundant WorkflowDONSubscriber; lint
patrickhuie19 Jul 31, 2026
a8829ad
extracting ResourceIdentity to a wrapper; simplifying reconcileOrphan…
patrickhuie19 Aug 6, 2026
27f3ad9
lint + fix tests
patrickhuie19 Aug 7, 2026
e6d049d
lint:
patrickhuie19 Aug 7, 2026
dcc0e66
Merge branch 'develop' into feat/shared-2712
patrickhuie19 Aug 10, 2026
4a4eb83
bumping common + lint
patrickhuie19 Aug 11, 2026
712d4fd
Merge branch 'develop' into feat/shared-2712
patrickhuie19 Aug 11, 2026
a3a78af
attempt at fixing local-cre in CI
patrickhuie19 Aug 11, 2026
9192de6
lint
patrickhuie19 Aug 11, 2026
2bb12c8
lint
patrickhuie19 Aug 11, 2026
cc8ab0f
lint again
patrickhuie19 Aug 11, 2026
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 core/cmd/shell_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func genTestEVMRelayers(t *testing.T, cfg chainlink.GeneralConfig, ds sqlutil.Da
f := chainlink.RelayerFactory{
Logger: lggr,
LoopRegistry: plugins.NewLoopRegistry(lggr, cfg.AppID().String(), cfg.Feature().LogPoller(), cfg.Database(),
cfg.Mercury(), cfg.Pyroscope(), cfg.AutoPprof(), cfg.Tracing(), cfg.Telemetry(), nil, "", cfg.LOOPP()),
cfg.Mercury(), cfg.Pyroscope(), cfg.AutoPprof(), cfg.Tracing(), cfg.Telemetry(), cfg.Metering(), nil, "", cfg.LOOPP()),
CapabilitiesRegistry: capabilities.NewRegistry(lggr),
}

Expand Down
1 change: 1 addition & 0 deletions core/config/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type AppConfig interface {
WebServer() WebServer
Tracing() Tracing
Telemetry() Telemetry
Metering() Metering
CRE() CRE
CCV() CCV
Billing() Billing
Expand Down
22 changes: 22 additions & 0 deletions core/config/docs/core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,28 @@ Enabled = false # Default
# By default, we only forward the go runtime metrics. Empty means forward everything.
Prefixes = ["go_"] # Default

# Metering configures durable resource metering emission and the coarse
# deployment/node identity dimensions stamped on emitted MeterRecords and
# MeterSnapshots.
[Metering]
# MeterRecordsEnabled enables durable MeterRecord emission for LOOP plugins.
MeterRecordsEnabled = false # Default
# MeterSnapshotsEnabled enables durable MeterSnapshot emission for LOOP plugins.
# Requires MeterRecordsEnabled = true.
MeterSnapshotsEnabled = false # Default
# Product is the deployment product identity dimension, e.g. 'cre'.
Product = 'cre' # Default
# Tenant is the human-readable tenant name, e.g. 'mainline'.
Tenant = '' # Default
# NumericTenantID is the numbered tenant identifier represented as a string.
NumericTenantID = '' # Default

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get the numeric tenant ID from a cresetting that @prashantkumar1982 created just fyi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right use of it? cresettings are sourced from job specs, no? I'm wary of adding dependencies into multiple sources of config. The cap registry and TOML is already two places to manage and keep in-sync.

# Environment is the deployment environment identity dimension, e.g. 'production'.
Environment = '' # Default
# Zone is the deployment zone identity dimension, e.g. 'wf-zone-a'.
Zone = '' # Default
# NodeID is the node's logical name, e.g. 'clp-cre-wf-zone-a-1' (not the CSA public key).
NodeID = '' # Default

[CRE.Streams]
# WsURL is the websockets url for the streams sdk config
WsURL = "streams.url" # Example
Expand Down
16 changes: 16 additions & 0 deletions core/config/metering_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package config

// Metering exposes durable resource-metering configuration: the emission
// toggles and the coarse deployment/node identity dimensions stamped on emitted
// MeterRecords and MeterSnapshots. These are passed via loop.EnvConfig to every LOOP
// plugin.
type Metering interface {
MeterRecordsEnabled() bool
MeterSnapshotsEnabled() bool
Product() string
Tenant() string
NumericTenantID() string
Environment() string
Zone() string
NodeID() string
}
71 changes: 71 additions & 0 deletions core/config/toml/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Core struct {
Mercury Mercury `toml:",omitempty"`
Capabilities Capabilities `toml:",omitempty"`
Telemetry Telemetry `toml:",omitempty"`
Metering Metering `toml:",omitempty"`
Comment on lines 66 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if just Metering is clear, especially alongside Telemetry. What about something like ResourceMetering? Or would it make sense as CRE.Metering?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are Metering and Telemetry related? I generally think of Telemetry as metrics/traces. Metering i almost always associate with billing. Telemetry can definitely include Metering if its fire and forget, but this Metering is durable by design.

CRE hierarchy is correct for the registry syncer, but not strictly correct as ResourceManagers will exist outside the CRE

Workflows Workflows `toml:",omitempty"`
CRE CreConfig `toml:",omitempty"`
Billing Billing `toml:",omitempty"`
Expand Down Expand Up @@ -112,6 +113,7 @@ func (c *Core) SetFrom(f *Core) {
c.JobDistributor.setFrom(&f.JobDistributor)
c.Tracing.setFrom(&f.Tracing)
c.Telemetry.setFrom(&f.Telemetry)
c.Metering.setFrom(&f.Metering)
c.CRE.setFrom(&f.CRE)
c.Billing.setFrom(&f.Billing)
c.BridgeStatusReporter.setFrom(&f.BridgeStatusReporter)
Expand Down Expand Up @@ -3163,6 +3165,75 @@ func (b *Telemetry) ValidateConfig() (err error) {
return err
}

// Metering configures durable resource metering emission and the coarse
// deployment/node identity dimensions stamped on emitted MeterRecords and
// MeterSnapshots. These are passed via loop.EnvConfig to every LOOP plugin.
type Metering struct {
// MeterRecordsEnabled enables durable MeterRecord emission for LOOP plugins.
MeterRecordsEnabled *bool
// MeterSnapshotsEnabled enables durable MeterSnapshot emission. Requires
// MeterRecordsEnabled to be true.
MeterSnapshotsEnabled *bool
// Product is the deployment product identity dimension, e.g. "cre".
Product *string
// Tenant is the human-readable tenant name, e.g. "mainline".
Tenant *string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this different from the cresettings TenantID?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TenantID is also included, the human readable name is helpful for o11y and debugging

// NumericTenantID is the numbered tenant identifier as a string.
NumericTenantID *string
// Environment is the deployment environment dimension, e.g. "production".
Environment *string
// Zone is the deployment zone dimension, e.g. "wf-zone-a".
Zone *string
// NodeID is the node's logical name, e.g. "clp-cre-wf-zone-a-1" (NOT the CSA
// public key)
NodeID *string
}

func (b *Metering) setFrom(f *Metering) {
if v := f.MeterRecordsEnabled; v != nil {
b.MeterRecordsEnabled = v
}
if v := f.MeterSnapshotsEnabled; v != nil {
b.MeterSnapshotsEnabled = v
}
if v := f.Product; v != nil {
b.Product = v
}
if v := f.Tenant; v != nil {
b.Tenant = v
}
if v := f.NumericTenantID; v != nil {
b.NumericTenantID = v
}
if v := f.Environment; v != nil {
b.Environment = v
}
if v := f.Zone; v != nil {
b.Zone = v
}
if v := f.NodeID; v != nil {
b.NodeID = v
}
}

func (b *Metering) ValidateConfig() (err error) {
if b.MeterSnapshotsEnabled != nil && *b.MeterSnapshotsEnabled && (b.MeterRecordsEnabled == nil || !*b.MeterRecordsEnabled) {
err = errors.Join(err, configutils.ErrInvalid{
Name: "MeterSnapshotsEnabled",
Value: true,
Msg: "requires MeterRecordsEnabled to be true",
})
}
if b.MeterRecordsEnabled != nil && *b.MeterRecordsEnabled && (b.NodeID == nil || *b.NodeID == "") {
err = errors.Join(err, configutils.ErrInvalid{
Name: "NodeID",
Value: "",
Msg: "must be non-empty when MeterRecordsEnabled is true (an empty NodeID collapses per-node snapshot dedup scope DON-wide)",
})
}
return err
}

type PrometheusBridge struct {
Enabled *bool
Prefixes []string
Expand Down
64 changes: 64 additions & 0 deletions core/config/toml/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,3 +860,67 @@ func durationPtr(d time.Duration) *commonconfig.Duration {
cd := *commonconfig.MustNewDuration(d)
return &cd
}

func TestMetering_ValidateConfig(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
config *Metering
expectError bool
errorMsg string
}{
{
name: "disabled with all nil fields",
config: &Metering{},
expectError: false,
},
{
name: "records enabled with non-empty NodeID",
config: &Metering{
MeterRecordsEnabled: new(true),
NodeID: new("clp-cre-wf-zone-a-1"),
},
expectError: false,
},
{
name: "records enabled with nil NodeID",
config: &Metering{
MeterRecordsEnabled: new(true),
NodeID: nil,
},
expectError: true,
errorMsg: "NodeID",
},
{
name: "records enabled with empty NodeID",
config: &Metering{
MeterRecordsEnabled: new(true),
NodeID: new(""),
},
expectError: true,
errorMsg: "NodeID",
},
{
name: "snapshots enabled without records enabled",
config: &Metering{
MeterSnapshotsEnabled: new(true),
NodeID: new("clp-cre-wf-zone-a-1"),
},
expectError: true,
errorMsg: "requires MeterRecordsEnabled to be true",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := tc.config.ValidateConfig()
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,16 @@
#docker_file = "core/chainlink.Dockerfile"
image = "chainlink-tmp:latest"
user_config_overrides = """
# [Telemetry], [Billing], and [Metering] are framework-managed and are
# REJECTED here (see validateUserConfigOverrides in system-tests/lib):
# the framework generates them itself, pointing telemetry at the
# chip-router so downstream subscribers (e.g. the CI test sink) see all
# events. To enable metering, set enable_metering = true on the nodeset.
[Log]
Level = 'debug'
JSONConsole = true

[Telemetry]
Enabled = true
# assumes that CTF's observability stack is running on the local host
# 4317 is OTEL ingress port of the stack (LGTM)
# command: ctf obs up
Endpoint = 'host.docker.internal:4317'
# default service-name and port used by locally running Chip Ingress
# command: go run . env chip-ingress-stack start
ChipIngressEndpoint = 'chip-ingress:50051'
InsecureConnection = true
TraceSampleRatio = 1
HeartbeatInterval = '30s'

# Remove this bit to allow downloading workflows from remote sources
[CRE.WorkflowFetcher]
URL = "file:///home/chainlink/workflows"

[Billing]
URL = "host.docker.internal:2223"
TLSEnabled = false
"""
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
name = "workflow"
don_family = "test-don-family"
don_types = ["workflow"]
enable_metering = true
override_mode = "all"
http_port_range_start = 10100

Expand Down Expand Up @@ -71,6 +72,7 @@
name = "capabilities"
don_family = "test-don-family"
don_types = ["capabilities"]
enable_metering = true
exposes_remote_capabilities = true
override_mode = "all"
http_port_range_start = 10200
Expand Down
18 changes: 17 additions & 1 deletion core/scripts/cre/environment/environment/chip_ingress_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,14 @@ func schemaCommitRefFromGoMod(ctx context.Context, repoRoot, targetModule string
// getSchemaSetFromGoMod resolves SchemaSets from chainlink-protos commits pinned in go.mod:
// - workflows (chip-cre.json) for CRE/workflow telemetry
// - node-platform (chip-schemas.json) for PluginRelayerConfigEmitter / common.v1.ChainPluginConfig
// - metering (chip-cll.meter.json) for durable resource metering (MeterRecord/MeterSnapshot on
// the cll.meter domain); without this, ChIP Ingress rejects those events at pre-publish encode
// time with "Subject 'cll-meter-metering.v1.MeterRecord' not found" and drops them silently.
func getSchemaSetFromGoMod(ctx context.Context) ([]chipingressset.SchemaSet, error) {
const (
workflowsModule = "github.com/smartcontractkit/chainlink-protos/workflows/go"
nodePlatformModule = "github.com/smartcontractkit/chainlink-protos/node-platform"
meteringModule = "github.com/smartcontractkit/chainlink-protos/metering/go"
)

repoRoot, err := filepath.Abs(relativePathToRepoRoot)
Expand All @@ -100,6 +104,12 @@ func getSchemaSetFromGoMod(ctx context.Context) ([]chipingressset.SchemaSet, err
}
framework.L.Info().Msgf("Extracted commit ref for %s: %s (from version: %s)", nodePlatformModule, npRef, npVer)

meteringRef, meteringVer, err := schemaCommitRefFromGoMod(ctx, repoRoot, meteringModule)
if err != nil {
return nil, err
}
framework.L.Info().Msgf("Extracted commit ref for %s: %s (from version: %s)", meteringModule, meteringRef, meteringVer)

return []chipingressset.SchemaSet{
{
URI: chainlinkProtosGitURI,
Expand All @@ -113,6 +123,12 @@ func getSchemaSetFromGoMod(ctx context.Context) ([]chipingressset.SchemaSet, err
SchemaDir: "node-platform",
ConfigFile: "chip-schemas.json",
},
{
URI: chainlinkProtosGitURI,
Ref: meteringRef,
SchemaDir: "metering",
ConfigFile: "chip-cll.meter.json",
},
}, nil
}

Expand Down Expand Up @@ -1045,6 +1061,6 @@ func fetchAndRegisterProtosCmd() *cobra.Command {
})
},
}
cmd.Flags().StringVarP(&chipIngressGRPCURL, "chip-ingress-grpc-url", "h", "localhost:"+chipingressset.DEFAULT_CHIP_INGRESS_GRPC_PORT, "Chip Ingress GRPC URL")
cmd.Flags().StringVarP(&chipIngressGRPCURL, "chip-ingress-grpc-url", "u", "localhost:"+chipingressset.DEFAULT_CHIP_INGRESS_GRPC_PORT, "Chip Ingress GRPC URL")
return cmd
}
2 changes: 1 addition & 1 deletion core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ require (
github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect
github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // indirect
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect
github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260710151514-27b5a126dabe // indirect
github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260729184203-90b4cdd48536 // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect
github.com/smartcontractkit/chainlink-protos/orchestrator v0.11.0 // indirect
github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect
Expand Down
4 changes: 2 additions & 2 deletions core/scripts/go.sum

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

2 changes: 1 addition & 1 deletion core/services/chainlink/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err
}
loopRegistry := plugins.NewLoopRegistry(globalLogger, cfg.AppID().String(), cfg.Feature().LogPoller(),
cfg.Database(), cfg.Mercury(), cfg.Pyroscope(), cfg.AutoPprof(), cfg.Tracing(), cfg.Telemetry(),
beholderAuthHeaders, csaPubKeyHex, cfg.LOOPP())
cfg.Metering(), beholderAuthHeaders, csaPubKeyHex, cfg.LOOPP())

relayerFactory := RelayerFactory{
Logger: opts.Logger,
Expand Down
4 changes: 4 additions & 0 deletions core/services/chainlink/config_general.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,10 @@ func (g *generalConfig) Telemetry() coreconfig.Telemetry {
return &telemetryConfig{s: g.c.Telemetry}
}

func (g *generalConfig) Metering() coreconfig.Metering {
return &meteringConfig{s: g.c.Metering}
}

func (g *generalConfig) CRE() coreconfig.CRE {
return &creConfig{s: g.secrets.CRE, c: g.c.CRE}
}
Expand Down
Loading
Loading