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
4 changes: 4 additions & 0 deletions go.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ flowchart LR
chainlink-common --> chainlink-protos/billing/go
chainlink-common --> chainlink-protos/cre/go
chainlink-common --> chainlink-protos/linking-service/go
chainlink-common --> chainlink-protos/metering/go
chainlink-common --> chainlink-protos/node-platform
chainlink-common --> chainlink-protos/storage-service
chainlink-common --> freeport
Expand All @@ -31,6 +32,8 @@ flowchart LR
click chainlink-protos/cre/go href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/linking-service/go
click chainlink-protos/linking-service/go href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/metering/go
click chainlink-protos/metering/go href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/node-platform
click chainlink-protos/node-platform href "https://github.com/smartcontractkit/chainlink-protos"
chainlink-protos/storage-service
Expand Down Expand Up @@ -66,6 +69,7 @@ flowchart LR
chainlink-protos/billing/go
chainlink-protos/cre/go
chainlink-protos/linking-service/go
chainlink-protos/metering/go
chainlink-protos/node-platform
chainlink-protos/storage-service
chainlink-protos/workflows/go
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ require (
github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b
github.com/smartcontractkit/chainlink-protos/metering/go v0.0.0-20260706185759-873029fd9019
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b
github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0
github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9
Expand Down
2 changes: 2 additions & 0 deletions go.sum

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

53 changes: 53 additions & 0 deletions pkg/loop/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ const (
envTelemetryPrometheusBridgeEnabled = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED"
envTelemetryPrometheusBridgePrefixes = "CL_TELEMETRY_PROMETHEUS_BRIDGE_PREFIXES"
envTelemetryLogCompressor = "CL_TELEMETRY_LOG_COMPRESSOR"
envMeterRecordsEnabled = "CL_METER_RECORDS_ENABLED"
envMeterSnapshotsEnabled = "CL_METER_SNAPSHOTS_ENABLED"
envMeterProduct = "CL_METER_PRODUCT"
envMeterTenant = "CL_METER_TENANT"
envMeterNumericTenantID = "CL_METER_NUMERIC_TENANT_ID"
envMeterEnvironment = "CL_METER_ENVIRONMENT"
envMeterZone = "CL_METER_ZONE"
envMeterNodeID = "CL_METER_NODE_ID"

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.

note to reader, DonID isn't here b/c a node can belong to many DONs; it is the responsibility of the loop to properly receive its DonID from Initialise in the standard cap interface (or for non-CRE use cases, elsewhere)

envChipIngressEndpoint = "CL_CHIP_INGRESS_ENDPOINT"
envChipIngressInsecureConnection = "CL_CHIP_INGRESS_INSECURE_CONNECTION"
Expand Down Expand Up @@ -171,6 +179,27 @@ type EnvConfig struct {
TelemetryPrometheusBridgeEnabled bool
TelemetryPrometheusBridgePrefixes []string
TelemetryLogCompressor string
MeterRecordsEnabled bool
MeterSnapshotsEnabled bool

// MeterProduct / MeterTenant / MeterNumericTenantID / MeterEnvironment /
// MeterZone / MeterNodeID are
// the static deployment+node identity dimensions used as coarse
// metering/billing rollup dimensions. They are resolved once from node
// config by the host and delivered to every LOOP plugin over the env, the
// same channel as the meter-record toggles above (rather than the
// standard-capabilities boundary). Any may be empty if the host did not
// provide it.
//
// MeterNodeID is the node's logical name (e.g. "clp-cre-wf-zone-a-1"),
// not the CSA public key; the CSA key rides emitted events separately as the
// node_csa_key attribute.
MeterProduct string
MeterTenant string
MeterNumericTenantID string
MeterEnvironment string
MeterZone string
MeterNodeID string

TracingEnabled bool
TracingCollectorTarget string
Expand Down Expand Up @@ -264,6 +293,14 @@ func (e *EnvConfig) AsCmdEnv() (env []string) {
add(envTelemetryPrometheusBridgeEnabled, strconv.FormatBool(e.TelemetryPrometheusBridgeEnabled))
add(envTelemetryPrometheusBridgePrefixes, strings.Join(e.TelemetryPrometheusBridgePrefixes, ","))
add(envTelemetryLogCompressor, e.TelemetryLogCompressor)
add(envMeterRecordsEnabled, strconv.FormatBool(e.MeterRecordsEnabled))
add(envMeterSnapshotsEnabled, strconv.FormatBool(e.MeterSnapshotsEnabled))
add(envMeterProduct, e.MeterProduct)
add(envMeterTenant, e.MeterTenant)
add(envMeterNumericTenantID, e.MeterNumericTenantID)
add(envMeterEnvironment, e.MeterEnvironment)
add(envMeterZone, e.MeterZone)
add(envMeterNodeID, e.MeterNodeID)

add(envChipIngressEndpoint, e.ChipIngressEndpoint)
add(envChipIngressInsecureConnection, strconv.FormatBool(e.ChipIngressInsecureConnection))
Expand Down Expand Up @@ -518,6 +555,22 @@ func (e *EnvConfig) parse() error {
e.CRESettings = os.Getenv(envCRESettings)
e.CRESettingsDefault = os.Getenv(envCRESettingsDefault)

e.MeterRecordsEnabled, err = getBool(envMeterRecordsEnabled)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envMeterRecordsEnabled, err)
}
e.MeterSnapshotsEnabled, err = getBool(envMeterSnapshotsEnabled)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envMeterSnapshotsEnabled, err)
}

e.MeterProduct = os.Getenv(envMeterProduct)
e.MeterTenant = os.Getenv(envMeterTenant)
e.MeterNumericTenantID = os.Getenv(envMeterNumericTenantID)
e.MeterEnvironment = os.Getenv(envMeterEnvironment)
e.MeterZone = os.Getenv(envMeterZone)
e.MeterNodeID = os.Getenv(envMeterNodeID)

return nil
}

Expand Down
24 changes: 24 additions & 0 deletions pkg/loop/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ func TestEnvConfig_parse(t *testing.T) {
envTelemetryLogStreamingEnabled: "false",
envTelemetryPrometheusBridgeEnabled: "true",
envTelemetryPrometheusBridgePrefixes: "foo,bar",
envMeterRecordsEnabled: "true",
envMeterSnapshotsEnabled: "false",
envMeterProduct: "cre-mainline",
envMeterTenant: "mainline",
envMeterNumericTenantID: "42",
envMeterEnvironment: "production",
envMeterZone: "wf-zone-a",
envMeterNodeID: "csa-pubkey-1",

envChipIngressEndpoint: "chip-ingress.example.com:50051",
envChipIngressInsecureConnection: "true",
Expand Down Expand Up @@ -199,6 +207,14 @@ var envCfgFull = EnvConfig{
TelemetryLogStreamingEnabled: false,
TelemetryPrometheusBridgeEnabled: true,
TelemetryPrometheusBridgePrefixes: []string{"foo", "bar"},
MeterRecordsEnabled: true,
MeterSnapshotsEnabled: false,
MeterProduct: "cre-mainline",
MeterTenant: "mainline",
MeterNumericTenantID: "42",
MeterEnvironment: "production",
MeterZone: "wf-zone-a",
MeterNodeID: "csa-pubkey-1",

ChipIngressEndpoint: "chip-ingress.example.com:50051",
ChipIngressInsecureConnection: true,
Expand Down Expand Up @@ -264,6 +280,14 @@ func TestEnvConfig_AsCmdEnv(t *testing.T) {
assert.Equal(t, "false", got[envTelemetryLogStreamingEnabled])
assert.Equal(t, "true", got[envTelemetryPrometheusBridgeEnabled])
assert.Equal(t, "foo,bar", got[envTelemetryPrometheusBridgePrefixes])
assert.Equal(t, "true", got[envMeterRecordsEnabled])
assert.Equal(t, "false", got[envMeterSnapshotsEnabled])
assert.Equal(t, "cre-mainline", got[envMeterProduct])
assert.Equal(t, "mainline", got[envMeterTenant])
assert.Equal(t, "42", got[envMeterNumericTenantID])
assert.Equal(t, "production", got[envMeterEnvironment])
assert.Equal(t, "wf-zone-a", got[envMeterZone])
assert.Equal(t, "csa-pubkey-1", got[envMeterNodeID])

// Assert ChipIngress environment variables
assert.Equal(t, "chip-ingress.example.com:50051", got[envChipIngressEndpoint])
Expand Down
157 changes: 157 additions & 0 deletions pkg/resourcemanager/meterable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package resourcemanager

import (
"context"

meteringpb "github.com/smartcontractkit/chainlink-protos/metering/go"
)

// Meterable is implemented by producers that manage durable billable
// resources (trigger registrations, workflow specs, log filters). A producer
// registers itself with a ResourceManager (see ResourceManager.Register) so it
// is polled once per snapshot tick for the absolute state of its currently
// active resources, in addition to emitting lifecycle edges inline via
// EmitMeterRecord.
type Meterable interface {
// ResourceIdentity returns the producer's base identity: the coarse
// dimensions (product, tenant, numeric_tenant_id, environment, zone, don, service) plus
// the service-level resource_pool / resource_pool_id. Per-resource billing
// fields (resource_type/resource_id/org_id/event_id/value) are carried by
// Utilizations on MeterRecord and MeterSnapshot.
ResourceIdentity() ResourceIdentity

// GetUtilization returns the current level of the producer's currently
// active resources, one SnapshotEntry per resource. The manager emits one
// MeterSnapshot per entry.
//
// It is called on the snapshot tick and MUST be a cheap, non-blocking
// read-snapshot of in-memory state: no network, no disk, no lock held
// across I/O. It must tolerate ctx cancellation (returning promptly, and
// nil/empty is acceptable) and tolerate concurrent registration of new
// resources. An empty or nil return is valid and means nothing is currently
// active: no snapshots are emitted, and billing zeroes the resource out by
// its absence from subsequent snapshots.
GetUtilization(ctx context.Context) []SnapshotEntry
}

// SnapshotEntry is the current level of one active resource at a snapshot tick.
// Identity is the base resource identity, and Utilizations carries one or more
// billed dimensions for that resource (resource_type/resource_id/org_id/event_id/value).
type SnapshotEntry struct {
Identity ResourceIdentity
Utilizations []*meteringpb.Utilization
}

// DeploymentIdentity carries the static deployment + node identity dimensions
// that are fixed for a LOOP plugin process. They are resolved once from node
// config by the host and delivered to every LOOP plugin over the environment
// (loop.EnvConfig), not the standard-capabilities boundary, so any LOOP plugin
// can populate the coarse metering rollup dimensions. Any field may be empty if
// the host did not provide it.
type DeploymentIdentity struct {

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.

Where does this get used? Are the env vars meant to plug into this struct?

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.

yes, the env vars plug into, passed over to the loop via env. They should then be set on the ResourceManagers config so that they can be sourced into the emission of each event.

// Product is the deployment product, e.g. "cre".
Product string
// Tenant is the human-readable deployment tenant name, e.g. "mainline" or
// "enterprise".
Tenant string
// NumericTenantID is the numbered tenant identifier as a string.
NumericTenantID string
// Environment is the deployment environment, e.g. "production", "staging".
Environment string
// Zone is the deployment zone, e.g. "wf-zone-a".
Zone string
// NodeID is the node's logical name, e.g. "clp-cre-wf-zone-a-1". It is NOT
// the CSA public key; it is a stable name the billing service can use to
// look up the node's CSA key in the workflow registry. The CSA key itself is
// carried separately as the node_csa_key event attribute.
NodeID string
}

// DonIdentity captures DON-specific identity dimensions as one unit.
type DonIdentity struct {
// DonID is the DON ID the emitting service belongs to.
DonID string
// NodeID is the node's logical name within the scope of the DON, e.g.
// "clp-cre-wf-zone-a-1". It is a human-readable ID, NOT the CSA
// public key. The prefix can be redundant with other fully-qualified
// dimensions, but helps readability. The CSA key is emitted separately via
// the node_csa_key attribute.
NodeID string
}

// ResourceIdentity is the structured, first-class identity of a durable
// resource. Its fields map 1:1 to metering.v1.ResourceIdentity so every
// emitted record carries each dimension as a discrete column rather than a
// parsed dotted string or out-of-band telemetry attribute.
type ResourceIdentity struct {
// Product is the deployment product, e.g. "cre".
Product string

// Tenant is the human-readable deployment tenant name, e.g. "mainline" or
// "enterprise".
Tenant string

// NumericTenantID is the numbered tenant identifier as a string.
NumericTenantID string

// Environment is the deployment environment, e.g. "production",
// "staging".
Environment string

// Zone is the deployment zone, e.g. "wf-zone-a".
Zone string

// Don groups DON-specific identity dimensions so consumers can
// branch on one struct instead of handling don/node permutations.
Don *DonIdentity

// Service is the stable service constant identifying the emitting service,
// e.g. "cron-trigger", "http-trigger", "evm-log-trigger",
// "workflow-syncer-v2". It must not
// encode deployment environment or zone.
Service string

// ResourcePool is the service-level resource pool the record applies to,
// e.g. "trigger_registrations", "log_filters", "workflow_storage".
ResourcePool string

// ResourcePoolID optionally scopes identity further within the resource pool.
ResourcePoolID string
}

// toProto converts r to its wire form. Field order mirrors the proto.
func (r ResourceIdentity) toProto() *meteringpb.ResourceIdentity {
pb := &meteringpb.ResourceIdentity{
Product: r.Product,
Tenant: r.Tenant,
NumericTenantId: r.NumericTenantID,
Environment: r.Environment,
Zone: r.Zone,
Service: r.Service,
ResourcePool: r.ResourcePool,
ResourcePoolId: r.ResourcePoolID,
}
if r.Don != nil {
pb.Don = &meteringpb.DonIdentity{
DonId: r.Don.DonID,
NodeId: r.Don.NodeID,
}
}
return pb
}

// DonID returns the DON ID when present.
func (r ResourceIdentity) DonID() string {
if r.Don == nil {
return ""
}
return r.Don.DonID
}

// NodeID returns the node ID when present.
func (r ResourceIdentity) NodeID() string {
if r.Don == nil {
return ""
}
return r.Don.NodeID
}
Loading
Loading