OTel Updates: Complex Attributes Now Supported Across All Signals

OTLP 1.9.0 adds support for maps, arrays, and byte arrays across all OTel signals. Here's when to use complex attributes and when to stick with flat.

OTel Updates: Complex Attributes Now Supported Across All Signals

Contents

TL;DR:

  • OTLP 1.9.0 adds maps, heterogeneous arrays, byte arrays, and empty values across all signals. Previously, only logs supported these types
  • Use complex attributes when data has inherent structure (like LLM tool definitions or GraphQL error lists) that loses meaning when flattened
  • Flat attributes remain the default choice. They’re widely supported and easier to query across most backends

Introduction

OpenTelemetry now supports maps, heterogeneous arrays, and byte arrays across all signals. Here’s where these new types help, and where simple primitives still fit naturally.

If you’ve worked with OpenTelemetry for a while, you know the key-value approach to attributes, the same flat pairs you attach to OpenTelemetry spans and their attributes. It’s simple, fast, and works well with how most telemetry backends store, index, and query data. Semantic conventions were built with this in mind, which is why so many common patterns map cleanly to flat attributes.

But some workloads carry richer structure. Capturing details from an LLM call, a GraphQL response, or any nested payload needs something more expressive.

Until now, that structure was hard to represent across signals. Metrics, traces, and logs all accepted primitives or arrays of primitives, so anything more complex meant flattening, serializing, or dropping pieces you wanted to keep.

OTLP 1.9.0 changes that. Complex attribute types are now supported everywhere, and API/SDK updates are rolling out across the ecosystem. As these land, you can carry structured data through your telemetry pipeline without losing clarity or the ability to query it.

What’s New in Complex Attributes

Starting with OTLP 1.9.0, OpenTelemetry supports these attribute types on all signals:

  • Maps (with string keys and values of any supported type)
  • Heterogeneous arrays (containing elements of any supported type)
  • Byte arrays
  • Empty values

This follows OTEP 4485 and its implementation in OTLP and the spec.

Most backends today are optimized for flat attributes. They query, index, and aggregate primitives efficiently. Complex attributes work differently: they’re not always indexed the same way, which affects how you query them later. The semantic conventions reflect this. They use flat attributes for metrics and other places where you’ll be filtering and grouping frequently.

So flat attributes stay the default when both options would work. If your data fits into key-value pairs, that’s the simpler path. If it doesn’t, complex attributes let you capture it without losing structure.

💡

You can pair these richer attribute types with clear, predictable naming. This guide on OpenTelemetry naming best practices covers how.

Where complex attributes earn their place

As the OpenTelemetry community builds out semantic conventions and instrumentations, the data being captured often has inherent structure. Flattening that data loses context or makes the telemetry harder to use.

A few examples:

  • LLM operations - Instrumenting LLM calls means tool definitions, input messages, and output messages: structured objects with nested fields. Keeping that structure intact makes the data easier to work with downstream.
  • GraphQL - GraphQL responses can include lists of structured errors, each with its own path, message, and extensions. Preserving this structure means you can see exactly which field caused which error, without reconstructing it from dozens of separate attributes.
  • Database operations - Batch operations often have properties that vary per item in the batch. You could capture the count, but access to individual query parameters or results gives you more detailed visibility into what happened.

Before adding complex attributes to all signals, the OpenTelemetry community explored a few other approaches. Each had different trade-offs.

What About Just Using Logs and Spans?

One approach was to keep complex attributes limited to logs and spans, where they were already supported. This works, but it means different signals support different attribute types. Writing instrumentation code, you’d have to track which signals accept which types. A consistent API across all signals is simpler.

What About Flattening?

Flattening is another option. Take a nested structure and flatten it into separate attributes with dot notation. This works well for simple maps of primitives. With arrays, though, there are some trade-offs.

Say you have this structure:

{
"data": [
{
"foo": "bar",
"baz": 42
}
]
}

You could flatten it to:

data.0.foo = "bar"
data.0.baz = 42

