Skip to content

[FEATURE] Storing OpenTelemetry map<string, AnyValue> fields in OpenSearch #5704

Description

@penghuo

Problems

In OpenTelemetry, the fields that carry the diagnostic signal — a log or span's attributes, and
the resource and scope attributes attached to them — are all typed map<string, AnyValue>: an
application-defined map from string keys to values of any type, whose set of keys keeps emerging.
attributes is only the most prominent such field; the problem is general to any
map<string, AnyValue> field. Storing this data has only two fundamental difficulties: its value
types change from one producer to the next and cannot be stored by unifying them into one type
,
and its number of distinct keys has no upper bound.

1. No type unification — one path carries many types

The same path can be a different type in different documents: status is sometimes 200 and
sometimes "OK"; duration is sometimes 1.5 and sometimes "1.5s". This is an inevitable
product of multi-language SDKs, version skew, and the fact that JSON itself does not distinguish int
from float — not a bug that can be fixed on the application side. The right goal is to preserve
every type
, not to force them into a single least-common type.

2. Unbounded paths — the number of distinct keys has no ceiling

The set of attribute keys is decided by applications and keeps growing: thousands of keys, changing
daily, plus unbounded-cardinality paths like k8s.labels.<anything> and
http.request.header.<anything>, and keys with IDs embedded in their names. Any layout that gives
each path its own column must therefore either cap how many columns it will create or pay a metadata
cost that grows without bound.


How OpenSearch stores this today

Every current option resolves one axis by giving up the other, or gives up the type entirely. Take
the type conflict first: dynamic mapping infers a field's type from the first value it sees and
freezes it, so when service A writes status: 200 (inferred as long) and service B later writes
status: "OK", service B's entire log line is rejected — a silent loss whose symptom ("service B's
logs are missing") is far removed from its cause ("what some other service wrote earlier"), across
teams and across time. The alternatives trade that away in different directions:

Approach Avoids type-conflict data loss? Bounds the field count? Limitations
Dynamic mapping (typed fields) No No A document whose type differs from the first value seen is rejected in full; every new key also adds a mapping entry that the cluster manager must broadcast to every node.
ignore_malformed Partly No The conflicting value is silently dropped from the index — it survives only in _source, where it cannot be queried; the field count still grows without bound.
Map every field to keyword Yes No Every value is stored as a string, so the type is gone: no numeric aggregation and no numeric range. Each key is still its own mapping entry, so the mapping grows without bound.
flat_object Yes Yes The whole subtree becomes one field and every value a string, so numeric aggregation and numeric range are impossible; the paths inside are invisible to the field browser, and it cannot feed a star-tree.
Derived / runtime fields Not applicable (reads _source) Not applicable The value is cast from _source by a per-document script at query time — correct, but with no doc_values or star-tree behind it, it does not scale to dashboards.
Normalize types at ingest Yes No Works only for keys you can predict and configure ahead of time — which unpredictable, application-defined attributes are not.
dynamic: false Yes (kept only in _source) Yes Dynamic fields are stored but not indexed, so they cannot be filtered, aggregated, or searched at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    PPLPiped processing languageenhancementNew feature or request

    Type

    No type

    Projects

    Status
    Not Started

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions