Skip to content

chipingress: send resource attributes as prefixed gRPC metadata - #2288

Draft
pkcll wants to merge 2 commits into
mainfrom
chip-ingress-resource-attributes-client-part-2
Draft

chipingress: send resource attributes as prefixed gRPC metadata#2288
pkcll wants to merge 2 commits into
mainfrom
chip-ingress-resource-attributes-client-part-2

Conversation

@pkcll

@pkcll pkcll commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #2267, which added resource-attribute support to the ChipIngress client via two
mechanisms: per-event CloudEvent extensions and per-request gRPC metadata. This keeps the metadata
path, gives it a namespace, and deletes the extension path.

Why metadata rather than per-event extensions

Resource attributes describe the producer, not any individual event. That is why OTLP factors
resource out of its payload rather than repeating it on every log record, and the same reasoning
applies here.

Per-event stamping also duplicated bytes: ~10 attributes across a 1,000-event batch is roughly 300 KB
of identical content, which counts against maxGRPCRequestSize and therefore reduces how many events
fit per batch.

Chip-ingress already fans connection-scoped values out onto every Kafka record — the verified auth
token becomes ce_csapublickey, the NOP lookup becomes ce_nodeoperatorname — via baseKafkaHeaders,
cached per (domain, entity, specVersion). Resource attributes now follow that same path.

The resource_ contract

Every emitted metadata key is resource_ + a key normalized to grpc's charset, which grpc-go gives as
[0-9a-z-_.] (internal/metadata.ValidateKey). Structure therefore survives — csa_public_key
becomes resource_csa_public_key rather than collapsing to csapublickey — which is what lets
chip-ingress emit the forwarded header verbatim.

The prefix is why neither side needs a reserved-key set. The header interceptor appends to
outgoing metadata rather than replacing it, so an attribute landing on an existing header name sends
two values under one key. For the CSA auth token that breaks authentication. Prefixed, that attribute
becomes resource_x-beholder-node-auth-token and collides with nothing — and the same holds for
authorization, te, content-type, the grpc- prefix and pseudo-headers.

So reservedMetadataHeaderNames, reservedMetadataKeys and isReservedMetadataKey are deleted
rather than extended
, replaced by a test asserting the property directly. Enumerated deny-lists have
to be maintained correctly forever; this one is closed by construction.

Validate, don't rewrite

An earlier revision of this PR normalized an invalid key by replacing disallowed characters with _,
and an invalid value by replacing each non-printable byte with ?. Both were reverted in favor of
validating and omitting: sanitizeMetadataKey now returns ("", false) for anything that doesn't
already match [0-9a-z-_.]+ (once lower-cased) or that ends in -bin, and a non-printable value is
dropped rather than byte-mangled.

Rewriting silently collapses distinct operator-configured keys into one gRPC metadata key — e.g. two
attributes that only differ by an illegal character both landing on the same sanitized name — which is
worse than dropping the offending attribute outright. SanitizeMetadataHeaders now returns
(map[string]string, []DroppedAttribute); DroppedAttribute{Key, Reason} records what was omitted and
why (invalid_key, invalid_value, duplicate_key, limit_exceeded), so a caller can warn and meter
instead of silently losing data. WithResourceAttributeHeaders does exactly that: it logs a warning
(via an overridable, no-op-by-default *zap.Logger set through SetResourceAttributeLogger) and
increments an otel counter (chipingress.resource_attribute.dropped, tagged by reason, against the
global MeterProvider) for every dropped attribute.

Limits

SanitizeMetadataHeaders now caps resource attributes at 32 entries, 128 bytes per key, 512 bytes per
value, and 4096 bytes of combined accepted key+value bytes (prefix excluded) — reserving headroom in
the gRPC HEADERS frame for authentication and normal gRPC metadata. Keys are processed in sorted order,
so which entries get dropped once a limit is hit is deterministic rather than map-iteration-order
dependent.

