Understand Attribute Placement and Cardinality¶
This guide covers how motel models resource attributes and span attributes, how to experiment with moving attributes between levels, and how to use attribute generators to explore cardinality impact before deploying changes to production.
Prerequisites¶
- motel installed
- A topology file (see Model your services)
- A tracing backend or the
--stdoutflag for local inspection
Resource attributes vs span attributes¶
motel distinguishes two levels of attributes, matching the OpenTelemetry data model:
- Resource attributes are defined under
services.<name>.resource_attributes. They describe the service itself and are attached to the OTel resource — stored once per service, not per span. These are static string key-value pairs. - Span attributes are defined under
services.<name>.operations.<op>.attributes. They describe individual operations and can vary per span using attribute generators. There is also a service-levelattributesfield that adds static key-value pairs to every span from the service.
services:
gateway:
resource_attributes: # OTel resource — stored once per service
deployment.environment: production
service.namespace: demo
operations:
GET /users:
duration: 30ms +/- 10ms
attributes: # span attributes — stored per span
http.request.method:
value: GET
http.response.status_code:
values: {200: 95, 404: 3, 500: 2}
Resource attributes appear once per service resource in the exported telemetry. Span attributes appear on each individual span. This distinction matters for storage cost, query performance, and how your backend indexes data.
See docs/examples/attribute-placement.yaml for a runnable example that uses both levels on the same service.
Experiment: move an attribute between levels¶
A practical way to understand the difference is to move an attribute from one level to the other and observe the result.
Start with a resource attribute¶
Create a file called placement-test.yaml:
version: 1
services:
api:
resource_attributes:
deployment.environment: staging
operations:
handle:
duration: 10ms
traffic:
rate: 10/s
Generate traces and inspect the output:
motel run --stdout --duration 3s placement-test.yaml | head -20
Notice that deployment.environment is attached to the OTel resource for the api service. In the --stdout JSON output, resource attributes appear in the Resource field. When sent to a real backend via --endpoint, resource attributes are stored once per service — not duplicated on every span.
Move it to a span attribute¶
Now move deployment.environment from the service level to the operation level:
version: 1
services:
api:
operations:
handle:
duration: 10ms
attributes:
deployment.environment:
value: staging
traffic:
rate: 10/s
Run the same command:
motel run --stdout --duration 3s placement-test.yaml | head -20
In the --stdout JSON, deployment.environment now appears in the span's Attributes array rather than the Resource field. When sent to a real backend, the difference is significant: resource attributes are stored once per service, while span attributes are stored on every individual span. Placing a constant value at the span level increases storage cost and may change how you query the attribute.
To see this distinction in practice, send the two versions to a collector and compare how your backend indexes them:
motel run --endpoint localhost:4318 --duration 10s placement-test.yaml
Rule of thumb: attributes that are constant for a service belong at the service level. Attributes that vary per request belong at the operation level.
Attribute generators and cardinality¶
Span attributes in motel use generators that control how many distinct values an attribute produces. This directly maps to cardinality — the number of unique values a backend must index.
Low cardinality: value and values¶
A value generator always produces the same string — cardinality of 1:
http.request.method:
value: GET
A values generator picks from a fixed set with weighted probability — cardinality equals the number of choices:
http.response.status_code:
values:
200: 95
404: 3
500: 2
These are safe for most backends. The set of distinct values is small and bounded.
High cardinality: sequence¶
A sequence generator produces a unique value for every span:
user.id:
sequence: "user-{n}"
This creates user-1, user-2, user-3, and so on — unbounded cardinality. Add this to a topology and send traffic to your backend to see how it handles high-cardinality attributes.
Numeric range: range¶
A range generator produces random integers within bounds:
http.response.content_length:
range: [0, 50000]
Cardinality is bounded by the range size but can still be high. A range of [0, 50000] produces up to 50,001 distinct values.
Controlled distribution: distribution¶
A distribution generator samples from a normal distribution:
queue.depth:
distribution:
mean: 100
stddev: 20
Values cluster around the mean but the theoretical range is unbounded.
Boolean: probability¶
A probability generator produces true/false with the given probability — cardinality of 2:
cache.hit:
probability: 0.8
Test cardinality impact on your backend¶
Combine these generators in a topology to simulate realistic and adversarial attribute patterns:
version: 1
services:
api:
resource_attributes:
deployment.environment: staging
operations:
handle:
duration: 15ms +/- 5ms
attributes:
http.request.method:
values: {"GET": 70, "POST": 20, "PUT": 10}
user.id:
sequence: "user-{n}"
http.response.status_code:
values: {200: 90, 400: 5, 500: 5}
response.size:
range: [100, 10000]
traffic:
rate: 50/s
Send this to your backend and monitor:
motel run --endpoint localhost:4318 --duration 60s cardinality-test.yaml
Watch for:
- Index growth — high-cardinality attributes like
user.idcause index bloat in most tracing backends - Query performance — try querying by
user.idvshttp.request.methodand compare response times - Storage cost — compare the data volume with and without the
user.idattribute
To isolate the effect of a single attribute, run the topology twice — once with the high-cardinality attribute and once without — and compare the results in your backend.
Semantic conventions and correct placement¶
Instead of hand-writing well-known attributes — and risking typos or wrong placement — you can set a domain on an operation and let motel generate convention-correct attributes for you:
operations:
GET /users:
duration: 30ms +/- 10ms
domain: http
The domain shorthand draws attribute names, types, and example values from the embedded OpenTelemetry semantic convention registry, so the generated attributes always match the conventions. See How motel uses OTel semantic conventions for details.
The --semconv flag points to a directory of additional semantic convention YAML files, which motel merges with the embedded conventions. Use it to make custom domains available to the domain shorthand:
motel run --stdout --semconv /path/to/semconv --duration 5s topology.yaml
Note that motel does not validate hand-written attribute names against the conventions — motel validate checks topology structure, not attribute spelling or placement. If you want convention-correct attributes, use domain; if you write attributes by hand, the names are emitted exactly as you wrote them.
Further reading¶
- Model your services — creating topology files from scratch or from traces
- CLI reference — full list of flags and options
- Basic topology example — a complete topology with resource and span attributes