This preserves the information, but you’re now embedding array indices in attribute names. If the array changes size, your attribute names change with it. Querying requires knowing the exact indices you want.

Alternatively, you could flip the structure:

data.foo = ["bar"]
data.baz = [42]

This avoids the index problem, but now everything becomes an array, even single values. The original structure, which objects contained which fields, gets lost in the transformation.

Both approaches preserve the data, but they change how you interact with it. The structure you had in the original data becomes harder to work with in queries and analysis.

What About String Serialization?

Another option is to serialize complex data to JSON strings and store those as attributes. This works, and some instrumentations already do it.

But without a standard serialization format, different libraries handle it differently. One uses compact JSON, another pretty-prints it. That variation makes the data harder to work with consistently across sources.

Serializing early in the pipeline (in the instrumentation itself) also limits what you can do with the data later. To truncate a large object, you might lose important fields. Extracting schema information or doing smart filtering is harder once everything is already a string.

Letting the backend handle serialization gives you more flexibility. The backend decides whether to preserve the structure, flatten it, or serialize it based on its storage model and query capabilities. Different backends make different choices, and instrumentations stay simpler.

💡

If you’re using an injector to automatically add attributes and spans, see this post on the OpenTelemetry Injector.

What This Means for Backends

If you’re building or maintaining an observability backend, the direction complex attributes are heading is toward native support for querying nested properties. Being able to filter by error.details.code or group by tool.parameters.model gives users a more natural way to work with structured data.

That kind of query support takes time to build, though. A practical intermediate step is to serialize complex attributes to JSON (or another format) at ingestion. This preserves the data so users can see it, and leaves the door open for richer query capabilities later.

The OTel Specification, semantic conventions, and API docs are being updated to clarify that backend support for complex attributes is still evolving. The guidance to use flat attributes when possible stands: complex attributes expand what’s possible, but flat attributes are still the more widely supported option.

When Should You Use Complex Attributes?

The choice comes down to what fits your data. Flat key-value pairs are the simpler path: they work everywhere, are well-supported across backends, and are straightforward to query.

Complex attributes make sense when the data has a structure that doesn’t flatten cleanly. If you’re recording a list of objects, each with multiple fields, and keeping that structure intact makes the data more useful, that’s where complex attributes help.

For semantic conventions and instrumentation libraries, this isn’t about going back and changing existing code. If something works with flat attributes today, leave it. Complex attributes are for new features where the data structure calls for them.

FAQs

What are complex attribute types in OpenTelemetry?

Complex attribute types in OpenTelemetry are attribute values that go beyond primitives and arrays of primitives: maps (with string keys), heterogeneous arrays, byte arrays, and empty values. Introduced in OTLP 1.9.0, they are now supported across all signals (traces, metrics, and logs), so you can carry nested, structured data through your telemetry pipeline without flattening or serializing it first.

When should you use complex attributes instead of flat key-value attributes?

Use complex attributes when your data has inherent structure that loses meaning when flattened, such as LLM tool definitions, GraphQL error lists, or per-item batch details. Flat key-value attributes remain the default choice because they are widely supported and easier to query and aggregate across backends, so reach for complex attributes only when the structure genuinely adds value.

How Last9 Handles Complex Attributes

Last9 accepts OTLP natively, which means complex attributes work out of the box. If you’re sending traces, metrics, or logs with maps or arrays, we ingest them without needing any special configuration on your end.

Once the data’s in, you can query across all your telemetry in one place. We support high-cardinality attributes without dropping data or timing out on queries. If you’re curious how it works with your setup, you can get started in about 5 minutes.

Additional Resources

To learn more about complex attribute types:

About the authors
Anjali Udasi

Anjali Udasi

Helping to make the tech a little less intimidating. I

Last9 logo and enter key

Start observing for free. No lock-in.

OpenTelemetry · Prometheus

Just update your config. Start seeing data on Last9 in seconds.

Datadog · New Relic · Others

We've got you covered. Bring over your dashboards & alerts in one click.

Built on Open Standards

100+ integrations. OTel native, works with your existing stack.