Changes

  • header_provider.gosanitizeMetadataKey validates instead of rewriting; adds
    isPrintableASCII; adds the four maxResourceAttribute* limit constants; adds DroppedAttribute
    and the reason* constants; SanitizeMetadataHeaders now validates, dedupes, enforces limits, and
    returns the dropped list alongside the accepted headers. SanitizeMetadataValue (the old byte-by-byte
    rewriter) is deleted — it had no callers outside this file's own tests once the check became a
    pass/fail predicate.
  • client.goWithResourceAttributeHeaders warns and meters every dropped attribute; adds the
    package-level resourceAttributeLogger (no-op by default) and SetResourceAttributeLogger to
    override it; adds the lazily-constructed resourceAttributeDropsCounter against the global otel
    meter. Also (from the prior revision): deletes EventOpt, NewEventWithOpts and
    WithResourceAttributeExtensions; NewEvent is the single event constructor again.
  • types.go — adds ResourceHeaderPrefix; deletes the reserved-key set and its predicate.
  • resource_attributes.go — deleted; folded into header_provider.go now that
    SanitizeMetadataHeaders is the sole consumer of the shared key/dedupe logic.

Exported API

Removed EventOpt, NewEventWithOpts, WithResourceAttributeExtensions, SanitizeMetadataValue
Added ResourceHeaderPrefix, DroppedAttribute, SetResourceAttributeLogger
Changed signature and behavior SanitizeMetadataHeaders now returns (map[string]string, []DroppedAttribute) and validates instead of rewriting

All removals were added by #2267 and have zero callers in either chainlink-common or chainlink —
verified by grep across both repos. NewStaticHeaderProvider and WithResourceAttributeHeaders keep
their signatures; WithResourceAttributeHeaders's behavior changes only in what it warns/meters.

Paired changes

  • Server: smartcontractkit/atlas#13201 forwards metadata carrying resource_ onto every Kafka
    record. Independent of this PR; consumers see nothing until both are deployed.
  • Beholder wiring: beholder: send resource attributes to chip ingress as gRPC metadata #2216 needs no code change for the resource_ prefix — it calls
    WithResourceAttributeHeaders, so both the prefixing and the new validate/limit/observability
    behavior happen transparently in here. It does need its pkg/chipingress pin bumped once this
    merges.

ResourceHeaderPrefix is deliberately duplicated as constants.ResourceHeaderPrefix in
chip-ingress — the same cross-repo duplication authHeaderKey already has between pkg/beholder and
pkg/chipingress. The two must stay byte-identical or forwarding silently stops.

Related PRs

This is the upstream PR in the resource-attribute stack:

Test plan

cd pkg/chipingress && go test ./... && go vet ./... && gofmt -l .

Tests bind 127.0.0.1:0, so they need a sandbox that permits listening. Notable cases: prefixing and
structure preservation; validate-don't-rewrite for both invalid keys and non-printable values (an
invalid entry is omitted with a reason, never rewritten); deterministic collapse of keys that validate
to the same name; each of the four limits (oversized key, oversized value, >32 attributes, >4096
combined bytes) dropping deterministically in sorted-key order; and — on a live gRPC connection — that
the CSA auth token arrives exactly once alongside prefixed attributes, including one deliberately named
after the auth header.

Resource attributes describe the producer, not any individual event. Carrying
them as per-event CloudEvent extensions repeated identical bytes for every
event in a batch — for a thousand events with ten attributes, roughly 300 KB
that counts against maxGRPCRequestSize and so reduces how many events fit per
batch. OTLP factors resource out of the payload for the same reason. Send them
once per request as gRPC metadata instead.

Every emitted key is ResourceHeaderPrefix ("resource_") followed by a key
normalized to grpc's charset, which grpc-go gives as [0-9a-z-_.]
(internal/metadata.ValidateKey). Structure therefore survives —
csa_public_key becomes resource_csa_public_key rather than collapsing to
csapublickey — which is what lets chip-ingress emit the forwarded header
verbatim. Values still go through SanitizeMetadataValue, because grpc-go fails
an entire RPC, auth header included, on one non-printable value. A trailing
"-bin" is rewritten so grpc does not try to base64-decode a plain-text
attribute.

The prefix is a wire contract with chip-ingress, which forwards metadata
carrying it onto every Kafka record a request produces. Requiring it inbound
and preserving it outbound keeps the namespace closed, and that is what
removes the need for a reserved-key set on either side. The header interceptor
appends to outgoing metadata rather than replacing, so an attribute named
X-Beholder-Node-Auth-Token would have sent a second value under the key
carrying the CSA node auth token and broken authentication; prefixed, it
becomes resource_x-beholder-node-auth-token and collides with nothing. The
same holds for authorization, te, content-type, the grpc- prefix and
pseudo-headers, so reservedMetadataHeaderNames, reservedMetadataKeys and
isReservedMetadataKey are all deleted rather than extended. A test asserts the
property directly, in place of the set it replaces.

