Skip to content

Commit 507d65f

Browse files
authored
Foreach hash id arg (#3716)
* add the option to hash the id when a string value is used * add debug log to display the mapping between string keys and hash values * changelog update
1 parent fa969e1 commit 507d65f

File tree

5 files changed

+88
-10
lines changed

5 files changed

+88
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Main (unreleased)
1414

1515
- (_Experimental_) Add an `array.group_by` stdlib function to group items in an array by a key. (@wildum)
1616

17+
### Enhancements
18+
19+
- Add `hash_string_id` argument to `foreach` block to hash the string representation of the pipeline id instead of using the string itself. (@wildum)
20+
1721
v1.9.0-rc.0
1822
-----------------
1923

docs/sources/reference/config-blocks/foreach.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ You can use the following arguments with `foreach`:
3535
| `var` | `string` | Name of the variable referring to the current item in the collection. | | yes |
3636
| `enable_metrics` | `bool` | Whether to expose debug metrics in the {{< param "PRODUCT_NAME" >}} `/metrics` endpoint. | `false` | no |
3737
| `id` | `string` | Name of the field to use from collection items for child component's identification. | `""` | no |
38+
| `hash_string_id` | `bool` | Whether to hash the string representation of the id of the collection items. | `false` | no |
3839

3940
The items in the `collection` list can be of any type [type][types], such as a bool, a string, a list, or a map.
4041

42+
When using a collection of strings or when the `id` field is a string, you can set `hash_string_id` to `true` to hash the string representation of the `id` field instead of using the string itself. This is recommended when the strings are long because the values will be used to identify the components that are created dynamically in metrics, logs and in the UI.
43+
4144
{{< admonition type="warning" >}}
4245
Setting `enable_metrics` to `true` when `collection` has lots of elements may cause a large number of metrics to appear on the {{< param "PRODUCT_NAME" >}} `/metric` endpoint.
4346
{{< /admonition >}}

internal/nodeconf/foreach/foreach.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ const (
1212
)
1313

1414
type Arguments struct {
15-
Collection []any `alloy:"collection,attr"`
16-
Var string `alloy:"var,attr"`
17-
Id string `alloy:"id,attr,optional"`
15+
Collection []any `alloy:"collection,attr"`
16+
Var string `alloy:"var,attr"`
17+
Id string `alloy:"id,attr,optional"`
18+
HashStringId bool `alloy:"hash_string_id,attr,optional"`
1819

1920
// EnableMetrics should be false by default.
2021
// That way users are protected from an explosion of debug metrics

internal/runtime/internal/controller/node_config_foreach.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"hash/fnv"
99
"path"
10+
"reflect"
1011
"strings"
1112
"sync"
1213
"time"
@@ -212,16 +213,20 @@ func (fn *ForeachConfigNode) evaluate(scope *vm.Scope) error {
212213
// We must create an ID from the collection entries to avoid recreating all components on every updates.
213214
// We track the hash counts because the collection might contain duplicates ([1, 1, 1] would result in the same ids
214215
// so we handle it by adding the count at the end -> [11, 12, 13]
215-
customComponentID := fmt.Sprintf("foreach_%s", objectFingerprint(id))
216+
customComponentID := fmt.Sprintf("foreach_%s", objectFingerprint(id, args.HashStringId))
216217
count := fn.customComponentHashCounts[customComponentID] // count = 0 if the key is not found
217218
fn.customComponentHashCounts[customComponentID] = count + 1
218219
customComponentID += fmt.Sprintf("_%d", count+1)
219220

220-
cc, err := fn.getOrCreateCustomComponent(customComponentID)
221+
cc, created, err := fn.getOrCreateCustomComponent(customComponentID)
221222
if err != nil {
222223
return err
223224
}
224225

226+
if created && args.HashStringId && id != nil && reflect.TypeOf(id).Kind() == reflect.String {
227+
level.Debug(fn.logger).Log("msg", "a new foreach pipeline was created", "value", id, "fingerprint", customComponentID)
228+
}
229+
225230
// Expose the current scope + the collection item that correspond to the child.
226231
vars := deepCopyMap(scope.Variables)
227232
vars[args.Var] = args.Collection[i]
@@ -253,18 +258,18 @@ func (fn *ForeachConfigNode) evaluate(scope *vm.Scope) error {
253258

254259
// Assumes that a lock is held,
255260
// so that fn.moduleController doesn't change while the function is running.
256-
func (fn *ForeachConfigNode) getOrCreateCustomComponent(customComponentID string) (CustomComponent, error) {
261+
func (fn *ForeachConfigNode) getOrCreateCustomComponent(customComponentID string) (CustomComponent, bool, error) {
257262
cc, exists := fn.customComponents[customComponentID]
258263
if exists {
259-
return cc, nil
264+
return cc, false, nil
260265
}
261266

262267
newCC, err := fn.moduleController.NewCustomComponent(customComponentID, func(exports map[string]any) {})
263268
if err != nil {
264-
return nil, fmt.Errorf("creating custom component: %w", err)
269+
return nil, true, fmt.Errorf("creating custom component: %w", err)
265270
}
266271
fn.customComponents[customComponentID] = newCC
267-
return newCC, nil
272+
return newCC, true, nil
268273
}
269274

270275
func (fn *ForeachConfigNode) UpdateBlock(b *ast.BlockStmt) {
@@ -420,10 +425,13 @@ func computeHash(s string) string {
420425
return hex.EncodeToString(hasher.Sum(nil))
421426
}
422427

423-
func objectFingerprint(id any) string {
428+
func objectFingerprint(id any, hashId bool) string {
424429
// TODO: Test what happens if there is a "true" string and a true bool in the collection.
425430
switch v := id.(type) {
426431
case string:
432+
if hashId {
433+
return computeHash(v)
434+
}
427435
return replaceNonAlphaNumeric(v)
428436
case int, bool:
429437
return fmt.Sprintf("%v", v)

internal/runtime/internal/controller/node_config_foreach_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,68 @@ func TestNonAlphaNumericString3(t *testing.T) {
276276
require.ElementsMatch(t, customComponentIds, []string{"foreach_123__s4_1", "foreach_123__s4_1_1"})
277277
}
278278

279+
func TestStringIDHash(t *testing.T) {
280+
config := `foreach "default" {
281+
collection = ["123./st%4$"]
282+
var = "num"
283+
hash_string_id = true
284+
template {
285+
}
286+
}`
287+
foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil)
288+
require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(make(map[string]interface{}))))
289+
customComponentIds := foreachConfigNode.moduleController.(*ModuleControllerMock).CustomComponents
290+
require.ElementsMatch(t, customComponentIds, []string{"foreach_1951d330e1267d082c816bfb3f40cce6eb9a8da9f6a6b9da09ace3c6514361cd_1"})
291+
}
292+
293+
func TestStringIDHashWithKey(t *testing.T) {
294+
config := `foreach "default" {
295+
collection = [obj1, obj2]
296+
var = "num"
297+
hash_string_id = true
298+
id = "label1"
299+
template {
300+
}
301+
}`
302+
foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil)
303+
vars := map[string]interface{}{
304+
"obj1": map[string]string{
305+
"label1": "123./st%4$",
306+
"label2": "b",
307+
},
308+
"obj2": map[string]string{
309+
"label1": "aaaaaaaaaaaaaaabbbbbbbbbcccccccccdddddddddeeeeeeeeeffffffffffgggggggggggghhhhhhhhhhiiiiiiiiiiijjjjjjjjjjjkkkkkkkkkklllll",
310+
},
311+
}
312+
require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(vars)))
313+
customComponentIds := foreachConfigNode.moduleController.(*ModuleControllerMock).CustomComponents
314+
require.ElementsMatch(t, customComponentIds, []string{"foreach_1951d330e1267d082c816bfb3f40cce6eb9a8da9f6a6b9da09ace3c6514361cd_1", "foreach_986cb398ec7d2d70a69bab597e8525ccc5c67594765a82ee7d0f011937cdec25_1"})
315+
}
316+
317+
func TestStringIDHashWithKeySameValue(t *testing.T) {
318+
config := `foreach "default" {
319+
collection = [obj1, obj2]
320+
var = "num"
321+
hash_string_id = true
322+
id = "label1"
323+
template {
324+
}
325+
}`
326+
foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil)
327+
vars := map[string]interface{}{
328+
"obj1": map[string]string{
329+
"label1": "123./st%4$",
330+
"label2": "b",
331+
},
332+
"obj2": map[string]string{
333+
"label1": "123./st%4$",
334+
},
335+
}
336+
require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(vars)))
337+
customComponentIds := foreachConfigNode.moduleController.(*ModuleControllerMock).CustomComponents
338+
require.ElementsMatch(t, customComponentIds, []string{"foreach_1951d330e1267d082c816bfb3f40cce6eb9a8da9f6a6b9da09ace3c6514361cd_1", "foreach_1951d330e1267d082c816bfb3f40cce6eb9a8da9f6a6b9da09ace3c6514361cd_2"})
339+
}
340+
279341
func TestCollectionNonArrayValue(t *testing.T) {
280342
config := `foreach "default" {
281343
collection = "aaa"

0 commit comments

Comments
 (0)