-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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 labelThis 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
ManagedMetricdefinitions to handle different field combinations - Example: If some resources have
metadata.labels.label1and 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
ManagedMetricresources 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(...)
metrics-operator/internal/orchestrator/projectionhelper.go
Lines 11 to 40 in c73ed2e
| // 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
- 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)- Documentation Updates: What examples should be added?