Skip to content

motel: Wide Attributes, Call Styles, and Run Statistics

2026-02-11T14:14:25Z

motel generates synthetic OTLP traces from a YAML topology definition. This demo walks through three features that address community-reported OTel failure modes: per-operation attributes for wide structured events, parallel/sequential call styles, and structured run statistics.

The example topology

The example config defines five services with per-operation attributes, weighted status codes, high-cardinality sequence fields, and a parallel call style on order-service.

cat docs/examples/basic-topology.yaml
# Five-service topology demonstrating motel capabilities
# Generates realistic traces with gateway, two backends, and two datastores

version: 1
services:
  gateway:
    resource_attributes:
      deployment.environment: production
      service.namespace: demo
    operations:
      GET /users:
        duration: 30ms +/- 10ms
        error_rate: 0.1%
        attributes:
          http.request.method:
            value: GET
          http.route:
            value: "/api/v1/users"
          http.response.status_code:
            values:
              200: 95
              404: 3
              500: 2
          user.id:
            sequence: "user-{n}"
        calls:
          - user-service.list
      POST /orders:
        duration: 80ms +/- 20ms
        error_rate: 0.5%
        attributes:
          http.request.method:
            value: POST
          http.route:
            value: "/api/v1/orders"
          http.response.status_code:
            values:
              201: 90
              400: 5
              500: 5
        calls:
          - order-service.create

  user-service:
    resource_attributes:
      deployment.environment: production
    operations:
      list:
        duration: 20ms +/- 5ms
        error_rate: 0.1%
        calls:
          - postgres.query

  order-service:
    resource_attributes:
      deployment.environment: production
    operations:
      create:
        duration: 50ms +/- 15ms
        error_rate: 0.5%
        call_style: parallel
        calls:
          - postgres.query
          - redis.get

  postgres:
    resource_attributes:
      db.system: postgresql
    operations:
      query:
        duration: 5ms +/- 2ms
        error_rate: 0.01%
        attributes:
          db.operation:
            values:
              SELECT: 70
              INSERT: 20
              UPDATE: 10

  redis:
    resource_attributes:
      db.system: redis
    operations:
      get:
        duration: 1ms +/- 0.5ms
        error_rate: 0.001%
        attributes:
          db.operation:
            value: GET

traffic:
  rate: 100/s

scenarios:
  - name: database degradation
    at: +5m
    duration: 10m
    override:
      postgres.query:
        duration: 500ms +/- 100ms
        error_rate: 15%

Validation

The validate command checks the topology for structural correctness, including the new attribute definitions and call style fields.

motel validate docs/examples/basic-topology.yaml
Configuration valid: 5 services, 2 root operations

Wide attributes on spans

Running with --stdout emits spans as JSON. Each gateway span carries per-operation attributes: static values (http.route), weighted random values (http.response.status_code), and high-cardinality sequences (user.id). This demonstrates why arbitrarily-wide structured events are more powerful than low-cardinality metrics.

motel run --stdout --duration 200ms docs/examples/basic-topology.yaml 2>/dev/null | grep 'GET /users' | head -1 | jq -r \
  '.Attributes | sort_by(.Key) | .[] | "  \(.Key): \(.Value.Value)"'
  deployment.environment: production
  http.request.method: GET
  http.response.status_code: 200
  http.route: /api/v1/users
  service.namespace: demo
  synth.operation: GET /users
  synth.service: gateway
  user.id: user-1

The gateway span carries service attributes (deployment.environment, service.namespace) alongside operation-specific attributes. The user.id field increments per trace, producing high-cardinality data that would be impossible to represent with metrics alone.

Parallel vs sequential call styles

The order-service uses call_style: parallel, so its two downstream calls (postgres and redis) start at the same time. We can verify this by checking whether the start times of sibling spans match.

motel run --stdout --duration 200ms docs/examples/basic-topology.yaml 2>/dev/null | jq -rs '
  group_by(.SpanContext.TraceID) |
  map(select(
    any(.Name == "create") and
    any(.Name == "query") and
    any(.Name == "get")
  )) | .[0] |
  (map(select(.Name == "query")) | .[0].StartTime) as $qt |
  (map(select(.Name == "get")) | .[0].StartTime) as $gt |
  "parallel (query and get share start time): \($qt == $gt)"'
parallel (query and get share start time): true

Run statistics

motel emits structured JSON to stderr at the end of a run. This addresses the silent-failure critique: if your observability pipeline drops data, compare these numbers against what arrived.

motel run --stdout --duration 1s docs/examples/basic-topology.yaml 2>&1 >/dev/null | tail -1 | jq -r '
  "fields: \(keys)",
  "traces > 0: \(.traces > 0)",
  "spans > traces: \(.spans > .traces)",
  "has rates: \(.traces_per_second > 0 and .spans_per_second > 0)"'
fields: ["elapsed_ms","error_rate","errors","spans","spans_per_second","traces","traces_per_second"]
traces > 0: true
spans > traces: true
has rates: true