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. |
Problems
In OpenTelemetry, the fields that carry the diagnostic signal — a log or span's
attributes, andthe
resourceandscopeattributes attached to them — are all typedmap<string, AnyValue>: anapplication-defined map from string keys to values of any type, whose set of keys keeps emerging.
attributesis only the most prominent such field; the problem is general to anymap<string, AnyValue>field. Storing this data has only two fundamental difficulties: its valuetypes 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:
statusis sometimes200andsometimes
"OK";durationis sometimes1.5and sometimes"1.5s". This is an inevitableproduct 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>andhttp.request.header.<anything>, and keys with IDs embedded in their names. Any layout that giveseach 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 aslong) and service B later writesstatus: "OK", service B's entire log line is rejected — a silent loss whose symptom ("service B'slogs 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:
ignore_malformed_source, where it cannot be queried; the field count still grows without bound.keywordflat_object_source)_sourceby a per-document script at query time — correct, but with no doc_values or star-tree behind it, it does not scale to dashboards.dynamic: false_source)