Skip to content

Expand attribute value type to support complex values everywhere and cleanup surrounding code#5266

Open
DylanRussell wants to merge 69 commits into
open-telemetry:mainfrom
DylanRussell:extended_attributes
Open

Expand attribute value type to support complex values everywhere and cleanup surrounding code#5266
DylanRussell wants to merge 69 commits into
open-telemetry:mainfrom
DylanRussell:extended_attributes

Conversation

@DylanRussell

@DylanRussell DylanRussell commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Description

This PR updates the code to support complex values everywhere as described in this OTEP -- basically it adds support for None type, Bytes type, heterogeneous arrays, and maps as attribute values.

I cleaned up the surrounding code, including refactoring BoundedAttributes.. this was in part possible because we no longer have to support 2 sets of attribute values.

BoundedAttributes is a dictionary sublcass used in the SDK for setting/storing/getting attributes. It's used for span attributes (and span event attributes and span link attributes), log attributes, instrumentation scope attributes, and resource attributes, but i think it's completely optional for metric points -- all the various metric point classes just accept a generic attributes value, and I think it's up to instrumentations to decide on whether to use it or not.. I don't see any guidance requiring people do it, and I don't see any usages of BoundedAttributes in contrib.

For the bytes type I just pass that through as is.. Currently we do that for Extended attributes but not regular attributes (which I'm removing), for regular attributes it was decided to a utf-8 string.. But bytes is a valid AnyValue that can be encoded as bytes in the OTLP proto, and we don't know how it was encoded, so I think passing it through makes the most sense..

Some minor follow up work is tracked in #5304

I believe the failing Public Symbols check is acceptable, it is due to:

  • expanding the Attributes type (additive change, should be acceptable.)
  • Removing AttributesAsKey type (unused in this repo and contrib, doesn't make sense as a type..)
  • Removing a very stale .pyi file in the SDK (opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi).

Type of change

Please delete options that are not relevant.

  • [ x] New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Unit tests

Does This PR Require a Contrib Repo Change?

Yes I had to submit open-telemetry/opentelemetry-python-contrib#4646 -- if someone attempts to use an old (anything less than the next release of) opentelemetry-instrumentation-logging with the next release of opentelemetry-api it will not work.. I'm confused about what to do to resolve that issue though..

Checklist:

  • [ x] Followed the style guidelines of this project
  • [ x] Changelogs have been updated
  • [ x] Unit tests have been added
  • [ x] Documentation has been updated

@DylanRussell
DylanRussell requested a review from a team as a code owner June 1, 2026 20:00

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 expands OpenTelemetry Python’s attribute value model across the API, SDK, and OTLP exporters to support “complex” values everywhere (e.g., None, heterogeneous arrays, and maps), aligning behavior with the referenced OTEP and updating surrounding utilities/tests accordingly.

Changes:

  • Broadens AnyValue/AttributeValue/Attributes typing and updates public API surfaces (tracing/logs/events/metrics) to accept complex attribute values.
  • Refactors attribute cleaning/storage (BoundedAttributes) and metric attribute-keying (_hash_attributes) to handle complex values consistently.
  • Updates OTLP proto/json encoding to represent None as an empty AnyValue and adjusts unit tests/benchmarks for the new semantics.

Reviewed changes

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

Show a summary per file
File Description
opentelemetry-sdk/tests/trace/test_trace.py Updates span attribute/event tests for complex values and bytes behavior.
opentelemetry-sdk/tests/resources/test_resources.py Updates resource attribute validation tests for new cleaning/stringify behavior.
opentelemetry-sdk/tests/metrics/test_view_instrument_match.py Updates metrics tests to use _hash_attributes aggregation keys.
opentelemetry-sdk/tests/metrics/test_measurement_consumer.py Adjusts a concurrency test to pass attributes explicitly.
opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py Switches instrumentation scope attributes typing to Attributes.
opentelemetry-sdk/src/opentelemetry/sdk/resources/init.py Aligns resource attribute typing/imports with new Attributes definition.
opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py Introduces _hash_attributes and deep-copies measurement attributes prior to aggregation.
opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/init.py Clarifies docs for attribute value length limits with string/bytes focus.
opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_exceptions.py Updates exception-attribute merging to the unified Attributes type.
opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/init.py Updates logging translation checks/warnings for AnyValue casting.
opentelemetry-sdk/src/opentelemetry/sdk/_events/init.py Updates event logger provider attribute typing to Attributes.
opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics_histogram.py Expands benchmarks to cover complex/mapping/array attribute shapes.
opentelemetry-api/tests/logs/test_proxy.py Updates logs proxy tests to Attributes.
opentelemetry-api/tests/events/test_proxy_event.py Updates events proxy tests to Attributes.
opentelemetry-api/tests/attributes/test_attributes.py Reworks attribute tests around new _clean_attribute_value/BoundedAttributes.
opentelemetry-api/src/opentelemetry/util/types.py Redefines AnyValue/AttributeValue and widens Attributes accordingly; removes _ExtendedAttributes.
opentelemetry-api/src/opentelemetry/trace/span.py Updates span API type hints to use collections.abc.Mapping and types.Attributes in NoOp implementation.
opentelemetry-api/src/opentelemetry/attributes/init.py Major refactor: new recursive cleaner and BoundedAttributes as a dict subclass.
opentelemetry-api/src/opentelemetry/_logs/_internal/init.py Updates log API typing from _ExtendedAttributes to Attributes.
opentelemetry-api/src/opentelemetry/_events/init.py Updates event API typing from _ExtendedAttributes to Attributes.
exporter/opentelemetry-exporter-otlp-proto-grpc/benchmarks/test_benchmark_trace_exporter.py Updates patch target to TraceServiceStub.
exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py Refactors OTLP proto log encoder tests around new null/AnyValue behavior.
exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py Adjusts encoder failure tests to use an unencodable object.
exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/init.py Stops using allow_null; encodes null as empty AnyValue.
exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/init.py Simplifies _encode_value and _encode_attributes around null-as-empty-AnyValue.
exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py Updates JSON log encoder tests for null-as-empty-AnyValue body.
exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py Updates JSON common encoder tests for null and unencodable values.
exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/init.py Removes allow_null usage in JSON log encoding.
exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/init.py Simplifies _encode_value and _encode_attributes around null-as-empty-AnyValue.
docs/conf.py Updates Sphinx type-hint resolution aliasing; now also touches metrics internals.
.changelog/5266.added Adds changelog entry for extended/complex attribute support across packages.
Comments suppressed due to low confidence (1)

opentelemetry-api/src/opentelemetry/trace/span.py:96

  • The note in Span.set_attributes says the behavior of None value attributes is undefined, but this PR explicitly adds support for None/null attribute values across the API/SDK. Please update the docstring to reflect the new supported semantics (even if None remains discouraged).
    def set_attributes(
        self, attributes: Mapping[str, types.AttributeValue]
    ) -> None:
        """Sets Attributes.

        Sets Attributes with the key and value passed as arguments dict.

        Note: The behavior of `None` value attributes is undefined, and hence
        strongly discouraged. It is also preferred to set attributes at span
        creation, instead of calling this method later since samplers can only
        consider information already present during span creation.
        """

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

Comment thread opentelemetry-api/src/opentelemetry/attributes/__init__.py Outdated
Comment thread opentelemetry-api/src/opentelemetry/attributes/__init__.py Outdated
Comment thread opentelemetry-api/src/opentelemetry/attributes/__init__.py
Comment thread docs/conf.py Outdated
Comment thread opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py Outdated
@DylanRussell

Copy link
Copy Markdown
Contributor Author

This is ready for another review now.. That other PR has been merged into main, and I've merged that main into this PR..

Comment thread opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi
@DylanRussell DylanRussell moved this from Reviewed PRs that need fixes to Approved PRs in Python PR digest Jul 7, 2026
Comment on lines +60 to +69
# Calling str(x) will use an object's `__str__` method if it exists, otherwise it will use it's `__repr__` method.
# If neither is defined it uses the base class's `object.__repr__` method, which returns a string that is hard to understand.
# So in that case we drop the key/value pair.
if (
type(key).__str__ is not object.__str__
or type(key).__repr__ is not object.__repr__
):
key = str(key)
else:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO it would be much more readable to just try to call str() and continue in the except block like we did before. Any strong reason for this pattern?

Comment on lines +79 to +82
if (
type(value).__str__ is not object.__str__
or type(value).__repr__ is not object.__repr__
):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same comment here

val, max_string_value_length
)
return MappingProxyType(cleaned_mapping)
_logger.warning(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add an exhaustiveness check?

We used this pattern to avoid raising an AssertionError at runtime

return f"{dict(self._dict)}"

def __getitem__(self, key: str) -> types.AnyValue:
def __getitem__(self, key: str) -> types.AttributeValue:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit as they are now aliases of each other, but can you make it consistent to use AttributeValue or AnyValue and not mix?

key: str, value: types.AnyValue, max_len: int | None
) -> types.AnyValue:
"""Checks if attribute value is valid and cleans it if required.
class BoundedAttributes(MutableMapping):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
class BoundedAttributes(MutableMapping):
class BoundedAttributes(MutableMapping[str, AnyValue]):

not sure why pyright isn't complaining

pass

LabelValue = AttributeValue
Attributes = Mapping[str, LabelValue]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should leave this to avoid breaking the public API 😕

# Immutable set to true so attributes cannot be added or removed after creation.
self._attributes = BoundedAttributes(
attributes=attributes, immutable=True
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Immutable doesn't seem relevant to this PR, can you split this out?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same comment about immutable, unless im misunderstanding.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC we added this to a pyi instead of the main py file because of some bugs in mypy. I agree we should try to collapse it back into the normal py file but can we do it in a separate PR?

(You should also merge some of the information in this pyi in the py file, for example ns_to_iso_str() is still untyped in the py file)

schema_url = ""
self._schema_url = schema_url
self._attributes = BoundedAttributes(attributes=attributes)
# Attributes cannot be added/removed after creation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same comment

@aabmass aabmass moved this from Approved PRs to Approved PRs that need fixes in Python PR digest Jul 24, 2026
| None
)

AttributeValue = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please leave to avoid breaking public API. We can mark them deprecated in a comment or docstring

| Sequence[float]
)
Attributes = Mapping[str, AttributeValue] | None
AttributesAsKey = tuple[

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please leave to avoid breaking public API. We can mark them deprecated in a comment or docstring

]

_ExtendedAttributes = Mapping[str, "AnyValue"]
Attributes = Mapping[str, AnyValue] | None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One subtle detail here is when this used as a return value, it is technically breaking users since we made the type more permissive.

For example, exporters now need to handle more variants of attribute types when serializing. I don't think there is any way around this but we should add it to the changelog IMO.

I wonder what other languages are doing to absorb this change to support complex attributes @lmolkova?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Approved PRs that need fixes

Development

Successfully merging this pull request may close these issues.

6 participants