Skip to content

Add support for map/slice dimensions for Metrics and ManagedMetrics #134

@HannesDyballa

Description

@HannesDyballa

What would you like to be added?

Support for exporting map and slice data types in metric dimensions, in addition to the currently supported primitive values.

Why is this needed?

Current Limitations

1. Verbose Configuration

Exporting multiple fields from a map requires defining each field separately:

dimensions:
  - name: label1
    fieldPath: "metadata.labels.label1"
  - name: label2
    fieldPath: "metadata.labels.label2"
  - name: label3
    fieldPath: "metadata.labels.label3"
  # ... one entry per label

This becomes unmanageable when:

  • You don't know all possible label keys in advance
  • Different resources have different sets of labels
  • Labels are dynamic and change over time

2. No Optional Field Support

The current implementation fails when a projected field is missing. This means:

  • A single missing label causes the entire metric export to fail
  • You must create multiple ManagedMetric definitions to handle different field combinations
  • Example: If some resources have metadata.labels.label1 and others don't, you need separate metrics for each variant

3. Cannot Export Complete Data Structures

There's no way to export:

  • All labels as a cohesive unit
  • All annotations together
  • Complete status conditions array
  • Entire nested objects like status.atProvider
  • Full resource definitions

This forces you to either:

  • Predict and individually map every possible field (impractical)
  • Miss important data that varies between resources
  • Create excessive ManagedMetric resources to cover all combinations

Use Cases

  • Downstream Processing: Export entire resources to OpenTelemetry Collector for flexible post-processing and filtering
  • Dynamic Label Export: Export all labels without knowing their keys in advance
  • Condition Tracking: Monitor all status conditions without individual dimensions
  • Provider Status: Export complete status.atProvider for resource details

Solution Proposal

Extend the dimension functionality to handle complex data types (maps and slices) by serializing them to JSON strings. This would allow exporting:

  • Entire resource objects
  • Maps (e.g., metadata.labels, metadata.annotations)
  • Slices (e.g., status.conditions)
  • Nested objects (e.g., status.atProvider)

Implementation Approach: Implicit Behavior

Automatically detect and serialize complex types to JSON strings without requiring additional configuration:

Example 1: Export map

  dimensions:
    - name: labels
      fieldPath: "metadata.labels"

Exported metric attribute:
labels: '{"label1": "lorem", "label2": "ipsum"}'

Example 2: Export slice

dimensions:
  - name: conditions
    fieldPath: "status.conditions"

Exported metric attribute:
conditions: '[{"lastTransitionTime":"2025-11-24T13:22:27Z","reason":"Available","status":"True","type":"Ready"}]'

Example 3: Export entire object

dimensions:
  - name: obj
    fieldPath: "."

Exported metric attribute:
obj: '{"kind":"Subaccount","apiVersion":"account.btp.sap.crossplane.io/v1alpha1","metadata":{...}}'

Required Code Changes

Modify nestedPrimitiveValue(...)

// nestedPrimitiveValue returns a string value based on the result of the client-go JSONPath parser.
// Returns false if the value is not found.
// Returns an error if the value is ambiguous or a collection type.
// Returns an error if the given path can't be parsed.
//
// String conversion of non-string primitives relies on the default format when printing the value.
// The input path is expected to be passed in dot-notation without brackets or a leading dot.
// The implementation is based on similar internal client-go jsonpath usages, like kubectl
func nestedPrimitiveValue(obj unstructured.Unstructured, path string) (string, bool, error) {
jp := jsonpath.New("projection").AllowMissingKeys(true)
if err := jp.Parse(fmt.Sprintf("{.%s}", path)); err != nil {
return "", false, fmt.Errorf("failed to parse path: %v", err)
}
results, err := jp.FindResults(obj.UnstructuredContent())
if err != nil {
return "", false, fmt.Errorf("failed to find results: %v", err)
}
if len(results) == 0 || len(results[0]) == 0 {
return "", false, nil
}
if len(results) > 1 || len(results[0]) > 1 {
return "", true, errors.New("fieldPath matches more than one value which is not supported")
}
value := results[0][0]
switch value.Interface().(type) {
case map[string]interface{}, []interface{}:
return "", true, errors.New("fieldPath results in collection type which is not supported")
}
return fmt.Sprintf("%v", value.Interface()), true, nil
}

Open Questions

  1. Explicit vs Implicit Behavior: Should we add an explicit type indicator?
dimensions:
- name: labels
    fieldPath: "metadata.labels"
    type: map  # optional: auto-detect if omitted; possible values:(map, slice, primitive)
  1. Documentation Updates: What examples should be added?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions