Skip to content

PoC: widen 'Attributes' to support the extended AnyValue#6579

Draft
trentm wants to merge 10 commits into
open-telemetry:mainfrom
trentm:trentm-extended-attributes-take2
Draft

PoC: widen 'Attributes' to support the extended AnyValue#6579
trentm wants to merge 10 commits into
open-telemetry:mainfrom
trentm:trentm-extended-attributes-take2

Conversation

@trentm
Copy link
Copy Markdown
Contributor

@trentm trentm commented Apr 10, 2026

Refs: #6349

Current status

This is a hacked-up proof of concept, to explore whether support for complex/extended attributes could be accomplished without a separate attributes type in the API.

Can we widen interface Attributes, and update the SDK package's usage of attributes?
Code-wise, I think this change so far shows: yes.

  • The big Q is whether this would be considered a breaking change in the API for users/instrumentations.
  • Possibly it could surprise code in SDK implementors, but they should be limiting on max API minor ver already. (FWIW, the sanitizeAttributes() already in the SDK packages now safely handle incoming attributes. The SDK was already necessarily defensive in checking the given attribute keys and values for validity.)

@trentm trentm self-assigned this Apr 10, 2026
@trentm
Copy link
Copy Markdown
Contributor Author

trentm commented Apr 10, 2026