Removes EventOpt, NewEventWithOpts and WithResourceAttributeExtensions, which
existed only for the extension path and have no callers in either repository;
NewEvent returns to being the single event constructor. With
SanitizeMetadataHeaders the sole consumer of the shared key helper, fold it in
and delete resource_attributes.go and the resourceAttrKey pair type — the
sanitized output map doubles as the dedupe set. SanitizeMetadataKey becomes
unexported, since nothing outside the package used it.

Adds ResourceHeaderPrefix. The same constant exists in chip-ingress as
constants.ResourceHeaderPrefix; duplicating a wire contract across
repositories matches how authHeaderKey is already spelled in both
pkg/beholder and pkg/chipingress, and the two must stay byte-identical or
forwarding silently stops.
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

⚠️ API Diff Results - github.com/smartcontractkit/chainlink-common/pkg/chipingress

⚠️ Breaking Changes (5)

./ (5)
  • EventOpt — 🗑️ Removed

  • NewEventWithOpts — 🗑️ Removed

  • SanitizeMetadataHeaders — Type changed:

func(
  map[string]string
)
- map[string]string
+ (map[string]string, []DroppedAttribute)
  • SanitizeMetadataValue — 🗑️ Removed

  • WithResourceAttributeExtensions — 🗑️ Removed

✅ Compatible Changes (3)

./ (3)
  • DroppedAttribute — ➕ Added

  • ResourceHeaderPrefix — ➕ Added

  • SetResourceAttributeLogger — ➕ Added


📄 View full apidiff report

Copilot AI left a comment

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.

Pull request overview

This PR updates pkg/chipingress resource-attribute propagation to use request-scoped gRPC metadata only, namespaced under a resource_ prefix, and removes the prior per-event CloudEvent extension mechanism. This aligns resource attributes with their producer-scoped semantics and avoids repeated per-event payload bloat while ensuring attributes cannot collide with reserved/auth metadata keys.

Changes:

  • Introduces ResourceHeaderPrefix = "resource_" as the wire contract for resource attributes sent via gRPC metadata.
  • Reworks SanitizeMetadataHeaders to (1) structure-preserving normalize keys to gRPC’s allowed charset and (2) always prefix with resource_, replacing the previous reserved-key deny-list approach.
  • Removes the CloudEvent extension path (EventOpt, NewEventWithOpts, WithResourceAttributeExtensions) and deletes the shared resource-attribute helper file, consolidating logic around metadata.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/chipingress/types.go Adds ResourceHeaderPrefix constant and removes reserved-key sets tied to the deleted CE-extension path.
pkg/chipingress/resource_attributes.go Deletes the shared resource-attribute key helper now made redundant by the new metadata sanitization approach.
pkg/chipingress/header_provider.go Implements structure-preserving metadata key normalization, applies resource_ prefix to all emitted keys, and removes reliance on reserved-key lists.
pkg/chipingress/header_provider_test.go Updates/expands tests to assert prefixing, normalization rules, -bin rewrite, deterministic collision handling, and non-collision with reserved/auth keys.
pkg/chipingress/client.go Removes CE-extension event opts and documents that resource attributes are request-scoped via WithResourceAttributeHeaders; small refactor for NOP lookup header key.
pkg/chipingress/client_test.go Removes CE-extension tests and adds an integration-style test asserting auth token coexists with prefixed resource attributes without collision.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 30 days with no activity.
Remove the stale label or comment or this will be closed in 7 days.

@github-actions github-actions Bot added the Stale label Aug 28, 2026
SanitizeMetadataHeaders now omits an invalid key, non-printable value,
duplicate, or over-limit attribute rather than rewriting it, so two
distinct configured keys can never collapse into one gRPC metadata
key and a non-printable value can never be silently byte-mangled.
Adds the 32-attribute / 128B-key / 512B-value / 4096B-total caps, and
warns plus meters every dropped attribute via WithResourceAttributeHeaders.
@github-actions github-actions Bot removed the Stale label Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants