From 6fb684d0a8425812891b76700206eaaae33f01d6 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:37:51 -0400 Subject: [PATCH 1/8] Wire resource attributes into Beholder ChipIngress emitters Propagate node resource attributes from beholder.Config.ResourceAttributes to the cre topic via ChipIngress CloudEvent extensions. Depends on the new chipingress API introduced in #2267. --- pkg/beholder/batch_emitter_service.go | 10 ++-- pkg/beholder/batch_emitter_service_test.go | 44 ++++++++++++++++++ pkg/beholder/chip_ingress_emitter.go | 48 +++++++++++++++---- pkg/beholder/chip_ingress_emitter_test.go | 54 ++++++++++++++++++++++ pkg/beholder/client.go | 7 ++- pkg/beholder/resource_attributes.go | 15 ++++++ pkg/beholder/resource_attributes_test.go | 28 +++++++++++ 7 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 pkg/beholder/resource_attributes.go create mode 100644 pkg/beholder/resource_attributes_test.go diff --git a/pkg/beholder/batch_emitter_service.go b/pkg/beholder/batch_emitter_service.go index a81fd82bc9..4a01d8d2b8 100644 --- a/pkg/beholder/batch_emitter_service.go +++ b/pkg/beholder/batch_emitter_service.go @@ -21,7 +21,8 @@ type ChipIngressBatchEmitterService struct { services.Service eng *services.Engine - batchClient *batch.Client + batchClient *batch.Client + resourceAttrs map[string]string metricAttrsCache sync.Map // map[string]otelmetric.MeasurementOption metrics batchEmitterMetrics @@ -84,8 +85,9 @@ func NewChipIngressBatchEmitterService(client chipingress.Client, cfg Config, lg } e := &ChipIngressBatchEmitterService{ - batchClient: batchClient, - metrics: metrics, + batchClient: batchClient, + resourceAttrs: resourceAttributesToStringMap(cfg.ResourceAttributes), + metrics: metrics, } e.Service, e.eng = services.Config{ @@ -136,7 +138,7 @@ func (e *ChipIngressBatchEmitterService) emitInternal(ctx context.Context, body attributes := newAttributes(attrKVs...) - event, err := chipingress.NewEvent(domain, entity, body, attributes) + event, err := chipingress.NewEventWithOpts(domain, entity, body, attributes, chipingress.WithResourceAttributeExtensions(e.resourceAttrs)) if err != nil { return fmt.Errorf("failed to create CloudEvent: %w", err) } diff --git a/pkg/beholder/batch_emitter_service_test.go b/pkg/beholder/batch_emitter_service_test.go index 35a321cdac..800240d5c9 100644 --- a/pkg/beholder/batch_emitter_service_test.go +++ b/pkg/beholder/batch_emitter_service_test.go @@ -121,6 +121,50 @@ func TestChipIngressBatchEmitterService_Emit(t *testing.T) { }) } +func TestChipIngressBatchEmitterService_ResourceAttributes(t *testing.T) { + clientMock := mocks.NewClient(t) + clientMock.EXPECT().Close().Return(nil).Maybe() + + var mu sync.Mutex + var receivedBatch *chipingress.CloudEventBatch + clientMock. + On("PublishBatch", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + mu.Lock() + defer mu.Unlock() + receivedBatch = args.Get(1).(*chipingress.CloudEventBatch) + }). + Return(nil, nil) + + cfg := newTestConfig() + cfg.ChipIngressSendInterval = 50 * time.Millisecond + cfg.ResourceAttributes = []attribute.KeyValue{attribute.String("chain_id", "1")} + + emitter, err := beholder.NewChipIngressBatchEmitterService(clientMock, cfg, newTestLogger(t)) + require.NoError(t, err) + require.NoError(t, emitter.Start(t.Context())) + + err = emitter.Emit(t.Context(), []byte("test-payload"), + beholder.AttrKeyDomain, "my-domain", + beholder.AttrKeyEntity, "my-entity", + ) + require.NoError(t, err) + + assert.Eventually(t, func() bool { + mu.Lock() + defer mu.Unlock() + return receivedBatch != nil + }, 2*time.Second, 10*time.Millisecond) + + require.NoError(t, emitter.Close()) + + mu.Lock() + defer mu.Unlock() + require.Len(t, receivedBatch.Events, 1) + require.NotNil(t, receivedBatch.Events[0].Attributes["chainid"]) + assert.Equal(t, "1", receivedBatch.Events[0].Attributes["chainid"].GetCeString()) +} + func TestChipIngressBatchEmitterService_CloudEventFormat(t *testing.T) { clientMock := mocks.NewClient(t) clientMock.EXPECT().Close().Return(nil).Maybe() diff --git a/pkg/beholder/chip_ingress_emitter.go b/pkg/beholder/chip_ingress_emitter.go index 4bca08b47f..f8525b4eeb 100644 --- a/pkg/beholder/chip_ingress_emitter.go +++ b/pkg/beholder/chip_ingress_emitter.go @@ -12,14 +12,24 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" ) +// resourceAttrExtensions holds resource attributes to stamp as CloudEvent extensions on every +// emitted event. It is stored behind a pointer on ChipIngressEmitter (rather than as a bare map +// field) so the struct itself stays a comparable type — a map field would make it incomparable, +// which is an exported-API-breaking change per apidiff. A nil *resourceAttrExtensions means no +// resource attributes are configured. +type resourceAttrExtensions struct { + attrs map[string]string +} + // ChipIngressEmitter wraps a synchronous chipingress.Client.Publish call // in a fire-and-forget goroutine so callers are never blocked. type ChipIngressEmitter struct { - client chipingress.Client - lggr logger.Logger - stopCh services.StopChan - wg services.WaitGroup - closed atomic.Bool + client chipingress.Client + lggr logger.Logger + resourceAttrs *resourceAttrExtensions + stopCh services.StopChan + wg services.WaitGroup + closed atomic.Bool } func NewChipIngressEmitter(client chipingress.Client) (Emitter, error) { @@ -31,8 +41,15 @@ type ChipIngressEmitterConfig struct { Lggr logger.Logger } -// New creates a ChipIngressEmitter from the config. +// New creates a ChipIngressEmitter from the config, with no resource attributes configured. func (c ChipIngressEmitterConfig) New(client chipingress.Client) (Emitter, error) { + return c.NewWithResourceAttributes(client, nil) +} + +// NewWithResourceAttributes creates a ChipIngressEmitter from the config, additionally stamping +// attrs as CloudEvent extensions (via chipingress.WithResourceAttributeExtensions) on every +// emitted event. +func (c ChipIngressEmitterConfig) NewWithResourceAttributes(client chipingress.Client, attrs map[string]string) (Emitter, error) { if client == nil { return nil, errors.New("chip ingress client is nil") } @@ -41,10 +58,16 @@ func (c ChipIngressEmitterConfig) New(client chipingress.Client) (Emitter, error lggr = logger.Nop() } + var resourceAttrs *resourceAttrExtensions + if len(attrs) > 0 { + resourceAttrs = &resourceAttrExtensions{attrs: attrs} + } + return &ChipIngressEmitter{ - client: client, - lggr: lggr, - stopCh: make(services.StopChan), + client: client, + lggr: lggr, + resourceAttrs: resourceAttrs, + stopCh: make(services.StopChan), }, nil } @@ -65,7 +88,12 @@ func (c *ChipIngressEmitter) Emit(ctx context.Context, body []byte, attrKVs ...a return err } - event, err := chipingress.NewEvent(sourceDomain, entityType, body, newAttributes(attrKVs...)) + var event chipingress.CloudEvent + if c.resourceAttrs != nil { + event, err = chipingress.NewEventWithOpts(sourceDomain, entityType, body, newAttributes(attrKVs...), chipingress.WithResourceAttributeExtensions(c.resourceAttrs.attrs)) + } else { + event, err = chipingress.NewEvent(sourceDomain, entityType, body, newAttributes(attrKVs...)) + } if err != nil { return err } diff --git a/pkg/beholder/chip_ingress_emitter_test.go b/pkg/beholder/chip_ingress_emitter_test.go index 11349d335b..0590befd5d 100644 --- a/pkg/beholder/chip_ingress_emitter_test.go +++ b/pkg/beholder/chip_ingress_emitter_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/chipingress" "github.com/smartcontractkit/chainlink-common/pkg/chipingress/mocks" "github.com/smartcontractkit/chainlink-common/pkg/logger" ) @@ -73,6 +74,59 @@ func TestChipIngressEmit(t *testing.T) { assert.Error(t, err) }) + t.Run("resource attributes are stamped as sanitized CE extensions", func(t *testing.T) { + clientMock := mocks.NewClient(t) + + var published *chipingress.CloudEventPb + clientMock. + On("Publish", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + published = args.Get(1).(*chipingress.CloudEventPb) + }). + Return(nil, nil) + clientMock.On("Close").Return(nil) + + emitter, err := beholder.ChipIngressEmitterConfig{ + Lggr: logger.Test(t), + }.NewWithResourceAttributes(clientMock, map[string]string{"chain_id": "1"}) + require.NoError(t, err) + + err = emitter.Emit(t.Context(), body, beholder.AttrKeyDomain, domain, beholder.AttrKeyEntity, entity) + require.NoError(t, err) + + require.NoError(t, emitter.Close()) + clientMock.AssertExpectations(t) + + require.NotNil(t, published) + require.NotNil(t, published.Attributes["chainid"]) + assert.Equal(t, "1", published.Attributes["chainid"].GetCeString()) + }) + + t.Run("New(client) with no resource attributes stamps no extra extensions (backward-compat)", func(t *testing.T) { + clientMock := mocks.NewClient(t) + + var published *chipingress.CloudEventPb + clientMock. + On("Publish", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + published = args.Get(1).(*chipingress.CloudEventPb) + }). + Return(nil, nil) + clientMock.On("Close").Return(nil) + + emitter, err := beholder.ChipIngressEmitterConfig{Lggr: logger.Test(t)}.New(clientMock) + require.NoError(t, err) + + err = emitter.Emit(t.Context(), body, beholder.AttrKeyDomain, domain, beholder.AttrKeyEntity, entity) + require.NoError(t, err) + + require.NoError(t, emitter.Close()) + clientMock.AssertExpectations(t) + + require.NotNil(t, published) + assert.Nil(t, published.Attributes["chainid"]) + }) + t.Run("logs error when Publish fails", func(t *testing.T) { clientMock := mocks.NewClient(t) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 73579eccc7..8850bcd5f5 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -191,6 +191,7 @@ func NewGRPCClient(cfg Config, otlploggrpcNew otlploggrpcFactory) (*Client, erro // eventually we will remove the dual source emitter and just use chip ingress if cfg.ChipIngressEmitterEnabled || cfg.ChipIngressEmitterGRPCEndpoint != "" { var opts []chipingress.Opt + resourceAttrs := resourceAttributesToStringMap(cfg.ResourceAttributes) if cfg.ChipIngressInsecureConnection { opts = append(opts, chipingress.WithInsecureConnection()) @@ -215,6 +216,10 @@ func NewGRPCClient(cfg Config, otlploggrpcNew otlploggrpcFactory) (*Client, erro opts = append(opts, chipingress.WithMeterProvider(meterProvider)) opts = append(opts, chipingress.WithTracerProvider(tracerProvider)) + if len(resourceAttrs) > 0 { + opts = append(opts, chipingress.WithHeaderProvider(chipingress.NewStaticHeaderProvider(chipingress.SanitizeMetadataHeaders(resourceAttrs)))) + } + chipIngressClient, err = chipingress.NewClient(cfg.ChipIngressEmitterGRPCEndpoint, opts...) if err != nil { return nil, err @@ -235,7 +240,7 @@ func NewGRPCClient(cfg Config, otlploggrpcNew otlploggrpcFactory) (*Client, erro // teardown after parent close hook completes. chipIngressEmitter = noCloseEmitter{Emitter: batchEmitterService} } else { - chipIngressEmitter, err = ChipIngressEmitterConfig{Lggr: lggr}.New(chipIngressClient) + chipIngressEmitter, err = ChipIngressEmitterConfig{Lggr: lggr}.NewWithResourceAttributes(chipIngressClient, resourceAttrs) if err != nil { return nil, fmt.Errorf("failed to create chip ingress emitter: %w", err) } diff --git a/pkg/beholder/resource_attributes.go b/pkg/beholder/resource_attributes.go new file mode 100644 index 0000000000..7888fd12ab --- /dev/null +++ b/pkg/beholder/resource_attributes.go @@ -0,0 +1,15 @@ +package beholder + +import "go.opentelemetry.io/otel/attribute" + +// resourceAttributesToStringMap converts OTel resource attributes into a plain string map, +// using attribute.Value.Emit for canonical stringification of any value type. This is the +// single source of truth used to derive both the gRPC metadata headers and the CloudEvent +// extension keys/values sent to ChipIngress, so both mechanisms stay consistent. +func resourceAttributesToStringMap(attrs []attribute.KeyValue) map[string]string { + m := make(map[string]string, len(attrs)) + for _, kv := range attrs { + m[string(kv.Key)] = kv.Value.Emit() + } + return m +} diff --git a/pkg/beholder/resource_attributes_test.go b/pkg/beholder/resource_attributes_test.go new file mode 100644 index 0000000000..35dceccdd8 --- /dev/null +++ b/pkg/beholder/resource_attributes_test.go @@ -0,0 +1,28 @@ +package beholder + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" +) + +func TestResourceAttributesToStringMap(t *testing.T) { + attrs := []attribute.KeyValue{ + attribute.String("chain_id", "1"), + attribute.Bool("is_bootstrap", true), + attribute.Int64("node_index", 42), + } + + got := resourceAttributesToStringMap(attrs) + + assert.Equal(t, map[string]string{ + "chain_id": "1", + "is_bootstrap": "true", + "node_index": "42", + }, got) +} + +func TestResourceAttributesToStringMap_Empty(t *testing.T) { + assert.Empty(t, resourceAttributesToStringMap(nil)) +} From fccea7a53c53407c0d70366e518ab9fb7291a4fe Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:38:11 -0400 Subject: [PATCH 2/8] Bump pkg/chipingress pseudo-version for resource attributes Root module must pin the submodule commit from #2267 that contains the new chipingress API. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 50ecc3555a..f9152b84fd 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260626151909-052e55e62e62 + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260721062847-690c417fc953 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 diff --git a/go.sum b/go.sum index 28bc26fe4c..fb47bfc175 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260626151909-052e55e62e62 h1:o7vfwNQjQbMKQ9YsZFQOxvU7RMXD/wKnZsX5N9sDS3w= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260626151909-052e55e62e62/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260721062847-690c417fc953 h1:1dbwnnkGAr6eACfI6kEhXMEtBH2mU/ZOANbmDztgwTk= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260721062847-690c417fc953/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= From 7986fe2d437ceb13c9511ca4b01ef7140437f73e Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:49:31 -0400 Subject: [PATCH 3/8] Bump pkg/chipingress pseudo-version after review feedback --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4542b65db3..fa88ddd785 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260721062813-b92a569e44a3 + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722054803-990339a3c9de 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 diff --git a/go.sum b/go.sum index 35ec7c65ea..93937069e1 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260721062813-b92a569e44a3 h1:Fu0IO65sVjmU6jcNlZMp0IvVPh6D4+SBnJiEr3UCeDs= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260721062813-b92a569e44a3/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722054803-990339a3c9de h1:6USC9QJYmFShMTO5/9ULI/V0Q7OjOADTRmnmhwtksbY= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722054803-990339a3c9de/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= From f2249b89ec57eb21dd33a16eff40e45142b8265e Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:56:09 -0400 Subject: [PATCH 4/8] Bump pkg/chipingress pseudo-version after shared helper extraction --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fa88ddd785..16455629a8 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722054803-990339a3c9de + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722055513-310a7b2afcea 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 diff --git a/go.sum b/go.sum index 93937069e1..771a00fb7c 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722054803-990339a3c9de h1:6USC9QJYmFShMTO5/9ULI/V0Q7OjOADTRmnmhwtksbY= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722054803-990339a3c9de/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722055513-310a7b2afcea h1:HPj8I/rzL3v75my5ybZThCPfl2rHyLQ+mflKahKkynE= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722055513-310a7b2afcea/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= From c595231b1fb714b33464a0d206eda1ff15fe97c7 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:19:34 -0400 Subject: [PATCH 5/8] Bump pkg/chipingress pseudo-version after WithResourceAttributeHeaders --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 16455629a8..d454543566 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722055513-310a7b2afcea + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722161838-36e9940d44a4 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 diff --git a/go.sum b/go.sum index 771a00fb7c..b9cbf6e6a4 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722055513-310a7b2afcea h1:HPj8I/rzL3v75my5ybZThCPfl2rHyLQ+mflKahKkynE= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722055513-310a7b2afcea/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722161838-36e9940d44a4 h1:S99tTVhpw/ndmt42XAAxY5IjPgwlQme8X/tkWuP7ljA= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722161838-36e9940d44a4/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= From 3a37b60bd12803d6bdcf0f4229de5db04315283f Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:28:30 -0400 Subject: [PATCH 6/8] Use WithResourceAttributeHeaders convenience Opt in beholder client --- pkg/beholder/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 6d6a790d94..5c0eedc06f 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -217,7 +217,7 @@ func NewGRPCClient(cfg Config, otlploggrpcNew otlploggrpcFactory) (*Client, erro opts = append(opts, chipingress.WithTracerProvider(tracerProvider)) if len(resourceAttrs) > 0 { - opts = append(opts, chipingress.WithHeaderProvider(chipingress.NewStaticHeaderProvider(chipingress.SanitizeMetadataHeaders(resourceAttrs)))) + opts = append(opts, chipingress.WithResourceAttributeHeaders(resourceAttrs)) } chipIngressClient, err = chipingress.NewClient(cfg.ChipIngressEmitterGRPCEndpoint, opts...) From e05ba6e511060353645858d3f4116cd2ef430917 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:48:15 -0400 Subject: [PATCH 7/8] Bump pkg/chipingress to PR 2267 main merge commit --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index d454543566..e12bf614df 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722161838-36e9940d44a4 + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260727152657-992a2cd2ec36 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 @@ -81,8 +81,8 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.45.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 - google.golang.org/grpc v1.80.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/yaml v1.4.0 @@ -156,7 +156,7 @@ require ( golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index b9cbf6e6a4..a811d57488 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722161838-36e9940d44a4 h1:S99tTVhpw/ndmt42XAAxY5IjPgwlQme8X/tkWuP7ljA= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260722161838-36e9940d44a4/go.mod h1:J5n1H3YFMfrZAPTc6eA5ByP/7hlNeZnPOvMf+m2L6xQ= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260727152657-992a2cd2ec36 h1:p+jHKdHfmlyA7HLcSF25IvAN4DuEKVpuxWCLVIHW4q4= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260727152657-992a2cd2ec36/go.mod h1:n7wTMqh5BGLDcL5XFLeWaAvf3PRqfRu7G1Uig07YrVc= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b h1:VDgJWDipihV9f7M5+d21d1RzSsg5rEv+iI12oN1VQbo= @@ -462,17 +462,17 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 4730d133fe673bf05b1615285d5764d7b4cf5145 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:45:52 +0100 Subject: [PATCH 8/8] beholder: send resource attributes to chip ingress as gRPC metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resource attributes from [Telemetry.ResourceAttributes] reached the OTel collector path but never the ChipIngress path, so they appeared as headers on beholder__platform__messages and not on cre. Wire them into the ChipIngress client. They travel once per request as gRPC metadata rather than being stamped on every CloudEvent. They describe the producer, not any individual event, which is why OTLP factors resource out of the payload rather than repeating it per log record; per-event stamping would also repeat roughly 300 KB of identical bytes for a batch of a thousand events with ten attributes, counting against maxGRPCRequestSize and reducing how many events fit per batch. Chip-ingress already fans connection-scoped values onto every Kafka record this way — nopInfoHeadersFromContext feeds baseKafkaHeaders, cached per (domain, entity, specVersion) — so this follows an existing server pattern. resourceAttributesToStringMap is the single conversion point, using attribute.Value.Emit for canonical stringification of any value type. Adds a regression test asserting on a real gRPC connection that configuring AuthHeaders together with ResourceAttributes leaves the CSA node auth token intact and delivers the attributes alongside it. The token travels as per-RPC credentials while attributes travel through a unary interceptor, so nothing previously covered the two mechanisms coexisting. No exported API is added or changed in pkg/beholder. This does not yet reach a Kafka consumer: chip-ingress reads incoming metadata only to authenticate, and forwarding it onto records is a server-side change still to come. Until that lands the attributes are visible to the service but not to consumers, so this does not on its own close the header gap between cre and beholder__platform__messages. --- pkg/beholder/batch_emitter_service.go | 10 +-- pkg/beholder/batch_emitter_service_test.go | 45 ----------- pkg/beholder/chip_ingress_emitter.go | 52 ++++-------- pkg/beholder/chip_ingress_emitter_test.go | 54 ------------- pkg/beholder/client.go | 2 +- pkg/beholder/client_test.go | 93 ++++++++++++++++++++++ 6 files changed, 112 insertions(+), 144 deletions(-) diff --git a/pkg/beholder/batch_emitter_service.go b/pkg/beholder/batch_emitter_service.go index 94cd1445c8..d2560a981e 100644 --- a/pkg/beholder/batch_emitter_service.go +++ b/pkg/beholder/batch_emitter_service.go @@ -22,8 +22,7 @@ type ChipIngressBatchEmitterService struct { services.Service eng *services.Engine - batchClient *batch.Client - resourceAttrs map[string]string + batchClient *batch.Client metricAttrsCache sync.Map // map[string]otelmetric.MeasurementOption metrics batchEmitterMetrics @@ -92,9 +91,8 @@ func NewChipIngressBatchEmitterService(client chipingress.Client, cfg Config, lg } e := &ChipIngressBatchEmitterService{ - batchClient: batchClient, - resourceAttrs: resourceAttributesToStringMap(cfg.ResourceAttributes), - metrics: metrics, + batchClient: batchClient, + metrics: metrics, } e.Service, e.eng = services.Config{ @@ -145,7 +143,7 @@ func (e *ChipIngressBatchEmitterService) emitInternal(ctx context.Context, body attributes := newAttributes(attrKVs...) - event, err := chipingress.NewEventWithOpts(domain, entity, body, attributes, chipingress.WithResourceAttributeExtensions(e.resourceAttrs)) + event, err := chipingress.NewEvent(domain, entity, body, attributes) if err != nil { return fmt.Errorf("failed to create CloudEvent: %w", err) } diff --git a/pkg/beholder/batch_emitter_service_test.go b/pkg/beholder/batch_emitter_service_test.go index 8178977617..085ac94aa7 100644 --- a/pkg/beholder/batch_emitter_service_test.go +++ b/pkg/beholder/batch_emitter_service_test.go @@ -124,50 +124,6 @@ func TestChipIngressBatchEmitterService_Emit(t *testing.T) { }) } -func TestChipIngressBatchEmitterService_ResourceAttributes(t *testing.T) { - clientMock := mocks.NewClient(t) - clientMock.EXPECT().Close().Return(nil).Maybe() - - var mu sync.Mutex - var receivedBatch *chipingress.CloudEventBatch - clientMock. - On("PublishBatch", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - mu.Lock() - defer mu.Unlock() - receivedBatch = args.Get(1).(*chipingress.CloudEventBatch) - }). - Return(nil, nil) - - cfg := newTestConfig() - cfg.ChipIngressSendInterval = 50 * time.Millisecond - cfg.ResourceAttributes = []attribute.KeyValue{attribute.String("chain_id", "1")} - - emitter, err := beholder.NewChipIngressBatchEmitterService(clientMock, cfg, newTestLogger(t)) - require.NoError(t, err) - require.NoError(t, emitter.Start(t.Context())) - - err = emitter.Emit(t.Context(), []byte("test-payload"), - beholder.AttrKeyDomain, "my-domain", - beholder.AttrKeyEntity, "my-entity", - ) - require.NoError(t, err) - - assert.Eventually(t, func() bool { - mu.Lock() - defer mu.Unlock() - return receivedBatch != nil - }, 2*time.Second, 10*time.Millisecond) - - require.NoError(t, emitter.Close()) - - mu.Lock() - defer mu.Unlock() - require.Len(t, receivedBatch.Events, 1) - require.NotNil(t, receivedBatch.Events[0].Attributes["chainid"]) - assert.Equal(t, "1", receivedBatch.Events[0].Attributes["chainid"].GetCeString()) -} - func TestChipIngressBatchEmitterService_CloudEventFormat(t *testing.T) { clientMock := mocks.NewClient(t) clientMock.EXPECT().Close().Return(nil).Maybe() @@ -664,7 +620,6 @@ func TestChipIngressBatchEmitterService_RPCError(t *testing.T) { }) } - func TestChipIngressBatchEmitterService_Metrics(t *testing.T) { t.Run("records events_sent on successful publish", func(t *testing.T) { reader, restore := useEmitterTestMeterProvider(t) diff --git a/pkg/beholder/chip_ingress_emitter.go b/pkg/beholder/chip_ingress_emitter.go index f8525b4eeb..fa52953dd8 100644 --- a/pkg/beholder/chip_ingress_emitter.go +++ b/pkg/beholder/chip_ingress_emitter.go @@ -12,24 +12,18 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" ) -// resourceAttrExtensions holds resource attributes to stamp as CloudEvent extensions on every -// emitted event. It is stored behind a pointer on ChipIngressEmitter (rather than as a bare map -// field) so the struct itself stays a comparable type — a map field would make it incomparable, -// which is an exported-API-breaking change per apidiff. A nil *resourceAttrExtensions means no -// resource attributes are configured. -type resourceAttrExtensions struct { - attrs map[string]string -} - // ChipIngressEmitter wraps a synchronous chipingress.Client.Publish call // in a fire-and-forget goroutine so callers are never blocked. +// +// Resource attributes are not stamped on events here. They describe the producer rather than any +// individual event, so they travel once per request as gRPC metadata configured on the client (see +// chipingress.WithResourceAttributeHeaders) rather than being repeated on every event. type ChipIngressEmitter struct { - client chipingress.Client - lggr logger.Logger - resourceAttrs *resourceAttrExtensions - stopCh services.StopChan - wg services.WaitGroup - closed atomic.Bool + client chipingress.Client + lggr logger.Logger + stopCh services.StopChan + wg services.WaitGroup + closed atomic.Bool } func NewChipIngressEmitter(client chipingress.Client) (Emitter, error) { @@ -41,15 +35,8 @@ type ChipIngressEmitterConfig struct { Lggr logger.Logger } -// New creates a ChipIngressEmitter from the config, with no resource attributes configured. +// New creates a ChipIngressEmitter from the config. func (c ChipIngressEmitterConfig) New(client chipingress.Client) (Emitter, error) { - return c.NewWithResourceAttributes(client, nil) -} - -// NewWithResourceAttributes creates a ChipIngressEmitter from the config, additionally stamping -// attrs as CloudEvent extensions (via chipingress.WithResourceAttributeExtensions) on every -// emitted event. -func (c ChipIngressEmitterConfig) NewWithResourceAttributes(client chipingress.Client, attrs map[string]string) (Emitter, error) { if client == nil { return nil, errors.New("chip ingress client is nil") } @@ -58,16 +45,10 @@ func (c ChipIngressEmitterConfig) NewWithResourceAttributes(client chipingress.C lggr = logger.Nop() } - var resourceAttrs *resourceAttrExtensions - if len(attrs) > 0 { - resourceAttrs = &resourceAttrExtensions{attrs: attrs} - } - return &ChipIngressEmitter{ - client: client, - lggr: lggr, - resourceAttrs: resourceAttrs, - stopCh: make(services.StopChan), + client: client, + lggr: lggr, + stopCh: make(services.StopChan), }, nil } @@ -88,12 +69,7 @@ func (c *ChipIngressEmitter) Emit(ctx context.Context, body []byte, attrKVs ...a return err } - var event chipingress.CloudEvent - if c.resourceAttrs != nil { - event, err = chipingress.NewEventWithOpts(sourceDomain, entityType, body, newAttributes(attrKVs...), chipingress.WithResourceAttributeExtensions(c.resourceAttrs.attrs)) - } else { - event, err = chipingress.NewEvent(sourceDomain, entityType, body, newAttributes(attrKVs...)) - } + event, err := chipingress.NewEvent(sourceDomain, entityType, body, newAttributes(attrKVs...)) if err != nil { return err } diff --git a/pkg/beholder/chip_ingress_emitter_test.go b/pkg/beholder/chip_ingress_emitter_test.go index 0590befd5d..11349d335b 100644 --- a/pkg/beholder/chip_ingress_emitter_test.go +++ b/pkg/beholder/chip_ingress_emitter_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/beholder" - "github.com/smartcontractkit/chainlink-common/pkg/chipingress" "github.com/smartcontractkit/chainlink-common/pkg/chipingress/mocks" "github.com/smartcontractkit/chainlink-common/pkg/logger" ) @@ -74,59 +73,6 @@ func TestChipIngressEmit(t *testing.T) { assert.Error(t, err) }) - t.Run("resource attributes are stamped as sanitized CE extensions", func(t *testing.T) { - clientMock := mocks.NewClient(t) - - var published *chipingress.CloudEventPb - clientMock. - On("Publish", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - published = args.Get(1).(*chipingress.CloudEventPb) - }). - Return(nil, nil) - clientMock.On("Close").Return(nil) - - emitter, err := beholder.ChipIngressEmitterConfig{ - Lggr: logger.Test(t), - }.NewWithResourceAttributes(clientMock, map[string]string{"chain_id": "1"}) - require.NoError(t, err) - - err = emitter.Emit(t.Context(), body, beholder.AttrKeyDomain, domain, beholder.AttrKeyEntity, entity) - require.NoError(t, err) - - require.NoError(t, emitter.Close()) - clientMock.AssertExpectations(t) - - require.NotNil(t, published) - require.NotNil(t, published.Attributes["chainid"]) - assert.Equal(t, "1", published.Attributes["chainid"].GetCeString()) - }) - - t.Run("New(client) with no resource attributes stamps no extra extensions (backward-compat)", func(t *testing.T) { - clientMock := mocks.NewClient(t) - - var published *chipingress.CloudEventPb - clientMock. - On("Publish", mock.Anything, mock.Anything). - Run(func(args mock.Arguments) { - published = args.Get(1).(*chipingress.CloudEventPb) - }). - Return(nil, nil) - clientMock.On("Close").Return(nil) - - emitter, err := beholder.ChipIngressEmitterConfig{Lggr: logger.Test(t)}.New(clientMock) - require.NoError(t, err) - - err = emitter.Emit(t.Context(), body, beholder.AttrKeyDomain, domain, beholder.AttrKeyEntity, entity) - require.NoError(t, err) - - require.NoError(t, emitter.Close()) - clientMock.AssertExpectations(t) - - require.NotNil(t, published) - assert.Nil(t, published.Attributes["chainid"]) - }) - t.Run("logs error when Publish fails", func(t *testing.T) { clientMock := mocks.NewClient(t) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 5c0eedc06f..a4fe75ae05 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -240,7 +240,7 @@ func NewGRPCClient(cfg Config, otlploggrpcNew otlploggrpcFactory) (*Client, erro // teardown after parent close hook completes. chipIngressEmitter = noCloseEmitter{Emitter: batchEmitterService} } else { - chipIngressEmitter, err = ChipIngressEmitterConfig{Lggr: lggr}.NewWithResourceAttributes(chipIngressClient, resourceAttrs) + chipIngressEmitter, err = ChipIngressEmitterConfig{Lggr: lggr}.New(chipIngressClient) if err != nil { return nil, fmt.Errorf("failed to create chip ingress emitter: %w", err) } diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 48dca5efff..35e7c843d5 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -6,18 +6,24 @@ import ( "encoding/hex" "errors" "fmt" + "net" "strings" + "sync" "testing" "time" + cepb "github.com/cloudevents/sdk-go/binding/format/protobuf/v2/pb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" otellog "go.opentelemetry.io/otel/log" sdklog "go.opentelemetry.io/otel/sdk/log" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" @@ -486,6 +492,93 @@ func TestNewGRPCClient_ChipIngressEmitter(t *testing.T) { }) } +// capturingChipServer records the gRPC metadata of the last Publish it handles. +type capturingChipServer struct { + pb.UnimplementedChipIngressServer + + mu sync.Mutex + lastMD metadata.MD +} + +func (s *capturingChipServer) Publish(ctx context.Context, _ *cepb.CloudEvent) (*pb.PublishResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.mu.Lock() + defer s.mu.Unlock() + s.lastMD = md + return &pb.PublishResponse{}, nil +} + +func (s *capturingChipServer) metadata() metadata.MD { + s.mu.Lock() + defer s.mu.Unlock() + return s.lastMD +} + +// TestNewGRPCClient_AuthHeaderCoexistsWithResourceAttributes is the beholder-level counterpart to +// chipingress' TestClient_AuthHeaderCoexistsWithResourceAttributes. Wiring resource attributes +// added a unary header interceptor to a connection that previously carried no context metadata at +// all, while the CSA node auth token travels separately as per-RPC credentials. This asserts on a +// real connection that configuring both leaves the auth token intact and delivers the resource +// attributes alongside it. +func TestNewGRPCClient_AuthHeaderCoexistsWithResourceAttributes(t *testing.T) { + const authHeaderKey = "X-Beholder-Node-Auth-Token" + const authToken = "1:abc:2:def" + + lis, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0") + require.NoError(t, err) + defer lis.Close() + + srv := grpc.NewServer() + capture := &capturingChipServer{} + pb.RegisterChipIngressServer(srv, capture) + go func() { _ = srv.Serve(lis) }() + defer srv.Stop() + + cfg := beholder.Config{ + OtelExporterGRPCEndpoint: "localhost:4317", + ChipIngressEmitterEnabled: true, + ChipIngressEmitterGRPCEndpoint: lis.Addr().String(), + ChipIngressInsecureConnection: true, + AuthHeaders: map[string]string{authHeaderKey: authToken}, + ResourceAttributes: []attribute.KeyValue{ + attribute.String("csa_public_key", "abc123"), + attribute.String("service.name", "chainlink"), + }, + } + + otlploggrpcNew := func(options ...otlploggrpc.Option) (sdklog.Exporter, error) { + return &mockLogExporter{}, nil + } + + client, err := beholder.NewGRPCClient(cfg, otlploggrpcNew) + require.NoError(t, err) + require.NotNil(t, client) + + require.NoError(t, client.Emitter.Emit(t.Context(), []byte("payload"), + beholder.AttrKeyDomain, "my-domain", + beholder.AttrKeyEntity, "my-entity", + beholder.AttrKeyDataSchema, "/schemas/ids/1001", + )) + + // ChipIngressEmitter.Emit publishes fire-and-forget in a goroutine. + require.Eventually(t, func() bool { return capture.metadata() != nil }, 5*time.Second, 10*time.Millisecond) + + md := capture.metadata() + assert.Equal(t, []string{authToken}, md.Get(authHeaderKey), + "the CSA auth token must arrive exactly once, unmodified") + + // Assert against the sanitizer rather than hardcoding key spellings: the property under test + // is that auth and resource attributes coexist, not how chipingress normalizes a key. + want := chipingress.SanitizeMetadataHeaders(map[string]string{ + "csa_public_key": "abc123", + "service.name": "chainlink", + }) + require.Len(t, want, 2, "both attributes must survive sanitization for this test to mean anything") + for key, val := range want { + assert.Equal(t, []string{val}, md.Get(key), "resource attribute %q missing from metadata", key) + } +} + func TestNewClient_Chip(t *testing.T) { t.Run("chip interface available with chip-ingress endpoint provided", func(t *testing.T) { client, err := beholder.NewClient(beholder.Config{