trentm added 3 commits April 10, 2026 11:08
… all of this needs to be part of *this* PR work
…ibuteValue (now deprecated) because AnyValue is in the spec for more than just attributes (e.g. LogRecord#body)
…d attributes in sdk-metrics (keyObjFromAnyValue util in core package)
Copy link
Copy Markdown
Contributor Author

@trentm trentm left a comment

Choose a reason for hiding this comment

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

Add some notes to self, and for reviews.

Comment thread api/src/metrics/Meter.ts
* @param [options] the metric options.
*/
createGauge<AttributesTypes extends MetricAttributes = MetricAttributes>(
createGauge<AttributesTypes extends Attributes = Attributes>(
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.

Q: What is the <AttributesTypes extends ...> generic on Instruments for?

Comment thread api/src/metrics/Meter.ts
* @param [options] the metric options.
*/
createHistogram<AttributesTypes extends MetricAttributes = MetricAttributes>(
createHistogram<AttributesTypes extends Attributes = Attributes>(
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.

There are a lot of s/{Metric,Span}Attributes/Attributes/ changes in the PR that could be done in a separate PR to make it smaller. I may end up separating them to help reviews.

Comment thread api/src/trace/link.ts
attributes?: Attributes;
// XXX A bit silly/incorrect to have this dropped count on the `Link` type
// used by Span#addLink. The user is not meant to specify this count.
// We could deprecate this, and then cope in the SDK.
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.

This is totally for a separate PR. This is a note to self.

Comment thread api/src/index.ts
// TODO: Remove ProxyTracerProvider export in the next major version.
export { ProxyTracerProvider } from './trace/ProxyTracerProvider';
export type { Sampler } from './trace/Sampler';
export { SamplingDecision, type SamplingResult } from './trace/SamplingResult';
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.

This belongs in a separate PR, if at all. I did this as I was working through the full API to grok usage of Attributes/AnyValue.

return isAttributeValueInternal(val, new WeakSet());
}

function isAttributeValueInternal(
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.

This impl is mostly from the current sdk-logs, e.g.: isLogAttributeValueInternal().

// XXX Want a version without limits: resources and metrics are exempt: https://opentelemetry.io/docs/specs/otel/common/#exempt-entities
// XXX Having to pass around attribute count (currentAttributesCount, etc.) is a pain. Not suggesting it now, because more work, but using a Map would be nice for the API.
// XXX Consider moving diag.warn()s out to caller.
export function addAttribute(
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.

// XXX use normalizeAttributes, keep droppedAttributeCount values,
// separate from the `links` sent to shouldSample. Then pass links
// *with* droppedAttributeCount to SpanImpl, which accepts those.
// I.e. SpanImpl type shoudl be `Array<Link & {droppedAttributeCount?: number}>`
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.

This belongs in a separate PR.

// XXX Should this be defensively dropping attributes with invalid type?

const keyObj = keyObjFromAnyValue(attributes);
return JSON.stringify(keyObj);
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.

I think this hashAttributes is the only "handling" of Attributes that is need to update sdk-metrics to support complex attributes.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.51%. Comparing base (87d0112) to head (34f8ee8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
api/src/metrics/NoopMeter.ts 75.00% 1 Missing ⚠️
packages/opentelemetry-sdk-trace-base/src/Span.ts 92.85% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (87d0112) and HEAD (34f8ee8). Click for more details.

HEAD has 17 uploads less than BASE
Flag BASE (87d0112) HEAD (34f8ee8)
18 1
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6579      +/-   ##
==========================================
- Coverage   95.74%   89.51%   -6.24%     
==========================================
  Files         375       84     -291     
  Lines       12704     1994   -10710     
  Branches     2998      417    -2581     
==========================================
- Hits        12164     1785   -10379     
+ Misses        540      209     -331     
Files with missing lines Coverage Δ
api/src/metrics/Metric.ts 100.00% <ø> (ø)
api/src/trace/NonRecordingSpan.ts 95.83% <100.00%> (ø)
api/src/trace/SamplingResult.ts 100.00% <ø> (ø)
...ackages/opentelemetry-sdk-trace-base/src/Tracer.ts 98.71% <ø> (ø)
api/src/metrics/NoopMeter.ts 95.34% <75.00%> (ø)
packages/opentelemetry-sdk-trace-base/src/Span.ts 98.32% <92.85%> (+0.01%) ⬆️

... and 297 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trentm
Copy link
Copy Markdown
Contributor Author

trentm commented Apr 10, 2026

instr-http tests are failing on a deepStrictEqual of span.attributes where a span's attributes include:

    'user_agent.original': undefined

We will need to discuss whether we actually want to allow the undefined "empty" value in JavaScript's AnyValue.
I suspect it is fairly common usage currently to set attributes objects with possibly-undefined values (for convenience), on the assumption that those values will get removed during serialization. E.g.:

const userAgent = headers['user-agent'];
const urlFull = getAbsoluteUrl(
requestOptions,
headers,
`${options.component}:`,
options.redactedQueryParams
);
const oldAttributes: Attributes = {
[ATTR_HTTP_URL]: urlFull,
[ATTR_HTTP_METHOD]: method,
[ATTR_HTTP_TARGET]: requestOptions.path || '/',
[ATTR_NET_PEER_NAME]: hostname,
[ATTR_HTTP_HOST]: headers.host ?? `${hostname}:${port}`,
};
const newAttributes: Attributes = {
// Required attributes
[ATTR_HTTP_REQUEST_METHOD]: normalizedMethod,
[ATTR_SERVER_ADDRESS]: hostname,
[ATTR_SERVER_PORT]: Number(port),
[ATTR_URL_FULL]: urlFull,
[ATTR_USER_AGENT_ORIGINAL]: userAgent,

The spec https://opentelemetry.io/docs/specs/otel/common/#anyvalue says:

an empty value if supported by the language, (e.g. null, undefined in JavaScript/TypeScript, None in Python, nil in Go/Ruby, not supported in Erlang, etc.)

But my guess is this wasn't fully vetted.
I haven't checked if the sdk-logs tests actually test for an undefined value on an attribute (or on LogRecord#body).

@pichlermarc
Copy link
Copy Markdown
Member

I mentioned that there's some ways to demonstrate why this is a breaking change.
I found that there's really only one way to demonstrate it and everything else is a variation of that one issue:

This is the type of code that compiles with the previous attribute type but breaks with the new one:

/**
 * A function that may validate or sanitize attributes before they are used in telemetry.
 */
function doStuffWithAttributes(attributes: Attributes) {
  for (const attrName in attributes) {
    const value = attributes[attrName];

    if (
      value == null ||
      typeof value === 'string' ||
      typeof value === 'number' ||
      typeof value === 'boolean'
    ) {
      // do whatever
      continue;
    }

    // tsc now knows that this can now only be an Array,
    // so it'll allow things that are available on Arrays.
    // In the future, this may be an object so it will
    //  - break during compile time. (TS2723: Cannot invoke an object which is possibly null or undefined)
    //  - break during runtime for already compiled code that caret-depends on the API (like instrumentations that receive attributes from a user-exposed hook)
    //
    // I think all other possible examples where are essentially variations of this one.
    //
    // SDK components should be safe since they should specify an upper limit for supported API versions.
    // Unfortunately, instrumentations are not bound by that.
    return value.filter(item => typeof item == 'string');
  }
}

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